Files
nexarch/web/portal/app/ModuleGrid.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

98 lines
2.7 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useI18n, resolveModuleHref } from "@nexarch/shl";
import type { ModuleEntry } from "../lib/modules";
import { getSession } from "../lib/http";
interface VisibleModulesResponse {
modules?: ModuleEntry[];
error?: string;
}
export async function fetchVisibleModules(
tenant: string,
role: string,
fetchImpl: typeof fetch = fetch
): Promise<ModuleEntry[]> {
const res = await fetchImpl(
`/api/visible-modules?tenant=${encodeURIComponent(tenant)}&role=${encodeURIComponent(role)}`
);
if (!res.ok) return [];
const body = (await res.json()) as VisibleModulesResponse;
return body.modules ?? [];
}
type State =
| { status: "loading" }
| { status: "error"; message: string }
| { status: "ready"; modules: ModuleEntry[] };
export function ModuleGrid() {
const { t } = useI18n();
const [state, setState] = useState<State>({ status: "loading" });
useEffect(() => {
let cancelled = false;
(async () => {
try {
const session = getSession();
await session.token();
const identity = session.identity();
if (!identity) {
if (!cancelled) setState({ status: "error", message: t("portal.noIdentity", "Anmeldung erforderlich.") });
return;
}
const modules = await fetchVisibleModules(identity.tenant, identity.role);
if (!cancelled) setState({ status: "ready", modules });
} catch (err) {
if (!cancelled) {
setState({ status: "error", message: (err as Error).message });
}
}
})();
return () => {
cancelled = true;
};
}, [t]);
if (state.status === "loading") return <p>{t("portal.loading", "Lade Übersicht …")}</p>;
if (state.status === "error") return <p role="alert">{state.message}</p>;
if (state.modules.length === 0) {
return <p>{t("portal.noModules", "Für Ihr Konto sind derzeit keine Module freigeschaltet.")}</p>;
}
return (
<ul
style={{
listStyle: "none",
margin: 0,
padding: 0,
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))",
gap: 16,
}}
>
{state.modules.map((mod) => (
<li key={mod.key}>
<a
href={resolveModuleHref(mod.href)}
style={{
display: "block",
minHeight: 44,
padding: 16,
border: "1px solid var(--shl-color-border, #d7dbe0)",
borderRadius: 8,
textDecoration: "none",
color: "inherit",
}}
>
<strong>{mod.label}</strong>
<p style={{ margin: "4px 0 0" }}>{mod.description}</p>
</a>
</li>
))}
</ul>
);
}