Files
nexarch/web/shl/components/ModuleNavShell.tsx
T
patrickandClaude Sonnet 5 78ab9a1300
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) 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 / 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
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
docs/shl: Tote Kanban-Verweise entfernt, Modul-Href-Dopplung aufgeloest
Fable-Review auf Code-Dopplung/interne Links (siehe Analyse). P0-Funde:

- Dangling Prosa-Verweise auf die per PR #367 geloeschten *-kanban/-Ordner
  in SKALIERUNGSKONZEPT.md, STORAGE-KONZEPT.md, MIGRATIONSKONZEPT.md,
  SICHERHEITSKONZEPT.md (7x "siehe X-kanban/README.md"), PRODUKTARCHITEKTUR.md
  um Hinweis ergaenzt, dass Boards jetzt in main-Historie/Gitea-PRs liegen.
- dms/internal/searchsync/searchsync_test.go:19: falscher Pfad im Kommentar
  ("internal/bulkedit"/"internal/jobqueue" statt "dms/internal/...").
- web/shl/components/ModuleNavShell.tsx: resolveHrefClientOnly war inhaltlich
  identisch zu resolveModuleHref in web/portal/app/ModuleGrid.tsx (zwei
  unabhaengig gepflegte Kopien derselben Hostname-Ersetzung). Als
  resolveModuleHref aus shl exportiert, ModuleGrid.tsx nutzt jetzt die
  gemeinsame Funktion, Test-Import entsprechend umgestellt.
