design v5: alle Seiten im neuen Look
- Gemeinsamer Lagekopf für beide Cockpits (Statusblock mit Schein, Kacheln mit Logo/Symbol und Zustandspunkt); KI-Box-Lampen als Dienst-Kacheln, Texte in normaler Schreibung. Alte Lampen-Komponente und Leuchtenfarben entfernt. - Instrumente, Zählwerk, Checkliste, Flugplan, Radar-Kasten im neuen Stil; Instrumente ruhig in Indigo, Farbe für Zustände erst ab der Grenze. - Übrige Seiten (Updates, Modelle, Nutzung, Suche, Monitoring, Dienste, Einstellungen, Protokoll, Dialoge): Glasflächen, feine Linien, Etiketten statt Instrumenten-Schild, Glas-Eingabefelder, deckende Dialoge. - Dienste der KI-Box und Geräte im Monitoring mit Symbol bzw. Logo. - Einstellungen: Karten mit Bereichs-Schein; Lizenzhinweis der Logos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
db452aa38a
commit
0d2687436e
@@ -4,12 +4,12 @@ import type { ErrorComponentProps } from "@tanstack/react-router"
|
|||||||
export function Absturz({ error }: ErrorComponentProps) {
|
export function Absturz({ error }: ErrorComponentProps) {
|
||||||
return (
|
return (
|
||||||
<div role="alert" className="flex flex-col items-start gap-3 rounded-2xl border border-rot-rand bg-rot-grund p-6 text-rot-text">
|
<div role="alert" className="flex flex-col items-start gap-3 rounded-2xl border border-rot-rand bg-rot-grund p-6 text-rot-text">
|
||||||
<p className="schild text-lg text-rot">Diese Ansicht ist abgestürzt</p>
|
<p className="text-lg font-semibold text-rot-text">Diese Ansicht ist abgestürzt</p>
|
||||||
<p className="text-[15px]">{error instanceof Error ? error.message : String(error)}</p>
|
<p className="text-[15px]">{error instanceof Error ? error.message : String(error)}</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
className="schild h-11 rounded-lg border border-rot-rand px-5 text-sm text-foreground hover:bg-[#3a1512]"
|
className="h-11 rounded-xl border border-rot/40 bg-rot/10 px-5 text-sm font-semibold text-rot-text hover:bg-rot/[0.18]"
|
||||||
>
|
>
|
||||||
Neu laden
|
Neu laden
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function Verbindung() {
|
|||||||
<span
|
<span
|
||||||
role="status"
|
role="status"
|
||||||
title={live ? "Verbunden" : lage === "verbinde" ? "Verbinde" : "Getrennt"}
|
title={live ? "Verbunden" : lage === "verbinde" ? "Verbinde" : "Getrennt"}
|
||||||
className={cn("schild flex items-center gap-1.5 text-[13px]", live ? "text-text-3" : "text-bernstein")}
|
className={cn("etikett flex items-center gap-1.5 text-[11px] tracking-[0.12em]", live ? "text-text-3" : "text-bernstein")}
|
||||||
>
|
>
|
||||||
<span aria-hidden className={cn("size-2 rounded-full", live ? "bg-gruen shadow-[0_0_8px_var(--gruen)]" : "bg-bernstein")} />
|
<span aria-hidden className={cn("size-2 rounded-full", live ? "bg-gruen shadow-[0_0_8px_var(--gruen)]" : "bg-bernstein")} />
|
||||||
<span className={live ? "sr-only" : undefined}>{live ? "Verbunden" : lage === "verbinde" ? "Verbinde" : "Getrennt"}</span>
|
<span className={live ? "sr-only" : undefined}>{live ? "Verbunden" : lage === "verbinde" ? "Verbinde" : "Getrennt"}</span>
|
||||||
@@ -79,7 +79,7 @@ export function Shell() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMenue(true)}
|
onClick={() => setMenue(true)}
|
||||||
className="-ml-2 flex size-11 shrink-0 items-center justify-center rounded-lg text-text-2 hover:bg-erhaben hover:text-foreground lg:hidden"
|
className="-ml-2 flex size-11 shrink-0 items-center justify-center rounded-lg text-text-2 hover:bg-white/[0.06] hover:text-foreground lg:hidden"
|
||||||
aria-label="Menü öffnen"
|
aria-label="Menü öffnen"
|
||||||
>
|
>
|
||||||
<Menu className="size-6" />
|
<Menu className="size-6" />
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function Auftraege({ alsPanel = false }: { alsPanel?: boolean }) {
|
|||||||
if (alsPanel) return <Panel titel="Aufträge">{inhalt}</Panel>
|
if (alsPanel) return <Panel titel="Aufträge">{inhalt}</Panel>
|
||||||
return (
|
return (
|
||||||
<section aria-label="Aufträge" className="flex flex-col gap-2.5">
|
<section aria-label="Aufträge" className="flex flex-col gap-2.5">
|
||||||
<h3 className="schild m-0 text-sm text-text-3">Aufträge</h3>
|
<h3 className="etikett m-0 text-[11.5px] tracking-[0.16em] text-text-3">Aufträge</h3>
|
||||||
{inhalt}
|
{inhalt}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ export function Bestaetigen({ offen, titel, text, knopf, neinKnopf = "Abbrechen"
|
|||||||
{/* Nie höher als der Bildschirm: Auf kleinen Handys (320 × 568) lagen die Knöpfe sonst außerhalb
|
{/* Nie höher als der Bildschirm: Auf kleinen Handys (320 × 568) lagen die Knöpfe sonst außerhalb
|
||||||
und ließen sich nicht erreichen. Der Text scrollt, die Knöpfe bleiben unten stehen — bündig,
|
und ließen sich nicht erreichen. Der Text scrollt, die Knöpfe bleiben unten stehen — bündig,
|
||||||
darum ohne Bodenpolster (sticky rechnet das Polster ein, darunter schien sonst Text durch). */}
|
darum ohne Bodenpolster (sticky rechnet das Polster ein, darunter schien sonst Text durch). */}
|
||||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto border-linie bg-panel pb-0 sm:max-w-lg">
|
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto border-white/10 bg-[#0e121a] pb-0 sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="schild pr-10 text-xl break-words">{titel}</DialogTitle>
|
<DialogTitle className="pr-10 text-[19px] leading-snug font-semibold break-words">{titel}</DialogTitle>
|
||||||
<DialogDescription className="text-[15px] leading-relaxed text-text-2">{text}</DialogDescription>
|
<DialogDescription className="text-[15px] leading-relaxed text-text-2">{text}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{children}
|
{children}
|
||||||
<DialogFooter className="sticky bottom-0 mb-0 gap-2 bg-erhaben sm:gap-2">
|
<DialogFooter className="sticky bottom-0 mb-0 gap-2 bg-[#121722] sm:gap-2">
|
||||||
<Button variant="outline" onClick={onNein}>
|
<Button variant="outline" onClick={onNein}>
|
||||||
{neinKnopf}
|
{neinKnopf}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -42,11 +42,12 @@ function Punkt({ h }: { h: Hinweis }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="flex flex-col gap-2.5 border-t border-linie pt-4">
|
<article className={cn("flex flex-col gap-2.5 rounded-2xl px-4 py-3.5 ring-1 ring-inset",
|
||||||
|
rot ? "bg-rot/[0.07] ring-rot/35" : "bg-bernstein/[0.06] ring-bernstein/30")}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="min-w-0 font-sans text-[17px] leading-snug font-semibold">{h.titel}</h3>
|
<h3 className="min-w-0 flex-1 font-sans text-[16.5px] leading-snug font-semibold">{h.titel}</h3>
|
||||||
<span aria-hidden className="hidden h-0 flex-grow border-b-2 border-dotted border-linie-stark sm:block" />
|
<span className={cn("etikett shrink-0 rounded-full px-2.5 py-0.5 text-[10.5px] tracking-[0.12em] ring-1 ring-inset",
|
||||||
<span className={cn("schild shrink-0 text-base", rot ? "text-rot" : "text-bernstein")}>
|
rot ? "bg-rot/12 text-rot-text ring-rot/40" : "bg-bernstein/10 text-bernstein ring-bernstein/35")}>
|
||||||
{rot ? "Jetzt" : "Prüfen"}
|
{rot ? "Jetzt" : "Prüfen"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +102,7 @@ export function Checkliste({ hinweise, verlauf }: { hinweise: Hinweis[]; verlauf
|
|||||||
)
|
)
|
||||||
if (hinweise.length === 0) {
|
if (hinweise.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 border-t border-linie pt-4">
|
<div className="flex flex-col gap-2">
|
||||||
<p className="text-[15px] text-text-2">Keine offenen Punkte. Der Wächter prüft jede Minute.</p>
|
<p className="text-[15px] text-text-2">Keine offenen Punkte. Der Wächter prüft jede Minute.</p>
|
||||||
{erledigtHeute.length > 0 && (
|
{erledigtHeute.length > 0 && (
|
||||||
<ul className="flex flex-col gap-1 text-sm text-text-3">
|
<ul className="flex flex-col gap-1 text-sm text-text-3">
|
||||||
@@ -116,7 +117,7 @@ export function Checkliste({ hinweise, verlauf }: { hinweise: Hinweis[]; verlauf
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-3">
|
||||||
{hinweise.map((h) => (
|
{hinweise.map((h) => (
|
||||||
<Punkt key={h.id} h={h} />
|
<Punkt key={h.id} h={h} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { cn } from "cn"
|
|||||||
import type { FlugplanEintrag } from "@/lib/typen"
|
import type { FlugplanEintrag } from "@/lib/typen"
|
||||||
import { flugplanZeit } from "@/lib/zeit"
|
import { flugplanZeit } from "@/lib/zeit"
|
||||||
|
|
||||||
const STATUS: Record<FlugplanEintrag["status"], { text: string; farbe: string }> = {
|
const STATUS: Record<FlugplanEintrag["status"], { text: string; farbe: string; punkt: string }> = {
|
||||||
erledigt: { text: "Erledigt", farbe: "text-gruen" },
|
erledigt: { text: "Erledigt", farbe: "text-gruen-text", punkt: "bg-gruen" },
|
||||||
fehler: { text: "Fehler", farbe: "text-rot" },
|
fehler: { text: "Fehler", farbe: "text-rot-text", punkt: "bg-rot" },
|
||||||
geplant: { text: "Geplant", farbe: "text-text-3" },
|
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 —
|
// Die Liste richtet sich nach der Breite ihres Feldes, nicht des Bildschirms (@container): Schmal —
|
||||||
@@ -14,15 +14,18 @@ const STATUS: Record<FlugplanEintrag["status"], { text: string; farbe: string }>
|
|||||||
function Zeile({ e }: { e: FlugplanEintrag }) {
|
function Zeile({ e }: { e: FlugplanEintrag }) {
|
||||||
const s = STATUS[e.status] ?? STATUS.geplant
|
const s = STATUS[e.status] ?? STATUS.geplant
|
||||||
// Updates sind das Wichtigste im Plan — sie stehen in Cyan.
|
// 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 (
|
return (
|
||||||
<li className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 border-t border-linie py-2.5 @sm:grid-cols-[112px_minmax(0,1fr)_auto]">
|
<li className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 border-t border-white/[0.06] py-2.5 first:border-t-0 @sm:grid-cols-[112px_minmax(0,1fr)_auto]">
|
||||||
<span className="ziffern col-start-1 row-start-1 text-sm text-text-3">{flugplanZeit(e.zeit)}</span>
|
<span className="ziffern col-start-1 row-start-1 text-sm text-text-3">{flugplanZeit(e.zeit)}</span>
|
||||||
<span className="col-span-2 row-start-2 min-w-0 text-base break-words @sm:col-span-1 @sm:col-start-2 @sm:row-start-1">
|
<span className="col-span-2 row-start-2 min-w-0 text-base break-words @sm:col-span-1 @sm:col-start-2 @sm:row-start-1">
|
||||||
{e.titel}
|
{e.titel}
|
||||||
{e.text && <span className="text-text-3">, {e.text}</span>}
|
{e.text && <span className="text-text-3">, {e.text}</span>}
|
||||||
</span>
|
</span>
|
||||||
<span className={cn("schild col-start-2 row-start-1 justify-self-end text-[13px] @sm:col-start-3", farbe)}>{s.text}</span>
|
<span className={cn("etikett col-start-2 row-start-1 flex items-center gap-1.5 justify-self-end text-[10.5px] tracking-[0.12em] @sm:col-start-3", farbe)}>
|
||||||
|
<span aria-hidden className={cn("size-1.5 rounded-full", s.punkt)} />
|
||||||
|
{s.text}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface Zone {
|
|||||||
|
|
||||||
export function Instrument({
|
export function Instrument({
|
||||||
anteil,
|
anteil,
|
||||||
farbe = "var(--cyan)",
|
farbe = "var(--box)",
|
||||||
zonen = [],
|
zonen = [],
|
||||||
mitte,
|
mitte,
|
||||||
unter,
|
unter,
|
||||||
@@ -28,23 +28,24 @@ export function Instrument({
|
|||||||
return (
|
return (
|
||||||
<figure className="m-0 flex min-w-0 flex-col items-center gap-1">
|
<figure className="m-0 flex min-w-0 flex-col items-center gap-1">
|
||||||
<svg viewBox="0 0 200 150" className="h-auto w-full max-w-[200px]" role="img" aria-label={ariaText}>
|
<svg viewBox="0 0 200 150" className="h-auto w-full max-w-[200px]" role="img" aria-label={ariaText}>
|
||||||
<path d={bogenPfad(0, 1, 75)} fill="none" stroke="var(--linie)" strokeWidth={12} strokeLinecap="round" />
|
<path d={bogenPfad(0, 1, 75)} fill="none" stroke="rgb(255 255 255 / 0.07)" strokeWidth={12} strokeLinecap="round" />
|
||||||
{zonen.map((z) => (
|
{zonen.map((z) => (
|
||||||
<path key={`${z.von}-${z.bis}`} d={bogenPfad(z.von, z.bis, 86)} fill="none" stroke={z.farbe} strokeWidth={4} strokeLinecap="round" />
|
<path key={`${z.von}-${z.bis}`} d={bogenPfad(z.von, z.bis, 86)} fill="none" stroke={z.farbe} strokeWidth={4} strokeLinecap="round" />
|
||||||
))}
|
))}
|
||||||
{zeigen && (
|
{zeigen && (
|
||||||
<path d={bogenPfad(0, anteil, 75)} fill="none" stroke={farbe} strokeWidth={12} strokeLinecap="round" />
|
<path d={bogenPfad(0, anteil, 75)} fill="none" stroke={farbe} strokeWidth={12} strokeLinecap="round"
|
||||||
|
style={{ filter: `drop-shadow(0 0 7px color-mix(in srgb, ${farbe} 60%, transparent))` }} />
|
||||||
)}
|
)}
|
||||||
<text x="100" y="100" textAnchor="middle" className="ziffern" style={{ fontSize: 34, fontWeight: 500, fill: "var(--foreground)" }}>
|
<text x="100" y="102" textAnchor="middle" style={{ fontFamily: "var(--font-anzeige)", fontSize: 40, fontWeight: 600, fill: "var(--foreground)" }}>
|
||||||
{mitte}
|
{mitte}
|
||||||
</text>
|
</text>
|
||||||
{unter && (
|
{unter && (
|
||||||
<text x="100" y="122" textAnchor="middle" style={{ fontFamily: "var(--font-anzeige)", fontSize: 14, fontWeight: 600, letterSpacing: "0.12em", fill: "var(--text-3)" }}>
|
<text x="100" y="124" textAnchor="middle" style={{ fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 500, fill: "var(--text-3)" }}>
|
||||||
{unter.toUpperCase()}
|
{unter}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
<figcaption className="schild text-[15px] text-text-3">{beschriftung}</figcaption>
|
<figcaption className="etikett text-[11.5px] tracking-[0.14em] text-text-3">{beschriftung}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function JobKarte({ job, onAbbrechen, onQuittieren, abbrechenGesperrt, eb
|
|||||||
const Titel = ebene === 3 ? "h3" : "h4"
|
const Titel = ebene === 3 ? "h3" : "h4"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article aria-label={`Auftrag: ${titel}`} className="flex flex-col gap-2.5 rounded-xl border border-linie bg-erhaben/50 p-3.5">
|
<article aria-label={`Auftrag: ${titel}`} className="flex flex-col gap-2.5 flaeche-innen rounded-2xl p-3.5">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<Titel className="m-0 min-w-0 text-[15px] leading-snug font-semibold break-words">{titel}</Titel>
|
<Titel className="m-0 min-w-0 text-[15px] leading-snug font-semibold break-words">{titel}</Titel>
|
||||||
<Chip art={zustand.art}>{zustand.text}</Chip>
|
<Chip art={zustand.art}>{zustand.text}</Chip>
|
||||||
|
|||||||
@@ -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<Lageart, { block: string; glut: string; text: string }> = {
|
||||||
|
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 <li>). */
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
const a = ART[art]
|
||||||
|
return (
|
||||||
|
<section aria-label={label} className="auftauchen flaeche relative overflow-hidden rounded-3xl">
|
||||||
|
<div aria-hidden className="glut-atmet pointer-events-none absolute -top-40 -left-32 size-[460px] rounded-full"
|
||||||
|
style={{ background: `radial-gradient(closest-side, color-mix(in srgb, ${a.glut} 20%, transparent), transparent)` }} />
|
||||||
|
<div className="relative grid gap-4 p-4 sm:p-5 lg:grid-cols-[280px_minmax(0,1fr)] lg:gap-5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onAnsehen}
|
||||||
|
disabled={!aktiv}
|
||||||
|
aria-label={`${oben}: ${mitte}`}
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[132px] flex-col items-start justify-center gap-1.5 rounded-2xl px-5 py-4 text-left ring-1 ring-inset transition-[filter]",
|
||||||
|
a.block, aktiv ? "cursor-pointer hover:brightness-125" : "cursor-default",
|
||||||
|
art === "gelb" && "lampe-atmet",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="etikett text-[11px] tracking-[0.18em] text-text-3">{oben}</span>
|
||||||
|
<span className={cn("text-[30px] leading-tight font-semibold tracking-[-0.01em] sm:text-[34px]", a.text)}>{mitte}</span>
|
||||||
|
<span className="text-[13.5px] text-text-2">{unten}</span>
|
||||||
|
</button>
|
||||||
|
<ul className="m-0 grid list-none grid-cols-1 gap-2 p-0 min-[480px]:grid-cols-2 xl:grid-cols-4">{children}</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAMPE_PUNKT: Record<LampenZustand, Punktzustand> = { ok: "ok", aus: "aus", warn: "warn", info: "info", fehler: "rot" }
|
||||||
|
const WERT: Record<LampenZustand, string> = {
|
||||||
|
ok: "text-text-3", aus: "text-text-3", warn: "text-bernstein", info: "text-cyan-text", fehler: "text-rot-text",
|
||||||
|
}
|
||||||
|
const ZUSTAND_TEXT: Record<LampenZustand, string> = {
|
||||||
|
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 = (
|
||||||
|
<>
|
||||||
|
<Geraetesymbol bild={bild} punkt={LAMPE_PUNKT[zustand]} />
|
||||||
|
<span className="flex min-w-0 flex-col gap-0.5">
|
||||||
|
<span className="truncate text-[14.5px] leading-snug font-medium">{name}</span>
|
||||||
|
<span className={cn("truncate text-[12.5px] leading-snug", WERT[zustand])}>{wert}</span>
|
||||||
|
<span className="sr-only">{ZUSTAND_TEXT[zustand]}</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
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 (
|
||||||
|
<li>
|
||||||
|
{onClick ? (
|
||||||
|
<button type="button" onClick={onClick} className={cn(stil, "transition-colors hover:border-white/20 hover:bg-white/[0.05]")}>
|
||||||
|
{inhalt}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className={stil}>{inhalt}</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<LampenZustand, { feld: string; name: string; wert: string }> = {
|
|
||||||
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<LampenZustand, string> = {
|
|
||||||
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 (
|
|
||||||
<li
|
|
||||||
className={cn(
|
|
||||||
"flex min-h-[58px] flex-col items-center justify-center gap-0.5 rounded-lg border px-2 py-2 text-center",
|
|
||||||
stil.feld,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className={cn("schild text-[17px] leading-none font-bold tracking-[0.18em]", stil.name)}>
|
|
||||||
{lampe.label}
|
|
||||||
</span>
|
|
||||||
<span className={cn("ziffern text-xs uppercase", stil.wert)}>{lampe.wert}</span>
|
|
||||||
<span className="sr-only">{ZUSTAND_TEXT[lampe.zustand]}</span>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -11,12 +11,12 @@ export function Protokollfenster({ offen, titel, text, onSchliessen }: {
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Sheet open={offen} onOpenChange={(o) => !o && onSchliessen()}>
|
<Sheet open={offen} onOpenChange={(o) => !o && onSchliessen()}>
|
||||||
<SheetContent side="right" className="border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl">
|
<SheetContent side="right" className="border-white/10 bg-[#0e121a] data-[side=right]:w-full data-[side=right]:sm:max-w-2xl">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle className="schild text-lg">Protokoll</SheetTitle>
|
<SheetTitle className="text-[19px] font-semibold">Protokoll</SheetTitle>
|
||||||
<SheetDescription className="text-text-2">{titel}</SheetDescription>
|
<SheetDescription className="text-text-2">{titel}</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<pre className="ziffern mx-4 mb-4 flex-1 overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2">
|
<pre className="ziffern mx-4 mb-4 flex-1 overflow-auto rounded-lg bg-black/30 ring-1 ring-white/[0.07] ring-inset p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2">
|
||||||
{text}
|
{text}
|
||||||
</pre>
|
</pre>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@@ -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 type { Lampe as LampenDaten, Start } from "@/lib/typen"
|
||||||
import { hauptleuchte, LEUCHTE } from "@/lib/anzeige"
|
|
||||||
import { Lampe } from "./Lampe"
|
|
||||||
|
|
||||||
type Zustand = Start["zustand"]
|
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 }: {
|
export function Warnpanel({ zustand, lampen, onAnsehen }: {
|
||||||
zustand: Zustand
|
zustand: Zustand
|
||||||
lampen: LampenDaten[]
|
lampen: LampenDaten[]
|
||||||
onAnsehen: () => void
|
onAnsehen: () => void
|
||||||
}) {
|
}) {
|
||||||
const h = hauptleuchte(zustand)
|
const h = hauptleuchte(zustand)
|
||||||
const aktiv = h.art === "gelb" || h.art === "rot"
|
|
||||||
return (
|
return (
|
||||||
<section aria-label="Warnpanel" className="grid gap-4 md:grid-cols-[260px_minmax(0,1fr)] md:gap-5">
|
<Lagekopf art={h.art} oben={h.oben} mitte={h.mitte} unten={h.unten} aktiv={h.art === "gelb" || h.art === "rot"}
|
||||||
<button
|
onAnsehen={onAnsehen} label="Warnpanel">
|
||||||
type="button"
|
{lampen.map((l) => (
|
||||||
onClick={onAnsehen}
|
<Lagekachel key={l.id} bild={boxbild(l.id)} name={l.label} wert={l.wert} zustand={l.zustand} />
|
||||||
disabled={!aktiv}
|
))}
|
||||||
aria-label={`${h.oben} ${h.mitte}`}
|
</Lagekopf>
|
||||||
className={cn(
|
|
||||||
"flex min-h-[130px] flex-col items-center justify-center gap-1 rounded-2xl border px-4 py-4 font-anzeige transition-colors",
|
|
||||||
LEUCHTE[h.art],
|
|
||||||
aktiv ? "cursor-pointer" : "cursor-default",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="schild text-sm tracking-[0.26em]">{h.oben}</span>
|
|
||||||
<span className="text-[40px] leading-none font-bold tracking-[0.04em] sm:text-[44px]">{h.mitte}</span>
|
|
||||||
<span className="font-sans text-sm opacity-80">{h.unten}</span>
|
|
||||||
</button>
|
|
||||||
<ul className="grid grid-cols-2 gap-2.5 rounded-2xl border border-linie bg-panel p-3 sm:grid-cols-4">
|
|
||||||
{lampen.map((l) => (
|
|
||||||
<Lampe key={l.id} lampe={l} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { laufzeit } from "@/lib/zeit"
|
|||||||
// es bei den alten 38 × 56 px je Ziffer.
|
// es bei den alten 38 × 56 px je Ziffer.
|
||||||
function Ziffer({ z }: { z: string }) {
|
function Ziffer({ z }: { z: string }) {
|
||||||
return (
|
return (
|
||||||
<span className="ziffern inline-flex h-[1.75em] w-[1.1875em] shrink-0 items-center justify-center rounded-md border border-[#2a3036] bg-[#0b0d0f] font-medium">
|
<span className="ziffern inline-flex h-[1.75em] w-[1.1875em] shrink-0 items-center justify-center rounded-lg border border-white/10 bg-gradient-to-b from-white/[0.07] to-white/[0.02] font-medium shadow-[inset_0_1px_0_rgb(255_255_255/0.06)]">
|
||||||
{z}
|
{z}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -27,12 +27,12 @@ export function Zaehlwerk({ sekunden }: { sekunden: number | null | undefined })
|
|||||||
>
|
>
|
||||||
<Ziffer z={lz ? tage[0] : "–"} />
|
<Ziffer z={lz ? tage[0] : "–"} />
|
||||||
<Ziffer z={lz ? tage[1] : "–"} />
|
<Ziffer z={lz ? tage[1] : "–"} />
|
||||||
<span className="schild mr-[0.444em] ml-[0.111em] text-[0.5625em] font-bold text-text-3">T</span>
|
<span className="etikett mr-[0.444em] ml-[0.111em] text-[0.5625em] text-text-3">T</span>
|
||||||
<Ziffer z={lz ? std[0] : "–"} />
|
<Ziffer z={lz ? std[0] : "–"} />
|
||||||
<Ziffer z={lz ? std[1] : "–"} />
|
<Ziffer z={lz ? std[1] : "–"} />
|
||||||
<span className="schild ml-[0.111em] text-[0.5625em] font-bold text-text-3">H</span>
|
<span className="etikett ml-[0.111em] text-[0.5625em] text-text-3">H</span>
|
||||||
</div>
|
</div>
|
||||||
<figcaption className="schild text-[15px] text-text-3">Ohne Neustart</figcaption>
|
<figcaption className="etikett text-[11.5px] tracking-[0.14em] text-text-3">Ohne Neustart</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ describe("bogenPfad", () => {
|
|||||||
|
|
||||||
describe("hauptleuchte", () => {
|
describe("hauptleuchte", () => {
|
||||||
it("bleibt ruhig, wenn nichts offen ist", () => {
|
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", () => {
|
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: "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: "rot", anzahl: 1 })).toMatchObject({ art: "rot", mitte: "1 Hinweis" })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("meldet einen schweigenden Wächter vor allem anderen", () => {
|
it("meldet einen schweigenden Wächter vor allem anderen", () => {
|
||||||
@@ -50,6 +50,6 @@ describe("Warnpanel", () => {
|
|||||||
expect(screen.getByText("Motor")).toBeInTheDocument()
|
expect(screen.getByText("Motor")).toBeInTheDocument()
|
||||||
expect(screen.getByText("b11057")).toBeInTheDocument()
|
expect(screen.getByText("b11057")).toBeInTheDocument()
|
||||||
expect(screen.getByText("braucht Aufmerksamkeit")).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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function Anzeige({ saeule, reihen, summe, className, links }: {
|
|||||||
links?: number
|
links?: number
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div role="status" className={cn("flex flex-col gap-1 rounded-lg border border-linie-stark bg-erhaben px-3 py-2", className)}
|
<div role="status" className={cn("flex flex-col gap-1 rounded-xl border border-white/12 bg-white/[0.04] px-3 py-2", className)}
|
||||||
style={links === undefined ? undefined : { left: links }}>
|
style={links === undefined ? undefined : { left: links }}>
|
||||||
<span className="ziffern text-xs text-text-3">{saeule.lang}</span>
|
<span className="ziffern text-xs text-text-3">{saeule.lang}</span>
|
||||||
{reihen.map((r) => (
|
{reihen.map((r) => (
|
||||||
@@ -42,7 +42,7 @@ function Anzeige({ saeule, reihen, summe, className, links }: {
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{reihen.length > 1 && (
|
{reihen.length > 1 && (
|
||||||
<span className="flex items-baseline gap-2 border-t border-linie pt-1 text-sm">
|
<span className="flex items-baseline gap-2 border-t border-white/[0.06] pt-1 text-sm">
|
||||||
<span className="ziffern font-semibold text-foreground">{zahl.format(summe)}</span>
|
<span className="ziffern font-semibold text-foreground">{zahl.format(summe)}</span>
|
||||||
<span className="text-text-3">zusammen</span>
|
<span className="text-text-3">zusammen</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -208,7 +208,7 @@ export function Saeulen({ saeulen, reihen, titel, summen = "alle", hoehe = 200,
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{saeulen.map((s, i) => (
|
{saeulen.map((s, i) => (
|
||||||
<tr key={s.schluessel} className="border-t border-linie">
|
<tr key={s.schluessel} className="border-t border-white/[0.06]">
|
||||||
<th scope="row" className="py-1 pr-3 text-left font-normal text-text-3">{s.lang}</th>
|
<th scope="row" className="py-1 pr-3 text-left font-normal text-text-3">{s.lang}</th>
|
||||||
{sichtbar.map((r) => (
|
{sichtbar.map((r) => (
|
||||||
<td key={r.name} className="ziffern py-1 pr-3 text-right text-text-2">{zahl.format(s.werte[r.name] ?? 0)}</td>
|
<td key={r.name} className="ziffern py-1 pr-3 text-right text-text-2">{zahl.format(s.werte[r.name] ?? 0)}</td>
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export function Verlauf({ reihen, einheit, langerZeitraum, hoehe = 180, titel }:
|
|||||||
{aktiv !== null && (
|
{aktiv !== null && (
|
||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
className="pointer-events-none absolute top-1 z-10 flex min-w-36 flex-col gap-1 rounded-lg border border-linie-stark bg-erhaben px-3 py-2 shadow-lg"
|
className="pointer-events-none absolute top-1 z-10 flex min-w-36 flex-col gap-1 rounded-xl border border-white/12 bg-white/[0.04] px-3 py-2 shadow-lg"
|
||||||
style={tipLinks > breite / 2 ? { right: breite - tipLinks + 10 } : { left: tipLinks + 10 }}
|
style={tipLinks > breite / 2 ? { right: breite - tipLinks + 10 } : { left: tipLinks + 10 }}
|
||||||
>
|
>
|
||||||
<span className="ziffern text-xs text-text-3">{zeitText(aktiv, true)}</span>
|
<span className="ziffern text-xs text-text-3">{zeitText(aktiv, true)}</span>
|
||||||
@@ -157,7 +157,7 @@ export function Verlauf({ reihen, einheit, langerZeitraum, hoehe = 180, titel }:
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{letzte.map(([t], i) => (
|
{letzte.map(([t], i) => (
|
||||||
<tr key={t} className="border-t border-linie">
|
<tr key={t} className="border-t border-white/[0.06]">
|
||||||
<td className="py-1 pr-4 text-text-3">{zeitText(t, langerZeitraum)}</td>
|
<td className="py-1 pr-4 text-text-3">{zeitText(t, langerZeitraum)}</td>
|
||||||
{reihen.map((r) => (
|
{reihen.map((r) => (
|
||||||
<td key={r.name} className="py-1 pr-4 text-text-2">
|
<td key={r.name} className="py-1 pr-4 text-text-2">
|
||||||
|
|||||||
@@ -1,22 +1,7 @@
|
|||||||
import { cn } from "cn"
|
import { Lagekachel, Lagekopf } from "@/components/cockpit/Lagekopf"
|
||||||
import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol"
|
import { geraetebild } from "@/lib/geraetebild"
|
||||||
import { geraetebild, type Punktzustand } from "@/lib/geraetebild"
|
|
||||||
import { type Lage, kurzname, lampeFuer } from "@/lib/homelab"
|
import { type Lage, kurzname, lampeFuer } from "@/lib/homelab"
|
||||||
import type { LampenZustand, ZielStand } from "@/lib/typen"
|
import type { 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<Lage["art"], { block: string; glut: string; text: string }> = {
|
|
||||||
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<LampenZustand, Punktzustand> = { ok: "ok", aus: "aus", warn: "warn", info: "info", fehler: "rot" }
|
|
||||||
const WERT: Record<LampenZustand, string> = {
|
|
||||||
ok: "text-text-3", aus: "text-text-3", warn: "text-bernstein", info: "text-cyan-text", fehler: "text-rot-text",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Lage im Homelab auf einen Blick: links der Statusblock (drücken führt dorthin, wo etwas zu tun ist), rechts je
|
* 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. */
|
/** Klick auf eine Kachel: Updates, wenn etwas ansteht, sonst die Zeile in der Geräte-Tabelle. */
|
||||||
onGeraet: (ziel: ZielStand, hatUpdate: boolean) => void
|
onGeraet: (ziel: ZielStand, hatUpdate: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const a = ART[lage.art]
|
|
||||||
const aktiv = lage.ziel !== null
|
|
||||||
return (
|
return (
|
||||||
<section aria-label="Lage im Homelab" className="auftauchen flaeche relative overflow-hidden rounded-3xl">
|
<Lagekopf art={lage.art} oben={lage.oben} mitte={lage.mitte} unten={lage.unten} aktiv={lage.ziel !== null}
|
||||||
<div aria-hidden className="glut-atmet pointer-events-none absolute -top-40 -left-32 size-[460px] rounded-full"
|
onAnsehen={onAnsehen} label="Lage im Homelab">
|
||||||
style={{ background: `radial-gradient(closest-side, color-mix(in srgb, ${a.glut} 20%, transparent), transparent)` }} />
|
{ziele.map((z) => {
|
||||||
<div className="relative grid gap-4 p-4 sm:p-5 lg:grid-cols-[280px_minmax(0,1fr)] lg:gap-5">
|
const l = lampeFuer(z, imUpdate.has(z.id))
|
||||||
<button
|
return (
|
||||||
type="button"
|
<Lagekachel key={z.id} bild={geraetebild(z)} name={kurzname(z)} wert={l.wert} zustand={l.zustand}
|
||||||
onClick={onAnsehen}
|
onClick={() => onGeraet(z, l.zustand === "info")} />
|
||||||
disabled={!aktiv}
|
)
|
||||||
aria-label={`${lage.oben}: ${lage.mitte}`}
|
})}
|
||||||
className={cn(
|
</Lagekopf>
|
||||||
"flex min-h-[132px] flex-col items-start justify-center gap-1.5 rounded-2xl px-5 py-4 text-left ring-1 ring-inset transition-colors",
|
|
||||||
a.block, aktiv ? "cursor-pointer hover:brightness-125" : "cursor-default",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="etikett text-[11px] tracking-[0.18em] text-text-3">{lage.oben}</span>
|
|
||||||
<span className={cn("text-[30px] leading-tight font-semibold tracking-[-0.01em] sm:text-[34px]", a.text)}>{lage.mitte}</span>
|
|
||||||
<span className="text-[13.5px] text-text-2">{lage.unten}</span>
|
|
||||||
</button>
|
|
||||||
<ul aria-label="Geräte" className="m-0 grid list-none grid-cols-1 gap-2 p-0 min-[480px]:grid-cols-2 xl:grid-cols-4">
|
|
||||||
{ziele.map((z) => {
|
|
||||||
const l = lampeFuer(z, imUpdate.has(z.id))
|
|
||||||
return (
|
|
||||||
<li key={z.id}>
|
|
||||||
<button type="button" onClick={() => onGeraet(z, l.zustand === "info")}
|
|
||||||
className="flaeche-innen group flex h-full w-full min-w-0 items-center gap-3 rounded-xl px-3 py-2.5 text-left transition-colors hover:border-white/20 hover:bg-white/[0.05]">
|
|
||||||
<Geraetesymbol bild={geraetebild(z)} punkt={PUNKT[l.zustand]} />
|
|
||||||
<span className="flex min-w-0 flex-col gap-0.5">
|
|
||||||
<span className="truncate text-[14.5px] leading-snug font-medium">{kurzname(z)}</span>
|
|
||||||
<span className={cn("truncate text-[12.5px] leading-snug", WERT[l.zustand])}>{l.wert}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function AlleKnopf({ anzahl, gesperrt }: { anzahl: number; gesperrt: bool
|
|||||||
|
|
||||||
const SCHRITT: Record<SammelSchrittStatus, { Symbol: typeof CircleCheck; farbe: string; text: string }> = {
|
const SCHRITT: Record<SammelSchrittStatus, { Symbol: typeof CircleCheck; farbe: string; text: string }> = {
|
||||||
wartet: { Symbol: CircleDashed, farbe: "text-text-3", text: "wartet" },
|
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" },
|
eingespielt: { Symbol: CircleCheck, farbe: "text-gruen", text: "eingespielt" },
|
||||||
zurueckgerollt: { Symbol: Undo2, farbe: "text-bernstein", text: "zurückgerollt" },
|
zurueckgerollt: { Symbol: Undo2, farbe: "text-bernstein", text: "zurückgerollt" },
|
||||||
fehler: { Symbol: CircleX, farbe: "text-rot", text: "Fehler" },
|
fehler: { Symbol: CircleX, farbe: "text-rot", text: "Fehler" },
|
||||||
|
|||||||
@@ -32,32 +32,24 @@ export function temperaturFarbe(grad: number | null | undefined) {
|
|||||||
if (grad == null) return "var(--text-3)"
|
if (grad == null) return "var(--text-3)"
|
||||||
if (grad >= 85) return "var(--rot)"
|
if (grad >= 85) return "var(--rot)"
|
||||||
if (grad >= 70) return "var(--bernstein)"
|
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 ---------------------------------------------------------------
|
// --- 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. */
|
/** Was die Hauptwarnleuchte sagt. Der schweigende Wächter geht allem anderen vor. */
|
||||||
export function hauptleuchte(z: Start["zustand"]) {
|
export function hauptleuchte(z: Start["zustand"]) {
|
||||||
if (!z.waechter_wach) {
|
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) {
|
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) {
|
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"
|
return z.stufe === "rot"
|
||||||
? { art: "rot" as const, oben: "Störung", mitte, unten: "Drücken zum Ansehen" }
|
? { 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" }
|
: { art: "gelb" as const, oben: "Achtung", mitte, unten: "Drücken zum Ansehen" }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Bird, Bot, Box, BrainCircuit, Clapperboard, Code, Container, DatabaseBackup, FileCode2, GitBranch, HardDrive, House,
|
AudioLines, Bird, Bot, Box, BrainCircuit, Clapperboard, Code, Container, DatabaseBackup, FileCode2, GitBranch, HardDrive,
|
||||||
Layers, type LucideIcon, Monitor, Route, Server, ShieldCheck, Shuffle, Workflow, Zap,
|
House, Layers, LayoutDashboard, type LucideIcon, Mic, Monitor, RefreshCw, Route, Server, ShieldCheck, Shuffle, Workflow,
|
||||||
|
Zap,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { LOGOS } from "@/lib/logos-liste"
|
import { LOGOS } from "@/lib/logos-liste"
|
||||||
import type { Bereich } from "@/lib/navigation"
|
import type { Bereich } from "@/lib/navigation"
|
||||||
@@ -81,6 +82,25 @@ const KI_BOX: Record<string, [LucideIcon, string?]> = {
|
|||||||
jobs: [Workflow],
|
jobs: [Workflow],
|
||||||
sicherung: [DatabaseBackup],
|
sicherung: [DatabaseBackup],
|
||||||
platte: [HardDrive],
|
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. */
|
/** Das Bild eines Bausteins oder einer Lampe der KI-Box. */
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ describe("kleine Helfer", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("Temperaturfarbe wechselt bei 70 und 85 Grad", () => {
|
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(70)).toBe("var(--bernstein)")
|
||||||
expect(temperaturFarbe(85)).toBe("var(--rot)")
|
expect(temperaturFarbe(85)).toBe("var(--rot)")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQueryClient } from "@tanstack/react-query"
|
|||||||
import { cn } from "cn"
|
import { cn } from "cn"
|
||||||
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
|
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
|
||||||
import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||||
|
import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol"
|
||||||
import { Panel } from "@/components/cockpit/Panel"
|
import { Panel } from "@/components/cockpit/Panel"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { type Dienst, type DiensteStand, useDienste, useProtokoll } from "@/lib/abfragen"
|
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,
|
dauer, erfolgText, groesse, istProblem, knopfText, neustartFrage, prozent, rang, sortiert, speicherAnteil, ZUSTAND,
|
||||||
zeitpunkt, zustandVon, zustandZusatz,
|
zeitpunkt, zustandVon, zustandZusatz,
|
||||||
} from "@/lib/dienste"
|
} from "@/lib/dienste"
|
||||||
|
import { dienstbild, type Punktzustand } from "@/lib/geraetebild"
|
||||||
import { melden } from "@/lib/meldungen"
|
import { melden } from "@/lib/meldungen"
|
||||||
import type { AktionsErgebnis } from "@/lib/typen"
|
import type { AktionsErgebnis } from "@/lib/typen"
|
||||||
import { gbText } from "@/lib/zeit"
|
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<DienstZustand, Punktzustand> = {
|
||||||
|
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
|
// 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.
|
// 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
|
|||||||
<span className="flex min-w-0 flex-col gap-1">
|
<span className="flex min-w-0 flex-col gap-1">
|
||||||
<span className="ziffern text-sm text-text-2">{children}</span>
|
<span className="ziffern text-sm text-text-2">{children}</span>
|
||||||
{anteil !== null && (
|
{anteil !== null && (
|
||||||
<span className="h-1.5 w-full max-w-40 overflow-hidden rounded-full bg-erhaben" aria-hidden>
|
<span className="h-1.5 w-full max-w-40 overflow-hidden rounded-full bg-white/[0.07]" aria-hidden>
|
||||||
<span className="block h-full rounded-full" style={{ width: `${Math.min(100, Math.max(2, anteil))}%`, background: "var(--reihe-1)" }} />
|
<span className="block h-full rounded-full" style={{ width: `${Math.min(100, Math.max(2, anteil))}%`, background: "var(--reihe-1)" }} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -69,7 +77,7 @@ function Protokoll({ d, id }: { d: Dienst; id: string }) {
|
|||||||
ref={feld}
|
ref={feld}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={`Letzte Protokollzeilen von ${d.name}`}
|
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}
|
{text}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -90,9 +98,9 @@ function Zeile({ d, gesamt, offen, startet, onProtokoll, onFrage }: {
|
|||||||
const neustarts = d.neustarts ?? 0
|
const neustarts = d.neustarts ?? 0
|
||||||
const protokollId = `protokoll-${d.unit}`
|
const protokollId = `protokoll-${d.unit}`
|
||||||
return (
|
return (
|
||||||
<div role="row" className={cn("grid gap-y-2.5 border-t border-linie py-3 xl:items-center", SPALTEN)}>
|
<div role="row" className={cn("grid gap-y-2.5 border-t border-white/[0.06] py-3 xl:items-center", SPALTEN)}>
|
||||||
<div role="rowheader" className="col-span-2 flex min-w-0 items-start gap-2.5 xl:col-span-1">
|
<div role="rowheader" className="col-span-2 flex min-w-0 items-center gap-3 xl:col-span-1">
|
||||||
<span aria-hidden className={cn("mt-1.5 size-2.5 shrink-0 rounded-full", z.punkt)} />
|
<Geraetesymbol bild={dienstbild(d.unit)} groesse="sm" punkt={PUNKT_DIENST[zustandVon(d)]} />
|
||||||
<span className="flex min-w-0 flex-col">
|
<span className="flex min-w-0 flex-col">
|
||||||
<span className="text-[15px] font-semibold break-words">{d.name}</span>
|
<span className="text-[15px] font-semibold break-words">{d.name}</span>
|
||||||
<span className="ziffern text-xs break-words text-text-3">
|
<span className="ziffern text-xs break-words text-text-3">
|
||||||
@@ -103,7 +111,7 @@ function Zeile({ d, gesamt, offen, startet, onProtokoll, onFrage }: {
|
|||||||
</div>
|
</div>
|
||||||
<span role="cell" className="flex min-w-0 flex-col gap-0.5">
|
<span role="cell" className="flex min-w-0 flex-col gap-0.5">
|
||||||
<Beschriftung>Zustand</Beschriftung>
|
<Beschriftung>Zustand</Beschriftung>
|
||||||
<span className={cn("schild text-xs", z.text)}>{z.wort}</span>
|
<span className={cn("etikett text-[10.5px] tracking-[0.12em]", z.text)}>{z.wort}</span>
|
||||||
{zusatz && <span className="text-xs text-text-3">{zusatz}</span>}
|
{zusatz && <span className="text-xs text-text-3">{zusatz}</span>}
|
||||||
{/* Ohne Tabelle stehen die Neustarts hier — und nur, wenn es welche gab. */}
|
{/* Ohne Tabelle stehen die Neustarts hier — und nur, wenn es welche gab. */}
|
||||||
{neustarts > 0 && (
|
{neustarts > 0 && (
|
||||||
@@ -187,7 +195,7 @@ function Tabelle({ stand, gewaehlt, startet, onProtokoll, onFrage }: {
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div role="table" aria-label="Dienste der KI-Box" className="flex flex-col">
|
<div role="table" aria-label="Dienste der KI-Box" className="flex flex-col">
|
||||||
<div role="row" className={cn("schild hidden pb-2 text-xs font-semibold text-text-3 xl:grid", SPALTEN)}>
|
<div role="row" className={cn("etikett hidden pb-2 text-[10.5px] tracking-[0.14em] text-text-3 xl:grid", SPALTEN)}>
|
||||||
<span role="columnheader">Dienst</span>
|
<span role="columnheader">Dienst</span>
|
||||||
<span role="columnheader">Zustand</span>
|
<span role="columnheader">Zustand</span>
|
||||||
<span role="columnheader">CPU</span>
|
<span role="columnheader">CPU</span>
|
||||||
|
|||||||
@@ -53,30 +53,35 @@ function neustartFenster(tag: number, uhrzeit: string): string {
|
|||||||
|
|
||||||
function Abschnitt({ titel, children }: { titel: string; children: ReactNode }) {
|
function Abschnitt({ titel, children }: { titel: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<section aria-label={titel} className="flex flex-col gap-3 border-t border-linie pt-4 first:border-t-0 first:pt-0">
|
<section aria-label={titel} className="flex flex-col gap-3 border-t border-white/[0.06] pt-4 first:border-t-0 first:pt-0">
|
||||||
<h3 className="schild text-sm tracking-[0.18em] text-text-2">{titel}</h3>
|
<h3 className="etikett text-[11.5px] tracking-[0.16em] text-text-2">{titel}</h3>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Eine Karte der Einstellungsseite; KI-Box und Homelab tragen ihre Bereichsfarbe als Streifen oben. */
|
/** Eine Karte der Einstellungsseite; KI-Box und Homelab tragen ihre Bereichsfarbe als Schein und Rand (Design v5). */
|
||||||
function Karte({ titel, text, farbe, children }: {
|
function Karte({ titel, text, farbe, children }: {
|
||||||
titel: string
|
titel: string
|
||||||
text: string
|
text: string
|
||||||
farbe?: "box" | "homelab"
|
farbe?: "box" | "homelab"
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const streifen = farbe === "box" ? "bg-box" : farbe === "homelab" ? "bg-lab" : "bg-linie-stark"
|
const glut = farbe === "box" ? "var(--box)" : farbe === "homelab" ? "var(--lab)" : null
|
||||||
const kopf = farbe === "box" ? "text-box-text" : farbe === "homelab" ? "text-lab-text" : "text-foreground"
|
const kopf = farbe === "box" ? "text-box-text" : farbe === "homelab" ? "text-lab-text" : "text-foreground"
|
||||||
|
const rand = farbe === "box" ? "[--rand:color-mix(in_srgb,var(--box)_28%,transparent)]"
|
||||||
|
: farbe === "homelab" ? "[--rand:color-mix(in_srgb,var(--lab)_28%,transparent)]" : ""
|
||||||
return (
|
return (
|
||||||
<section aria-label={titel} className="relative flex min-w-0 flex-col gap-4 overflow-hidden rounded-2xl border border-linie bg-panel p-5 sm:p-6">
|
<section aria-label={titel} className={cn("flaeche relative flex min-w-0 flex-col gap-4 overflow-hidden rounded-3xl p-5 sm:p-6", rand)}>
|
||||||
<span aria-hidden className={cn("absolute inset-x-0 top-0 h-1", streifen)} />
|
{glut && (
|
||||||
<div className="flex flex-col gap-1">
|
<div aria-hidden className="pointer-events-none absolute -top-40 -left-32 size-[400px] rounded-full"
|
||||||
<h2 className={cn("schild text-lg font-bold tracking-[0.22em]", kopf)}>{titel}</h2>
|
style={{ background: `radial-gradient(closest-side, color-mix(in srgb, ${glut} 16%, transparent), transparent)` }} />
|
||||||
|
)}
|
||||||
|
<div className="relative flex flex-col gap-1">
|
||||||
|
<h2 className={cn("text-[19px] leading-tight font-semibold", kopf)}>{titel}</h2>
|
||||||
<p className="text-sm text-text-3">{text}</p>
|
<p className="text-sm text-text-3">{text}</p>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
<div className="relative flex min-w-0 flex-col gap-4">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -151,7 +156,7 @@ function HfZugang({ offen }: { offen: boolean }) {
|
|||||||
value={token}
|
value={token}
|
||||||
onChange={(e) => setToken(e.target.value)}
|
onChange={(e) => setToken(e.target.value)}
|
||||||
placeholder="hf_…"
|
placeholder="hf_…"
|
||||||
className="h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan"
|
className="h-11 min-w-0 flex-1 rounded-xl border border-white/12 bg-white/[0.04] px-3 text-[15px] outline-none focus:border-cyan/60 focus:bg-white/[0.06]"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={!token.trim() || s?.schreibbar === false || speichert}>
|
<Button type="submit" disabled={!token.trim() || s?.schreibbar === false || speichert}>
|
||||||
{speichert ? "Speichert …" : "Speichern"}
|
{speichert ? "Speichert …" : "Speichern"}
|
||||||
@@ -240,7 +245,7 @@ function Zeitfenster({ e, laedt, fehler }: { e: EinstellungenStand | undefined;
|
|||||||
const wahl = entwurf ?? { tag: f.tag, uhrzeit: f.uhrzeit }
|
const wahl = entwurf ?? { tag: f.tag, uhrzeit: f.uhrzeit }
|
||||||
const geaendert = wahl.tag !== f.tag || wahl.uhrzeit !== f.uhrzeit
|
const geaendert = wahl.tag !== f.tag || wahl.uhrzeit !== f.uhrzeit
|
||||||
const zeiten = ZEITEN.includes(wahl.uhrzeit) ? ZEITEN : [...ZEITEN, wahl.uhrzeit].sort()
|
const zeiten = ZEITEN.includes(wahl.uhrzeit) ? ZEITEN : [...ZEITEN, wahl.uhrzeit].sort()
|
||||||
const feld = "h-11 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] text-foreground outline-none focus:border-cyan"
|
const feld = "h-11 rounded-xl border border-white/12 bg-white/[0.04] px-3 text-[15px] text-foreground outline-none focus:border-cyan/60 focus:bg-white/[0.06]"
|
||||||
return (
|
return (
|
||||||
<Abschnitt titel="Update-Zeitfenster">
|
<Abschnitt titel="Update-Zeitfenster">
|
||||||
<p className="text-sm text-text-2">
|
<p className="text-sm text-text-2">
|
||||||
@@ -450,7 +455,7 @@ function ArcaneSchluessel({ h }: { h: HomelabEinstellungen }) {
|
|||||||
value={wert}
|
value={wert}
|
||||||
onChange={(e) => setWert(e.target.value)}
|
onChange={(e) => setWert(e.target.value)}
|
||||||
placeholder={a.schluessel === "hinterlegt" ? "Neuen Schlüssel eintragen …" : "Schlüssel aus Arcane einfügen …"}
|
placeholder={a.schluessel === "hinterlegt" ? "Neuen Schlüssel eintragen …" : "Schlüssel aus Arcane einfügen …"}
|
||||||
className="h-11 w-full rounded-lg border border-linie-stark bg-erhaben pr-3 pl-9 text-[15px] outline-none focus:border-lab"
|
className="h-11 w-full rounded-xl border border-white/12 bg-white/[0.04] pr-3 pl-9 text-[15px] outline-none focus:border-lab/60 focus:bg-white/[0.06]"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<Button type="submit" disabled={!wert.trim() || speichern.isPending}>
|
<Button type="submit" disabled={!wert.trim() || speichern.isPending}>
|
||||||
@@ -559,7 +564,7 @@ export function EinstellungenSeite() {
|
|||||||
<RadarSchalter {...boxProps} />
|
<RadarSchalter {...boxProps} />
|
||||||
<HfZugang offen />
|
<HfZugang offen />
|
||||||
<Abschnitt titel="Hermes">
|
<Abschnitt titel="Hermes">
|
||||||
<a href="/hermes-ui/" target="_blank" rel="noopener" className="flex h-11 items-center gap-2 self-start rounded-lg border border-linie-stark bg-erhaben px-4 text-[15px] hover:border-text-3">
|
<a href="/hermes-ui/" target="_blank" rel="noopener" className="flex h-11 items-center gap-2 self-start rounded-xl border border-white/12 bg-white/[0.04] px-4 text-[15px] font-semibold hover:border-white/20 hover:bg-white/[0.08]">
|
||||||
<ExternalLink className="size-4" aria-hidden /> Hermes-Dashboard öffnen
|
<ExternalLink className="size-4" aria-hidden /> Hermes-Dashboard öffnen
|
||||||
</a>
|
</a>
|
||||||
<p className="text-xs text-text-3">Chat, Sitzungen und Cron-Jobs verwaltet Hermes selbst. Es fragt nach seiner eigenen Anmeldung.</p>
|
<p className="text-xs text-text-3">Chat, Sitzungen und Cron-Jobs verwaltet Hermes selbst. Es fragt nach seiner eigenen Anmeldung.</p>
|
||||||
@@ -572,6 +577,11 @@ export function EinstellungenSeite() {
|
|||||||
<Karte titel="Software-Stand" text="Welcher Stand auf beiden Instanzen läuft.">
|
<Karte titel="Software-Stand" text="Welcher Stand auf beiden Instanzen läuft.">
|
||||||
<Stand offen />
|
<Stand offen />
|
||||||
</Karte>
|
</Karte>
|
||||||
|
{/* Pflicht der Lizenz CC BY 4.0 (frontend/public/logos/QUELLE.md). */}
|
||||||
|
<p className="text-xs text-text-3 xl:col-span-2">
|
||||||
|
Logos der Dienste: <a href="https://selfh.st/icons" target="_blank" rel="noopener" className="underline-offset-4 hover:underline">selfh.st/icons</a>,
|
||||||
|
Lizenz CC BY 4.0. Die Marken gehören ihren Inhabern und dienen hier nur dem Wiedererkennen.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function Eintrag({ e, geraet }: { e: ProtokollEintrag; geraet: string | null })
|
|||||||
// mit Symbol.for(…) zurück — eine gleichnamige Konstante darunter ließ die Seite dort abstürzen.
|
// mit Symbol.for(…) zurück — eine gleichnamige Konstante darunter ließ die Seite dort abstürzen.
|
||||||
const Zeichen = SYMBOL[e.art] ?? Cog
|
const Zeichen = SYMBOL[e.art] ?? Cog
|
||||||
return (
|
return (
|
||||||
<li className="grid grid-cols-[64px_18px_minmax(0,1fr)] gap-x-3 border-t border-linie py-2.5 first:border-t-0">
|
<li className="grid grid-cols-[64px_18px_minmax(0,1fr)] gap-x-3 border-t border-white/[0.06] py-2.5 first:border-t-0">
|
||||||
<span className="ziffern pt-0.5 text-xs text-text-3">{uhr.format(new Date(e.zeit * 1000))}</span>
|
<span className="ziffern pt-0.5 text-xs text-text-3">{uhr.format(new Date(e.zeit * 1000))}</span>
|
||||||
<span className="flex flex-col items-center pt-1">
|
<span className="flex flex-col items-center pt-1">
|
||||||
<span aria-hidden className={cn("size-2.5 rounded-full", s.punkt)} />
|
<span aria-hidden className={cn("size-2.5 rounded-full", s.punkt)} />
|
||||||
@@ -74,7 +74,7 @@ function Eintrag({ e, geraet }: { e: ProtokollEintrag; geraet: string | null })
|
|||||||
{offen ? "Ausgabe verbergen" : "Ausgabe zeigen"}
|
{offen ? "Ausgabe verbergen" : "Ausgabe zeigen"}
|
||||||
</button>
|
</button>
|
||||||
{offen && (
|
{offen && (
|
||||||
<pre className="ziffern m-0 max-h-72 overflow-auto rounded-md border border-linie bg-[#0b0d0f] p-2.5 text-xs whitespace-pre-wrap text-text-2">
|
<pre className="ziffern m-0 max-h-72 overflow-auto rounded-xl bg-black/30 ring-1 ring-white/[0.07] ring-inset p-2.5 text-xs whitespace-pre-wrap text-text-2">
|
||||||
{e.details}
|
{e.details}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@@ -118,7 +118,7 @@ export function HomelabProtokollSeite() {
|
|||||||
<button key={a.id} type="button" onClick={() => setArt(a.id)} aria-pressed={art === a.id}
|
<button key={a.id} type="button" onClick={() => setArt(a.id)} aria-pressed={art === a.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 rounded-full border px-3.5 text-sm",
|
"h-9 rounded-full border px-3.5 text-sm",
|
||||||
art === a.id ? "border-lab-rand bg-lab-grund text-lab-text" : "border-linie text-text-2 hover:border-linie-stark hover:text-foreground",
|
art === a.id ? "border-lab-rand bg-lab-grund text-lab-text" : "border-white/10 text-text-2 hover:border-white/20 hover:text-foreground",
|
||||||
)}>
|
)}>
|
||||||
{a.name}
|
{a.name}
|
||||||
</button>
|
</button>
|
||||||
@@ -138,7 +138,7 @@ export function HomelabProtokollSeite() {
|
|||||||
<div className={cn("flex flex-col gap-5", protokoll.isFetching && "opacity-80")}>
|
<div className={cn("flex flex-col gap-5", protokoll.isFetching && "opacity-80")}>
|
||||||
{tage.map((t) => (
|
{tage.map((t) => (
|
||||||
<section key={t.titel} aria-label={t.titel} className="flex flex-col gap-1">
|
<section key={t.titel} aria-label={t.titel} className="flex flex-col gap-1">
|
||||||
<h3 className="schild text-sm tracking-[0.18em] text-lab-text">{t.titel}</h3>
|
<h3 className="etikett text-[11.5px] tracking-[0.16em] text-lab-text">{t.titel}</h3>
|
||||||
<ol className="m-0 flex list-none flex-col p-0">
|
<ol className="m-0 flex list-none flex-col p-0">
|
||||||
{t.eintraege.map((e, i) => {
|
{t.eintraege.map((e, i) => {
|
||||||
// Das Gerät als Schild — außer, der Titel nennt es schon.
|
// Das Gerät als Schild — außer, der Titel nennt es schon.
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ function Kachel({ label, wert, zusatz, art = "grau" }: {
|
|||||||
art?: keyof typeof WERT_FARBE
|
art?: keyof typeof WERT_FARBE
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 flex-col gap-1.5 rounded-xl border border-linie bg-erhaben/50 px-3.5 py-3">
|
<div className="flex min-w-0 flex-col gap-1.5 flaeche-innen rounded-2xl px-3.5 py-3">
|
||||||
<span className="schild text-xs tracking-[0.18em] text-text-3">{label}</span>
|
<span className="etikett text-[10.5px] tracking-[0.14em] text-text-3">{label}</span>
|
||||||
<span className={cn("font-anzeige text-[28px] leading-none font-bold", WERT_FARBE[art])}>{wert}</span>
|
<span className={cn("font-anzeige text-[28px] leading-none font-bold", WERT_FARBE[art])}>{wert}</span>
|
||||||
{zusatz && <span className="text-xs text-text-3">{zusatz}</span>}
|
{zusatz && <span className="text-xs text-text-3">{zusatz}</span>}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@ function Zelle({ label, wert, zusatz, leise }: { label: string; wert: string; zu
|
|||||||
function AbsenderTabelle({ zeilen }: { zeilen: AbsenderZeile[] }) {
|
function AbsenderTabelle({ zeilen }: { zeilen: AbsenderZeile[] }) {
|
||||||
return (
|
return (
|
||||||
<div role="table" aria-label="Nutzung je Absender" className="flex flex-col">
|
<div role="table" aria-label="Nutzung je Absender" className="flex flex-col">
|
||||||
<div role="row" className={cn("schild hidden gap-x-4 pb-2 text-xs text-text-3 lg:grid", SPALTEN)}>
|
<div role="row" className={cn("etikett hidden gap-x-4 pb-2 text-[10.5px] tracking-[0.14em] text-text-3 lg:grid", SPALTEN)}>
|
||||||
<span role="columnheader">Absender</span>
|
<span role="columnheader">Absender</span>
|
||||||
<span role="columnheader">Anfragen</span>
|
<span role="columnheader">Anfragen</span>
|
||||||
<span role="columnheader">Typische Antwort</span>
|
<span role="columnheader">Typische Antwort</span>
|
||||||
@@ -56,7 +56,7 @@ function AbsenderTabelle({ zeilen }: { zeilen: AbsenderZeile[] }) {
|
|||||||
<span role="columnheader">Zuletzt</span>
|
<span role="columnheader">Zuletzt</span>
|
||||||
</div>
|
</div>
|
||||||
{zeilen.map((a) => (
|
{zeilen.map((a) => (
|
||||||
<div key={a.name} role="row" className={cn("grid grid-cols-2 gap-x-4 gap-y-2.5 border-t border-linie py-3 lg:items-start", SPALTEN)}>
|
<div key={a.name} role="row" className={cn("grid grid-cols-2 gap-x-4 gap-y-2.5 border-t border-white/[0.06] py-3 lg:items-start", SPALTEN)}>
|
||||||
<span role="rowheader" className="col-span-2 flex min-w-0 items-center gap-2 text-[15px] font-medium lg:col-span-1">
|
<span role="rowheader" className="col-span-2 flex min-w-0 items-center gap-2 text-[15px] font-medium lg:col-span-1">
|
||||||
<span aria-hidden className="size-2.5 shrink-0 rounded-[2px]" style={{ background: a.farbe }} />
|
<span aria-hidden className="size-2.5 shrink-0 rounded-[2px]" style={{ background: a.farbe }} />
|
||||||
<span className="min-w-0 break-words">{a.name}</span>
|
<span className="min-w-0 break-words">{a.name}</span>
|
||||||
@@ -66,7 +66,7 @@ function AbsenderTabelle({ zeilen }: { zeilen: AbsenderZeile[] }) {
|
|||||||
<span className="ziffern">{anzahlText(a.anfragen)}</span>
|
<span className="ziffern">{anzahlText(a.anfragen)}</span>
|
||||||
<span className="text-text-3"> · {anteilText(a.anteil)} aller Anfragen</span>
|
<span className="text-text-3"> · {anteilText(a.anteil)} aller Anfragen</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="h-1.5 w-full max-w-56 overflow-hidden rounded-full bg-erhaben" aria-hidden>
|
<span className="h-1.5 w-full max-w-56 overflow-hidden rounded-full bg-white/[0.07]" aria-hidden>
|
||||||
<span className="block h-full rounded-full" style={{ width: `${Math.max(1, a.anteil * 100)}%`, background: a.farbe }} />
|
<span className="block h-full rounded-full" style={{ width: `${Math.max(1, a.anteil * 100)}%`, background: a.farbe }} />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -86,9 +86,9 @@ function AbsenderTabelle({ zeilen }: { zeilen: AbsenderZeile[] }) {
|
|||||||
|
|
||||||
function Diagramm({ titel, zusatz, children }: { titel: string; zusatz: string; children: ReactNode }) {
|
function Diagramm({ titel, zusatz, children }: { titel: string; zusatz: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<section aria-label={titel} className="flex min-w-0 flex-col gap-3 rounded-xl border border-linie p-4">
|
<section aria-label={titel} className="flex min-w-0 flex-col gap-3 flaeche-innen rounded-2xl p-4">
|
||||||
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
|
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
|
||||||
<h3 className="schild text-sm tracking-[0.18em] text-text-2">{titel}</h3>
|
<h3 className="etikett text-[11.5px] tracking-[0.16em] text-text-2">{titel}</h3>
|
||||||
<span className="text-xs text-text-3">{zusatz}</span>
|
<span className="text-xs text-text-3">{zusatz}</span>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
@@ -101,7 +101,7 @@ function Ladevorgaenge({ modelle, rollen }: { modelle: ModellLadungen[]; rollen:
|
|||||||
if (modelle.length === 0) return null
|
if (modelle.length === 0) return null
|
||||||
const gesamt = modelle.every((m) => m.anzahl !== null) ? modelle.reduce((s, m) => s + (m.anzahl ?? 0), 0) : null
|
const gesamt = modelle.every((m) => m.anzahl !== null) ? modelle.reduce((s, m) => s + (m.anzahl ?? 0), 0) : null
|
||||||
return (
|
return (
|
||||||
<details className="group rounded-xl border border-linie bg-erhaben/40 px-4 open:pb-2">
|
<details className="group flaeche-innen rounded-2xl px-4 open:pb-2">
|
||||||
<summary className="flex min-h-12 cursor-pointer list-none flex-wrap items-center justify-between gap-x-3 gap-y-1 py-2 [&::-webkit-details-marker]:hidden">
|
<summary className="flex min-h-12 cursor-pointer list-none flex-wrap items-center justify-between gap-x-3 gap-y-1 py-2 [&::-webkit-details-marker]:hidden">
|
||||||
<span className="flex items-center gap-2 text-[15px] font-medium">
|
<span className="flex items-center gap-2 text-[15px] font-medium">
|
||||||
<ChevronRight className="size-4 shrink-0 text-text-3 transition-transform group-open:rotate-90" aria-hidden />
|
<ChevronRight className="size-4 shrink-0 text-text-3 transition-transform group-open:rotate-90" aria-hidden />
|
||||||
@@ -116,7 +116,7 @@ function Ladevorgaenge({ modelle, rollen }: { modelle: ModellLadungen[]; rollen:
|
|||||||
wenn es auf Abruf gebraucht wurde.
|
wenn es auf Abruf gebraucht wurde.
|
||||||
</p>
|
</p>
|
||||||
<div role="table" aria-label="Ladevorgänge je Modell" className="flex flex-col">
|
<div role="table" aria-label="Ladevorgänge je Modell" className="flex flex-col">
|
||||||
<div role="row" className="schild hidden gap-x-4 py-2 text-xs text-text-3 sm:grid sm:grid-cols-[minmax(0,1.5fr)_70px_130px_minmax(0,1.5fr)]">
|
<div role="row" className="etikett hidden gap-x-4 py-2 text-[10.5px] tracking-[0.14em] text-text-3 sm:grid sm:grid-cols-[minmax(0,1.5fr)_70px_130px_minmax(0,1.5fr)]">
|
||||||
<span role="columnheader">Modell</span>
|
<span role="columnheader">Modell</span>
|
||||||
<span role="columnheader">Geladen</span>
|
<span role="columnheader">Geladen</span>
|
||||||
<span role="columnheader">Zuletzt</span>
|
<span role="columnheader">Zuletzt</span>
|
||||||
@@ -124,7 +124,7 @@ function Ladevorgaenge({ modelle, rollen }: { modelle: ModellLadungen[]; rollen:
|
|||||||
</div>
|
</div>
|
||||||
{modelle.map((m) => (
|
{modelle.map((m) => (
|
||||||
<div key={m.modell} role="row"
|
<div key={m.modell} role="row"
|
||||||
className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 border-t border-linie py-2.5 sm:grid-cols-[minmax(0,1.5fr)_70px_130px_minmax(0,1.5fr)] sm:items-baseline">
|
className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 border-t border-white/[0.06] py-2.5 sm:grid-cols-[minmax(0,1.5fr)_70px_130px_minmax(0,1.5fr)] sm:items-baseline">
|
||||||
<span role="rowheader" className="flex min-w-0 flex-col">
|
<span role="rowheader" className="flex min-w-0 flex-col">
|
||||||
<span className="ziffern text-sm break-all">{m.modell}</span>
|
<span className="ziffern text-sm break-all">{m.modell}</span>
|
||||||
{rollen[m.modell] && <span className="text-xs text-text-3">{rollen[m.modell]}</span>}
|
{rollen[m.modell] && <span className="text-xs text-text-3">{rollen[m.modell]}</span>}
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ export function ModellSuche({ offen, onSchliessen, rollenJetzt }: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={offen} onOpenChange={(o) => !o && onSchliessen()}>
|
<Sheet open={offen} onOpenChange={(o) => !o && onSchliessen()}>
|
||||||
<SheetContent side="right" className="gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-xl">
|
<SheetContent side="right" className="gap-3 overflow-y-auto border-white/10 bg-[#0e121a] data-[side=right]:w-full data-[side=right]:sm:max-w-xl">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle className="schild text-lg">Modelle selbst suchen</SheetTitle>
|
<SheetTitle className="text-[19px] font-semibold">Modelle selbst suchen</SheetTitle>
|
||||||
<SheetDescription className="text-text-2">Auf Hugging Face suchen, Quantisierung wählen, installieren.</SheetDescription>
|
<SheetDescription className="text-text-2">Auf Hugging Face suchen, Quantisierung wählen, installieren.</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<form
|
<form
|
||||||
@@ -91,7 +91,7 @@ export function ModellSuche({ offen, onSchliessen, rollenJetzt }: {
|
|||||||
value={eingabe}
|
value={eingabe}
|
||||||
onChange={(e) => setEingabe(e.target.value)}
|
onChange={(e) => setEingabe(e.target.value)}
|
||||||
placeholder="z. B. Qwen3.8 GGUF"
|
placeholder="z. B. Qwen3.8 GGUF"
|
||||||
className="h-11 min-w-0 flex-1 rounded-lg border border-linie-stark bg-erhaben px-3 text-[15px] outline-none focus:border-cyan"
|
className="h-11 min-w-0 flex-1 rounded-xl border border-white/12 bg-white/[0.04] px-3 text-[15px] outline-none focus:border-cyan/60 focus:bg-white/[0.06]"
|
||||||
/>
|
/>
|
||||||
<Button type="submit">Suchen</Button>
|
<Button type="submit">Suchen</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -112,7 +112,7 @@ export function ModellSuche({ offen, onSchliessen, rollenJetzt }: {
|
|||||||
key={t.repo}
|
key={t.repo}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setRepo(t.repo)}
|
onClick={() => setRepo(t.repo)}
|
||||||
className="flex min-h-11 items-center justify-between gap-3 border-t border-linie py-2 text-left hover:text-cyan"
|
className="flex min-h-11 items-center justify-between gap-3 border-t border-white/[0.06] py-2 text-left hover:text-cyan"
|
||||||
>
|
>
|
||||||
<span className="ziffern min-w-0 text-sm break-all">{t.repo}</span>
|
<span className="ziffern min-w-0 text-sm break-all">{t.repo}</span>
|
||||||
<span className="ziffern shrink-0 text-xs text-text-3">{zahl(t.downloads)} ↓</span>
|
<span className="ziffern shrink-0 text-xs text-text-3">{zahl(t.downloads)} ↓</span>
|
||||||
@@ -131,7 +131,7 @@ export function ModellSuche({ offen, onSchliessen, rollenJetzt }: {
|
|||||||
<select
|
<select
|
||||||
value={rolle}
|
value={rolle}
|
||||||
onChange={(e) => setRolle(e.target.value)}
|
onChange={(e) => setRolle(e.target.value)}
|
||||||
className="h-11 min-w-0 rounded-lg border border-linie-stark bg-erhaben px-3 text-foreground"
|
className="h-11 min-w-0 rounded-xl border border-white/12 bg-white/[0.04] px-3 text-foreground"
|
||||||
>
|
>
|
||||||
<option value="">ohne Rolle (nur installieren)</option>
|
<option value="">ohne Rolle (nur installieren)</option>
|
||||||
{Object.entries(ROLLEN).map(([wert, name]) => (
|
{Object.entries(ROLLEN).map(([wert, name]) => (
|
||||||
@@ -155,7 +155,7 @@ export function ModellSuche({ offen, onSchliessen, rollenJetzt }: {
|
|||||||
const name = q.quant ?? q.name ?? "?"
|
const name = q.quant ?? q.name ?? "?"
|
||||||
const bytes = q.total_bytes ?? q.size_bytes
|
const bytes = q.total_bytes ?? q.size_bytes
|
||||||
return (
|
return (
|
||||||
<div key={name} className="flex items-center justify-between gap-3 border-t border-linie py-2">
|
<div key={name} className="flex items-center justify-between gap-3 border-t border-white/[0.06] py-2">
|
||||||
<span className="ziffern text-sm">{name}</span>
|
<span className="ziffern text-sm">{name}</span>
|
||||||
<span className="flex items-center gap-3">
|
<span className="flex items-center gap-3">
|
||||||
{bytes ? <span className="ziffern text-xs text-text-3">{gbText(bytes, 1)}</span> : null}
|
{bytes ? <span className="ziffern text-xs text-text-3">{gbText(bytes, 1)}</span> : null}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function Speicher({ modelle, laufend }: { modelle: Modell[]; laufend: string[] }
|
|||||||
const pct = (g: number) => `${Math.min(100, (g / gesamt) * 100)}%`
|
const pct = (g: number) => `${Math.min(100, (g / gesamt) * 100)}%`
|
||||||
return (
|
return (
|
||||||
<Panel titel="Speicher" rechts={<span className="ziffern text-sm text-text-2">{Math.round(belegt)} von {Math.round(gesamt)} GB belegt · Grenze ~{GRENZE_GB} GB</span>}>
|
<Panel titel="Speicher" rechts={<span className="ziffern text-sm text-text-2">{Math.round(belegt)} von {Math.round(gesamt)} GB belegt · Grenze ~{GRENZE_GB} GB</span>}>
|
||||||
<div className="relative h-8 rounded-lg bg-erhaben" role="img" aria-label={`Speicher: ${Math.round(belegt)} von ${Math.round(gesamt)} GB belegt`}>
|
<div className="relative h-8 overflow-hidden rounded-xl bg-white/[0.06]" role="img" aria-label={`Speicher: ${Math.round(belegt)} von ${Math.round(gesamt)} GB belegt`}>
|
||||||
<div className="absolute inset-y-0 left-0 flex overflow-hidden rounded-lg" style={{ width: pct(belegt) }}>
|
<div className="absolute inset-y-0 left-0 flex overflow-hidden rounded-lg" style={{ width: pct(belegt) }}>
|
||||||
<div className="h-full bg-cyan" style={{ width: `${belegt ? (dateien / belegt) * 100 : 0}%` }} />
|
<div className="h-full bg-cyan" style={{ width: `${belegt ? (dateien / belegt) * 100 : 0}%` }} />
|
||||||
<div className="h-full bg-cyan/35" style={{ width: `${belegt ? (rest / belegt) * 100 : 0}%` }} />
|
<div className="h-full bg-cyan/35" style={{ width: `${belegt ? (rest / belegt) * 100 : 0}%` }} />
|
||||||
@@ -69,8 +69,8 @@ function Rolle({ titel, modell, zwilling, laeuft, nutzer }: {
|
|||||||
const laden = useModellLaden()
|
const laden = useModellLaden()
|
||||||
if (!modell) {
|
if (!modell) {
|
||||||
return (
|
return (
|
||||||
<article className="flex flex-col gap-3.5 rounded-2xl border border-dashed border-linie-stark p-5">
|
<article className="flex flex-col gap-3.5 rounded-2xl border border-dashed border-white/15 p-5">
|
||||||
<div className="flex items-center justify-between"><span className="schild text-sm text-text-3">{titel}</span><Chip>Frei</Chip></div>
|
<div className="flex items-center justify-between"><span className="etikett text-[11px] tracking-[0.14em] text-text-3">{titel}</span><Chip>Frei</Chip></div>
|
||||||
<h3 className="font-anzeige text-xl font-semibold text-text-2">Nicht belegt</h3>
|
<h3 className="font-anzeige text-xl font-semibold text-text-2">Nicht belegt</h3>
|
||||||
<p className="text-sm text-text-2">Nur belegen, wenn ein Modell hier echten Mehrwert bringt. Das Radar meldet Kandidaten.</p>
|
<p className="text-sm text-text-2">Nur belegen, wenn ein Modell hier echten Mehrwert bringt. Das Radar meldet Kandidaten.</p>
|
||||||
</article>
|
</article>
|
||||||
@@ -80,9 +80,9 @@ function Rolle({ titel, modell, zwilling, laeuft, nutzer }: {
|
|||||||
// Seit 24.09.: Hirn und Coder sehen über ihren Bild-Zwilling (gleiche Gewichte, ohne Draft).
|
// Seit 24.09.: Hirn und Coder sehen über ihren Bild-Zwilling (gleiche Gewichte, ohne Draft).
|
||||||
const bilder = Boolean(modell.capabilities?.vision) || Boolean(zwilling)
|
const bilder = Boolean(modell.capabilities?.vision) || Boolean(zwilling)
|
||||||
return (
|
return (
|
||||||
<article className="flex flex-col gap-3.5 rounded-2xl border border-linie bg-panel p-5">
|
<article className="flex flex-col gap-3.5 flaeche rounded-2xl p-5">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="schild text-sm text-text-3">{titel}</span>
|
<span className="etikett text-[11px] tracking-[0.14em] text-text-3">{titel}</span>
|
||||||
<Chip art={laeuft ? "gruen" : "grau"}>{laeuft ? "Geladen" : "Auf Abruf"}</Chip>
|
<Chip art={laeuft ? "gruen" : "grau"}>{laeuft ? "Geladen" : "Auf Abruf"}</Chip>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="ziffern m-0 text-lg font-medium break-all">{modell.name}</h3>
|
<h3 className="ziffern m-0 text-lg font-medium break-all">{modell.name}</h3>
|
||||||
@@ -116,14 +116,14 @@ function Vergleich({ test }: { test: RadarTest }) {
|
|||||||
const zeilen = Object.entries(test.vergleich ?? {}) as [string, Vergleichszeile][]
|
const zeilen = Object.entries(test.vergleich ?? {}) as [string, Vergleichszeile][]
|
||||||
if (zeilen.length === 0) return null
|
if (zeilen.length === 0) return null
|
||||||
return (
|
return (
|
||||||
<div role="table" aria-label="Messwerte gegen das heutige Modell" className="rounded-lg border border-linie bg-erhaben/50 px-3 py-2 text-sm">
|
<div role="table" aria-label="Messwerte gegen das heutige Modell" className="flaeche-innen rounded-xl px-3 py-2 text-sm">
|
||||||
<div role="row" className="schild grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 pb-1 text-xs text-text-3">
|
<div role="row" className="etikett grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 pb-1 text-[10.5px] tracking-[0.14em] text-text-3">
|
||||||
<span role="columnheader">Messwert</span>
|
<span role="columnheader">Messwert</span>
|
||||||
<span role="columnheader" className="text-right">Kandidat</span>
|
<span role="columnheader" className="text-right">Kandidat</span>
|
||||||
<span role="columnheader" className="text-right">Heute</span>
|
<span role="columnheader" className="text-right">Heute</span>
|
||||||
</div>
|
</div>
|
||||||
{zeilen.map(([name, z]) => (
|
{zeilen.map(([name, z]) => (
|
||||||
<div key={name} role="row" className="grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 border-t border-linie py-1">
|
<div key={name} role="row" className="grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 border-t border-white/[0.06] py-1">
|
||||||
<span role="rowheader" className="text-text-2">{name}</span>
|
<span role="rowheader" className="text-text-2">{name}</span>
|
||||||
<span role="cell" className={`ziffern text-right ${URTEIL_FARBE[z.urteil ?? ""] ?? "text-text-2"}`}>
|
<span role="cell" className={`ziffern text-right ${URTEIL_FARBE[z.urteil ?? ""] ?? "text-text-2"}`}>
|
||||||
{messwert(z.kandidat, z.einheit)}
|
{messwert(z.kandidat, z.einheit)}
|
||||||
@@ -142,7 +142,7 @@ export function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest
|
|||||||
const offen = ["bestanden", "wartet", "neu", "getestet"].includes(k.status)
|
const offen = ["bestanden", "wartet", "neu", "getestet"].includes(k.status)
|
||||||
const hirn = k.rolle === "hirn"
|
const hirn = k.rolle === "hirn"
|
||||||
return (
|
return (
|
||||||
<li className="flex flex-col gap-2 border-t border-linie py-3.5">
|
<li className="flex flex-col gap-2 border-t border-white/[0.06] py-3.5">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="ziffern text-[15px] font-medium">{k.name}</span>
|
<span className="ziffern text-[15px] font-medium">{k.name}</span>
|
||||||
<Chip art="cyan">{hirn ? "fürs Hirn" : "fürs Coden"}</Chip>
|
<Chip art="cyan">{hirn ? "fürs Hirn" : "fürs Coden"}</Chip>
|
||||||
@@ -294,7 +294,7 @@ function RadarPanel({ hirn, coder }: { hirn: string | undefined; coder: string |
|
|||||||
)}
|
)}
|
||||||
{d && d.tests.length > 0 && (
|
{d && d.tests.length > 0 && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h3 className="schild text-xs text-text-3">Letzte Tests</h3>
|
<h3 className="etikett text-[10.5px] tracking-[0.12em] text-text-3">Letzte Tests</h3>
|
||||||
{d.tests.slice(0, 3).map((t) => (
|
{d.tests.slice(0, 3).map((t) => (
|
||||||
<p key={t.id} className="text-sm text-text-2">
|
<p key={t.id} className="text-sm text-text-2">
|
||||||
<span className="ziffern text-text-3">{flugplanZeit(t.datum)}</span> · {t.zusammenfassung}
|
<span className="ziffern text-text-3">{flugplanZeit(t.datum)}</span> · {t.zusammenfassung}
|
||||||
@@ -312,7 +312,7 @@ const LOESCHKNOPF = "text-rot-text hover:bg-rot-grund hover:text-rot-text"
|
|||||||
/** Eine eingeklappte Liste mit Zusammenfassung — lange Aufräum-Listen erst auf Wunsch. */
|
/** Eine eingeklappte Liste mit Zusammenfassung — lange Aufräum-Listen erst auf Wunsch. */
|
||||||
function Klappe({ titel, zusatz, children }: { titel: string; zusatz: string; children: ReactNode }) {
|
function Klappe({ titel, zusatz, children }: { titel: string; zusatz: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<details className="group rounded-xl border border-linie bg-erhaben/40 px-4 open:pb-2">
|
<details className="group flaeche-innen rounded-2xl px-4 open:pb-2">
|
||||||
<summary className="flex min-h-12 cursor-pointer list-none items-center justify-between gap-3 [&::-webkit-details-marker]:hidden">
|
<summary className="flex min-h-12 cursor-pointer list-none items-center justify-between gap-3 [&::-webkit-details-marker]:hidden">
|
||||||
<span className="flex items-center gap-2 text-[15px] font-medium">
|
<span className="flex items-center gap-2 text-[15px] font-medium">
|
||||||
<ChevronRight className="size-4 text-text-3 transition-transform group-open:rotate-90" aria-hidden />
|
<ChevronRight className="size-4 text-text-3 transition-transform group-open:rotate-90" aria-hidden />
|
||||||
@@ -358,7 +358,7 @@ function AufraeumenPanel() {
|
|||||||
<Klappe titel="Modelle und Ordner" zusatz={`${kandidaten.length} Einträge · bis zu ${summe.toLocaleString("de-DE", { maximumFractionDigits: 0 })} GB`}>
|
<Klappe titel="Modelle und Ordner" zusatz={`${kandidaten.length} Einträge · bis zu ${summe.toLocaleString("de-DE", { maximumFractionDigits: 0 })} GB`}>
|
||||||
<ul className="m-0 flex list-none flex-col p-0">
|
<ul className="m-0 flex list-none flex-col p-0">
|
||||||
{kandidaten.map((k) => (
|
{kandidaten.map((k) => (
|
||||||
<li key={`${k.art}-${k.name}`} className="flex flex-wrap items-center justify-between gap-3 border-t border-linie py-3">
|
<li key={`${k.art}-${k.name}`} className="flex flex-wrap items-center justify-between gap-3 border-t border-white/[0.06] py-3">
|
||||||
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
||||||
<span className="flex flex-wrap items-center gap-2">
|
<span className="flex flex-wrap items-center gap-2">
|
||||||
<span className="ziffern text-sm break-all">{k.name}</span>
|
<span className="ziffern text-sm break-all">{k.name}</span>
|
||||||
@@ -426,7 +426,7 @@ function Altreste() {
|
|||||||
</p>
|
</p>
|
||||||
<ul className="m-0 flex list-none flex-col p-0">
|
<ul className="m-0 flex list-none flex-col p-0">
|
||||||
{reste.map((a) => (
|
{reste.map((a) => (
|
||||||
<li key={a.id} className="flex flex-wrap items-center justify-between gap-3 border-t border-linie py-3">
|
<li key={a.id} className="flex flex-wrap items-center justify-between gap-3 border-t border-white/[0.06] py-3">
|
||||||
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
||||||
<span className="flex flex-wrap items-center gap-2">
|
<span className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-sm">{a.name}</span>
|
<span className="text-sm">{a.name}</span>
|
||||||
@@ -521,7 +521,7 @@ export function ModelleSeite() {
|
|||||||
{weitere.length === 0 && <p className="text-[15px] text-text-2">Keine weiteren Einträge.</p>}
|
{weitere.length === 0 && <p className="text-[15px] text-text-2">Keine weiteren Einträge.</p>}
|
||||||
<ul className="m-0 flex list-none flex-col p-0">
|
<ul className="m-0 flex list-none flex-col p-0">
|
||||||
{weitere.map((m) => (
|
{weitere.map((m) => (
|
||||||
<li key={m.name} className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-t border-linie py-2.5 sm:grid-cols-[minmax(0,1fr)_120px_160px]">
|
<li key={m.name} className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-t border-white/[0.06] py-2.5 sm:grid-cols-[minmax(0,1fr)_120px_160px]">
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
<span className="ziffern block text-sm break-all">{m.name}</span>
|
<span className="ziffern block text-sm break-all">{m.name}</span>
|
||||||
<span className="text-xs text-text-3">{m.aliases.length ? m.aliases.join(", ") : "ohne Rolle"}</span>
|
<span className="text-xs text-text-3">{m.aliases.length ? m.aliases.join(", ") : "ohne Rolle"}</span>
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { type ReactNode, useEffect, useState } from "react"
|
|||||||
import { useRouterState } from "@tanstack/react-router"
|
import { useRouterState } from "@tanstack/react-router"
|
||||||
import { cn } from "cn"
|
import { cn } from "cn"
|
||||||
import { Zustandsfeld } from "@/components/cockpit/Chip"
|
import { Zustandsfeld } from "@/components/cockpit/Chip"
|
||||||
|
import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol"
|
||||||
import { Panel } from "@/components/cockpit/Panel"
|
import { Panel } from "@/components/cockpit/Panel"
|
||||||
import { Kennzahl } from "@/components/diagramme/Kennzahl"
|
import { Kennzahl } from "@/components/diagramme/Kennzahl"
|
||||||
import { Verlauf } from "@/components/diagramme/Verlauf"
|
import { Verlauf } from "@/components/diagramme/Verlauf"
|
||||||
import { useBoxMesswerte, useHomelabMesswerte, useInstanz, usePartner } from "@/lib/abfragen"
|
import { useBoxMesswerte, useHomelabMesswerte, useInstanz, usePartner } from "@/lib/abfragen"
|
||||||
|
import { geraetebild } from "@/lib/geraetebild"
|
||||||
import { laufzeit, letzterWert, zustandBei } from "@/lib/messung"
|
import { laufzeit, letzterWert, zustandBei } from "@/lib/messung"
|
||||||
import { useLetzteMetrik } from "@/lib/strom"
|
import { useLetzteMetrik } from "@/lib/strom"
|
||||||
import type { GeraetMesswerte, Zeitraum } from "@/lib/typen"
|
import type { GeraetMesswerte, Zeitraum } from "@/lib/typen"
|
||||||
@@ -25,7 +27,7 @@ function ZeitraumWahl({ wert, onWahl, farbe }: { wert: Zeitraum; onWahl: (z: Zei
|
|||||||
<div role="group" aria-label="Zeitraum" className="flex flex-wrap gap-2">
|
<div role="group" aria-label="Zeitraum" className="flex flex-wrap gap-2">
|
||||||
{ZEITRAEUME.map((z) => (
|
{ZEITRAEUME.map((z) => (
|
||||||
<button key={z.id} type="button" onClick={() => onWahl(z.id)} aria-pressed={wert === z.id}
|
<button key={z.id} type="button" onClick={() => onWahl(z.id)} aria-pressed={wert === z.id}
|
||||||
className={cn("h-9 rounded-full border px-4 text-sm", wert === z.id ? aktiv : "border-linie text-text-2 hover:border-linie-stark hover:text-foreground")}>
|
className={cn("h-9 rounded-full border px-4 text-sm", wert === z.id ? aktiv : "border-white/10 text-text-2 hover:border-white/20 hover:text-foreground")}>
|
||||||
{z.name}
|
{z.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -35,9 +37,9 @@ function ZeitraumWahl({ wert, onWahl, farbe }: { wert: Zeitraum; onWahl: (z: Zei
|
|||||||
|
|
||||||
function Diagrammkarte({ titel, rechts, children }: { titel: string; rechts?: ReactNode; children: ReactNode }) {
|
function Diagrammkarte({ titel, rechts, children }: { titel: string; rechts?: ReactNode; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<section aria-label={titel} className="flex min-w-0 flex-col gap-3 rounded-2xl border border-linie bg-panel p-4 sm:p-5">
|
<section aria-label={titel} className="flex min-w-0 flex-col gap-3 flaeche rounded-2xl p-4 sm:p-5">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h3 className="schild text-sm tracking-[0.18em] text-text-2">{titel}</h3>
|
<h3 className="etikett text-[11.5px] tracking-[0.16em] text-text-2">{titel}</h3>
|
||||||
{rechts}
|
{rechts}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
@@ -95,9 +97,12 @@ export function BoxMonitoringSeite() {
|
|||||||
function GastKarte({ g, zeitraum }: { g: GeraetMesswerte; zeitraum: Zeitraum }) {
|
function GastKarte({ g, zeitraum }: { g: GeraetMesswerte; zeitraum: Zeitraum }) {
|
||||||
const a = g.aktuell
|
const a = g.aktuell
|
||||||
return (
|
return (
|
||||||
<section id={g.id} aria-label={g.name} className="flex min-w-0 scroll-mt-6 flex-col gap-3 rounded-2xl border border-linie bg-panel p-4 sm:p-5">
|
<section id={g.id} aria-label={g.name} className="flex min-w-0 scroll-mt-6 flex-col gap-3 flaeche rounded-2xl p-4 sm:p-5">
|
||||||
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<h3 className="text-[15px] font-semibold">{g.name}</h3>
|
<span className="flex min-w-0 items-center gap-3">
|
||||||
|
<Geraetesymbol bild={geraetebild({ art: g.art, name: g.name })} groesse="sm" />
|
||||||
|
<h3 className="text-[15px] font-semibold">{g.name}</h3>
|
||||||
|
</span>
|
||||||
<span className="ziffern text-xs text-text-3">
|
<span className="ziffern text-xs text-text-3">
|
||||||
CPU {a.cpu != null ? `${Math.round(a.cpu)} %` : "–"} · RAM {a.ram != null ? `${Math.round(a.ram)} %` : "–"} · läuft {laufzeit(a.uptime_s)}
|
CPU {a.cpu != null ? `${Math.round(a.cpu)} %` : "–"} · RAM {a.ram != null ? `${Math.round(a.ram)} %` : "–"} · läuft {laufzeit(a.uptime_s)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { CSSProperties } from "react"
|
||||||
import { Link } from "@tanstack/react-router"
|
import { Link } from "@tanstack/react-router"
|
||||||
import { Checkliste } from "@/components/cockpit/Checkliste"
|
import { Checkliste } from "@/components/cockpit/Checkliste"
|
||||||
import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||||
@@ -25,7 +26,7 @@ function Instrumente({ box }: { box: Start["box"] }) {
|
|||||||
const ramAnteil = ramBelegt != null && ramGesamt ? ramBelegt / ramGesamt : null
|
const ramAnteil = ramBelegt != null && ramGesamt ? ramBelegt / ramGesamt : null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section aria-label="Instrumente" className="grid grid-cols-2 gap-x-4 gap-y-6 rounded-2xl border border-linie bg-panel px-4 py-5 lg:grid-cols-4 lg:px-6">
|
<section aria-label="Instrumente" className="auftauchen flaeche grid grid-cols-2 gap-x-4 gap-y-6 rounded-3xl px-4 py-5 lg:grid-cols-4 lg:px-6" style={{ "--i": 1 } as CSSProperties}>
|
||||||
<Instrument
|
<Instrument
|
||||||
beschriftung="Speicher"
|
beschriftung="Speicher"
|
||||||
anteil={ramAnteil}
|
anteil={ramAnteil}
|
||||||
@@ -48,7 +49,7 @@ function Instrumente({ box }: { box: Start["box"] }) {
|
|||||||
<Instrument
|
<Instrument
|
||||||
beschriftung="Platte"
|
beschriftung="Platte"
|
||||||
anteil={platte ? platte.percent / 100 : null}
|
anteil={platte ? platte.percent / 100 : null}
|
||||||
farbe={platte && platte.percent >= 90 ? "var(--rot)" : platte && platte.percent >= 80 ? "var(--bernstein)" : "var(--cyan)"}
|
farbe={platte && platte.percent >= 90 ? "var(--rot)" : platte && platte.percent >= 80 ? "var(--bernstein)" : "var(--box)"}
|
||||||
mitte={platte ? `${Math.round(platte.percent)}%` : "–"}
|
mitte={platte ? `${Math.round(platte.percent)}%` : "–"}
|
||||||
unter={platte ? `${gbText(platte.used)} / ${gbText(platte.total)}` : undefined}
|
unter={platte ? `${gbText(platte.used)} / ${gbText(platte.total)}` : undefined}
|
||||||
ariaText={platte ? `Platte zu ${Math.round(platte.percent)} Prozent belegt` : "Platte unbekannt"}
|
ariaText={platte ? `Platte zu ${Math.round(platte.percent)} Prozent belegt` : "Platte unbekannt"}
|
||||||
@@ -65,9 +66,9 @@ function RadarKasten({ radar }: { radar: Radar | undefined }) {
|
|||||||
const offen = radar.kandidaten.filter((k) => ["bestanden", "wartet", "neu"].includes(k.status))
|
const offen = radar.kandidaten.filter((k) => ["bestanden", "wartet", "neu"].includes(k.status))
|
||||||
const vorn = offen.find((k) => k.status === "bestanden") ?? offen[0]
|
const vorn = offen.find((k) => k.status === "bestanden") ?? offen[0]
|
||||||
return (
|
return (
|
||||||
<div className="mt-auto flex flex-wrap items-center justify-between gap-3 rounded-xl border border-cyan-rand bg-cyan-grund px-4 py-3.5">
|
<div className="mt-auto flex flex-wrap items-center justify-between gap-3 rounded-2xl bg-cyan/[0.06] px-4 py-3.5 ring-1 ring-cyan/30 ring-inset">
|
||||||
<div className="flex min-w-0 flex-col gap-1">
|
<div className="flex min-w-0 flex-col gap-1">
|
||||||
<span className="schild text-sm text-cyan">
|
<span className="etikett text-[11px] tracking-[0.14em] text-cyan-text">
|
||||||
Radar · {offen.length} Kandidat{offen.length === 1 ? "" : "en"}
|
Radar · {offen.length} Kandidat{offen.length === 1 ? "" : "en"}
|
||||||
</span>
|
</span>
|
||||||
{vorn ? (
|
{vorn ? (
|
||||||
@@ -109,7 +110,7 @@ export function StartSeite() {
|
|||||||
<Panel
|
<Panel
|
||||||
titel="Checkliste"
|
titel="Checkliste"
|
||||||
id="checkliste"
|
id="checkliste"
|
||||||
rechts={<span className={offen ? "ziffern text-sm text-bernstein" : "ziffern text-sm text-gruen"}>{offen ? `${offen} offen` : "alles erledigt"}</span>}
|
rechts={<Chip art={offen ? "bernstein" : "gruen"}>{offen ? `${offen} offen` : "alles erledigt"}</Chip>}
|
||||||
>
|
>
|
||||||
<Checkliste hinweise={start.hinweise} verlauf={start.verlauf} />
|
<Checkliste hinweise={start.hinweise} verlauf={start.verlauf} />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function Zusammenfassung({ text }: { text: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-expanded={offen}
|
aria-expanded={offen}
|
||||||
onClick={() => setOffen(!offen)}
|
onClick={() => setOffen(!offen)}
|
||||||
className="-mb-2 inline-flex h-11 items-center text-sm text-cyan hover:underline"
|
className="-mb-2 inline-flex h-11 items-center text-sm text-cyan-text hover:underline"
|
||||||
>
|
>
|
||||||
{offen ? "Weniger zeigen" : "Ganze Zusammenfassung zeigen"}
|
{offen ? "Weniger zeigen" : "Ganze Zusammenfassung zeigen"}
|
||||||
</button>
|
</button>
|
||||||
@@ -69,13 +69,13 @@ function Paketliste({ d }: { d: UpdateDetails }) {
|
|||||||
if (pakete.length === 0 && zurueck.length === 0) return null
|
if (pakete.length === 0 && zurueck.length === 0) return null
|
||||||
return (
|
return (
|
||||||
<details className="group">
|
<details className="group">
|
||||||
<summary className="inline-flex h-11 cursor-pointer items-center gap-1 text-sm text-cyan hover:underline">
|
<summary className="inline-flex h-11 cursor-pointer items-center gap-1 text-sm text-cyan-text hover:underline">
|
||||||
<ChevronRight aria-hidden className="size-4 transition-transform group-open:rotate-90" />
|
<ChevronRight aria-hidden className="size-4 transition-transform group-open:rotate-90" />
|
||||||
{pakete.length === 0
|
{pakete.length === 0
|
||||||
? `Was Ubuntu zurückhält (${zurueck.length})`
|
? `Was Ubuntu zurückhält (${zurueck.length})`
|
||||||
: pakete.length === 1 ? "Das Paket ansehen" : `Alle ${pakete.length} Pakete ansehen`}
|
: pakete.length === 1 ? "Das Paket ansehen" : `Alle ${pakete.length} Pakete ansehen`}
|
||||||
</summary>
|
</summary>
|
||||||
<ul className="ziffern m-0 flex max-h-64 list-none flex-col gap-1 overflow-auto rounded-md border border-linie bg-[#0b0d0f] p-2.5 text-xs">
|
<ul className="ziffern m-0 flex max-h-64 list-none flex-col gap-1 overflow-auto rounded-xl bg-black/30 ring-1 ring-white/[0.07] ring-inset p-2.5 text-xs">
|
||||||
{pakete.map((p) => (
|
{pakete.map((p) => (
|
||||||
<li key={p.name} className="break-all">
|
<li key={p.name} className="break-all">
|
||||||
<span className="text-foreground">{p.name}</span>{" "}
|
<span className="text-foreground">{p.name}</span>{" "}
|
||||||
@@ -139,7 +139,7 @@ function BausteinZeile({ id, name, details, hatUpdate, neuLautStart, fest, gespe
|
|||||||
const neuZelle = laedt ? (
|
const neuZelle = laedt ? (
|
||||||
<Skelett breit="w-20" />
|
<Skelett breit="w-20" />
|
||||||
) : neu ? (
|
) : neu ? (
|
||||||
<span className="text-cyan">{neu}</span>
|
<span className="text-cyan-text">{neu}</span>
|
||||||
) : fehler ? (
|
) : fehler ? (
|
||||||
<span className="text-bernstein">nicht geprüft</span>
|
<span className="text-bernstein">nicht geprüft</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -176,7 +176,7 @@ function BausteinZeile({ id, name, details, hatUpdate, neuLautStart, fest, gespe
|
|||||||
<div
|
<div
|
||||||
role="row"
|
role="row"
|
||||||
aria-busy={laedt || undefined}
|
aria-busy={laedt || undefined}
|
||||||
className="grid grid-cols-1 gap-x-4 gap-y-2 border-t border-linie py-3.5 md:grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] md:items-start"
|
className="grid grid-cols-1 gap-x-4 gap-y-2 border-t border-white/[0.06] py-3.5 md:grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] md:items-start"
|
||||||
>
|
>
|
||||||
<span role="rowheader" className="text-[15px] font-semibold">{name}</span>
|
<span role="rowheader" className="text-[15px] font-semibold">{name}</span>
|
||||||
<span role="cell" className="ziffern -mt-1 text-sm text-text-2 md:hidden">
|
<span role="cell" className="ziffern -mt-1 text-sm text-text-2 md:hidden">
|
||||||
@@ -221,7 +221,7 @@ const PUNKT: Record<UpdateLauf["stufe"], string> = {
|
|||||||
/** Ein Update-Lauf mit dem Ergebnis je Baustein — so, wie autoupdate.sh es gemeldet hat. */
|
/** Ein Update-Lauf mit dem Ergebnis je Baustein — so, wie autoupdate.sh es gemeldet hat. */
|
||||||
function LaufEintrag({ lauf }: { lauf: UpdateLauf }) {
|
function LaufEintrag({ lauf }: { lauf: UpdateLauf }) {
|
||||||
return (
|
return (
|
||||||
<li className="flex flex-col gap-2 border-t border-linie py-3">
|
<li className="flex flex-col gap-2 border-t border-white/[0.06] py-3">
|
||||||
{/* Schmales Feld: Zeit und Ergebnis oben, der Anlass darunter über die volle Breite. */}
|
{/* Schmales Feld: Zeit und Ergebnis oben, der Anlass darunter über die volle Breite. */}
|
||||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 @sm:grid-cols-[112px_minmax(0,1fr)_auto]">
|
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 @sm:grid-cols-[112px_minmax(0,1fr)_auto]">
|
||||||
<span className="ziffern col-start-1 row-start-1 text-sm text-text-3">{flugplanZeit(lauf.start)}</span>
|
<span className="ziffern col-start-1 row-start-1 text-sm text-text-3">{flugplanZeit(lauf.start)}</span>
|
||||||
@@ -297,7 +297,7 @@ export function UpdatesSeite() {
|
|||||||
<div role="table" aria-label="Bausteine und ihre Updates" className="flex flex-col">
|
<div role="table" aria-label="Bausteine und ihre Updates" className="flex flex-col">
|
||||||
<div
|
<div
|
||||||
role="row"
|
role="row"
|
||||||
className="schild hidden grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] gap-x-4 pb-2 text-xs font-semibold text-text-3 md:grid"
|
className="etikett hidden grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] gap-x-4 pb-2 text-[10.5px] tracking-[0.14em] text-text-3 md:grid"
|
||||||
>
|
>
|
||||||
<span role="columnheader">Baustein</span>
|
<span role="columnheader">Baustein</span>
|
||||||
<span role="columnheader">Läuft</span>
|
<span role="columnheader">Läuft</span>
|
||||||
@@ -352,7 +352,7 @@ export function UpdatesSeite() {
|
|||||||
sicherungsInhalt = (
|
sicherungsInhalt = (
|
||||||
<ul className="m-0 flex list-none flex-col p-0">
|
<ul className="m-0 flex list-none flex-col p-0">
|
||||||
{sicherungen.data.backups.slice(0, 5).map((s) => (
|
{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">
|
<li key={s.file} className="flex items-center justify-between gap-3 border-t border-white/[0.06] py-2.5">
|
||||||
<span className="ziffern text-sm">{sicherungsZeit(s)}</span>
|
<span className="ziffern text-sm">{sicherungsZeit(s)}</span>
|
||||||
<span className="flex items-center gap-3">
|
<span className="flex items-center gap-3">
|
||||||
<span className="ziffern text-xs text-text-3">{s.size_mb.toLocaleString("de-DE")} MB</span>
|
<span className="ziffern text-xs text-text-3">{s.size_mb.toLocaleString("de-DE")} MB</span>
|
||||||
@@ -395,7 +395,7 @@ export function UpdatesSeite() {
|
|||||||
wird gesichert, danach geprüft.
|
wird gesichert, danach geprüft.
|
||||||
</p>
|
</p>
|
||||||
{updateLaeuft && (
|
{updateLaeuft && (
|
||||||
<p role="status" className="text-[15px] text-cyan">
|
<p role="status" className="text-[15px] text-cyan-text">
|
||||||
Gerade läuft ein Update. Weitere lassen sich erst starten, wenn es fertig ist.
|
Gerade läuft ein Update. Weitere lassen sich erst starten, wenn es fertig ist.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user