Refactor: Frontend-Fundament — Format-Utils, Dialog-Ref, Typen (Phase 3a)

- Neuer lib/format.ts: gb/fmtBytes/fmtSize/fmtEta/fmtCtx zentral; aus
  DashboardView/SystemView/ModelsView entdoppelt und importiert.
- CustomDialog: document.getElementById -> useRef (kein DOM-Query, robuster).
- api.ts: GitInfo/ComponentVersion/Versions typisiert, SystemStatus.versions
  ergänzt; App.tsx nutzt SystemStatus statt any.

tsc grün, Build grün, Dashboard verifiziert (keine Konsolenfehler, Formatierer
rendern identisch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-26 14:36:28 +02:00
parent 341ea870bb
commit 74f64731ab
9 changed files with 146 additions and 113 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-BYvMJHPL.js"></script> <script type="module" crossorigin src="/assets/index-L9oobut2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DiSNgbNY.css"> <link rel="stylesheet" crossorigin href="/assets/index-DiSNgbNY.css">
</head> </head>
<body> <body>
+3 -3
View File
@@ -11,14 +11,14 @@ import { AgentView } from "@/views/AgentView"
import { GuideView } from "@/views/GuideView" import { GuideView } from "@/views/GuideView"
import { Placeholder } from "@/views/Placeholder" import { Placeholder } from "@/views/Placeholder"
import { SystemDrawer } from "@/components/SystemDrawer" import { SystemDrawer } from "@/components/SystemDrawer"
import { api, type Health } from "@/lib/api" import { api, type Health, type SystemStatus } from "@/lib/api"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export default function App() { export default function App() {
const [view, setView] = useState<ViewId>("dashboard") const [view, setView] = useState<ViewId>("dashboard")
const [health, setHealth] = useState<Health | null>(null) const [health, setHealth] = useState<Health | null>(null)
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true") const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true")
const [sysStatus, setSysStatus] = useState<any | null>(null) const [sysStatus, setSysStatus] = useState<SystemStatus | null>(null)
const [drawerOpen, setDrawerOpen] = useState(false) const [drawerOpen, setDrawerOpen] = useState(false)
const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance") const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance")
@@ -30,7 +30,7 @@ export default function App() {
}, []) }, [])
useEffect(() => { useEffect(() => {
const loadSys = () => api<any>("/api/system/status").then(setSysStatus).catch(() => {}) const loadSys = () => api<SystemStatus>("/api/system/status").then(setSysStatus).catch(() => {})
loadSys() loadSys()
const t = setInterval(loadSys, 20000) const t = setInterval(loadSys, 20000)
return () => clearInterval(t) return () => clearInterval(t)
+5 -6
View File
@@ -1,3 +1,4 @@
import { useRef } from "react"
import { X } from "lucide-react" import { X } from "lucide-react"
export interface CustomDialogProps { export interface CustomDialogProps {
@@ -10,6 +11,7 @@ export interface CustomDialogProps {
} }
export function CustomDialog({ type, title, message, defaultValue, onConfirm, onCancel }: CustomDialogProps) { export function CustomDialog({ type, title, message, defaultValue, onConfirm, onCancel }: CustomDialogProps) {
const inputRef = useRef<HTMLInputElement>(null)
return ( return (
<div className="fixed inset-0 z-[99] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"> <div className="fixed inset-0 z-[99] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
<div className="w-full max-w-sm rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200"> <div className="w-full max-w-sm rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
@@ -26,15 +28,14 @@ export function CustomDialog({ type, title, message, defaultValue, onConfirm, on
{type === "prompt" && ( {type === "prompt" && (
<input <input
ref={inputRef}
type="text" type="text"
id="custom-dialog-input"
defaultValue={defaultValue} defaultValue={defaultValue}
className="w-full h-9 px-3 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground" className="w-full h-9 px-3 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
autoFocus autoFocus
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
const val = (document.getElementById("custom-dialog-input") as HTMLInputElement)?.value onConfirm(inputRef.current?.value)
onConfirm(val)
} }
}} }}
/> />
@@ -51,9 +52,7 @@ export function CustomDialog({ type, title, message, defaultValue, onConfirm, on
)} )}
<button <button
onClick={() => { onClick={() => {
const val = type === "prompt" const val = type === "prompt" ? inputRef.current?.value : undefined
? (document.getElementById("custom-dialog-input") as HTMLInputElement)?.value
: undefined
onConfirm(val) onConfirm(val)
}} }}
className="h-8 px-4 flex items-center justify-center text-xs font-semibold text-primary-foreground bg-primary hover:bg-primary/95 rounded-lg transition-colors cursor-pointer" className="h-8 px-4 flex items-center justify-center text-xs font-semibold text-primary-foreground bg-primary hover:bg-primary/95 rounded-lg transition-colors cursor-pointer"
+29
View File
@@ -130,6 +130,34 @@ export interface RoutingResp {
gateway_reachable: boolean gateway_reachable: boolean
} }
export interface GitInfo {
hash: string
date: string
subject: string
branch: string
dirty: boolean
path: string
}
// Engine kann git-, binary- oder unbekannte Version sein — Felder je nach `type`.
export interface ComponentVersion {
type: "git" | "binary" | "unknown"
hash?: string
date?: string
subject?: string
branch?: string
dirty?: boolean
path?: string
version_text?: string
}
export interface Versions {
mc2: GitInfo | null
engine: ComponentVersion
hermes_ui: GitInfo | null
hermes_agent: GitInfo | null
}
export interface SystemStatus { export interface SystemStatus {
cpu: { percent: number; cores: number | null } cpu: { percent: number; cores: number | null }
ram: { total: number; used: number; percent: number } ram: { total: number; used: number; percent: number }
@@ -142,6 +170,7 @@ export interface SystemStatus {
} | null } | null
temp: { cpu?: number; gpu?: number } | null temp: { cpu?: number; gpu?: number } | null
disk: { total: number; used: number; percent: number } | null disk: { total: number; used: number; percent: number } | null
versions?: Versions
} }
export interface ServicesResp { export interface ServicesResp {
+32
View File
@@ -0,0 +1,32 @@
// Zentrale Formatierungs-Helfer (vorher in einzelnen Views dupliziert).
/** Bytes → GB als String mit einer Nachkommastelle (z.B. "14.1"). */
export function gb(b: number): string {
return (b / 1024 ** 3).toFixed(1)
}
/** Bytes → "1.2 GB" / "512 MB"; leer bei 0/undefined. */
export function fmtBytes(b?: number): string {
if (!b) return ""
if (b > 1024 ** 3) return `${(b / 1024 ** 3).toFixed(1)} GB`
return `${(b / 1024 ** 2).toFixed(0)} MB`
}
/** Bytes → "1.2 GB" / "512 MB"; "—" bei 0/undefined/null. */
export function fmtSize(b?: number | null): string {
if (!b) return "—"
const g = b / 1024 ** 3
return g >= 1 ? `${g.toFixed(1)} GB` : `${(b / 1024 ** 2).toFixed(0)} MB`
}
/** Sekunden → "3 min" / "45 s"; leer bei 0/undefined. */
export function fmtEta(s?: number): string {
if (!s) return ""
const m = Math.floor(s / 60)
return m > 0 ? `${m} min` : `${s} s`
}
/** Kontextlänge → "32k"; "—" bei null. */
export function fmtCtx(c: number | null): string {
return c ? `${Math.round(c / 1024)}k` : "—"
}
+1 -4
View File
@@ -2,13 +2,10 @@ import { useEffect, useState } from "react"
import { Bot, Cpu, ExternalLink, Brain, Layers, Plus, ShieldAlert, X, Power, Shield, Download, RefreshCw, Check, Coins } from "lucide-react" import { Bot, Cpu, ExternalLink, Brain, Layers, Plus, ShieldAlert, X, Power, Shield, Download, RefreshCw, Check, Coins } from "lucide-react"
import { api, type SystemStatus, type AgentStatus, type ModelInfo, type Memory, type UpdatesResp, type Job } from "@/lib/api" import { api, type SystemStatus, type AgentStatus, type ModelInfo, type Memory, type UpdatesResp, type Job } from "@/lib/api"
import { cn, resolveExternalUrl } from "@/lib/utils" import { cn, resolveExternalUrl } from "@/lib/utils"
import { gb } from "@/lib/format"
import { CustomDialog } from "@/components/CustomDialog" import { CustomDialog } from "@/components/CustomDialog"
function gb(b: number) {
return (b / 1024 ** 3).toFixed(1)
}
function RadialGauge({ value, label, detail }: { value: number; label: string; detail?: string }) { function RadialGauge({ value, label, detail }: { value: number; label: string; detail?: string }) {
const radius = 24 const radius = 24
const circ = 2 * Math.PI * radius const circ = 2 * Math.PI * radius
+1 -22
View File
@@ -3,21 +3,10 @@ import { Download, Search, Trash2, Edit3, Star, Layers, Activity, HardDrive, X,
import { api, type DiscoverResp, type Fit, type Job, type ModelInfo, type RoutingResp, type UpdatesResp, type ConnectResp } from "@/lib/api" import { api, type DiscoverResp, type Fit, type Job, type ModelInfo, type RoutingResp, type UpdatesResp, type ConnectResp } from "@/lib/api"
import { CapsChips } from "@/components/CapsChips" import { CapsChips } from "@/components/CapsChips"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { fmtBytes, fmtEta, fmtSize, fmtCtx } from "@/lib/format"
import { CustomDialog } from "@/components/CustomDialog" import { CustomDialog } from "@/components/CustomDialog"
function fmtBytes(b?: number) {
if (!b) return ""
if (b > 1024 ** 3) return `${(b / 1024 ** 3).toFixed(1)} GB`
return `${(b / 1024 ** 2).toFixed(0)} MB`
}
function fmtEta(s?: number) {
if (!s) return ""
const m = Math.floor(s / 60)
return m > 0 ? `${m} min` : `${s} s`
}
function JobsBar({ onError }: { onError?: (msg: string) => void }) { function JobsBar({ onError }: { onError?: (msg: string) => void }) {
const [jobs, setJobs] = useState<Job[]>([]) const [jobs, setJobs] = useState<Job[]>([])
const [errorMsg, setErrorMsg] = useState<string | null>(null) const [errorMsg, setErrorMsg] = useState<string | null>(null)
@@ -100,16 +89,6 @@ function JobsBar({ onError }: { onError?: (msg: string) => void }) {
) )
} }
function fmtSize(b?: number | null) {
if (!b) return "—"
const gb = b / 1024 ** 3
return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(b / 1024 ** 2).toFixed(0)} MB`
}
function fmtCtx(c: number | null) {
return c ? `${Math.round(c / 1024)}k` : "—"
}
function FitBadge({ fit }: { fit: Fit }) { function FitBadge({ fit }: { fit: Fit }) {
const tone = { const tone = {
perfect: "bg-emerald-500/15 text-emerald-400 border border-emerald-500/20", perfect: "bg-emerald-500/15 text-emerald-400 border border-emerald-500/20",
+1 -4
View File
@@ -2,13 +2,10 @@ import { useEffect, useState } from "react"
import { ExternalLink, RefreshCw, Save, Cpu, HardDrive, Cpu as GpuIcon, Activity } from "lucide-react" import { ExternalLink, RefreshCw, Save, Cpu, HardDrive, Cpu as GpuIcon, Activity } from "lucide-react"
import { api, type ServicesResp, type SystemStatus } from "@/lib/api" import { api, type ServicesResp, type SystemStatus } from "@/lib/api"
import { cn, resolveExternalUrl } from "@/lib/utils" import { cn, resolveExternalUrl } from "@/lib/utils"
import { gb } from "@/lib/format"
import { CustomDialog } from "@/components/CustomDialog" import { CustomDialog } from "@/components/CustomDialog"
function gb(b: number) {
return (b / 1024 ** 3).toFixed(1)
}
function DiagnosticBar({ label, percent, detail, icon: Icon }: { label: string; percent: number; detail?: string; icon: any }) { function DiagnosticBar({ label, percent, detail, icon: Icon }: { label: string; percent: number; detail?: string; icon: any }) {
const barColor = percent > 90 const barColor = percent > 90
? "bg-red-500 shadow-md shadow-red-500/20" ? "bg-red-500 shadow-md shadow-red-500/20"