- web/portal/lib/modules.test.ts: KNOWN_REAL_PERMISSIONS-Whitelist hatte
  kennzahlen.read (migrations/0019_kennzahlen_policy_rule.up.sql, real
  existierend seit ANA-02/PR #362) nie nachgetragen, Test lief seitdem
  unbemerkt im Fehler. Ergaenzt.

go test/vitest: web/portal (15/15) und web/shl (61/61) gruen auf Testhost 131.
Verbleibender P0-Punkt (14 Web-Apps ohne ModuleNavShell) braucht
Produktentscheidung -- mehrere Registry-Keys (ai/mail/workflow/connect)
sind bereits durch eine jeweils ANDERE App belegt als die ohne Navigation
(z.B. mail-search hat "mail", mail-postfach nicht), daher keine mechanische
Zuordnung moeglich ohne zu raten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-19 13:09:53 +02:00

142 lines
5.5 KiB
TypeScript

"use client";
// Globale Navigation ueber alle Modul-Frontends (SHL-01 Akzeptanzkriterium 1).
// Statische Registry statt Live-Abruf: die Modul-Frontends dieses Testhosts
// nutzen unterschiedliche, noch nicht vereinheitlichte Anmeldemodelle
// (Core-Sitzung vs. feste Demo-Kennung per Umgebungsvariable) -- ein
// gemeinsamer Live-Abruf ueber /api/visible-modules wuerde fuer die Haelfte
// der Module eine Core-Anmeldung voraussetzen, die sie bisher nicht haben.
// Einzige Quelle fuer die Modul-Liste (SHL-01 P1-Review): web/portal
// importiert MODULE_REGISTRY von hier statt einer separaten JSON-Kopie
// (web/portal/lib/modules.ts). description/permission sind optional, weil
// das statische Nav hier (ModuleNavShell) nur key/label/href braucht;
// portal/lib/modules.ts nutzt die zusaetzlichen Felder fuer die
// rechte-gefilterte visible-modules-API.
import { useEffect, useState } from "react";
import type { ReactNode } from "react";
import { Shell } from "./Shell";
export interface ModuleRegistryEntry {
key: string;
label: string;
href: string;
description?: string;
permission?: string;
}
export const MODULE_REGISTRY: ModuleRegistryEntry[] = [
{
key: "dms", label: "Dokumente", href: "http://localhost:3101",
description: "Dokumente durchsuchen und verwalten.", permission: "dms.document.read",
},
{
key: "dms_upload", label: "Dokumente hochladen", href: "http://localhost:3107",
description: "Dokumente und Kassenbelege hochladen, inklusive mobiler Kamera-Erfassung.", permission: "dms.upload.read",
},
{
key: "mail", label: "Postfach", href: "http://localhost:3102",
description: "E-Mails durchsuchen und verwalten.", permission: "mail.message.read",
},
{
key: "workflow", label: "Aufgaben & Freigaben", href: "http://localhost:3103",
description: "Freigaben bearbeiten, Akten einsehen, Aufgaben verwalten.", permission: "workflow.approval.read",
},
{
key: "workflow_cases", label: "Akten & Vorgänge", href: "http://localhost:3108",
description: "Akten, Vermerke und Wiedervorlagen einsehen.", permission: "workflow.case.read",
},
{
key: "routing_rules", label: "Routing-Regeln", href: "http://localhost:3109",
description: "Regelbasiertes Routing anlegen und testen.", permission: "workflow.routingrule.read",
},
{
key: "ai", label: "KI-Funktionen", href: "http://localhost:3104",
description: "Zusammenfassungen, Extraktion und semantische Suche.", permission: "ai.summary.read",
},
{
key: "ai_extraction", label: "Extraktions-Korrektur", href: "http://localhost:3110",
description: "Von der KI extrahierte Werte prüfen und korrigieren.", permission: "ai.extraction.read",
},
{
key: "connect", label: "Externe Anbindungen", href: "http://localhost:3105",
description: "Status externer Systemanbindungen (ERP, DATEV, CRM).", permission: "connect.status.read",
},
{
key: "connect_datev", label: "DATEV-Export", href: "http://localhost:3111",
description: "DATEV-Exporte anstoßen und Übertragungsprotokoll einsehen.", permission: "connect.datev.read",
},
{
key: "tenant_admin", label: "Mandantenverwaltung", href: "http://localhost:3106",
description: "Mandanten und Import-Verzeichnisse verwalten.", permission: "core.tenant.admin",
},
{
key: "rbac_admin", label: "Rollen & Rechte", href: "http://localhost:3112",
description: "Rollen und Berechtigungen verwalten.", permission: "core.tenant.admin",
},
{
key: "kennzahlen_dashboard", label: "Kennzahlen", href: "http://localhost:3113",
description: "Löschkennzahlen je Tenant und Modul einsehen.", permission: "kennzahlen.read",
},
{ key: "portal", label: "Übersicht", href: "http://localhost:3900" },
];
export interface ModuleNavShellProps {
activeKey: string;
children: ReactNode;
}
// Ersetzt "localhost" im Modul-Href durch den tatsaechlichen Hostnamen,
// auf dem die Seite ausgeliefert wird (Testhosts binden Module unter
// derselben IP auf unterschiedlichen Ports). Einzige Quelle fuer diese
// Umschreibung (SHL-01 P1-Review): vorher identisch dupliziert als
// resolveModuleHref in web/portal/app/ModuleGrid.tsx.
export function resolveModuleHref(
href: string,
currentHostname: string = typeof window !== "undefined" ? window.location.hostname : "localhost"
): string {
try {
const url = new URL(href);
if (url.hostname === "localhost" && currentHostname !== "localhost") {
url.hostname = currentHostname;
}
return url.toString();
} catch {
return href;
}
}
// Liefert serverseitig/beim ersten Client-Render immer den unveraenderten
// Original-Href (identisch zu SSR), damit React 19 keine
// Hydration-Mismatch-Warnung wirft. Der Hostname-Tausch passiert erst in
// einem useEffect nach dem Mount, analog zum bestehenden themeInit-Pattern.
function resolveHrefClientOnly(href: string): string {
return resolveModuleHref(href);
}
function tenantLabel(): string {
if (typeof process === "undefined") return "";
return process.env.NEXT_PUBLIC_TENANT_SLUG ?? process.env.NEXT_PUBLIC_MAIL_TENANT_SLUG ?? "demo";
}
export function ModuleNavShell({ activeKey, children }: ModuleNavShellProps) {
const [hrefs, setHrefs] = useState<Record<string, string>>({});
useEffect(() => {
setHrefs(Object.fromEntries(MODULE_REGISTRY.map((m) => [m.key, resolveHrefClientOnly(m.href)])));
}, []);
const modules = MODULE_REGISTRY.map((m) => ({
key: m.key,
label: m.label,
href: hrefs[m.key] ?? m.href,
active: m.key === activeKey,
}));
return (
<Shell modules={modules} tenantLabel={tenantLabel()}>
{children}
</Shell>
);
}