homelab: Wächter und Geräteliste sehen volle Gastplatten (AdGuard stand bei 100 %)

Der Ausfuehrer schickt die Belegung der rootfs laufender Container mit; ab 85 % gelb, ab 95 % rot, in der
Geraeteliste als eigene Spalte und auf der Lampe des Geraets. Anlass: AdGuard (2 GB) war voll, das
Abfrageprotokoll schrieb nicht mehr und die Paketsuche scheiterte, ohne dass es eine Anzeige sagte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 21:00:44 +02:00
co-authored by Claude Opus 5.5
parent eed36e9764
commit c08a144006
30 changed files with 146 additions and 32 deletions
@@ -3,7 +3,9 @@ import { RefreshCw } from "lucide-react"
import { cn } from "cn"
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
import { Button } from "@/components/ui/button"
import { appBaustein, artText, listenAlter, systemBaustein, versionText } from "@/lib/homelab"
import {
appBaustein, artText, listenAlter, PLATTE_GELB, PLATTE_ROT, plattenAnteil, systemBaustein, versionText,
} from "@/lib/homelab"
import type { BausteinStand, ZielAktion, ZielStand } from "@/lib/typen"
type Frage = { ziel: ZielStand; aktion: ZielAktion } | null
@@ -40,6 +42,19 @@ function SystemText({ b }: { b: BausteinStand }) {
)
}
/** Belegung der Platte: ruhig bis 85 %, dann bernstein, ab 95 % rot. */
function PlatteZelle({ ziel }: { ziel: ZielStand }) {
const anteil = plattenAnteil(ziel)
if (anteil === null) return <span className="text-text-3">–</span>
const farbe = anteil >= PLATTE_ROT ? "text-rot-text" : anteil >= PLATTE_GELB ? "text-bernstein" : "text-text-2"
const gb = (n: number) => (n / 1e9).toFixed(1).replace(".", ",")
return (
<span className={cn("ziffern text-sm", farbe)} title={`${gb(ziel.platte!.belegt)} von ${gb(ziel.platte!.gesamt)} GB belegt`}>
{Math.min(100, Math.round(anteil * 100))} %{anteil >= PLATTE_ROT ? " voll" : ""}
</span>
)
}
function Zeile({ ziel, laeuft, gesperrt, onFrage }: {
ziel: ZielStand
laeuft: string | null
@@ -53,7 +68,7 @@ function Zeile({ ziel, laeuft, gesperrt, onFrage }: {
return (
<div
role="row"
className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 border-t border-linie py-3 md:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_minmax(0,1.5fr)_130px] md:items-center"
className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 border-t border-linie py-3 md:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_minmax(0,1.5fr)_90px_130px] md:items-center"
>
<div role="rowheader" className="flex min-w-0 flex-col">
<span className="text-[15px] font-semibold break-words">{ziel.name}</span>
@@ -72,6 +87,10 @@ function Zeile({ ziel, laeuft, gesperrt, onFrage }: {
</Button>
)}
</span>
<span role="cell" className="col-span-2 md:col-span-1">
<span className="text-sm text-text-3 md:hidden">Platte </span>
<PlatteZelle ziel={ziel} />
</span>
<span
role="cell"
className={cn(
@@ -101,11 +120,12 @@ export function GeraeteListe({ ziele, laufend, gesperrt, onAktion }: {
<div role="table" aria-label="Geräte im Homelab" className="flex flex-col">
<div
role="row"
className="schild hidden grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_minmax(0,1.5fr)_130px] gap-x-4 pb-2 text-xs font-semibold text-text-3 md:grid"
className="schild hidden grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_minmax(0,1.5fr)_90px_130px] gap-x-4 pb-2 text-xs font-semibold text-text-3 md:grid"
>
<span role="columnheader">Gerät</span>
<span role="columnheader">App</span>
<span role="columnheader">Pakete</span>
<span role="columnheader">Platte</span>
<span role="columnheader">Netz</span>
</div>
{ziele.map((z) => (
@@ -80,4 +80,10 @@ describe("GeraeteListe", () => {
fireEvent.click(within(screen.getByRole("dialog")).getByRole("button", { name: "Nach Updates suchen" }))
expect(start).toHaveBeenCalledWith(suchen)
})
it("zeigt die Belegung der Platte und sagt „voll“ ab 95 %", () => {
render(<GeraeteListe ziele={[{ ...netbird, platte: { belegt: 1_960_000_000, gesamt: 2_000_000_000 } }]}
laufend={new Map()} gesperrt={false} onAktion={() => {}} />)
expect(screen.getByText("98 % voll")).toHaveAttribute("title", "2,0 von 2,0 GB belegt")
})
})
+6
View File
@@ -53,6 +53,12 @@ describe("lampeFuer", () => {
expect(lampeFuer(arcane, false).label).toBe("Arcane")
})
it("warnt bei voller Platte vor allem anderen außer Stille", () => {
expect(lampeFuer({ ...netbird, platte: { belegt: 98, gesamt: 100 } }, true)).toMatchObject({ zustand: "fehler", wert: "Platte voll" })
expect(lampeFuer({ ...npm, platte: { belegt: 90, gesamt: 100 } }, false)).toMatchObject({ zustand: "warn", wert: "Platte 90 %" })
expect(lampeFuer({ ...npm, platte: { belegt: 50, gesamt: 100 } }, false).zustand).toBe("ok")
})
it("meldet ein stummes Gerät rot und ein laufendes Update als Info", () => {
expect(lampeFuer({ ...npm, erreichbar: false }, false).zustand).toBe("fehler")
expect(lampeFuer(npm, true)).toMatchObject({ zustand: "info", wert: "Update läuft" })
+14
View File
@@ -81,10 +81,24 @@ export function neuText(b: BausteinStand): string {
return b.kurz || b.verfuegbar || "neu"
}
/** Anteil der belegten Platte (0…1) oder null, wo unbekannt. Ab 85 % gelb, ab 95 % rot — wie der Wächter. */
export function plattenAnteil(ziel: ZielStand): number | null {
const p = ziel.platte
return p && p.gesamt > 0 ? p.belegt / p.gesamt : null
}
export const PLATTE_GELB = 0.85
export const PLATTE_ROT = 0.95
export function lampeFuer(ziel: ZielStand, laeuft: boolean): Lampe {
const label = kurzname(ziel)
const platte = plattenAnteil(ziel)
if (ziel.erreichbar === false) return { id: ziel.id, label, zustand: "fehler", wert: "antwortet nicht" }
if (platte !== null && platte >= PLATTE_ROT) return { id: ziel.id, label, zustand: "fehler", wert: "Platte voll" }
if (laeuft) return { id: ziel.id, label, zustand: "info", wert: "Update läuft" }
if (platte !== null && platte >= PLATTE_GELB) {
return { id: ziel.id, label, zustand: "warn", wert: `Platte ${Math.round(platte * 100)} %` }
}
const neu = ziel.bausteine.filter((b) => b.zustand === "neu")
if (neu.length > 0) {
const b = neu.find((x) => APP.has(x.id)) ?? neu[0]
+2
View File
@@ -280,6 +280,8 @@ export interface ZielStand {
rueckweg: string | null
/** Kurzform des Rückwegs; ältere Stände liefern nur den Text. */
rueckweg_art?: "snapshot" | "sicherung" | "keiner" | null
/** Belegung der Platte in Bytes, wo bekannt (laufende Container). */
platte?: { belegt: number; gesamt: number } | null
zaehlung: Partial<Record<BausteinZustand, number>>
offen: number
}