portal: Betriebszustand an den Modul-Kacheln
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
44c0919724
commit
63dc5c5a6f
5 files changed
+80
-1
No files matched your search
@@ -6,7 +6,7 @@ import { getSession } from "../lib/http";
|
||||
import { StatTile } from "./Tile";
|
||||
import type { KennzahlenSummary, ModuleStatusRow } from "../lib/adminData";
|
||||
|
||||
async function getJson<T>(path: string): Promise<T | null> {
|
||||
export async function getJson<T>(path: string): Promise<T | null> {
|
||||
try {
|
||||
const res = await getSession().fetch(path);
|
||||
return res.ok ? ((await res.json()) as T) : null;
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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<string[]>([]);
|
||||
const [live, setLive] = useState("");
|
||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||
const [statusRows, setStatusRows] = useState<ModuleStatusRow[] | null>(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 (
|
||||
<li key={mod.key} className="np-tile-wrap" data-density={compact ? "compact" : undefined}>
|
||||
<ModuleTile
|
||||
@@ -192,6 +209,7 @@ function Overview({ modules, worklist, who }: OverviewProps) {
|
||||
description={mod.description}
|
||||
href={resolveModuleHref(mod.href)}
|
||||
badgeText={badge !== null && badge > 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))}
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { statusForModule } from "./moduleStatus";
|
||||
|
||||
const row = (name: string, status: string) => ({ name, status, lastChecked: null });
|
||||
const mod = { key: "workflow_cases", label: "Akten & Wiedervorlagen" };
|
||||
|
||||
describe("statusForModule", () => {
|
||||
it("liefert null ohne Statusdaten oder ohne Treffer", () => {
|
||||
expect(statusForModule(null, mod)).toBeNull();
|
||||
expect(statusForModule([row("dms", "up")], mod)).toBeNull();
|
||||
});
|
||||
it("trifft auf Schluessel und Bezeichnung, unabhaengig von Schreibweise", () => {
|
||||
expect(statusForModule([row("Workflow-Cases", "up")], mod)).toBe("up");
|
||||
expect(statusForModule([row("akten wiedervorlagen", "down")], mod)).toBe("down");
|
||||
});
|
||||
it("down gewinnt, unbekannt ist nicht up", () => {
|
||||
expect(statusForModule([row("workflow_cases", "up"), row("Akten & Wiedervorlagen", "down")], mod)).toBe("down");
|
||||
expect(statusForModule([row("workflow_cases", "up"), row("Akten & Wiedervorlagen", "degraded")], mod)).toBe("unknown");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ModuleStatusRow } from "./adminData";
|
||||
|
||||
const norm = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
|
||||
// Die Statusseite kennt Pruefziele mit frei vergebenem Namen; nur Treffer auf Schluessel oder Bezeichnung zaehlen.
|
||||
export function statusForModule(rows: ModuleStatusRow[] | null, mod: { key: string; label: string }): string | null {
|
||||
if (!rows) return null;
|
||||
const keys = new Set([norm(mod.key), norm(mod.label)]);
|
||||
const hits = rows.filter((r) => keys.has(norm(r.name)));
|
||||
if (hits.length === 0) return null;
|
||||
if (hits.some((r) => r.status === "down")) return "down";
|
||||
return hits.every((r) => r.status === "up") ? "up" : "unknown";
|
||||
}
|
||||
Reference in new issue
Block a user