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:
+73
-73
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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` : "—"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user