ANA-02: Kennzahlen-Dashboard ins Portal einbinden, globale Navigation nachziehen
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (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 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
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (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
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

ModuleNavShell/MODULE_REGISTRY existierte bisher nur live auf Host 204,
nie committet - alle 12 Module (ai-extraction-review, ai-summaries,
connect-status, datev-export, dms-upload, mail-search, rbac-admin,
routing-rules, tenant-admin, workflow-approvals, workflow-cases) jetzt
mit ModuleNavShell verdrahtet, inklusive Export-Fix in web/shl/index.ts.

Kennzahlen-Dashboard (ANA-02) neu in Registry/Navigation aufgenommen,
RBAC-Recht kennzahlen.read per Migration an tenant_admin vergeben
(analog 0018_module_policy_rules).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-10-19 00:11:57 +02:00
1 parent adab4cbfac
commit 3a7d2d4696
18 files changed
+157 -28

No files matched your search

@@ -0,0 +1 @@
DELETE FROM policy_rules WHERE granted_by = 'migration:0019_kennzahlen_policy_rule';
@@ -0,0 +1,18 @@
-- ANA-02: Kennzahlen-Dashboard braucht ein eigenes Recht, analog zum
-- core.tenant.admin-Muster aus 0018 — nur tenant_admin, da aggregierte
-- Loeschkennzahlen ueber alle Module hinweg sensibler sind als einzelne
-- Modul-Leserechte.
WITH grants(role, permission) AS (
VALUES
('tenant_admin', 'kennzahlen.read')
),
ins AS (
INSERT INTO policy_rules (role, permission, granted_by)
SELECT role, permission, 'migration:0019_kennzahlen_policy_rule' FROM grants
ON CONFLICT (role, permission) DO NOTHING
RETURNING role, permission
)
INSERT INTO policy_rule_changes (role, permission, action, actor, version)
SELECT i.role, i.permission, 'grant', 'migration:0019_kennzahlen_policy_rule',
COALESCE((SELECT MAX(c.version) FROM policy_rule_changes c WHERE c.role = i.role AND c.permission = i.permission), 0) + 1
FROM ins i;
+2 -2
View File
@@ -1,5 +1,5 @@
import Script from "next/script";
import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Extraktions-Korrektur",
@@ -36,7 +36,7 @@ export default function RootLayout({
<style>{focusVisibleStyle}</style>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="ai_extraction">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+2 -2
View File
@@ -1,5 +1,5 @@
import Script from "next/script";
import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Zusammenfassungen",
@@ -36,7 +36,7 @@ export default function RootLayout({
<style>{focusVisibleStyle}</style>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="ai">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+2 -2
View File
@@ -1,5 +1,5 @@
import Script from "next/script";
import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Verbindungsstatus",
@@ -30,7 +30,7 @@ export default function RootLayout({
>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="connect">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+2 -2
View File
@@ -1,5 +1,5 @@
import Script from "next/script";
import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH DATEV-Export",
@@ -28,7 +28,7 @@ export default function RootLayout({
>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="connect_datev">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+3 -3
View File
@@ -1,4 +1,4 @@
import { ThemeProvider, I18nProvider, ToastProvider, typography } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography } from "@nexarch/shl";
import { AccountBar } from "../components/AccountBar";
export const metadata = {
@@ -22,10 +22,10 @@ export default function RootLayout({
>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>
<ToastProvider><ModuleNavShell activeKey="dms_upload">
<AccountBar />
{children}
</ToastProvider>
</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+2 -2
View File
@@ -1,4 +1,4 @@
import { ThemeProvider, I18nProvider, ToastProvider, typography } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Kennzahlen",
@@ -21,7 +21,7 @@ export default function RootLayout({
>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="kennzahlen_dashboard">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+2 -2
View File
@@ -1,5 +1,5 @@
import Script from "next/script";
import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Mail-Suche",
@@ -43,7 +43,7 @@ export default function RootLayout({
<style>{focusVisibleStyle}</style>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="mail">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+7
View File
@@ -82,5 +82,12 @@
"description": "Rollen und Berechtigungen verwalten.",
"href": "http://localhost:3112",
"permission": "core.tenant.admin"
},
{
"key": "kennzahlen_dashboard",
"label": "Kennzahlen",
"description": "Löschkennzahlen je Tenant und Modul einsehen.",
"href": "http://localhost:3113",
"permission": "kennzahlen.read"
}
]
+2 -2
View File
@@ -1,4 +1,4 @@
import { ThemeProvider, I18nProvider, ToastProvider, typography } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Rechteverwaltung",
@@ -21,7 +21,7 @@ export default function RootLayout({
>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="rbac_admin">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+2 -2
View File
@@ -1,5 +1,5 @@
import Script from "next/script";
import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Routing-Regeln",
@@ -36,7 +36,7 @@ export default function RootLayout({
<style>{focusVisibleStyle}</style>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="routing_rules">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+73
View File
@@ -0,0 +1,73 @@
"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.
// Dieselbe Liste wie web/portal/config/modules.json, hier dupliziert, weil
// jedes Modul sein eigenes, unabhaengig gebautes Next.js-Bundle ist.
import type { ReactNode } from "react";
import { Shell } from "./Shell";
export interface ModuleRegistryEntry {
key: string;
label: string;
href: string;
}
export const MODULE_REGISTRY: ModuleRegistryEntry[] = [
{ key: "dms", label: "Dokumente", href: "http://localhost:3101" },
{ key: "dms_upload", label: "Dokumente hochladen", href: "http://localhost:3107" },
{ key: "mail", label: "Postfach", href: "http://localhost:3102" },
{ key: "workflow", label: "Aufgaben & Freigaben", href: "http://localhost:3103" },
{ key: "workflow_cases", label: "Akten & Vorgänge", href: "http://localhost:3108" },
{ key: "routing_rules", label: "Routing-Regeln", href: "http://localhost:3109" },
{ key: "ai", label: "KI-Funktionen", href: "http://localhost:3104" },
{ key: "ai_extraction", label: "Extraktions-Korrektur", href: "http://localhost:3110" },
{ key: "connect", label: "Externe Anbindungen", href: "http://localhost:3105" },
{ key: "connect_datev", label: "DATEV-Export", href: "http://localhost:3111" },
{ key: "tenant_admin", label: "Mandantenverwaltung", href: "http://localhost:3106" },
{ key: "rbac_admin", label: "Rollen & Rechte", href: "http://localhost:3112" },
{ key: "kennzahlen_dashboard", label: "Kennzahlen", href: "http://localhost:3113" },
{ key: "portal", label: "Übersicht", href: "http://localhost:3900" },
];
export interface ModuleNavShellProps {
activeKey: string;
children: ReactNode;
}
function resolveHref(href: string): string {
try {
const url = new URL(href);
if (url.hostname === "localhost" && typeof window !== "undefined" && window.location.hostname !== "localhost") {
url.hostname = window.location.hostname;
}
return url.toString();
} catch {
return 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 modules = MODULE_REGISTRY.map((m) => ({
key: m.key,
label: m.label,
href: resolveHref(m.href),
active: m.key === activeKey,
}));
return (
<Shell modules={modules} tenantLabel={tenantLabel()}>
{children}
</Shell>
);
}
+30 -3
View File
@@ -40,14 +40,41 @@ const responsiveStyles = `
outline-offset: 2px;
}
.shl-shell *, .shl-shell *::before, .shl-shell *::after { box-sizing: inherit; }
.shl-shell-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; padding: 8px 16px; }
.shl-shell-header nav ul { display: flex; gap: 8px; list-style: none; margin: 0; padding: 0; flex-wrap: wrap; }
.shl-shell-header nav a,
.shl-shell-header {
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px;
padding: 10px 20px;
background: var(--shl-color-surface-raised, #fff);
border-bottom: 1px solid var(--shl-color-border, #d7dbe0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
position: sticky; top: 0; z-index: 10;
}
.shl-shell-header nav ul { display: flex; gap: 4px; list-style: none; margin: 0; padding: 0; flex-wrap: wrap; }
.shl-shell-header nav a {
min-height: 44px; display: inline-flex; align-items: center; justify-content: center; padding: 0 14px;
border-radius: 8px; font-weight: 500; text-decoration: none;
color: var(--shl-color-text-secondary, #4b5563);
transition: background-color 0.15s ease, color 0.15s ease;
}
.shl-shell-header nav a:hover { background: var(--shl-color-accent-subtle, #eef3ff); }
.shl-shell-header nav a[aria-current="page"] {
background: var(--shl-color-accent-subtle, #eef3ff);
color: var(--shl-color-accent, #1d4ed8);
font-weight: 600;
}
.shl-shell-header button,
.shl-nav-toggle {
min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center; padding: 0 12px;
border-radius: 8px;
}
.shl-nav-toggle { display: none; background: none; border: 1px solid var(--shl-color-border, #d7dbe0); font-size: 1.25rem; }
.shl-shell-context { display: flex; align-items: center; gap: 12px; }
.shl-tenant-context {
font-size: 0.85rem; font-weight: 500; color: var(--shl-color-text-secondary, #4b5563);
padding: 4px 10px; border-radius: 999px; background: var(--shl-color-surface, #f5f6f8);
}
.shl-shell-context button {
background: none; border: 1px solid var(--shl-color-border, #d7dbe0);
}
@media (max-width: 768px) {
.shl-nav-toggle { display: inline-flex; }
.shl-shell-header nav { display: none; width: 100%; order: 3; }
+3
View File
@@ -14,6 +14,9 @@ export type { Locale } from "./i18n/messages";
export { Shell } from "./components/Shell";
export type { ShellProps, ModuleLink } from "./components/Shell";
export { ModuleNavShell, MODULE_REGISTRY } from "./components/ModuleNavShell";
export type { ModuleNavShellProps, ModuleRegistryEntry } from "./components/ModuleNavShell";
export { Dialog } from "./components/Dialog";
export type { DialogProps } from "./components/Dialog";
+2 -2
View File
@@ -1,4 +1,4 @@
import { ThemeProvider, I18nProvider, ToastProvider, typography } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Mandantenverwaltung",
@@ -21,7 +21,7 @@ export default function RootLayout({
>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="tenant_admin">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+2 -2
View File
@@ -1,5 +1,5 @@
import Script from "next/script";
import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Genehmigungen",
@@ -36,7 +36,7 @@ export default function RootLayout({
<style>{focusVisibleStyle}</style>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="workflow">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>
+2 -2
View File
@@ -1,5 +1,5 @@
import Script from "next/script";
import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
import { ThemeProvider, I18nProvider, ToastProvider, ModuleNavShell, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl";
export const metadata = {
title: "NEXARCH Akten",
@@ -36,7 +36,7 @@ export default function RootLayout({
<style>{focusVisibleStyle}</style>
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{children}</ToastProvider>
<ToastProvider><ModuleNavShell activeKey="workflow_cases">{children}</ModuleNavShell></ToastProvider>
</I18nProvider>
</ThemeProvider>
</body>