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
+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>
)
}