Iteration Teil B: Modul „Abhängigkeiten & kritische Pfade"

Referenz: docs/ISMS-Abhaengigkeitskarte-GEFIM.html

- Graph-Ableitung serverseitig (dependency-graph.ts) aus Process,
  ProcessAsset(role), Asset, AssetRelation — keine redundante Haltung.
  Kritikalität je Knoten (BIA / max C·I·A), kritische Kanten (beide
  Endknoten ≥ Schwellwert), kritischer Pfad (DFS), SPOF-Erkennung
  (transitiv abhängige kritische Prozesse ≥ N)
- React-Flow-Canvas (Topology-Look, dunkle Leinwand + Punktraster):
  Custom-Nodes mit Typ-Icon, Kritikalität, Status-Punkt, SPOF-Badge;
  smoothstep-Kanten mit Labels; kritische Kanten rot + Glow
- dagre-Auto-Layout (links Prozesse → rechts Assets); Toolbar mit Suche
  (Fokus/Zentrieren), Toggle „Kritische Pfade", Fit, PNG-Export;
  Knoten-Klick hebt zusammenhängenden Pfad hervor, Rest abgedunkelt
- Analyse-Panel: kritische Prozesse/Kanten, SPOF-Liste, kritischster
  Pfad, Legende
- Eigener Menüpunkt „Abhängigkeiten"; Absprung aus Asset-/Prozess-Detail
- Theme-Tokens für React Flow (Controls, kritische-Kanten-Glow)

