Welle 1 · Vorhersage „voll in etwa N Tagen“: Pool, NAS, Systemplatte, Container, VMs und die Platte der KI-Box

Aus dem Verlauf der Woche (Tagesmittel, Schutz gegen einmalige Sprünge, schneller werdendes Volllaufen zählt sofort):
gelb unter 14, rot unter 3 Tagen. Der Wächter meldet es, auch wenn der Füllstand selbst noch unauffällig ist;
Speicherkarte, Geräte-Tabelle, Instrumente der Übersicht und die KI-Box-Seite zeigen es an. Jeder Bericht des
Ausführers schreibt dafür die Belegungen in die Messreihe „speicher“.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 16:21:38 +02:00
co-authored by Claude Opus 5.5
parent 34360c9644
commit f7a9b8ad50
19 changed files with 691 additions and 72 deletions
@@ -15,6 +15,7 @@ export function Instrument({
unter,
beschriftung,
ariaText,
hinweis,
}: {
anteil: number | null
farbe?: string
@@ -23,6 +24,8 @@ export function Instrument({
unter?: string
beschriftung: string
ariaText: string
/** Eine Zeile unter der Beschriftung, etwa „voll in etwa 9 Tagen“ (seit 25.09.2026); className setzt die Farbe. */
hinweis?: { text: string; className: string } | null
}) {
const zeigen = anteil != null && anteil > 0.004
return (
@@ -46,6 +49,7 @@ export function Instrument({
)}
</svg>
<figcaption className="etikett text-[11.5px] tracking-[0.14em] text-text-3">{beschriftung}</figcaption>
{hinweis && <span className={`text-center text-xs ${hinweis.className}`}>{hinweis.text}</span>}
</figure>
)
}
@@ -2,6 +2,7 @@ import type { CSSProperties } from "react"
import { cn } from "cn"
import type { Bereich } from "@/lib/navigation"
import { type Einheit, wertText } from "@/lib/messung"
import { STUFE_FARBE, type VorhersageStufe } from "@/lib/prognose"
import type { Messpunkt } from "@/lib/typen"
import { Sparkline } from "./Sparkline"
@@ -26,7 +27,9 @@ function randpunkt(anteil: number, r: number) {
return { x: MITTE + r * Math.cos(winkel), y: MITTE + r * Math.sin(winkel) }
}
export function Messring({ label, wert, einheit, punkte, zustand = "ruhig", bereich, grenzen, max = 100, zeitraumText, i = 0 }: {
export function Messring({
label, wert, einheit, punkte, zustand = "ruhig", bereich, grenzen, max = 100, zeitraumText, i = 0, hinweis,
}: {
label: string
wert: number | null | undefined
einheit: Einheit
@@ -39,6 +42,8 @@ export function Messring({ label, wert, einheit, punkte, zustand = "ruhig", bere
zeitraumText: string
/** Reihenfolge für das Auftauchen. */
i?: number
/** Eine Zeile unter der Linie, etwa „voll in etwa 9 Tagen“ (seit 25.09.2026). */
hinweis?: { text: string; stufe: VorhersageStufe } | null
}) {
const z = ZUSTAND[zustand]
const farbe = zustand === "rot" ? "var(--rot)" : zustand === "gelb" ? "var(--bernstein)" : bereich === "box" ? "var(--box)" : "var(--lab)"
@@ -77,6 +82,7 @@ export function Messring({ label, wert, einheit, punkte, zustand = "ruhig", bere
? <Sparkline punkte={punkte} max={max} farbe={farbe} hoehe={20} beschriftung={`${label}, ${zeitraumText}`} />
: <span className="block h-5" aria-hidden />}
</div>
{hinweis && <span className={cn("text-center text-[11px] leading-tight", STUFE_FARBE[hinweis.stufe])}>{hinweis.text}</span>}
</div>
)
}
+12 -3
View File
@@ -4,6 +4,7 @@ import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol"
import { geraetebild } from "@/lib/geraetebild"
import { artText, plattenAnteil } from "@/lib/homelab"
import { laufzeit } from "@/lib/messung"
import { knappeVorhersage, STUFE_FARBE } from "@/lib/prognose"
import type { GeraetMesswerte, ZielStand } from "@/lib/typen"
const SPALTEN = "md:grid-cols-[minmax(0,1.35fr)_110px_minmax(0,1fr)_minmax(0,1.2fr)_minmax(0,1fr)_90px]"
@@ -11,8 +12,14 @@ const SPALTEN = "md:grid-cols-[minmax(0,1.35fr)_110px_minmax(0,1fr)_minmax(0,1.2
const gb = (b: number) => (b / 1e9).toFixed(1).replace(".", ",")
/** Balken mit Wert (v5): ruhig in der Farbe des Homelabs, ab gelb bernstein, ab rot rot — der Text sagt es zusätzlich
* in Zahlen. */
function Balken({ wert, gelb, rot, zusatz }: { wert: number | null | undefined; gelb: number; rot: number; zusatz?: string }) {
* in Zahlen. Darunter auf Wunsch eine Zeile (bei der Platte: „voll in etwa 9 Tagen“, seit 25.09.2026). */
function Balken({ wert, gelb, rot, zusatz, unter }: {
wert: number | null | undefined
gelb: number
rot: number
zusatz?: string
unter?: { text: string; stufe: keyof typeof STUFE_FARBE } | null
}) {
if (wert === null || wert === undefined) return <span className="text-sm text-text-3">–</span>
const farbe = wert >= rot ? "var(--rot)" : wert >= gelb ? "var(--bernstein)" : "color-mix(in srgb, var(--lab) 80%, transparent)"
const text = wert >= rot ? "text-rot-text" : wert >= gelb ? "text-bernstein" : "text-text-2"
@@ -24,6 +31,7 @@ function Balken({ wert, gelb, rot, zusatz }: { wert: number | null | undefined;
<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, wert))}%`, background: farbe }} />
</span>
{unter && <span className={cn("text-xs", STUFE_FARBE[unter.stufe])}>{unter.text}</span>}
</span>
)
}
@@ -43,6 +51,7 @@ function rang(z: ZielStand): number {
*/
export function Gesundheit({ ziele, messwerte }: { ziele: ZielStand[]; messwerte: GeraetMesswerte[] | undefined }) {
const mess = new Map((messwerte ?? []).map((g) => [g.id, g.aktuell]))
const prognose = new Map((messwerte ?? []).map((g) => [g.id, g.prognose?.platte]))
const sortiert = [...ziele].sort((a, b) => rang(a) - rang(b))
return (
<div role="table" aria-label="Zustand der Geräte" className="flex flex-col">
@@ -77,7 +86,7 @@ export function Gesundheit({ ziele, messwerte }: { ziele: ZielStand[]; messwerte
</span>
<span role="cell"><span className="text-xs text-text-3 md:hidden">CPU </span><Balken wert={a?.cpu} gelb={85} rot={95} /></span>
<span role="cell"><span className="text-xs text-text-3 md:hidden">Speicher </span><Balken wert={a?.ram} gelb={85} rot={95} zusatz={ram} /></span>
<span role="cell"><span className="text-xs text-text-3 md:hidden">Platte </span><Balken wert={platte} gelb={80} rot={90} /></span>
<span role="cell"><span className="text-xs text-text-3 md:hidden">Platte </span><Balken wert={platte} gelb={80} rot={90} unter={knappeVorhersage(prognose.get(z.id))} /></span>
<span role="cell" className="ziffern text-sm text-text-2"><span className="font-sans text-xs text-text-3 md:hidden">Läuft seit </span>{laufzeit(a?.uptime_s)}</span>
</div>
)
@@ -35,6 +35,21 @@ describe("Speicher", () => {
expect(screen.getByText(/Alle 8 Geräte gesichert/)).toBeInTheDocument()
})
it("sagt, wann es bei diesem Tempo voll ist (seit 25.09.2026)", () => {
render(<Speicher daten={{
...daten,
pools: [{ ...daten.pools[0], vorhersage: { tage: 9.4, rate: 5.4, anteil: 49, grund: null } }],
systemplatte: { ...daten.systemplatte!, vorhersage: { tage: null, rate: 0, anteil: 50, grund: "gleich" } },
nas: [{ ...daten.nas[0], vorhersage: { tage: null, rate: null, anteil: null, grund: "verlauf" } }],
sicherungen: { ...daten.sicherungen, ziele: [{ speicher: "pbs-qnap", art: "pbs", aktiv: true, belegt: 1_517_685_571_584,
gesamt: 3_847_503_609_856, vorhersage: { tage: 200, rate: 0.3, anteil: 39, grund: null } }] },
}} />)
const pool = screen.getByText("+5,4 % am Tag · voll in etwa 9 Tagen")
expect(pool).toHaveClass("text-bernstein")
expect(screen.getByText("Belegung gleichbleibend")).toBeInTheDocument()
expect(screen.getByText("Ziel pbs-qnap: 39 % belegt · voll in etwa 7 Monaten")).toHaveClass("text-text-3")
})
it("klappt die Vorschläge erst auf, wenn der Pool knapp wird", () => {
const { container, rerender } = render(<Speicher daten={daten} />)
const klappe = container.querySelector("details")!
+24 -1
View File
@@ -4,9 +4,11 @@ import { cn } from "cn"
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
import { Chip, type ChipArt } from "@/components/cockpit/Chip"
import { Button } from "@/components/ui/button"
import { STUFE_FARBE, tempoText, vollText, vorhersageStufe } from "@/lib/prognose"
import { auftraegeText, groesse, STUFE_TEXT } from "@/lib/speicher"
import type {
HomelabSpeicher, NasStand, SicherungsLage, SpeicherEmpfehlung, SpeicherPool, SpeicherStufe, Systemplatte, ZielAktion,
HomelabSpeicher, NasStand, SicherungsLage, SpeicherEmpfehlung, SpeicherPool, SpeicherStufe, Systemplatte, Vorhersage,
ZielAktion,
} from "@/lib/typen"
import { flugplanZeit } from "@/lib/zeit"
@@ -35,6 +37,15 @@ function Fuellstand({ anteil, stufe, name }: { anteil: number; stufe: SpeicherSt
)
}
/** Das Tempo der Belegung (seit 25.09.2026): „+1,2 % am Tag · voll in etwa 9 Tagen“, unter 14 Tagen bernstein, unter
* 3 Tagen rot. Ohne zwei Tage Verlauf steht nichts da. */
function Tempo({ v }: { v?: Vorhersage | null }) {
if (v?.grund === "gleich") return <p className="text-xs text-text-3">Belegung gleichbleibend</p>
const voll = vollText(v)
if (!voll) return null
return <p className={cn("ziffern text-xs", STUFE_FARBE[vorhersageStufe(v)])}>{tempoText(v)} · {voll}</p>
}
function Kachel({ titel, unter, chip, chipText, children }: {
titel: string
unter?: string
@@ -67,6 +78,7 @@ function PoolKachel({ p }: { p: SpeicherPool }) {
{p.meta_anteil !== null && (
<p className="text-xs text-text-3">Metadaten {Math.round(p.meta_anteil * 100)} % belegt</p>
)}
<Tempo v={p.vorhersage} />
</Kachel>
)
}
@@ -78,6 +90,7 @@ function SystemplatteKachel({ s }: { s: Systemplatte }) {
<p className="ziffern text-sm text-text-2">
{Math.round(s.anteil * 100)} % · {groesse(s.belegt)} von {groesse(s.gesamt)} · {groesse(s.frei)} frei
</p>
<Tempo v={s.vorhersage} />
</Kachel>
)
}
@@ -100,6 +113,7 @@ function NasKachel({ n }: { n: NasStand }) {
<p className="ziffern text-sm text-text-2">
{Math.round(n.anteil * 100)} % · {groesse(n.belegt)} von {groesse(n.gesamt)} · {groesse((n.gesamt ?? 0) - (n.belegt ?? 0))} frei
</p>
<Tempo v={n.vorhersage} />
</>
)
)}
@@ -131,6 +145,15 @@ function SicherungsKachel({ s }: { s: SicherungsLage }) {
))}
</ul>
)}
{s.ziele.filter((z) => z.gesamt).map((z) => {
const voll = vollText(z.vorhersage)
return (
<p key={z.speicher} className={cn("ziffern text-xs", voll ? STUFE_FARBE[vorhersageStufe(z.vorhersage)] : "text-text-3")}>
Ziel {z.speicher}: {Math.round(((z.belegt ?? 0) / (z.gesamt ?? 1)) * 100)} % belegt
{voll ? ` · ${voll}` : z.vorhersage?.grund === "gleich" ? " · gleichbleibend" : ""}
</p>
)
})}
</Kachel>
)
}