umbau(boxwart): neue Oberflaeche im Cockpit-Stil (Richtung A), Werkzeuge angehoben
Drei Seiten statt zehn: Start, Updates, Modelle — am PC mit Kopfnavigation, am Handy mit Leiste unten. Umsetzung von Mockup A („Cockpit“), vom User am 23.09. gewaehlt. - Start: Hauptwarnleuchte + 8 Warnlampen, Rundinstrumente mit Live-Werten aus dem Strom (Speicher, Temperatur, Platte) und Laufzeit-Zaehlwerk, Checkliste der Waechter-Hinweise mit ihren Knoepfen, Flugplan (heute gelaufen / geplant), Radar-Kasten. - Updates: Bausteine mit „Laeuft → Neu“ und Zusammenfassung, laufende Auftraege, Verlauf der Sonntagslaeufe (neu: GET /api/updates/verlauf), Sicherungen samt Zurueckspielen mit Rueckfrage. - Modelle: Speicherbalken, Rollen Hirn/Coder/Dritte Rolle, wer die Modelle nutzt (7 Tage + 24 h je Stunde), Modell-Radar, weitere Eintraege, Modelle selbst suchen. - Schubladen: Dienste mit Protokoll und Neustart, Einstellungen (HF-Zugang), Hermes-Link. - Werkzeuge: Vite 8, React 19.3 mit React Compiler 1.0 (Babel), vitest 5, Tailwind 4.3, shadcn 4 (Radix) fuer Dialog/Schublade/Knopf, Schriften Barlow/Barlow Condensed/ JetBrains Mono. Entfernt: recharts, cmdk, Kraftgraph, zustand, Inter, Space Grotesk. - Startbuendel 115 KB gzip (Budget 140); Updates/Modelle/Schubladen laden bei Bedarf. - 16 Oberflaechen-Tests (Instrument-Bogen, Hauptleuchte, Zeitformate, Versionen). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3d2c9549fb
commit
907289d7dc
@@ -0,0 +1,225 @@
|
||||
import { useState } from "react"
|
||||
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
|
||||
import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
useJetztSichern, useJobs, useSicherungen, useStart, useUpdateDetails, useUpdateStarten, useUpdateVerlauf,
|
||||
useZurueckspielen,
|
||||
} from "@/lib/abfragen"
|
||||
import { versionen } from "@/lib/anzeige"
|
||||
import type { Job, Sicherung } from "@/lib/typen"
|
||||
import { flugplanZeit } from "@/lib/zeit"
|
||||
|
||||
const BAUSTEINE = [
|
||||
{ id: "os", name: "Betriebssystem" },
|
||||
{ id: "engine", name: "Motor (llama.cpp)" },
|
||||
{ id: "swap", name: "llama-swap" },
|
||||
{ id: "hermes", name: "Hermes" },
|
||||
] as const
|
||||
|
||||
function BausteinZeile({ id, name, hatUpdate, onStart, laeuftGerade }: {
|
||||
id: string
|
||||
name: string
|
||||
hatUpdate: boolean
|
||||
onStart: () => void
|
||||
laeuftGerade: boolean
|
||||
}) {
|
||||
const { data, isPending } = useUpdateDetails(id)
|
||||
const v = versionen(id, data)
|
||||
const neu = hatUpdate ? v.neu ?? "neu" : null
|
||||
const was = !hatUpdate
|
||||
? "Aktuell."
|
||||
: data?.summary?.trim() || (id === "os" && data?.count ? "Paketliste unten in den Details." : isPending ? "Wird gelesen …" : "")
|
||||
return (
|
||||
<tr className="border-t border-linie align-top">
|
||||
<th scope="row" className="py-3.5 pr-4 text-left font-sans text-[15px] font-semibold">{name}</th>
|
||||
<td className="ziffern py-3.5 pr-4 text-sm text-text-2">{v.laeuft}</td>
|
||||
<td className="ziffern py-3.5 pr-4 text-sm">{neu ? <span className="text-cyan">{neu}</span> : <span className="text-text-3">–</span>}</td>
|
||||
<td className="py-3.5 pr-4 text-sm leading-relaxed text-text-2">
|
||||
<span className="line-clamp-3 whitespace-pre-line">{was}</span>
|
||||
</td>
|
||||
<td className="py-2.5 text-right">
|
||||
{hatUpdate && (
|
||||
<Button variant="outline" size="sm" onClick={onStart} disabled={laeuftGerade}>
|
||||
Aktualisieren
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function JobKasten({ job }: { job: Job & { group?: string; log?: string[] } }) {
|
||||
const art = job.state === "done" ? "gruen" : job.state === "failed" ? "rot" : "cyan"
|
||||
const text = job.state === "done" ? "Fertig" : job.state === "failed" ? "Fehler" : job.state === "queued" ? "Wartet" : "Läuft"
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-xl border border-linie bg-erhaben/50 p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-[15px] font-semibold">{job.label}</span>
|
||||
<Chip art={art}>{text}</Chip>
|
||||
</div>
|
||||
{job.log && job.log.length > 0 && (
|
||||
<pre className="ziffern max-h-40 overflow-auto rounded-md bg-[#0b0d0f] p-2 text-xs whitespace-pre-wrap text-text-2">
|
||||
{job.log.slice(-8).join("\n")}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function sicherungsZeit(s: Sicherung) {
|
||||
return s.mtime ? flugplanZeit(new Date(s.mtime * 1000).toISOString()) : s.snapshot
|
||||
}
|
||||
|
||||
export function UpdatesSeite() {
|
||||
const start = useStart()
|
||||
const jobs = useJobs()
|
||||
const verlauf = useUpdateVerlauf()
|
||||
const sicherungen = useSicherungen()
|
||||
const updateStarten = useUpdateStarten()
|
||||
const sichern = useJetztSichern()
|
||||
const zurueck = useZurueckspielen()
|
||||
const [frage, setFrage] = useState<null | { art: "alle" } | { art: "zurueck"; datei: string; zeit: string }>(null)
|
||||
|
||||
const mitUpdate = new Set(start.data?.updates.bausteine.map((b) => b.id) ?? [])
|
||||
const wartungsJobs = (jobs.data?.jobs ?? []).filter((j) => (j as { group?: string }).group === "maintenance")
|
||||
const laeuft = wartungsJobs.some((j) => j.state === "running" || j.state === "queued")
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel
|
||||
titel="Updates"
|
||||
rechts={
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
<Button variant="outline" onClick={() => updateStarten.mutate("pruefen")} disabled={updateStarten.isPending}>
|
||||
Nach Neuem suchen
|
||||
</Button>
|
||||
<Button onClick={() => setFrage({ art: "alle" })} disabled={laeuft || mitUpdate.size === 0}>
|
||||
Alles jetzt aktualisieren
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="-mt-2 text-[15px] text-text-2">
|
||||
Jeden Sonntag um 04:30 automatisch. Vorher wird gesichert, danach geprüft.
|
||||
</p>
|
||||
{start.isPending ? (
|
||||
<Zustandsfeld text="Updates werden gelesen …" />
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[680px] border-collapse">
|
||||
<thead>
|
||||
<tr className="schild text-left text-xs text-text-3">
|
||||
<th scope="col" className="pb-2 font-semibold">Baustein</th>
|
||||
<th scope="col" className="pb-2 font-semibold">Läuft</th>
|
||||
<th scope="col" className="pb-2 font-semibold">Neu</th>
|
||||
<th scope="col" className="pb-2 font-semibold">Was sich ändert</th>
|
||||
<th scope="col" className="pb-2 text-right font-semibold"><span className="sr-only">Aktion</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{BAUSTEINE.map((b) => (
|
||||
<BausteinZeile
|
||||
key={b.id}
|
||||
id={b.id}
|
||||
name={b.name}
|
||||
hatUpdate={mitUpdate.has(b.id)}
|
||||
laeuftGerade={laeuft}
|
||||
onStart={() => updateStarten.mutate(b.id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{wartungsJobs.length > 0 && (
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<h3 className="schild text-sm text-text-3">Laufende und letzte Aufträge</h3>
|
||||
{wartungsJobs.slice(-3).reverse().map((j) => <JobKasten key={j.id} job={j} />)}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
<div className="grid gap-5 md:gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
|
||||
<Panel titel="Verlauf">
|
||||
{verlauf.data?.laeufe.length ? (
|
||||
<ol className="m-0 flex list-none flex-col p-0">
|
||||
{verlauf.data.laeufe.map((l) => {
|
||||
const ok = l.status === "completed"
|
||||
const art = ok ? "gruen" : l.status === "failed" ? "rot" : "cyan"
|
||||
return (
|
||||
<li key={`${l.start}-${l.job}`} className="grid grid-cols-[112px_minmax(0,1fr)_auto] items-start gap-3 border-t border-linie py-3">
|
||||
<span className="ziffern text-sm text-text-3">{l.start ? flugplanZeit(l.start) : "–"}</span>
|
||||
<span className="min-w-0 text-[15px]">
|
||||
{l.job}
|
||||
{l.fehler && <span className="block text-sm text-rot-text">{l.fehler}</span>}
|
||||
</span>
|
||||
<Chip art={art}>{ok ? "Durchgelaufen" : l.status === "failed" ? "Fehlgeschlagen" : "Läuft"}</Chip>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
) : (
|
||||
<p className="text-[15px] text-text-2">
|
||||
{verlauf.isPending ? "Wird gelesen …" : "Noch keine Update-Läufe aufgezeichnet."}
|
||||
</p>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
titel="Sicherungen"
|
||||
rechts={
|
||||
<Button variant="outline" size="sm" onClick={() => sichern.mutate(undefined)} disabled={sichern.isPending}>
|
||||
Jetzt sichern
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p className="-mt-2 text-[15px] text-text-2">Täglich um 03:35, mit Kopie auf einem zweiten Gerät.</p>
|
||||
<ul className="m-0 flex list-none flex-col p-0">
|
||||
{(sicherungen.data?.backups ?? []).slice(0, 5).map((s) => (
|
||||
<li key={s.file} className="flex items-center justify-between gap-3 border-t border-linie py-2.5">
|
||||
<span className="ziffern text-sm">{sicherungsZeit(s)}</span>
|
||||
<span className="flex items-center gap-3">
|
||||
<span className="ziffern text-xs text-text-3">{s.size_mb.toLocaleString("de-DE")} MB</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!sicherungen.data?.available}
|
||||
onClick={() => setFrage({ art: "zurueck", datei: s.file, zeit: sicherungsZeit(s) })}
|
||||
>
|
||||
Zurückspielen
|
||||
</Button>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<Bestaetigen
|
||||
offen={frage?.art === "alle"}
|
||||
titel="Alles jetzt aktualisieren?"
|
||||
text="Betriebssystem, Motor, llama-swap und Hermes werden nacheinander aktualisiert. Vorher wird gesichert, danach geprüft. Lucy ist dabei ein paar Minuten nicht erreichbar."
|
||||
knopf="Aktualisieren"
|
||||
onNein={() => setFrage(null)}
|
||||
onJa={() => {
|
||||
updateStarten.mutate("alle")
|
||||
setFrage(null)
|
||||
}}
|
||||
/>
|
||||
<Bestaetigen
|
||||
offen={frage?.art === "zurueck"}
|
||||
gefahr
|
||||
titel="Sicherung zurückspielen?"
|
||||
text={`Die Einstellungen von Hermes und llama-swap werden auf den Stand von ${frage?.art === "zurueck" ? frage.zeit : ""} gesetzt. Vorher wird der jetzige Stand gesichert. Die Dienste starten danach neu.`}
|
||||
knopf="Zurückspielen"
|
||||
onNein={() => setFrage(null)}
|
||||
onJa={() => {
|
||||
if (frage?.art === "zurueck") zurueck.mutate(frage.datei)
|
||||
setFrage(null)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user