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:
Hitonabi
2026-09-23 21:39:07 +02:00
co-authored by Claude Opus 5.5
parent 3d2c9549fb
commit 907289d7dc
223 changed files with 8572 additions and 15393 deletions
+173
View File
@@ -0,0 +1,173 @@
// Alle Abfragen und Aktionen der Oberfläche an einem Ort (TanStack Query).
//
// Takte: Steht der Live-Strom, meldet er Änderungen selbst — die Abfragen sind dann nur
// Sicherheitsnetz und laufen fünfmal seltener.
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { api, post } from "./api"
import { melden } from "./meldungen"
import { stromVerbunden } from "./strom"
import type {
AktionsErgebnis, Job, Modell, Nutzung, Radar, Sicherung, Start,
} from "./typen"
const takt = (ms: number) => () => (stromVerbunden() ? ms * 5 : ms)
export const useStart = () =>
useQuery({ queryKey: ["start"], queryFn: () => api<Start>("/api/start"), refetchInterval: takt(20_000) })
export const useModelle = () =>
useQuery({
queryKey: ["models"],
queryFn: () => api<{ models: Modell[]; running: string[] }>("/api/models"),
refetchInterval: takt(30_000),
})
export const useNutzung = () =>
useQuery({ queryKey: ["nutzung"], queryFn: () => api<Nutzung>("/api/modelle/nutzung"), refetchInterval: 600_000 })
export const useRadar = () =>
useQuery({ queryKey: ["radar"], queryFn: () => api<Radar>("/api/radar"), retry: false, refetchInterval: 300_000 })
export const useJobs = () =>
useQuery({
queryKey: ["jobs"],
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
// Läuft etwas, wollen wir den Fortschritt sehen; sonst reicht der Strom.
refetchInterval: (q) =>
q.state.data?.jobs.some((j) => j.state === "running" || j.state === "queued") ? 2000 : takt(30_000)(),
})
export const useSicherungen = () =>
useQuery({
queryKey: ["sicherungen"],
queryFn: () => api<{ available: boolean; backups: Sicherung[] }>("/api/zeitmaschine"),
})
export interface UpdateDetails {
kind: string
installed_build?: number | null
latest_build?: number | null
latest_tag?: string | null
count?: number
packages?: { name: string; current: string; candidate: string }[]
summary?: string
action_needed?: boolean | null
action_text?: string
error?: string
}
export const useUpdateDetails = (art: string | null) =>
useQuery({
queryKey: ["update-details", art],
queryFn: () => api<UpdateDetails>(`/api/maintenance/update-details?kind=${art}`),
enabled: !!art,
staleTime: 600_000,
})
export interface UpdateLauf {
start: string | null
ende: string | null
status: string
fehler: string | null
job: string
}
export const useUpdateVerlauf = () =>
useQuery({ queryKey: ["updates-verlauf"], queryFn: () => api<{ laeufe: UpdateLauf[] }>("/api/updates/verlauf") })
export const useProtokoll = (dienst: string | null) =>
useQuery({
queryKey: ["protokoll", dienst],
queryFn: () => api<{ ok: boolean; text?: string; err?: string }>(
`/api/maintenance/logs?service=${encodeURIComponent(dienst ?? "")}&lines=200`),
enabled: !!dienst,
})
export interface Dienst {
name: string
unit: string
ok: boolean
scope: string
}
export const useDienste = () =>
useQuery({ queryKey: ["dienste"], queryFn: () => api<{ services: Dienst[] }>("/api/system/services") })
// --- Aktionen ----------------------------------------------------------------
function useAktion<V, E extends AktionsErgebnis | Record<string, unknown>>(
fn: (v: V) => Promise<E>,
opt: { erfolg: (v: V, e: E) => string; neuLaden: string[] },
) {
const qc = useQueryClient()
return useMutation({
mutationFn: fn,
onSuccess: (e, v) => {
const ergebnis = e as AktionsErgebnis
if (ergebnis && ergebnis.ok === false) {
melden("fehler", ergebnis.detail || ergebnis.err || "Das hat nicht geklappt.")
} else {
melden("erfolg", opt.erfolg(v, e))
}
for (const k of opt.neuLaden) qc.invalidateQueries({ queryKey: [k] })
},
onError: (f: Error) => melden("fehler", f.message),
})
}
export const useHinweisAktion = () =>
useAktion(
({ hinweis, aktion }: { hinweis: string; aktion: string; label: string }) =>
post<AktionsErgebnis>(`/api/hinweise/${encodeURIComponent(hinweis)}/aktion/${aktion}`),
{ erfolg: (v) => `${v.label}: erledigt. Der Wächter prüft in einer Minute nach.`, neuLaden: ["start"] },
)
const UPDATE_PFADE: Record<string, string> = {
alle: "/api/maintenance/update-all",
pruefen: "/api/maintenance/check-updates",
os: "/api/maintenance/os-update",
engine: "/api/maintenance/engine-update",
swap: "/api/maintenance/swap-update",
hermes: "/api/maintenance/hermes-update",
}
export const useUpdateStarten = () =>
useAktion((art: string) => post<AktionsErgebnis>(UPDATE_PFADE[art]), {
erfolg: (art) => (art === "pruefen" ? "Suche nach Neuem läuft." : "Update läuft. Den Fortschritt siehst du unten."),
neuLaden: ["jobs", "start"],
})
export const useJetztSichern = () =>
useAktion(() => post<AktionsErgebnis>("/api/system/backup"), {
erfolg: () => "Sicherung erstellt.",
neuLaden: ["sicherungen", "start"],
})
export const useZurueckspielen = () =>
useAktion((datei: string) => post<AktionsErgebnis>("/api/zeitmaschine/restore", { file: datei }), {
erfolg: () => "Zurückspielen läuft. Die Dienste starten gleich neu.",
neuLaden: ["sicherungen"],
})
export const useModellLaden = () =>
useAktion((name: string) => post<AktionsErgebnis>(`/api/models/${encodeURIComponent(name)}/load`), {
erfolg: (name) => `${name} wird geladen.`,
neuLaden: ["models", "start"],
})
export const useRadarAktion = () =>
useAktion(
({ id, aktion }: { id: string; aktion: "uebernehmen" | "verwerfen"; name: string }) =>
post<AktionsErgebnis>(`/api/radar/${encodeURIComponent(id)}/${aktion}`),
{
erfolg: (v) => (v.aktion === "uebernehmen" ? `${v.name} übernommen.` : `${v.name} verworfen.`),
neuLaden: ["radar", "models", "start"],
},
)
export const useRadarSuche = () =>
useAktion(() => post<AktionsErgebnis>("/api/radar/suche"), {
erfolg: () => "Radar sucht nach neuen Modellen.",
neuLaden: ["radar"],
})