e84386b03e
- components/system/rows.tsx: SERVICES + UpdateRow + ServiceRow + formatBytes - components/system/SettingsTab.tsx: Einstellungen-Tab inkl. eigenem State (localStorage) - components/system/UpdateDetailModal.tsx: Update-Detail-Fenster (os/engine/swap/hermes) - Preview-verifiziert gegen die Live-Box: alle 3 Tabs rendern, Speichern-Flow (Dialog + localStorage) und Logs-Tab (Service-Select) funktionieren Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
128 lines
7.2 KiB
TypeScript
128 lines
7.2 KiB
TypeScript
import { ArrowRight, Bot, ExternalLink, GitCommit, Package, RefreshCw, Server, Shield, Shuffle, X } from "lucide-react"
|
|
import { cn } from "@/lib/utils"
|
|
import type { Job, UpdateDetails } from "@/lib/api"
|
|
|
|
export type UpdateDetailState = { kind: "os" | "engine" | "swap" | "hermes"; loading: boolean; data: UpdateDetails | null }
|
|
|
|
// Update-Detail-Fenster des SystemDrawers (Review P2-14: extrahiert): zeigt VOR dem
|
|
// Anwenden, was genau aktualisiert wird (apt-Pakete, Engine-Builds, Hermes-Commits).
|
|
export function UpdateDetailModal({ detail, maintenanceJob, onClose, onApply }: {
|
|
detail: UpdateDetailState
|
|
maintenanceJob?: Job
|
|
onClose: () => void
|
|
onApply: () => void
|
|
}) {
|
|
const d = detail.data
|
|
const meta = {
|
|
os: { icon: Shield, cls: "text-cyan-400", title: "OS-Pakete (apt)" },
|
|
engine: { icon: Server, cls: "text-violet-400", title: "Inferenz-Engine (llama.cpp)" },
|
|
swap: { icon: Shuffle, cls: "text-fuchsia-400", title: "Router (llama-swap)" },
|
|
hermes: { icon: Bot, cls: "text-amber-400", title: "Hermes-Agent" },
|
|
}[detail.kind]
|
|
const Icon = meta.icon
|
|
const nothing = !d ? true
|
|
: detail.kind === "os" ? (d.count ?? 0) === 0
|
|
: detail.kind === "hermes" ? (d.behind ?? 0) === 0
|
|
: (d.installed_build != null && d.latest_build != null && d.latest_build <= d.installed_build)
|
|
return (
|
|
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4">
|
|
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
|
|
<div className="relative w-full max-w-lg max-h-[80vh] flex flex-col rounded-2xl border border-border/60 bg-card shadow-2xl font-sans text-foreground">
|
|
{/* Header */}
|
|
<div className="flex h-14 shrink-0 items-center justify-between border-b border-border/40 px-5">
|
|
<div className="flex items-center gap-2">
|
|
<Icon className={cn("h-4.5 w-4.5", meta.cls)} />
|
|
<h3 className="text-sm font-semibold">{meta.title}</h3>
|
|
</div>
|
|
<button onClick={onClose} className="flex h-7 w-7 items-center justify-center rounded-lg border border-border/40 text-muted-foreground hover:bg-accent transition-colors">
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="flex-1 overflow-y-auto p-5 text-xs space-y-3 scrollbar-thin">
|
|
{detail.loading ? (
|
|
<div className="flex h-24 items-center justify-center gap-2 text-muted-foreground">
|
|
<RefreshCw className="h-4 w-4 animate-spin" /> Details werden geladen…
|
|
</div>
|
|
) : d?.error ? (
|
|
<div className="rounded-lg border border-red-500/30 bg-red-500/5 p-3 text-red-400">{d.error}</div>
|
|
) : detail.kind === "os" ? (
|
|
(d?.count ?? 0) === 0 ? (
|
|
<div className="text-muted-foreground">Keine Pakete zu aktualisieren — System ist aktuell.</div>
|
|
) : (
|
|
<>
|
|
<div className="text-muted-foreground">{d!.count} Paket(e) werden aktualisiert:</div>
|
|
<div className="space-y-1">
|
|
{d!.packages!.map((p) => (
|
|
<div key={p.name} className="flex items-center justify-between gap-2 rounded-md border border-border/40 bg-background/30 px-2.5 py-1.5">
|
|
<span className="flex items-center gap-1.5 font-mono text-[11px] truncate">
|
|
<Package className="h-3 w-3 text-cyan-400 shrink-0" />{p.name}
|
|
</span>
|
|
<span className="flex items-center gap-1 font-mono text-[10px] text-muted-foreground shrink-0">
|
|
<span>{p.current}</span><ArrowRight className="h-3 w-3" /><span className="text-emerald-400">{p.candidate}</span>
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)
|
|
) : detail.kind === "engine" || detail.kind === "swap" ? (
|
|
<>
|
|
<div className="flex items-center gap-2 font-mono text-[11px]">
|
|
<span className="rounded-md border border-border/40 bg-background/30 px-2 py-1">Build {d?.installed_build ?? "?"}</span>
|
|
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground" />
|
|
<span className="rounded-md border border-emerald-500/30 bg-emerald-500/5 px-2 py-1 text-emerald-400">Build {d?.latest_build ?? "?"}</span>
|
|
</div>
|
|
{(d?.name || d?.latest_tag) && (
|
|
<div className="text-muted-foreground">Release: <span className="text-foreground">{d?.name}</span>{d?.latest_tag ? ` (${d.latest_tag})` : ""}</div>
|
|
)}
|
|
{d?.url && (
|
|
<a href={d.url} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-primary hover:underline">
|
|
Release-Notes auf GitHub <ExternalLink className="h-3 w-3" />
|
|
</a>
|
|
)}
|
|
{d?.body && (
|
|
<pre className="whitespace-pre-wrap rounded-lg border border-border/40 bg-background/30 p-3 text-[10px] leading-relaxed text-muted-foreground max-h-60 overflow-y-auto scrollbar-thin">{d.body}</pre>
|
|
)}
|
|
</>
|
|
) : (
|
|
// hermes
|
|
(d?.commits?.length ?? 0) === 0 ? (
|
|
<div className="text-muted-foreground">Keine neuen Commits — Hermes-Agent ist bereits aktuell.</div>
|
|
) : (
|
|
<>
|
|
<div className="text-muted-foreground">{d!.behind} neue Commit(s) auf <span className="font-mono text-foreground">origin/{d!.branch}</span>:</div>
|
|
<div className="space-y-1">
|
|
{d!.commits!.map((c) => (
|
|
<div key={c.hash} className="flex items-start gap-2 rounded-md border border-border/40 bg-background/30 px-2.5 py-1.5">
|
|
<GitCommit className="h-3.5 w-3.5 text-primary shrink-0 mt-0.5" />
|
|
<div className="min-w-0">
|
|
<div className="text-[11px] truncate">{c.subject}</div>
|
|
<div className="font-mono text-[9px] text-muted-foreground">{c.hash} · {c.when}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground leading-normal">Vor dem Update wird automatisch ein Backup erstellt; danach startet der Hermes-Gateway neu.</p>
|
|
</>
|
|
)
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex gap-3 border-t border-border/40 p-4 shrink-0">
|
|
<button onClick={onClose} className="h-9 flex-1 rounded-lg border border-border/60 bg-background/20 text-xs font-semibold text-muted-foreground hover:bg-accent transition-colors cursor-pointer">
|
|
Schließen
|
|
</button>
|
|
<button onClick={onApply} disabled={detail.loading || nothing || !!maintenanceJob}
|
|
title={maintenanceJob ? `Update läuft bereits: ${maintenanceJob.label}` : undefined}
|
|
className="h-9 flex-1 rounded-lg bg-primary text-xs font-semibold text-primary-foreground hover:opacity-90 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default">
|
|
{maintenanceJob ? "Update läuft…" : "Jetzt aktualisieren"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|