Welle 1 · Totmannschalter: Wächter meldet Abläufe, die gar nicht erst laufen; Seite Dienste zeigt die Zeitpläne
services/ablaeufe.py: je systemd-Timer der Box der erwartete Takt plus Karenz (LastTriggerUSec über --timestamp=unix), Hermes-Cron-Jobs mit verpasstem next_run_at, der Runner der Gitea-Ampel (Admin-API, Token aus ~/.git-credentials, nie ausgegeben). Überfällig oder gestoppt → Hinweis nach fünf Takten, die Sonntags-Updates rot. GET /api/system/ablaeufe; Panel „Zeitpläne“ unter den Diensten. Tests fragen Gitea nie echt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4b57a12257
commit
34360c9644
@@ -0,0 +1,76 @@
|
||||
import { cn } from "cn"
|
||||
import { Chip } from "@/components/cockpit/Chip"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
import { useAblaeufe } from "@/lib/abfragen"
|
||||
import type { Ablauf } from "@/lib/typen"
|
||||
import { flugplanZeit, vor } from "@/lib/zeit"
|
||||
|
||||
// Die geplanten Abläufe der Box mit letztem und nächstem Lauf — der Totmannschalter (Ausbauplan Welle 1, seit
|
||||
// 25.09.2026). Was länger schweigt als erlaubt, steht oben und rot; der Wächter macht daraus einen Hinweis.
|
||||
|
||||
const STATUS: Record<Ablauf["status"], { wort: string; punkt: string; text: string }> = {
|
||||
ok: { wort: "pünktlich", punkt: "bg-gruen", text: "text-text-3" },
|
||||
ueberfaellig: { wort: "überfällig", punkt: "bg-rot shadow-[0_0_8px_var(--rot)]", text: "text-rot-text" },
|
||||
gestoppt: { wort: "läuft nicht", punkt: "bg-rot shadow-[0_0_8px_var(--rot)]", text: "text-rot-text" },
|
||||
aus: { wort: "abgeschaltet", punkt: "bg-text-3", text: "text-text-3" },
|
||||
}
|
||||
|
||||
const SPALTEN = "md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_minmax(0,1fr)_110px]"
|
||||
const zeit = (s: number) => flugplanZeit(new Date(s * 1000).toISOString())
|
||||
|
||||
function Zeile({ a }: { a: Ablauf }) {
|
||||
const s = STATUS[a.status] ?? STATUS.ok
|
||||
return (
|
||||
<div role="row" className={cn("grid grid-cols-2 gap-x-4 gap-y-1.5 border-t border-white/[0.06] py-3 md:items-center", SPALTEN)}>
|
||||
<div role="rowheader" className="col-span-2 flex min-w-0 items-start gap-2.5 md:col-span-1">
|
||||
<span aria-hidden className={cn("mt-1.5 size-2 shrink-0 rounded-full", s.punkt)} />
|
||||
<span className="flex min-w-0 flex-col">
|
||||
<span className="text-[15px] font-semibold break-words">{a.name}</span>
|
||||
<span className="text-[13px] text-text-3">{a.takt}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span role="cell" className="text-sm text-text-2">
|
||||
<span className="text-xs text-text-3 md:hidden">Zuletzt </span>
|
||||
{a.letzter ? <span className="ziffern">{vor(a.letzter)}</span> : <span className="text-text-3">–</span>}
|
||||
</span>
|
||||
<span role="cell" className="text-sm text-text-2">
|
||||
<span className="text-xs text-text-3 md:hidden">Nächster </span>
|
||||
{a.naechster ? <span className="ziffern">{zeit(a.naechster).toLowerCase()}</span> : <span className="text-text-3">–</span>}
|
||||
</span>
|
||||
<span role="cell" className={cn("etikett text-[10.5px] tracking-[0.12em]", s.text)}>{s.wort}</span>
|
||||
{a.text && a.status !== "ok" && (
|
||||
<p className={cn("col-span-2 m-0 text-sm md:col-span-4 md:pl-[18px]", s.text)}>{a.text}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** „Zeitpläne“: je Ablauf Takt, letzter und nächster Lauf, Zustand. */
|
||||
export function Zeitplaene() {
|
||||
const abfrage = useAblaeufe()
|
||||
const liste = abfrage.data?.ablaeufe ?? []
|
||||
const probleme = liste.filter((a) => a.status === "ueberfaellig" || a.status === "gestoppt").length
|
||||
return (
|
||||
<Panel titel="Zeitpläne" rechts={abfrage.data ? <Chip art={probleme ? "rot" : "gruen"}>{probleme ? `${probleme} überfällig` : "alle pünktlich"}</Chip> : undefined}>
|
||||
<p className="-mt-2 text-[15px] text-text-2">
|
||||
Was die Box regelmäßig von selbst tut. Bleibt ein Lauf länger aus als erwartet, steht er hier oben und der
|
||||
Wächter meldet es — ein Stillstand wie im September fällt so am ersten Morgen auf.
|
||||
</p>
|
||||
{abfrage.isPending ? (
|
||||
<p className="text-sm text-text-3">Wird gelesen …</p>
|
||||
) : abfrage.error ? (
|
||||
<p role="alert" className="text-sm text-rot-text">Die Zeitpläne lassen sich gerade nicht lesen: {abfrage.error.message}</p>
|
||||
) : (
|
||||
<div role="table" aria-label="Zeitpläne" className="flex flex-col">
|
||||
<div role="row" className={cn("etikett hidden gap-x-4 pb-2.5 text-[10.5px] tracking-[0.14em] text-text-3 md:grid", SPALTEN)}>
|
||||
<span role="columnheader">Ablauf</span>
|
||||
<span role="columnheader">Zuletzt</span>
|
||||
<span role="columnheader">Nächster</span>
|
||||
<span role="columnheader">Zustand</span>
|
||||
</div>
|
||||
{liste.map((a) => <Zeile key={a.id} a={a} />)}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { fehlerText, laeuftNoch } from "./anzeige"
|
||||
import { melden } from "./meldungen"
|
||||
import { stromVerbunden } from "./strom"
|
||||
import type {
|
||||
AktionsErgebnis, Altrest, AufraeumKandidat, BoxMesswerte, EinstellungenStand, Hinweis, HomelabEinstellungen,
|
||||
Ablauf, AktionsErgebnis, Altrest, AufraeumKandidat, BoxMesswerte, EinstellungenStand, Hinweis, HomelabEinstellungen,
|
||||
HomelabLauf, HomelabMesswerte, HomelabSpeicher, HomelabStand, Instanz, InstanzMitStand, Job, Modell, Nutzung, PartnerStatus,
|
||||
ProtokollEintrag, Radar, SammelPlanSchritt, Sammellauf, Sicherung, Start, TelegramTest, UpdateDetails, ZielAktion,
|
||||
ZielStand, Zeitraum,
|
||||
@@ -146,6 +146,14 @@ export interface DiensteStand {
|
||||
kerne?: number | null
|
||||
}
|
||||
|
||||
/** Die geplanten Abläufe der Box — der Totmannschalter (seit 25.09.2026). */
|
||||
export const useAblaeufe = () =>
|
||||
useQuery({
|
||||
queryKey: ["ablaeufe"],
|
||||
queryFn: () => api<{ ablaeufe: Ablauf[] }>("/api/system/ablaeufe"),
|
||||
refetchInterval: 60_000,
|
||||
})
|
||||
|
||||
/** Die Dienste-Seite misst die CPU zwischen zwei Abfragen — deshalb ein fester, kurzer Takt, solange sie offen ist. */
|
||||
export const useDienste = () =>
|
||||
useQuery({
|
||||
|
||||
@@ -638,3 +638,18 @@ export interface HomelabSnapshots {
|
||||
/** Das Aufräumen arbeitet gerade seine Liste ab. */
|
||||
aufraeumen_laeuft?: boolean
|
||||
}
|
||||
|
||||
// --- Totmannschalter (GET /api/system/ablaeufe, seit 25.09.2026) ---------------------------------------------------
|
||||
|
||||
/** Ein geplanter Ablauf der Box: systemd-Timer, Hermes-Cron-Job oder der Runner der Gitea-Ampel. */
|
||||
export interface Ablauf {
|
||||
id: string
|
||||
name: string
|
||||
art: "zeitplan" | "hermes" | "ampel"
|
||||
takt: string
|
||||
/** Unix-Sekunden, null = noch nie bzw. unbekannt */
|
||||
letzter: number | null
|
||||
naechster: number | null
|
||||
status: "ok" | "ueberfaellig" | "gestoppt" | "aus"
|
||||
text: string | null
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
|
||||
import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
import { Zeitplaene } from "@/components/cockpit/Zeitplaene"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { type Dienst, type DiensteStand, useDienste, useProtokoll } from "@/lib/abfragen"
|
||||
import { fehlerText } from "@/lib/anzeige"
|
||||
@@ -276,44 +277,47 @@ export function DiensteSeite() {
|
||||
const kerne = stand.kerne ? `allen ${stand.kerne} Kernen` : "allen Kernen"
|
||||
|
||||
return (
|
||||
<Panel titel="Dienste" rechts={<Lage dienste={stand.services} stand={dienste.dataUpdatedAt} />}>
|
||||
<p className="-mt-2 text-[15px] text-text-2">
|
||||
Die Dienste der KI-Box: ob sie laufen, was sie gerade an CPU und Speicher brauchen, seit wann sie laufen und was
|
||||
sie zuletzt ins Protokoll geschrieben haben. Probleme stehen oben.
|
||||
</p>
|
||||
{stand.services.length === 0 ? (
|
||||
<p className="text-[15px] text-text-2">Keine Dienste gefunden.</p>
|
||||
) : (
|
||||
<Tabelle
|
||||
stand={stand}
|
||||
gewaehlt={gewaehlt}
|
||||
startet={startet}
|
||||
onProtokoll={(unit) => setGewaehlt(gewaehlt === unit ? null : unit)}
|
||||
onFrage={(d) => {
|
||||
setFrage(d)
|
||||
setFragt(true)
|
||||
<>
|
||||
<Panel titel="Dienste" rechts={<Lage dienste={stand.services} stand={dienste.dataUpdatedAt} />}>
|
||||
<p className="-mt-2 text-[15px] text-text-2">
|
||||
Die Dienste der KI-Box: ob sie laufen, was sie gerade an CPU und Speicher brauchen, seit wann sie laufen und was
|
||||
sie zuletzt ins Protokoll geschrieben haben. Probleme stehen oben.
|
||||
</p>
|
||||
{stand.services.length === 0 ? (
|
||||
<p className="text-[15px] text-text-2">Keine Dienste gefunden.</p>
|
||||
) : (
|
||||
<Tabelle
|
||||
stand={stand}
|
||||
gewaehlt={gewaehlt}
|
||||
startet={startet}
|
||||
onProtokoll={(unit) => setGewaehlt(gewaehlt === unit ? null : unit)}
|
||||
onFrage={(d) => {
|
||||
setFrage(d)
|
||||
setFragt(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<p className="m-0 text-xs leading-relaxed text-text-3">
|
||||
CPU: Anteil an {kerne} der Box, gemessen zwischen zwei Abfragen (alle zehn Sekunden). Speicher: was systemd dem
|
||||
Dienst zurechnet; der Balken zeigt den Anteil an {speicher} der Box. Bei der Engine zählen die geladenen Modelle
|
||||
mit — sie liegen im Grafikspeicher, den systemd keinem Dienst zurechnet. Neustarts: wie oft systemd den Dienst
|
||||
nach einem Absturz von selbst wieder gestartet hat. „Schläft“ heißt: mit Absicht abgeschaltet, kein Fehler.
|
||||
</p>
|
||||
<Bestaetigen
|
||||
offen={fragt && !!rueckfrage}
|
||||
titel={rueckfrage?.titel ?? ""}
|
||||
text={rueckfrage?.text ?? ""}
|
||||
knopf={rueckfrage?.knopf ?? ""}
|
||||
gefahr={rueckfrage?.knopf === "Neu starten"}
|
||||
gesperrt={startet !== null}
|
||||
onJa={() => {
|
||||
setFragt(false)
|
||||
if (frage) void starten(frage)
|
||||
}}
|
||||
onNein={() => setFragt(false)}
|
||||
/>
|
||||
)}
|
||||
<p className="m-0 text-xs leading-relaxed text-text-3">
|
||||
CPU: Anteil an {kerne} der Box, gemessen zwischen zwei Abfragen (alle zehn Sekunden). Speicher: was systemd dem
|
||||
Dienst zurechnet; der Balken zeigt den Anteil an {speicher} der Box. Bei der Engine zählen die geladenen Modelle
|
||||
mit — sie liegen im Grafikspeicher, den systemd keinem Dienst zurechnet. Neustarts: wie oft systemd den Dienst
|
||||
nach einem Absturz von selbst wieder gestartet hat. „Schläft“ heißt: mit Absicht abgeschaltet, kein Fehler.
|
||||
</p>
|
||||
<Bestaetigen
|
||||
offen={fragt && !!rueckfrage}
|
||||
titel={rueckfrage?.titel ?? ""}
|
||||
text={rueckfrage?.text ?? ""}
|
||||
knopf={rueckfrage?.knopf ?? ""}
|
||||
gefahr={rueckfrage?.knopf === "Neu starten"}
|
||||
gesperrt={startet !== null}
|
||||
onJa={() => {
|
||||
setFragt(false)
|
||||
if (frage) void starten(frage)
|
||||
}}
|
||||
onNein={() => setFragt(false)}
|
||||
/>
|
||||
</Panel>
|
||||
</Panel>
|
||||
<Zeitplaene />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user