Files
mission-control-v2/frontend/src/components/cockpit/Zeitplaene.tsx
T
HitonabiandClaude Opus 5.5 34360c9644 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>
2026-09-25 16:01:21 +02:00

77 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}