Verifiziert: 10 Knoten/9 Kanten korrekt aus Demo-Daten, SPOF (Cloud-
Hoster, Kundendatenbank), kritischster Pfad, Toggle & Such-Fokus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Martin
2026-07-03 09:53:49 +02:00
co-authored by Claude Opus 4.8
parent 9e54c7788f
commit d9685b65b9
11 changed files with 1002 additions and 3 deletions
+131
View File
@@ -0,0 +1,131 @@
import { getTranslations } from "next-intl/server";
import { requireSession } from "@/server/auth";
import { dbForTenant } from "@/server/db";
import { requirePermission } from "@/server/rbac";
import { buildDependencyGraph } from "@/server/dependency-graph";
import { PageHead, SectTitle } from "@/components/mockup-ui";
import { DependencyGraphView } from "@/components/dependency-graph";
export default async function DependenciesPage() {
const session = await requireSession();
requirePermission(session, "asset:read");
const t = await getTranslations("dependencies");
const tType = await getTranslations("assetType");
const db = dbForTenant(session.user.tenantId);
const graph = await buildDependencyGraph(db);
const kindLabels: Record<string, string> = {
process: "Prozess",
INFORMATION: tType("INFORMATION"),
SYSTEM: tType("SYSTEM"),
APPLICATION: tType("APPLICATION"),
LOCATION: tType("LOCATION"),
SUPPLIER: tType("SUPPLIER"),
PERSON: tType("PERSON"),
DATA: tType("DATA"),
};
const labels = {
search: t("search"),
criticalToggle: t("criticalToggle"),
fit: t("fit"),
};
return (
<main className="flex-1 p-6">
<PageHead crumb={t("crumb")} title={t("title")} sub={t("sub")} />
{graph.nodes.length === 0 ? (
<div className="shadow-card rounded-xl border bg-card p-10 text-center text-sm text-muted-foreground">
{t("empty")}
</div>
) : (
<div className="grid gap-4 xl:grid-cols-[1fr_18rem]">
<DependencyGraphView graph={graph} kindLabels={kindLabels} labels={labels} />
<div className="space-y-4">
{/* Kennzahlen */}
<div className="grid grid-cols-2 gap-3">
<div className="shadow-card rounded-xl border bg-card p-3">
<div className="text-[11.5px] font-semibold text-muted-foreground">
{t("critProcesses")}
</div>
<div className="mt-1 font-heading text-2xl font-bold text-[#ff6b6b]">
{graph.analysis.criticalProcessCount}
</div>
</div>
<div className="shadow-card rounded-xl border bg-card p-3">
<div className="text-[11.5px] font-semibold text-muted-foreground">
{t("critEdges")}
</div>
<div className="mt-1 font-heading text-2xl font-bold">
{graph.analysis.criticalEdgeCount}
</div>
</div>
</div>
{/* SPOF */}
<div className="shadow-card rounded-xl border bg-card p-4">
<SectTitle title={t("spofTitle")} />
{graph.analysis.spofs.length === 0 ? (
<p className="mt-2 text-sm text-muted-foreground">{t("spofNone")}</p>
) : (
<ul className="mt-2 space-y-2 text-sm">
{graph.analysis.spofs.slice(0, 6).map((s) => (
<li key={s.id} className="rounded-lg border border-[rgba(255,107,107,0.3)] bg-[rgba(255,107,107,0.08)] p-2.5">
<div className="font-semibold text-[#ff6b6b]">{s.name}</div>
<div className="text-xs text-muted-foreground">
{t("spofHint", { count: s.count })}
</div>
</li>
))}
</ul>
)}
</div>
{/* Kritischster Pfad */}
<div className="shadow-card rounded-xl border bg-card p-4">
<SectTitle title={t("critPathTitle")} />
{graph.analysis.longestCriticalPath.length === 0 ? (
<p className="mt-2 text-sm text-muted-foreground">{t("critPathNone")}</p>
) : (
<div className="mt-2 flex flex-wrap items-center gap-1 text-[12.5px]">
{graph.analysis.longestCriticalPath.map((n, i) => (
<span key={i} className="flex items-center gap-1">
{i > 0 && <span className="text-[#ff6b6b]"></span>}
<span className="rounded-md bg-[rgba(255,107,107,0.12)] px-2 py-0.5 font-medium text-[#ffd0d0]">
{n}
</span>
</span>
))}
</div>
)}
</div>
{/* Legende */}
<div className="shadow-card rounded-xl border bg-card p-4 text-[12.5px]">
<SectTitle title={t("legend")} />
<ul className="mt-2 space-y-1.5 text-muted-foreground">
<li className="flex items-center gap-2">
<span className="h-[3px] w-6 rounded bg-[#ff6b6b]" /> {t("legCritical")}
</li>
<li className="flex items-center gap-2">
<span className="h-[2px] w-6 rounded bg-[#4a5372]" /> {t("legStandard")}
</li>
<li className="flex items-center gap-2">
<span className="rounded bg-[rgba(255,107,107,0.18)] px-1.5 py-px text-[9.5px] font-bold text-[#ff6b6b]">
SPOF
</span>{" "}
{t("legSpof")}
</li>
<li className="flex items-center gap-2">
<span className="inline-block size-2.5 rounded-full bg-[#ff6b6b]" /> {t("legCrit")}
</li>
</ul>
</div>
</div>
</div>
)}
</main>
);
}
+1 -1
View File
@@ -41,7 +41,7 @@ export default async function AppLayout({
{ href: "/incidents", label: t("incidents"), icon: Siren, enabled: false },
{ href: "/policies", label: t("policies"), icon: BookOpenText, enabled: false },
{ href: "/chat", label: t("chat"), icon: MessagesSquare, enabled: false },
{ href: "/dependencies", label: t("dependencies"), icon: Network, enabled: false },
{ href: "/dependencies", label: t("dependencies"), icon: Network, enabled: true },
{ href: "/evidence", label: t("evidence"), icon: FolderCheck, enabled: false },
{ href: "/suppliers", label: t("suppliers"), icon: Truck, enabled: false },
{ href: "/review", label: t("review"), icon: LineChart, enabled: false },
+19
View File
@@ -184,3 +184,22 @@ body {
background-size: 26px 26px;
}
}
/* React Flow — Dark-Anpassungen + Glow für kritische Kanten (Teil B) */
.react-flow__edge.critical-edge .react-flow__edge-path {
filter: drop-shadow(0 0 4px rgba(255, 107, 107, 0.7));
}
.react-flow__controls-button {
background: var(--elevated) !important;
border-color: var(--panel-brd) !important;
color: var(--muted-foreground) !important;
}
.react-flow__controls-button:hover {
background: var(--accent) !important;
}
.react-flow__controls-button svg {
fill: currentColor;
}
.react-flow__edge-text {
fill: var(--muted-foreground);
}