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:
co-authored by
Claude Opus 5.5
parent
28888890ec
commit
7a456c8efa
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user