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>
77 lines
4.0 KiB
TypeScript
77 lines
4.0 KiB
TypeScript
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>
|
||
)
|
||
}
|