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:
co-authored by
Claude Opus 5.5
parent
34360c9644
commit
f7a9b8ad50
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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")!
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user