diff --git a/frontend/src/app/Absturz.tsx b/frontend/src/app/Absturz.tsx index f19a3eb..30411fd 100644 --- a/frontend/src/app/Absturz.tsx +++ b/frontend/src/app/Absturz.tsx @@ -4,12 +4,12 @@ import type { ErrorComponentProps } from "@tanstack/react-router" export function Absturz({ error }: ErrorComponentProps) { return (
-

Diese Ansicht ist abgestürzt

+

Diese Ansicht ist abgestürzt

{error instanceof Error ? error.message : String(error)}

diff --git a/frontend/src/app/Shell.tsx b/frontend/src/app/Shell.tsx index 7eccf23..08f7bdb 100644 --- a/frontend/src/app/Shell.tsx +++ b/frontend/src/app/Shell.tsx @@ -44,7 +44,7 @@ function Verbindung() { {live ? "Verbunden" : lage === "verbinde" ? "Verbinde" : "Getrennt"} @@ -79,7 +79,7 @@ export function Shell() { diff --git a/frontend/src/components/cockpit/Checkliste.tsx b/frontend/src/components/cockpit/Checkliste.tsx index 69f779d..8736d31 100644 --- a/frontend/src/components/cockpit/Checkliste.tsx +++ b/frontend/src/components/cockpit/Checkliste.tsx @@ -42,11 +42,12 @@ function Punkt({ h }: { h: Hinweis }) { } return ( -
+
-

{h.titel}

- - +

{h.titel}

+ {rot ? "Jetzt" : "Prüfen"}
@@ -101,7 +102,7 @@ export function Checkliste({ hinweise, verlauf }: { hinweise: Hinweis[]; verlauf ) if (hinweise.length === 0) { return ( -
+

Keine offenen Punkte. Der Wächter prüft jede Minute.

{erledigtHeute.length > 0 && (
    @@ -116,7 +117,7 @@ export function Checkliste({ hinweise, verlauf }: { hinweise: Hinweis[]; verlauf ) } return ( -
    +
    {hinweise.map((h) => ( ))} diff --git a/frontend/src/components/cockpit/Flugplan.tsx b/frontend/src/components/cockpit/Flugplan.tsx index 79decd6..6ec3118 100644 --- a/frontend/src/components/cockpit/Flugplan.tsx +++ b/frontend/src/components/cockpit/Flugplan.tsx @@ -2,10 +2,10 @@ import { cn } from "cn" import type { FlugplanEintrag } from "@/lib/typen" import { flugplanZeit } from "@/lib/zeit" -const STATUS: Record = { - erledigt: { text: "Erledigt", farbe: "text-gruen" }, - fehler: { text: "Fehler", farbe: "text-rot" }, - geplant: { text: "Geplant", farbe: "text-text-3" }, +const STATUS: Record = { + erledigt: { text: "Erledigt", farbe: "text-gruen-text", punkt: "bg-gruen" }, + fehler: { text: "Fehler", farbe: "text-rot-text", punkt: "bg-rot" }, + geplant: { text: "Geplant", farbe: "text-text-3", punkt: "bg-white/25" }, } // Die Liste richtet sich nach der Breite ihres Feldes, nicht des Bildschirms (@container): Schmal — @@ -14,15 +14,18 @@ const STATUS: Record function Zeile({ e }: { e: FlugplanEintrag }) { const s = STATUS[e.status] ?? STATUS.geplant // Updates sind das Wichtigste im Plan — sie stehen in Cyan. - const farbe = e.status === "geplant" && /update/i.test(e.titel) ? "text-cyan" : s.farbe + const farbe = e.status === "geplant" && /update/i.test(e.titel) ? "text-cyan-text" : s.farbe return ( -
  • +
  • {flugplanZeit(e.zeit)} {e.titel} {e.text && , {e.text}} - {s.text} + + + {s.text} +
  • ) } diff --git a/frontend/src/components/cockpit/Instrument.tsx b/frontend/src/components/cockpit/Instrument.tsx index abc602d..1712b7e 100644 --- a/frontend/src/components/cockpit/Instrument.tsx +++ b/frontend/src/components/cockpit/Instrument.tsx @@ -9,7 +9,7 @@ export interface Zone { export function Instrument({ anteil, - farbe = "var(--cyan)", + farbe = "var(--box)", zonen = [], mitte, unter, @@ -28,23 +28,24 @@ export function Instrument({ return (
    - + {zonen.map((z) => ( ))} {zeigen && ( - + )} - + {mitte} {unter && ( - - {unter.toUpperCase()} + + {unter} )} -
    {beschriftung}
    +
    {beschriftung}
    ) } diff --git a/frontend/src/components/cockpit/JobKarte.tsx b/frontend/src/components/cockpit/JobKarte.tsx index 5d7739c..9f93bc4 100644 --- a/frontend/src/components/cockpit/JobKarte.tsx +++ b/frontend/src/components/cockpit/JobKarte.tsx @@ -62,7 +62,7 @@ export function JobKarte({ job, onAbbrechen, onQuittieren, abbrechenGesperrt, eb const Titel = ebene === 3 ? "h3" : "h4" return ( -
    +
    {titel} {zustand.text} diff --git a/frontend/src/components/cockpit/Lagekopf.tsx b/frontend/src/components/cockpit/Lagekopf.tsx new file mode 100644 index 0000000..762638a --- /dev/null +++ b/frontend/src/components/cockpit/Lagekopf.tsx @@ -0,0 +1,97 @@ +import type { ReactNode } from "react" +import { cn } from "cn" +import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol" +import type { Geraetebild, Punktzustand } from "@/lib/geraetebild" +import type { LampenZustand } from "@/lib/typen" + +// Der Kopf beider Cockpits (Design v5, 25.09.2026): links der Statusblock mit Schein in der Farbe der Lage — drücken +// führt dorthin, wo etwas zu tun ist —, rechts die Kacheln der Geräte bzw. Dienste. Zustand immer auch in Worten. + +export type Lageart = "ok" | "info" | "gelb" | "rot" | "stumm" + +const ART: Record = { + ok: { block: "bg-gruen/[0.07] ring-gruen/30", glut: "var(--gruen)", text: "text-gruen" }, + info: { block: "bg-cyan/[0.07] ring-cyan/30", glut: "var(--cyan)", text: "text-cyan-text" }, + gelb: { block: "bg-bernstein/[0.08] ring-bernstein/35", glut: "var(--bernstein)", text: "text-bernstein" }, + rot: { block: "bg-rot/10 ring-rot/40", glut: "var(--rot)", text: "text-rot-text" }, + stumm: { block: "bg-white/[0.04] ring-white/15", glut: "var(--text-3)", text: "text-text-2" }, +} + +export function Lagekopf({ art, oben, mitte, unten, aktiv, onAnsehen, label, children }: { + art: Lageart + oben: string + mitte: string + unten: string + aktiv: boolean + onAnsehen: () => void + /** Name des Abschnitts für Screenreader („Lage im Homelab“). */ + label: string + /** Die Kacheln rechts (eine Liste von
  • ). */ + children: ReactNode +}) { + const a = ART[art] + return ( +
    +
    +
    + +
      {children}
    +
    +
    + ) +} + +const LAMPE_PUNKT: Record = { ok: "ok", aus: "aus", warn: "warn", info: "info", fehler: "rot" } +const WERT: Record = { + ok: "text-text-3", aus: "text-text-3", warn: "text-bernstein", info: "text-cyan-text", fehler: "text-rot-text", +} +const ZUSTAND_TEXT: Record = { + ok: "in Ordnung", aus: "aus", warn: "braucht Aufmerksamkeit", info: "Info", fehler: "Störung", +} + +/** Eine Kachel im Lagekopf: Bild mit Zustandspunkt, Name, darunter der Wert in der Farbe des Zustands. */ +export function Lagekachel({ bild, name, wert, zustand, onClick }: { + bild: Geraetebild + name: string + wert: string + zustand: LampenZustand + onClick?: () => void +}) { + const inhalt = ( + <> + + + {name} + {wert} + {ZUSTAND_TEXT[zustand]} + + + ) + const stil = "flaeche-innen flex h-full w-full min-w-0 items-center gap-3 rounded-xl px-3 py-2.5 text-left" + return ( +
  • + {onClick ? ( + + ) : ( +
    {inhalt}
    + )} +
  • + ) +} diff --git a/frontend/src/components/cockpit/Lampe.tsx b/frontend/src/components/cockpit/Lampe.tsx deleted file mode 100644 index dc541ad..0000000 --- a/frontend/src/components/cockpit/Lampe.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { cn } from "cn" -import type { Lampe as LampenDaten, LampenZustand } from "@/lib/typen" - -// Warnlampen wie im Flugzeug: Grün = läuft, dunkel = aus/auf Abruf, Bernstein = kümmern, -// Cyan = Info, Rot = Störung. Bernstein und Rot leuchten, alles andere bleibt ruhig. -const LAMPEN_STIL: Record = { - ok: { feld: "border-gruen-rand bg-gruen-grund", name: "text-gruen", wert: "text-gruen-text" }, - aus: { feld: "border-aus-rand bg-aus-grund", name: "text-aus-text", wert: "text-text-3" }, - warn: { feld: "border-bernstein bg-bernstein-lampe lampe-atmet", name: "text-bernstein", wert: "text-[#f2c46b]" }, - info: { feld: "border-cyan-rand bg-cyan-grund", name: "text-cyan", wert: "text-cyan-text" }, - fehler: { - feld: "border-rot bg-rot-grund shadow-[0_0_18px_rgba(255,90,79,0.35)]", - name: "text-rot", - wert: "text-rot-text", - }, -} - -const ZUSTAND_TEXT: Record = { - ok: "in Ordnung", - aus: "aus", - warn: "braucht Aufmerksamkeit", - info: "Info", - fehler: "Störung", -} - -export function Lampe({ lampe }: { lampe: LampenDaten }) { - const stil = LAMPEN_STIL[lampe.zustand] ?? LAMPEN_STIL.aus - return ( -
  • - - {lampe.label} - - {lampe.wert} - {ZUSTAND_TEXT[lampe.zustand]} -
  • - ) -} diff --git a/frontend/src/components/cockpit/Protokollfenster.tsx b/frontend/src/components/cockpit/Protokollfenster.tsx index 1a6019f..9282d2d 100644 --- a/frontend/src/components/cockpit/Protokollfenster.tsx +++ b/frontend/src/components/cockpit/Protokollfenster.tsx @@ -11,12 +11,12 @@ export function Protokollfenster({ offen, titel, text, onSchliessen }: { }) { return ( !o && onSchliessen()}> - + - Protokoll + Protokoll {titel} -
    +        
               {text}
             
    diff --git a/frontend/src/components/cockpit/Warnpanel.tsx b/frontend/src/components/cockpit/Warnpanel.tsx index 741d224..b1c2ff2 100644 --- a/frontend/src/components/cockpit/Warnpanel.tsx +++ b/frontend/src/components/cockpit/Warnpanel.tsx @@ -1,39 +1,26 @@ -import { cn } from "cn" +import { Lagekachel, Lagekopf } from "@/components/cockpit/Lagekopf" +import { hauptleuchte } from "@/lib/anzeige" +import { boxbild } from "@/lib/geraetebild" import type { Lampe as LampenDaten, Start } from "@/lib/typen" -import { hauptleuchte, LEUCHTE } from "@/lib/anzeige" -import { Lampe } from "./Lampe" type Zustand = Start["zustand"] +/** + * Der Kopf des KI-Box-Cockpits: links die Hauptleuchte (drücken führt zur Checkliste, wenn etwas offen ist), rechts + * je Dienst eine Kachel — Motor, Hirn, Coder, Hermes, Aufträge, Sicherung, Platte, Updates. + */ export function Warnpanel({ zustand, lampen, onAnsehen }: { zustand: Zustand lampen: LampenDaten[] onAnsehen: () => void }) { const h = hauptleuchte(zustand) - const aktiv = h.art === "gelb" || h.art === "rot" return ( -
    - -
      - {lampen.map((l) => ( - - ))} -
    -
    + + {lampen.map((l) => ( + + ))} + ) } diff --git a/frontend/src/components/cockpit/Zaehlwerk.tsx b/frontend/src/components/cockpit/Zaehlwerk.tsx index 23c9903..d31c840 100644 --- a/frontend/src/components/cockpit/Zaehlwerk.tsx +++ b/frontend/src/components/cockpit/Zaehlwerk.tsx @@ -5,7 +5,7 @@ import { laufzeit } from "@/lib/zeit" // es bei den alten 38 × 56 px je Ziffer. function Ziffer({ z }: { z: string }) { return ( - + {z} ) @@ -27,12 +27,12 @@ export function Zaehlwerk({ sekunden }: { sekunden: number | null | undefined }) > - T + T - H + H
    -
    Ohne Neustart
    +
    Ohne Neustart
    ) } diff --git a/frontend/src/components/cockpit/cockpit.test.tsx b/frontend/src/components/cockpit/cockpit.test.tsx index 86a2fb0..fad5816 100644 --- a/frontend/src/components/cockpit/cockpit.test.tsx +++ b/frontend/src/components/cockpit/cockpit.test.tsx @@ -22,12 +22,12 @@ describe("bogenPfad", () => { describe("hauptleuchte", () => { it("bleibt ruhig, wenn nichts offen ist", () => { - expect(hauptleuchte(ruhig)).toMatchObject({ art: "ok", mitte: "IN ORDNUNG" }) + expect(hauptleuchte(ruhig)).toMatchObject({ art: "ok", mitte: "In Ordnung" }) }) it("zeigt Bernstein bei gelben und Rot bei roten Hinweisen", () => { - expect(hauptleuchte({ ...ruhig, stufe: "gelb", anzahl: 2 })).toMatchObject({ art: "gelb", mitte: "2 HINWEISE" }) - expect(hauptleuchte({ ...ruhig, stufe: "rot", anzahl: 1 })).toMatchObject({ art: "rot", mitte: "1 HINWEIS" }) + expect(hauptleuchte({ ...ruhig, stufe: "gelb", anzahl: 2 })).toMatchObject({ art: "gelb", mitte: "2 Hinweise" }) + expect(hauptleuchte({ ...ruhig, stufe: "rot", anzahl: 1 })).toMatchObject({ art: "rot", mitte: "1 Hinweis" }) }) it("meldet einen schweigenden Wächter vor allem anderen", () => { @@ -50,6 +50,6 @@ describe("Warnpanel", () => { expect(screen.getByText("Motor")).toBeInTheDocument() expect(screen.getByText("b11057")).toBeInTheDocument() expect(screen.getByText("braucht Aufmerksamkeit")).toBeInTheDocument() - expect(screen.getByRole("button", { name: /Achtung 2 HINWEISE/ })).toBeEnabled() + expect(screen.getByRole("button", { name: /Achtung: 2 Hinweise/ })).toBeEnabled() }) }) diff --git a/frontend/src/components/diagramme/Saeulen.tsx b/frontend/src/components/diagramme/Saeulen.tsx index 5e714cf..a3b1440 100644 --- a/frontend/src/components/diagramme/Saeulen.tsx +++ b/frontend/src/components/diagramme/Saeulen.tsx @@ -31,7 +31,7 @@ function Anzeige({ saeule, reihen, summe, className, links }: { links?: number }) { return ( -
    {saeule.lang} {reihen.map((r) => ( @@ -42,7 +42,7 @@ function Anzeige({ saeule, reihen, summe, className, links }: { ))} {reihen.length > 1 && ( - + {zahl.format(summe)} zusammen @@ -208,7 +208,7 @@ export function Saeulen({ saeulen, reihen, titel, summen = "alle", hoehe = 200, {saeulen.map((s, i) => ( - + {s.lang} {sichtbar.map((r) => ( {zahl.format(s.werte[r.name] ?? 0)} diff --git a/frontend/src/components/diagramme/Verlauf.tsx b/frontend/src/components/diagramme/Verlauf.tsx index 62b8bda..b7ae6ef 100644 --- a/frontend/src/components/diagramme/Verlauf.tsx +++ b/frontend/src/components/diagramme/Verlauf.tsx @@ -132,7 +132,7 @@ export function Verlauf({ reihen, einheit, langerZeitraum, hoehe = 180, titel }: {aktiv !== null && (
    breite / 2 ? { right: breite - tipLinks + 10 } : { left: tipLinks + 10 }} > {zeitText(aktiv, true)} @@ -157,7 +157,7 @@ export function Verlauf({ reihen, einheit, langerZeitraum, hoehe = 180, titel }: {letzte.map(([t], i) => ( - + {zeitText(t, langerZeitraum)} {reihen.map((r) => ( diff --git a/frontend/src/components/homelab/Lagebild.tsx b/frontend/src/components/homelab/Lagebild.tsx index 6a9d333..6841fd1 100644 --- a/frontend/src/components/homelab/Lagebild.tsx +++ b/frontend/src/components/homelab/Lagebild.tsx @@ -1,22 +1,7 @@ -import { cn } from "cn" -import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol" -import { geraetebild, type Punktzustand } from "@/lib/geraetebild" +import { Lagekachel, Lagekopf } from "@/components/cockpit/Lagekopf" +import { geraetebild } from "@/lib/geraetebild" import { type Lage, kurzname, lampeFuer } from "@/lib/homelab" -import type { LampenZustand, ZielStand } from "@/lib/typen" - -// Farbe der Lage (Design v5): Tönung und Schein des Statusblocks. Zustand immer auch in Worten (oben/mitte/unten). -const ART: Record = { - ok: { block: "bg-gruen/[0.07] ring-gruen/30", glut: "var(--gruen)", text: "text-gruen" }, - info: { block: "bg-cyan/[0.07] ring-cyan/30", glut: "var(--cyan)", text: "text-cyan-text" }, - gelb: { block: "bg-bernstein/[0.08] ring-bernstein/35", glut: "var(--bernstein)", text: "text-bernstein" }, - rot: { block: "bg-rot/10 ring-rot/40", glut: "var(--rot)", text: "text-rot-text" }, - stumm: { block: "bg-white/[0.04] ring-white/15", glut: "var(--text-3)", text: "text-text-2" }, -} - -const PUNKT: Record = { ok: "ok", aus: "aus", warn: "warn", info: "info", fehler: "rot" } -const WERT: Record = { - ok: "text-text-3", aus: "text-text-3", warn: "text-bernstein", info: "text-cyan-text", fehler: "text-rot-text", -} +import type { ZielStand } from "@/lib/typen" /** * Die Lage im Homelab auf einen Blick: links der Statusblock (drücken führt dorthin, wo etwas zu tun ist), rechts je @@ -30,45 +15,16 @@ export function Lagebild({ lage, ziele, imUpdate, onAnsehen, onGeraet }: { /** Klick auf eine Kachel: Updates, wenn etwas ansteht, sonst die Zeile in der Geräte-Tabelle. */ onGeraet: (ziel: ZielStand, hatUpdate: boolean) => void }) { - const a = ART[lage.art] - const aktiv = lage.ziel !== null return ( -
    -
    -
    - -
      - {ziele.map((z) => { - const l = lampeFuer(z, imUpdate.has(z.id)) - return ( -
    • - -
    • - ) - })} -
    -
    -
    + + {ziele.map((z) => { + const l = lampeFuer(z, imUpdate.has(z.id)) + return ( + onGeraet(z, l.zustand === "info")} /> + ) + })} + ) } diff --git a/frontend/src/components/homelab/Sammellauf.tsx b/frontend/src/components/homelab/Sammellauf.tsx index 7e7c302..f5ba4c9 100644 --- a/frontend/src/components/homelab/Sammellauf.tsx +++ b/frontend/src/components/homelab/Sammellauf.tsx @@ -61,7 +61,7 @@ export function AlleKnopf({ anzahl, gesperrt }: { anzahl: number; gesperrt: bool const SCHRITT: Record = { wartet: { Symbol: CircleDashed, farbe: "text-text-3", text: "wartet" }, - laeuft: { Symbol: LoaderCircle, farbe: "text-cyan", text: "läuft" }, + laeuft: { Symbol: LoaderCircle, farbe: "text-cyan-text", text: "läuft" }, eingespielt: { Symbol: CircleCheck, farbe: "text-gruen", text: "eingespielt" }, zurueckgerollt: { Symbol: Undo2, farbe: "text-bernstein", text: "zurückgerollt" }, fehler: { Symbol: CircleX, farbe: "text-rot", text: "Fehler" }, diff --git a/frontend/src/lib/anzeige.ts b/frontend/src/lib/anzeige.ts index b5dbb38..d525ebf 100644 --- a/frontend/src/lib/anzeige.ts +++ b/frontend/src/lib/anzeige.ts @@ -32,32 +32,24 @@ export function temperaturFarbe(grad: number | null | undefined) { if (grad == null) return "var(--text-3)" if (grad >= 85) return "var(--rot)" if (grad >= 70) return "var(--bernstein)" - return "var(--gruen)" + // Im Normalfall ruhig in der Farbe der KI-Box (Design v5) — Farbe für Zustände erst ab der Grenze. + return "var(--box)" } // --- Hauptwarnleuchte --------------------------------------------------------------- -/** Farben der großen Leuchte (Warnpanel der Startseite, Lage im Homelab). */ -export const LEUCHTE: Record<"ok" | "info" | "stumm" | "gelb" | "rot", string> = { - ok: "border-gruen-rand bg-gruen-grund text-gruen", - info: "border-cyan-rand bg-cyan-grund text-cyan", - stumm: "border-aus-rand bg-aus-grund text-aus-text", - gelb: "border-2 border-bernstein bg-bernstein-grund text-bernstein-hell lampe-atmet", - rot: "border-2 border-rot bg-rot-grund text-rot shadow-[0_0_32px_rgba(255,90,79,0.3)]", -} - /** Was die Hauptwarnleuchte sagt. Der schweigende Wächter geht allem anderen vor. */ export function hauptleuchte(z: Start["zustand"]) { if (!z.waechter_wach) { - return { art: "stumm" as const, oben: "Wächter", mitte: "SCHWEIGT", unten: "Seit über 5 Minuten kein Prüflauf" } + return { art: "stumm" as const, oben: "Wächter", mitte: "Schweigt", unten: "Seit über 5 Minuten kein Prüflauf" } } if (z.update_laeuft) { - return { art: "info" as const, oben: "Wartung", mitte: "UPDATE LÄUFT", unten: "Hinweise ruhen bis zum Ende" } + return { art: "info" as const, oben: "Wartung", mitte: "Update läuft", unten: "Hinweise ruhen bis zum Ende" } } if (z.anzahl === 0) { - return { art: "ok" as const, oben: "Alles", mitte: "IN ORDNUNG", unten: "Keine offenen Hinweise" } + return { art: "ok" as const, oben: "Alles", mitte: "In Ordnung", unten: "Keine offenen Hinweise" } } - const mitte = `${z.anzahl} HINWEIS${z.anzahl === 1 ? "" : "E"}` + const mitte = `${z.anzahl} Hinweis${z.anzahl === 1 ? "" : "e"}` return z.stufe === "rot" ? { art: "rot" as const, oben: "Störung", mitte, unten: "Drücken zum Ansehen" } : { art: "gelb" as const, oben: "Achtung", mitte, unten: "Drücken zum Ansehen" } diff --git a/frontend/src/lib/geraetebild.ts b/frontend/src/lib/geraetebild.ts index 0951f23..c6d48a6 100644 --- a/frontend/src/lib/geraetebild.ts +++ b/frontend/src/lib/geraetebild.ts @@ -1,6 +1,7 @@ import { - Bird, Bot, Box, BrainCircuit, Clapperboard, Code, Container, DatabaseBackup, FileCode2, GitBranch, HardDrive, House, - Layers, type LucideIcon, Monitor, Route, Server, ShieldCheck, Shuffle, Workflow, Zap, + AudioLines, Bird, Bot, Box, BrainCircuit, Clapperboard, Code, Container, DatabaseBackup, FileCode2, GitBranch, HardDrive, + House, Layers, LayoutDashboard, type LucideIcon, Mic, Monitor, RefreshCw, Route, Server, ShieldCheck, Shuffle, Workflow, + Zap, } from "lucide-react" import { LOGOS } from "@/lib/logos-liste" import type { Bereich } from "@/lib/navigation" @@ -81,6 +82,25 @@ const KI_BOX: Record = { jobs: [Workflow], sicherung: [DatabaseBackup], platte: [HardDrive], + updates: [RefreshCw], +} + +// Die Dienste der KI-Box (Seite „Dienste und Protokolle“), nach ihrer systemd-Unit. +const DIENSTE: [RegExp, LucideIcon, string?][] = [ + [/^hermes/, Bot, "hermes-agent"], + [/^llama-swap/, Shuffle], + [/^mc2-gateway/, Route], + [/^mc2-steward/, ShieldCheck], + [/^mission-control/, LayoutDashboard], + [/^voice/, Mic], + [/^lucy-stimme/, AudioLines], +] + +/** Das Bild eines Dienstes der KI-Box. */ +export function dienstbild(unit: string): Geraetebild { + const treffer = DIENSTE.find(([muster]) => muster.test(unit)) + const logo = treffer?.[2] + return { Icon: treffer?.[1] ?? Box, bereich: "box", logo: logo && LOGOS.has(logo) ? logo : undefined } } /** Das Bild eines Bausteins oder einer Lampe der KI-Box. */ diff --git a/frontend/src/lib/zeit.test.ts b/frontend/src/lib/zeit.test.ts index 01f65e6..bb8d626 100644 --- a/frontend/src/lib/zeit.test.ts +++ b/frontend/src/lib/zeit.test.ts @@ -36,7 +36,7 @@ describe("kleine Helfer", () => { }) it("Temperaturfarbe wechselt bei 70 und 85 Grad", () => { - expect(temperaturFarbe(37)).toBe("var(--gruen)") + expect(temperaturFarbe(37)).toBe("var(--box)") // ruhig in der Farbe der KI-Box (Design v5) expect(temperaturFarbe(70)).toBe("var(--bernstein)") expect(temperaturFarbe(85)).toBe("var(--rot)") }) diff --git a/frontend/src/views/Dienste.tsx b/frontend/src/views/Dienste.tsx index c1100a5..2ebd8fd 100644 --- a/frontend/src/views/Dienste.tsx +++ b/frontend/src/views/Dienste.tsx @@ -3,6 +3,7 @@ import { useQueryClient } from "@tanstack/react-query" import { cn } from "cn" import { Bestaetigen } from "@/components/cockpit/Bestaetigen" import { Chip, Zustandsfeld } from "@/components/cockpit/Chip" +import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol" import { Panel } from "@/components/cockpit/Panel" import { Button } from "@/components/ui/button" import { type Dienst, type DiensteStand, useDienste, useProtokoll } from "@/lib/abfragen" @@ -12,9 +13,16 @@ import { dauer, erfolgText, groesse, istProblem, knopfText, neustartFrage, prozent, rang, sortiert, speicherAnteil, ZUSTAND, zeitpunkt, zustandVon, zustandZusatz, } from "@/lib/dienste" +import { dienstbild, type Punktzustand } from "@/lib/geraetebild" import { melden } from "@/lib/meldungen" import type { AktionsErgebnis } from "@/lib/typen" import { gbText } from "@/lib/zeit" +import type { DienstZustand } from "@/lib/abfragen" + +// Zustand eines Dienstes als Punkt am Symbol (Design v5): Wort und Farbe stehen zusätzlich in der Spalte „Zustand“. +const PUNKT_DIENST: Record = { + laeuft: "ok", startet: "warn", stoppt: "warn", antwortet_nicht: "rot", gestoppt: "rot", gescheitert: "rot", schlaeft: "aus", +} // Die Dienste der KI-Box als Tabelle wie die Geräte im Homelab-Cockpit (seit 25.09.2026 eine eigene Seite statt der // Schublade): Zustand, CPU, Speicher, Laufzeit und Neustarts je Dienst, das Protokoll klappt unter der Zeile auf. @@ -42,7 +50,7 @@ function Balken({ anteil, children }: { anteil: number | null; children: ReactNo {children} {anteil !== null && ( - + )} @@ -69,7 +77,7 @@ function Protokoll({ d, id }: { d: Dienst; id: string }) { ref={feld} tabIndex={0} aria-label={`Letzte Protokollzeilen von ${d.name}`} - className="ziffern m-0 max-h-[50vh] w-full overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap break-words text-text-2" + className="ziffern m-0 max-h-[50vh] w-full overflow-auto rounded-lg bg-black/30 ring-1 ring-white/[0.07] ring-inset p-3 text-xs leading-relaxed whitespace-pre-wrap break-words text-text-2" > {text} @@ -90,9 +98,9 @@ function Zeile({ d, gesamt, offen, startet, onProtokoll, onFrage }: { const neustarts = d.neustarts ?? 0 const protokollId = `protokoll-${d.unit}` return ( -
    -
    - +
    +
    + {d.name} @@ -103,7 +111,7 @@ function Zeile({ d, gesamt, offen, startet, onProtokoll, onFrage }: {
    Zustand - {z.wort} + {z.wort} {zusatz && {zusatz}} {/* Ohne Tabelle stehen die Neustarts hier — und nur, wenn es welche gab. */} {neustarts > 0 && ( @@ -187,7 +195,7 @@ function Tabelle({ stand, gewaehlt, startet, onProtokoll, onFrage }: { }) { return (
    -