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>
90 lines
4.6 KiB
TypeScript
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>
|
|
)
|
|
}
|