Files
nexarch/web/status-page/app/page.tsx
T

126 lines
4.1 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { fetchOverview, fetchHistory, type ModuleStatus, type HistoryEntry } from "@/lib/api";
const POLL_INTERVAL_MS = 5000;
export default function Page() {
const [modules, setModules] = useState<ModuleStatus[]>([]);
const [error, setError] = useState<string | null>(null);
const [selected, setSelected] = useState<string | null>(null);
const [history, setHistory] = useState<HistoryEntry[] | null>(null);
const [historyError, setHistoryError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function poll() {
try {
const data = await fetchOverview();
if (!cancelled) {
setModules(data);
setError(null);
}
} catch (e: any) {
// Ein nicht antwortendes Backend darf die zuletzt bekannte Ansicht
// nicht loeschen und die Seite nicht unbedienbar machen
// (Akzeptanzkriterium 2) — nur eine Fehlermeldung anzeigen, alte
// Daten bleiben sichtbar.
if (!cancelled) {
setError(e.message ?? "Unbekannter Fehler beim Laden der Uebersicht");
}
}
}
poll();
const id = setInterval(poll, POLL_INTERVAL_MS);
return () => {
cancelled = true;
clearInterval(id);
};
}, []);
async function openHistory(name: string) {
setSelected(name);
setHistoryError(null);
try {
const data = await fetchHistory(name);
setHistory(data);
} catch (e: any) {
setHistoryError(e.message ?? "Verlauf konnte nicht geladen werden");
setHistory(null);
}
}
const anyDown = modules.some((m) => m.status === "down");
return (
<main style={{ maxWidth: 800, margin: "0 auto", padding: "2rem 1rem" }}>
<h1>Systemstatus</h1>
{error && (
<p style={{ color: "#c62828", background: "#fdecea", padding: "0.5rem", borderRadius: 4 }} role="alert">
Uebersicht konnte gerade nicht aktualisiert werden: {error}. Zuletzt bekannter Stand wird weiter angezeigt.
</p>
)}
{!error && anyDown && (
<p style={{ color: "#c62828", background: "#fdecea", padding: "0.5rem", borderRadius: 4 }}>
Mindestens ein Modul ist derzeit nicht erreichbar.
</p>
)}
<ul style={{ listStyle: "none", padding: 0 }}>
{modules.map((m) => (
<li
key={m.name}
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
background: "white",
padding: "0.75rem 1rem",
marginBottom: "0.5rem",
borderRadius: 8,
borderLeft: `6px solid ${m.status === "up" ? "#2e7d32" : "#c62828"}`,
}}
>
<div>
<strong>{m.name}</strong>
<div style={{ fontSize: "0.85rem", color: "#666" }}>
{m.status === "up" ? "Verfügbar" : "Nicht verfügbar"}
{m.last_checked && ` — zuletzt geprüft ${new Date(m.last_checked).toLocaleString("de-DE")}`}
</div>
</div>
<button onClick={() => openHistory(m.name)}>Verlauf</button>
</li>
))}
{modules.length === 0 && !error && <li>Lade Modulstatus</li>}
</ul>
{selected && (
<section style={{ background: "white", padding: "1rem", borderRadius: 8, marginTop: "1.5rem" }}>
<h2>Verlauf: {selected}</h2>
{historyError && (
<p style={{ color: "#c62828" }} role="alert">
{historyError}
</p>
)}
{history && (
<ul>
{history.map((h, i) => (
<li key={i}>
{new Date(h.changed_at).toLocaleString("de-DE")} {h.status === "up" ? "verfügbar" : "nicht verfügbar"}
</li>
))}
{history.length === 0 && <li>Keine Statusänderungen bisher.</li>}
</ul>
)}
<button onClick={() => setSelected(null)}>Schließen</button>
</section>
)}
</main>
);
}