homelab: alte Kernel per Knopf entfernen; Systemplatte des Proxmox-Hosts im Blick

User-Wunsch 25.09.: Auf dem Host lagen 25 Kernel (~23 GB), 20 davon hält apt für
entbehrlich (19,7 GB).
- Ausführer: host.kernel_alt/kernel_bleiben aus apt-get -s autoremove (nur
  Kernel, nie laufender, nächster oder festgepinnter), host.systemplatte;
  neue Aktion kernel_aufraeumen bestimmt die Liste selbst und purgt.
- Homelab-Teil: Lauf „kernel“ (Ergebnis aufgeraeumt), POST
  /api/homelab/ziele/pve/kernel-aufraeumen, Vorschlag mit Knopf, Wächter für die
  Systemplatte (80/90 %).
- Oberfläche: Kachel Systemplatte, Knopf „Alte Kernel entfernen“ mit Rückfrage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 12:16:14 +02:00
co-authored by Claude Opus 5.5
parent 28888890ec
commit 7a456c8efa
36 changed files with 377 additions and 60 deletions
+69 -13
View File
@@ -1,9 +1,13 @@
import type { ReactNode } from "react"
import { type ReactNode, useState } from "react"
import { ChevronRight } from "lucide-react"
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 { auftraegeText, groesse, STUFE_TEXT } from "@/lib/speicher"
import type { HomelabSpeicher, NasStand, SicherungsLage, SpeicherPool, SpeicherStufe } from "@/lib/typen"
import type {
HomelabSpeicher, NasStand, SicherungsLage, SpeicherEmpfehlung, SpeicherPool, SpeicherStufe, Systemplatte, ZielAktion,
} from "@/lib/typen"
import { flugplanZeit } from "@/lib/zeit"
const CHIP: Record<SpeicherStufe | "keine", ChipArt> = { ok: "gruen", gelb: "bernstein", rot: "rot", keine: "grau" }
@@ -65,6 +69,17 @@ function PoolKachel({ p }: { p: SpeicherPool }) {
)
}
function SystemplatteKachel({ s }: { s: Systemplatte }) {
return (
<Kachel titel="Systemplatte" unter="Proxmox-Host: Kernel, Sicherungen vor Updates" chip={CHIP[s.stufe]} chipText={STUFE_TEXT[s.stufe]}>
<Fuellstand anteil={s.anteil} stufe={s.stufe} name="Systemplatte des Proxmox-Hosts" />
<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>
</Kachel>
)
}
function NasKachel({ n }: { n: NasStand }) {
const chipText = n.problem ? (n.stufe === "rot" ? "gestört" : "unklar") : STUFE_TEXT[n.stufe]
return (
@@ -122,13 +137,21 @@ function SicherungsKachel({ s }: { s: SicherungsLage }) {
* Speicher und Sicherungen des Homelabs: Speicherpool, NAS und die nächtlichen Sicherungen, darunter was im Pool
* Platz bringt. Die Vorschläge sind aufgeklappt, sobald der Pool knapp wird (ab 80 %).
*/
export function Speicher({ daten }: { daten: HomelabSpeicher }) {
const knapp = daten.pools.some((p) => p.stufe !== "ok")
export function Speicher({ daten, laeuft = null, gesperrt = false, onAktion }: {
daten: HomelabSpeicher
/** Läuft auf dem Proxmox-Host gerade etwas (Update, Kernel-Aufräumen)? Dann kein Knopf. */
laeuft?: string | null
gesperrt?: boolean
onAktion?: (a: ZielAktion) => void
}) {
const [frage, setFrage] = useState<ZielAktion | null>(null)
const knapp = daten.pools.some((p) => p.stufe !== "ok") || (daten.systemplatte?.stufe ?? "ok") !== "ok"
const summe = daten.empfehlungen.reduce((n, e) => n + (e.platz ?? 0), 0)
return (
<div className="flex flex-col gap-4">
<div className="grid gap-3 md:grid-cols-3">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
{daten.pools.map((p) => <PoolKachel key={p.speicher} p={p} />)}
{daten.systemplatte && <SystemplatteKachel s={daten.systemplatte} />}
{daten.nas.length > 0 ? (
daten.nas.map((n) => <NasKachel key={n.ziel} n={n} />)
) : (
@@ -143,7 +166,7 @@ export function Speicher({ daten }: { daten: HomelabSpeicher }) {
<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">
<ChevronRight className="size-4 text-text-3 transition-transform group-open:rotate-90" aria-hidden />
Was im Speicherpool Platz bringt
Was Platz bringt
</span>
<span className="ziffern text-sm text-text-2">
{daten.empfehlungen.length} {daten.empfehlungen.length === 1 ? "Vorschlag" : "Vorschläge"}
@@ -152,17 +175,50 @@ export function Speicher({ daten }: { daten: HomelabSpeicher }) {
</summary>
<ol className="flex flex-col">
{daten.empfehlungen.map((e) => (
<li key={e.titel} className="flex flex-col gap-1 border-t border-linie py-3 first:border-t-0 first:pt-1">
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
<span className="text-[15px] font-medium break-words">{e.titel}</span>
<span className="ziffern text-sm text-text-2">{e.platz ? `≈ ${groesse(e.platz)}` : "Größe unbekannt"}</span>
</div>
<p className="text-sm break-words text-text-2">{e.text}</p>
</li>
<Vorschlag key={e.titel} e={e} laeuft={laeuft} gesperrt={gesperrt || !onAktion} onFrage={setFrage} />
))}
</ol>
</details>
)}
<Bestaetigen
offen={!!frage}
titel={frage ? `Proxmox-Host: ${frage.label}` : ""}
text={frage?.frage ?? ""}
knopf={frage?.label ?? ""}
gesperrt={gesperrt}
onJa={() => {
if (frage && onAktion) onAktion(frage)
setFrage(null)
}}
onNein={() => setFrage(null)}
/>
</div>
)
}
const ORT: Record<string, string> = { pool: "Speicherpool", systemplatte: "Systemplatte des Hosts" }
function Vorschlag({ e, laeuft, gesperrt, onFrage }: {
e: SpeicherEmpfehlung
laeuft: string | null
gesperrt: boolean
onFrage: (a: ZielAktion) => void
}) {
return (
<li className="flex flex-col gap-1 border-t border-linie py-3 first:border-t-0 first:pt-1">
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
<span className="text-[15px] font-medium break-words">{e.titel}</span>
<span className="ziffern text-sm text-text-2">{e.platz ? `≈ ${groesse(e.platz)}` : "Größe unbekannt"}</span>
</div>
{e.ort && <span className="schild text-xs text-text-3">{ORT[e.ort] ?? e.ort}</span>}
<p className="text-sm break-words text-text-2">{e.text}</p>
{e.aktion && (laeuft ? (
<p role="status" className="text-sm text-cyan">Läuft: {laeuft}</p>
) : (
<Button variant="outline" size="sm" className="mt-1 self-start" disabled={gesperrt} onClick={() => onFrage(e.aktion!)}>
{e.aktion.label}
</Button>
))}
</li>
)
}