feat(2.0): Phase 5 — Betrieb, Observability + Politur

Backup: services/backup.py (Memory-SQLite inkl. WAL/SHM + Configs, retain 7)
+ deploy/backup.sh + POST /api/system/backup + GET /api/system/backups.
Services-Health: GET /api/system/services (aggregierte Reachability +
Observability-Links). Frontend: SystemView mit Dienste-Health + Backup-
Button + Links; Theme-Toggle (Hell/Dunkel, persistent).

Lokal verifiziert: Backup-Snapshot + services-Aggregat (TestClient),
Frontend-Build + Browser (Light-Mode bestaetigt via computed styles).
Damit Phasen 0-5 lokal fertig. Docs aktualisiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-25 08:29:27 +02:00
parent c773dd7eae
commit 1f4c987652
13 changed files with 239 additions and 45 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
<meta name="theme-color" content="#0d1117" />
<link rel="manifest" href="/manifest.webmanifest" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-umMcKqw6.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-nVsgoG2p.css">
<script type="module" crossorigin src="/assets/index-BYe3GDjZ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CatWSvf4.css">
</head>
<body>
<div id="root"></div>
+16 -1
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"
import { Command as CommandIcon } from "lucide-react"
import { Command as CommandIcon, Moon, Sun } from "lucide-react"
import { NAV, type ViewId } from "@/nav"
import { CommandPalette } from "@/components/CommandPalette"
import { ModelsView } from "@/views/ModelsView"
@@ -15,6 +15,7 @@ import { cn } from "@/lib/utils"
export default function App() {
const [view, setView] = useState<ViewId>("models")
const [health, setHealth] = useState<Health | null>(null)
const [dark, setDark] = useState(() => localStorage.getItem("mc_theme") !== "light")
useEffect(() => {
const load = () => api<Health>("/api/health").then(setHealth).catch(() => setHealth(null))
@@ -23,6 +24,11 @@ export default function App() {
return () => clearInterval(t)
}, [])
useEffect(() => {
document.documentElement.classList.toggle("dark", dark)
localStorage.setItem("mc_theme", dark ? "dark" : "light")
}, [dark])
const active = NAV.find((n) => n.id === view)!
return (
@@ -78,6 +84,14 @@ export default function App() {
<div className="flex min-w-0 flex-1 flex-col">
<header className="flex h-14 shrink-0 items-center justify-between border-b border-border px-6">
<div className="text-sm text-muted-foreground">{active.hint}</div>
<div className="flex items-center gap-2">
<button
onClick={() => setDark((d) => !d)}
className="flex h-8 w-8 items-center justify-center rounded-md border border-border text-muted-foreground hover:bg-accent"
title="Hell/Dunkel"
>
{dark ? <Sun className="h-3.5 w-3.5" /> : <Moon className="h-3.5 w-3.5" />}
</button>
<button
onClick={() => {
const ev = new KeyboardEvent("keydown", { key: "k", metaKey: true })
@@ -89,6 +103,7 @@ export default function App() {
<span>Springen</span>
<kbd className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">K</kbd>
</button>
</div>
</header>
<main className="flex-1 overflow-y-auto p-6">
+5
View File
@@ -88,6 +88,11 @@ export interface SystemStatus {
disk: { total: number; used: number; percent: number } | null
}
export interface ServicesResp {
services: { name: string; url: string; ok: boolean }[]
links: { engine_ui: string; gateway: string; hermes_webui: string }
}
export interface ConnectTool {
label: string
lang: string
+51 -2
View File
@@ -1,5 +1,7 @@
import { useEffect, useState } from "react"
import { api, type SystemStatus } from "@/lib/api"
import { ExternalLink, Save } from "lucide-react"
import { api, type ServicesResp, type SystemStatus } from "@/lib/api"
import { cn } from "@/lib/utils"
function gb(b: number) {
return (b / 1024 ** 3).toFixed(1)
@@ -22,15 +24,30 @@ function Bar({ label, percent, detail }: { label: string; percent: number; detai
export function SystemView() {
const [s, setS] = useState<SystemStatus | null>(null)
const [svc, setSvc] = useState<ServicesResp | null>(null)
const [error, setError] = useState("")
const [backupMsg, setBackupMsg] = useState("")
useEffect(() => {
const load = () => api<SystemStatus>("/api/system/status").then(setS).catch((e) => setError(String(e)))
const load = () => {
api<SystemStatus>("/api/system/status").then(setS).catch((e) => setError(String(e)))
api<ServicesResp>("/api/system/services").then(setSvc).catch(() => {})
}
load()
const t = setInterval(load, 3000)
return () => clearInterval(t)
}, [])
async function doBackup() {
setBackupMsg("…")
try {
const r = await api<{ ok: boolean; snapshot: string; files: string[] }>("/api/system/backup", { method: "POST" })
setBackupMsg(r.ok ? `Snapshot ${r.snapshot} (${r.files.length} Dateien)` : "Nichts zu sichern")
} catch (e) {
setBackupMsg(`Fehler: ${e}`)
}
}
return (
<div className="space-y-4">
<div>
@@ -75,6 +92,38 @@ export function SystemView() {
{s.temp.gpu != null && <span>GPU {s.temp.gpu} °C</span>}
</div>
)}
{/* Dienste-Health + Observability-Links */}
{svc && (
<div className="rounded-xl border border-border bg-card p-4">
<div className="mb-3 text-sm font-medium">Dienste</div>
<div className="grid gap-2 sm:grid-cols-2">
{svc.services.map((x) => (
<div key={x.name} className="flex items-center gap-2 text-sm">
<span className={cn("h-2 w-2 rounded-full", x.ok ? "bg-emerald-500" : "bg-amber-500")} />
<span>{x.name}</span>
<span className="ml-auto font-mono text-xs text-muted-foreground">{x.url}</span>
</div>
))}
</div>
<div className="mt-3 flex flex-wrap gap-3 text-xs">
<a href={svc.links.engine_ui} target="_blank" rel="noopener" className="flex items-center gap-1 text-primary hover:underline">
<ExternalLink className="h-3 w-3" /> Engine-Logs (llama-swap /ui)
</a>
<a href={svc.links.gateway} target="_blank" rel="noopener" className="flex items-center gap-1 text-primary hover:underline">
<ExternalLink className="h-3 w-3" /> Gateway
</a>
</div>
</div>
)}
{/* Backup */}
<div className="flex items-center gap-3">
<button onClick={doBackup} className="flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm hover:bg-accent">
<Save className="h-3.5 w-3.5 text-primary" /> Backup jetzt
</button>
{backupMsg && <span className="text-xs text-muted-foreground">{backupMsg}</span>}
</div>
</div>
)
}