Files
mission-control-v2/frontend/src/components/homelab/Wartungsfenster.tsx
T
HitonabiandClaude Opus 5.5 9da666d04e Welle 3 · Wartungsfenster und Update-Regel je Gerät (So 03:00–04:15, Start: Sicherheitsupdates automatisch)
Der Steward des Homelab-Teils prüft jede Minute, ob das Fenster offen ist, und arbeitet dann einmal je Tag den Plan ab:
je Gerät nach seiner Regel — nur melden, Sicherheitsupdates automatisch (Standard, User-Entscheid) oder alles im
Fenster —, mit den bekannten Update-Läufen (Rückweg vorher, Prüfung danach, bei Rot zurück). Nach dem Fenster beginnt
kein Schritt mehr, Erfolge kommen als eine Meldung am Ende, Fehler sofort; der Proxmox-Host startet nie von selbst neu.
Der Ausführer meldet dafür die Namen der Sicherheitspakete und spielt auf Wunsch nur diese ein (Gäste und Host);
neuer Baustein „sicherheit“. Dazu „Version überspringen“ je App, die Einstellungen unter Einstellungen → Homelab und
„Wartung“ statt „Störung“ im Cockpit, solange das Fenster läuft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:33:38 +02:00

90 lines
4.6 KiB
TypeScript

import { useState } from "react"
import { useHomelab, useWartung, useWartungSetzen } from "@/lib/abfragen"
import type { WartungsRegel } from "@/lib/typen"
import { flugplanZeit } from "@/lib/zeit"
// Wartungsfenster und Update-Regel je Gerät (Ausbauplan Welle 3, Punkt 17, seit 25.09.2026). Der Steward des
// Homelab-Teils spielt im Fenster ein, was die Regel erlaubt; der Proxmox-Host startet nie von selbst neu.
const TAGE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
const REGEL: Record<WartungsRegel, string> = {
melden: "Nur melden",
sicherheit: "Sicherheitsupdates automatisch",
alles: "Alles automatisch im Fenster",
}
const feld = "h-10 rounded-xl border border-white/12 bg-white/[0.04] px-3 text-[15px] text-foreground outline-none focus:border-lab/60"
function RegelWahl({ wert, onWahl, label, mitStandard }: {
wert: WartungsRegel | null
onWahl: (r: WartungsRegel | null) => void
label: string
mitStandard?: string
}) {
return (
<select aria-label={label} value={wert ?? ""} onChange={(e) => onWahl((e.target.value || null) as WartungsRegel | null)}
className={`${feld} min-w-0`}>
{mitStandard && <option value="">Wie Standard ({mitStandard})</option>}
{(Object.keys(REGEL) as WartungsRegel[]).map((r) => <option key={r} value={r}>{REGEL[r]}</option>)}
</select>
)
}
export function Wartungsfenster({ aktiv }: { aktiv: boolean }) {
const w = useWartung(aktiv)
const ziele = useHomelab(aktiv).data?.ziele ?? []
const setzen = useWartungSetzen()
const [fenster, setFenster] = useState<{ tag: number; von: string; bis: string } | null>(null)
if (!w.data) return <p className="text-sm text-text-3">{w.isPending ? "Wird gelesen …" : "Das Wartungsfenster lässt sich gerade nicht lesen."}</p>
const f = fenster ?? w.data.fenster
const geaendert = !!fenster && JSON.stringify(fenster) !== JSON.stringify(w.data.fenster)
const s = w.data.stand
return (
<div className="flex flex-col gap-4">
<p className="m-0 text-sm text-text-2">
Im Fenster spielt der Orchestrator ein, was die Regel des Geräts erlaubt — mit Snapshot vorher und Rückweg bei Rot.
Einzelne Erfolge kommen gesammelt als eine Meldung, Fehler sofort. Den Proxmox-Host startet er nie von selbst neu.
</p>
<div className="flex flex-wrap items-end gap-2">
<label className="flex flex-col gap-1 text-sm text-text-2">
Tag
<select className={feld} value={f.tag} onChange={(e) => setFenster({ ...f, tag: Number(e.target.value) })}>
{TAGE.map((t, i) => <option key={t} value={i}>{t}</option>)}
</select>
</label>
<label className="flex flex-col gap-1 text-sm text-text-2">
von
<input type="time" className={feld} value={f.von} onChange={(e) => setFenster({ ...f, von: e.target.value })} />
</label>
<label className="flex flex-col gap-1 text-sm text-text-2">
bis
<input type="time" className={feld} value={f.bis} onChange={(e) => setFenster({ ...f, bis: e.target.value })} />
</label>
<button type="button" disabled={!geaendert || setzen.isPending}
onClick={() => setzen.mutate({ fenster: f }, { onSuccess: () => setFenster(null) })}
className="h-10 rounded-xl bg-lab/20 px-4 text-sm font-medium text-lab-text ring-1 ring-lab/40 ring-inset disabled:opacity-40">
Fenster speichern
</button>
</div>
<p className="m-0 text-xs text-text-3">
Nächstes Fenster: {flugplanZeit(new Date(w.data.naechstes * 1000).toISOString()).toLowerCase()}
{s ? ` · zuletzt ${s.datum.split("-").reverse().join(".")}: ${s.status === "laeuft" ? "läuft gerade" : `${s.schritte.length} Schritte`}` : ""}
</p>
<label className="flex flex-col gap-1 text-sm text-text-2">
Standard für alle Geräte (auch neue)
<RegelWahl label="Standard-Regel" wert={w.data.standard} onWahl={(r) => r && setzen.mutate({ standard: r })} />
</label>
<div role="table" aria-label="Regel je Gerät" className="flex flex-col">
{ziele.map((z) => (
<div role="row" key={z.id} className="flex flex-wrap items-center justify-between gap-2 border-t border-white/[0.06] py-2">
<span role="rowheader" className="text-[15px]">{z.name}</span>
<span role="cell">
<RegelWahl label={`Regel für ${z.name}`} wert={w.data.regeln[z.id] ?? null} mitStandard={REGEL[w.data.standard]}
onWahl={(r) => setzen.mutate({ regeln: { [z.id]: r } })} />
</span>
</div>
))}
</div>
</div>
)
}