From 63dc5c5a6fd1cf2ff51a2bf7344e04ee0e90d7cb Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 28 Oct 2026 23:56:02 +0100 Subject: [PATCH] portal: Betriebszustand an den Modul-Kacheln Die Kachel zeigt Punkt und Text, wenn die Statusseite ein Pruefziel kennt, dessen Name dem Modul-Schluessel oder der Bezeichnung entspricht. Ohne Berechtigung (403) oder ohne Statusquelle bleibt die Kachel unveraendert. Co-Authored-By: Claude Sonnet 5.5 --- web/portal/app/AdminPanel.tsx | 2 +- web/portal/app/ModuleGrid.render.test.tsx | 28 +++++++++++++++++++++++ web/portal/app/ModuleGrid.tsx | 18 +++++++++++++++ web/portal/lib/moduleStatus.test.ts | 20 ++++++++++++++++ web/portal/lib/moduleStatus.ts | 13 +++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 web/portal/lib/moduleStatus.test.ts create mode 100644 web/portal/lib/moduleStatus.ts diff --git a/web/portal/app/AdminPanel.tsx b/web/portal/app/AdminPanel.tsx index 09030194..87a61e19 100644 --- a/web/portal/app/AdminPanel.tsx +++ b/web/portal/app/AdminPanel.tsx @@ -6,7 +6,7 @@ import { getSession } from "../lib/http"; import { StatTile } from "./Tile"; import type { KennzahlenSummary, ModuleStatusRow } from "../lib/adminData"; -async function getJson(path: string): Promise { +export async function getJson(path: string): Promise { try { const res = await getSession().fetch(path); return res.ok ? ((await res.json()) as T) : null; diff --git a/web/portal/app/ModuleGrid.render.test.tsx b/web/portal/app/ModuleGrid.render.test.tsx index 007c9a1b..e9bcf387 100644 --- a/web/portal/app/ModuleGrid.render.test.tsx +++ b/web/portal/app/ModuleGrid.render.test.tsx @@ -188,3 +188,31 @@ describe("Befehlsleiste im Portal", () => { expect(btn.textContent).toContain("Strg K"); }); }); + +describe("Status an der Kachel", () => { + beforeEach(() => localStorage.clear()); + + function replyWithStatus(status: Response) { + fetchMock.mockImplementation(async (url: string) => { + if (url === "/api/visible-modules") return new Response(JSON.stringify({ modules: mods }), { status: 200 }); + if (url === "/api/worklist") return new Response(JSON.stringify({ approvals: 0, tasks: 0, remindersDue: 0 }), { status: 200 }); + if (url === "/api/system-status") return status; + return new Response("", { status: 404 }); + }); + } + + it("zeigt den Zustand nur an Kacheln mit passendem Pruefziel", async () => { + replyWithStatus(new Response(JSON.stringify({ modules: [{ name: "DMS", status: "down", lastChecked: null }] }), { status: 200 })); + renderGrid(); + const dms = await screen.findByRole("link", { name: /Dokumente/ }); + await vi.waitFor(() => expect(dms.textContent).toContain("gestört")); + expect(screen.getByRole("link", { name: /Rollen & Rechte/ }).textContent).not.toContain("gestört"); + }); + + it("ohne Berechtigung (403) bleiben die Kacheln ohne Status", async () => { + replyWithStatus(new Response("", { status: 403 })); + renderGrid(); + const dms = await screen.findByRole("link", { name: /Dokumente/ }); + expect(dms.textContent).not.toMatch(/in Betrieb|gestört|unbekannt/); + }); +}); diff --git a/web/portal/app/ModuleGrid.tsx b/web/portal/app/ModuleGrid.tsx index e323e266..2d3303d4 100644 --- a/web/portal/app/ModuleGrid.tsx +++ b/web/portal/app/ModuleGrid.tsx @@ -6,6 +6,9 @@ import type { ModuleEntry } from "../lib/modules"; import type { Worklist } from "../lib/worklist"; import { getSession, getWorkflowSession } from "../lib/http"; import { ModuleTile, StatTile } from "./Tile"; +import { getJson, statusKey } from "./AdminPanel"; +import { statusForModule } from "../lib/moduleStatus"; +import type { ModuleStatusRow } from "../lib/adminData"; import { Skeleton } from "./Skeleton"; import { CommandPalette } from "./CommandPalette"; import type { CommandItem } from "../lib/commandSearch"; @@ -137,6 +140,17 @@ function Overview({ modules, worklist, who }: OverviewProps) { const [collapsed, setCollapsed] = useState([]); const [live, setLive] = useState(""); const [paletteOpen, setPaletteOpen] = useState(false); + const [statusRows, setStatusRows] = useState(null); + + useEffect(() => { + let cancelled = false; + getJson<{ modules: ModuleStatusRow[] }>("/api/system-status").then((s) => { + if (!cancelled) setStatusRows(s?.modules ?? null); + }); + return () => { + cancelled = true; + }; + }, []); useEffect(() => { function onKey(e: KeyboardEvent) { @@ -184,6 +198,9 @@ function Overview({ modules, worklist, who }: OverviewProps) { const renderTile = (mod: ModuleEntry, compact = false) => { const badge = badgeFor(mod.key, worklist); const isFav = favorites.includes(mod.key); + const raw = statusForModule(statusRows, mod); + const state = raw === null ? null : statusKey(raw); + const fallback = { up: "in Betrieb", down: "gestört", unknown: "unbekannt" } as const; return (
  • 0 ? `${badge} ${t("portal.badge.open", "offen")}` : undefined} + status={state ? { state, text: t(`portal.status.${state}`, fallback[state]) } : undefined} onOpen={() => setRecent(pushRecent(mod.key, keys))} />