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