From 790a349f1f6b2015bfb608eb08e920b32de345fc Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 9 Feb 2026 23:42:25 -0800 Subject: [PATCH] Wire compliance hub overview to aggregated live data --- src/app/api/compliance/overview/route.ts | 179 ++++++++++ src/app/compliance/compliance-hub-client.tsx | 333 ++++++++++--------- src/app/compliance/page.tsx | 1 + 3 files changed, 355 insertions(+), 158 deletions(-) create mode 100644 src/app/api/compliance/overview/route.ts diff --git a/src/app/api/compliance/overview/route.ts b/src/app/api/compliance/overview/route.ts new file mode 100644 index 0000000..2f1d3e3 --- /dev/null +++ b/src/app/api/compliance/overview/route.ts @@ -0,0 +1,179 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { NextResponse } from "next/server"; +import { readGovernancePolicies } from "@/lib/governance"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const AUDIT_DATA_PATH = path.join(process.cwd(), "data", "compliance-audits.json"); +const AUDIT_STATUSES = ["pass", "fail", "partial", "na"] as const; + +type AuditStatus = (typeof AUDIT_STATUSES)[number]; + +interface ComplianceAudit { + id: string; + framework: string; + controlId: string; + controlName: string; + status: AuditStatus; + evidence: string; + auditor: string; + auditDate: string; + nextReviewDate: string; + notes: string; +} + +interface FrameworkBreakdown { + framework: string; + score: number; + passed: number; + partial: number; + failed: number; + notApplicable: number; + totalApplicable: number; + totalControls: number; +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +function isAuditStatus(value: unknown): value is AuditStatus { + return typeof value === "string" && AUDIT_STATUSES.includes(value as AuditStatus); +} + +function isValidDateString(value: unknown): value is string { + if (!isNonEmptyString(value)) { + return false; + } + + return !Number.isNaN(new Date(value).getTime()); +} + +function isComplianceAudit(value: unknown): value is ComplianceAudit { + if (!value || typeof value !== "object") { + return false; + } + + const entry = value as Record; + + return ( + isNonEmptyString(entry.id) && + isNonEmptyString(entry.framework) && + isNonEmptyString(entry.controlId) && + isNonEmptyString(entry.controlName) && + isAuditStatus(entry.status) && + isNonEmptyString(entry.evidence) && + isNonEmptyString(entry.auditor) && + isValidDateString(entry.auditDate) && + isValidDateString(entry.nextReviewDate) && + isNonEmptyString(entry.notes) + ); +} + +async function readAuditData(): Promise { + const raw = await fs.readFile(AUDIT_DATA_PATH, "utf-8"); + const parsed = JSON.parse(raw) as unknown; + + if (!Array.isArray(parsed) || !parsed.every(isComplianceAudit)) { + throw new Error("Invalid compliance audit dataset"); + } + + return parsed; +} + +function calculateFrameworkBreakdown(audits: ComplianceAudit[]): FrameworkBreakdown[] { + const frameworkMap = new Map(); + + for (const audit of audits) { + const bucket = frameworkMap.get(audit.framework) ?? []; + bucket.push(audit); + frameworkMap.set(audit.framework, bucket); + } + + return Array.from(frameworkMap.entries()) + .map(([framework, frameworkAudits]) => { + const passed = frameworkAudits.filter((audit) => audit.status === "pass").length; + const partial = frameworkAudits.filter((audit) => audit.status === "partial").length; + const failed = frameworkAudits.filter((audit) => audit.status === "fail").length; + const notApplicable = frameworkAudits.filter((audit) => audit.status === "na").length; + + const totalApplicable = passed + partial + failed; + const weightedPass = passed + partial * 0.5; + const score = totalApplicable > 0 ? Math.round((weightedPass / totalApplicable) * 100) : 0; + + return { + framework, + score, + passed, + partial, + failed, + notApplicable, + totalApplicable, + totalControls: frameworkAudits.length, + }; + }) + .sort((a, b) => b.score - a.score); +} + +function calculateOverallScore(frameworkBreakdown: FrameworkBreakdown[], policyActiveRate: number): number { + if (frameworkBreakdown.length === 0) { + return Math.round(policyActiveRate); + } + + const frameworkAverage = + frameworkBreakdown.reduce((sum, framework) => sum + framework.score, 0) / frameworkBreakdown.length; + + return Math.round(frameworkAverage * 0.75 + policyActiveRate * 0.25); +} + +export async function GET() { + try { + const [audits, policies] = await Promise.all([readAuditData(), readGovernancePolicies()]); + + const frameworkBreakdown = calculateFrameworkBreakdown(audits); + + const policyStatusCounts = { + active: policies.filter((policy) => policy.status === "active").length, + review: policies.filter((policy) => policy.status === "review").length, + draft: policies.filter((policy) => policy.status === "draft").length, + total: policies.length, + }; + + const policyActiveRate = + policyStatusCounts.total > 0 ? (policyStatusCounts.active / policyStatusCounts.total) * 100 : 0; + + const recentAudits = [...audits] + .sort((a, b) => new Date(b.auditDate).getTime() - new Date(a.auditDate).getTime()) + .slice(0, 5); + + return NextResponse.json( + { + overallComplianceScore: calculateOverallScore(frameworkBreakdown, policyActiveRate), + frameworkBreakdown, + policyStatusCounts, + recentAudits, + summary: { + totalAudits: audits.length, + totalFrameworks: frameworkBreakdown.length, + activePolicies: policyStatusCounts.active, + }, + }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); + } catch { + return NextResponse.json( + { + error: "Unable to load compliance overview", + }, + { + status: 500, + } + ); + } +} diff --git a/src/app/compliance/compliance-hub-client.tsx b/src/app/compliance/compliance-hub-client.tsx index 7da97b3..bd2ef92 100644 --- a/src/app/compliance/compliance-hub-client.tsx +++ b/src/app/compliance/compliance-hub-client.tsx @@ -1,49 +1,70 @@ /* eslint-disable react/no-unescaped-entities */ "use client"; -import { useEffect, useMemo, useState } from "react"; -import { AlertTriangle, CheckCircle2, ChevronDown, CircleX } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { AlertTriangle, ArrowRight } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; -import type { ComplianceCheck, ComplianceFramework } from "@/lib/complianceHub"; -interface ComplianceApiResponse { +type AuditStatus = "pass" | "fail" | "partial" | "na"; + +interface FrameworkBreakdown { + framework: string; + score: number; + passed: number; + partial: number; + failed: number; + notApplicable: number; + totalApplicable: number; + totalControls: number; +} + +interface RecentAudit { + id: string; + framework: string; + controlId: string; + controlName: string; + status: AuditStatus; + auditDate: string; + auditor: string; +} + +interface ComplianceOverviewResponse { overallComplianceScore: number; - frameworks: ComplianceFramework[]; - checks: ComplianceCheck[]; + frameworkBreakdown: FrameworkBreakdown[]; + policyStatusCounts: { + active: number; + review: number; + draft: number; + total: number; + }; + recentAudits: RecentAudit[]; summary: { + totalAudits: number; totalFrameworks: number; - totalChecks: number; - passingChecks: number; + activePolicies: number; }; } -const FILTERS = [ - { label: "All categories", value: "all" }, - { label: "Framework: GDPR", value: "gdpr" }, - { label: "Framework: SOC2", value: "soc2" }, - { label: "Framework: HIPAA", value: "hipaa" }, - { label: "Framework: ISO27001", value: "iso27001" }, - { label: "Framework: CCPA", value: "ccpa" }, - { label: "Category: Privacy", value: "privacy" }, - { label: "Category: Security", value: "security" }, - { label: "Category: Governance", value: "governance" }, - { label: "Category: Risk", value: "risk" }, - { label: "Category: Healthcare", value: "healthcare" }, -]; - -function statusBadgeClass(status: ComplianceFramework["status"]) { - if (status === "compliant") { - return "bg-emerald-500/15 text-emerald-300 border-emerald-500/40"; - } - - if (status === "partial") { - return "bg-amber-500/15 text-amber-300 border-amber-500/40"; - } - - return "bg-rose-500/15 text-rose-300 border-rose-500/40"; -} +const SUB_PAGE_LINKS = [ + { + title: "Audit Tracker", + description: "Inspect control-level findings and framework performance.", + href: "/compliance/audit", + }, + { + title: "Governance Framework", + description: "Review operational, security, and policy governance standards.", + href: "/compliance/governance", + }, + { + title: "Responsible AI", + description: "See ethics-oriented policies and responsible AI principles.", + href: "/compliance/responsible-ai", + }, +] as const; function scoreBarClass(score: number) { if (score >= 85) { @@ -57,6 +78,13 @@ function scoreBarClass(score: number) { return "bg-rose-400"; } +function statusChipClass(status: AuditStatus) { + if (status === "pass") return "text-emerald-300 border-emerald-500/40 bg-emerald-500/10"; + if (status === "partial") return "text-amber-300 border-amber-500/40 bg-amber-500/10"; + if (status === "na") return "text-slate-300 border-slate-500/40 bg-slate-500/10"; + return "text-rose-300 border-rose-500/40 bg-rose-500/10"; +} + function formatDate(dateInput: string) { const date = new Date(dateInput); @@ -72,36 +100,34 @@ function formatDate(dateInput: string) { } export default function ComplianceHubClient() { - const [selectedCategory, setSelectedCategory] = useState("all"); - const [data, setData] = useState(null); + const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let isCancelled = false; - async function loadComplianceData() { + async function loadOverview() { setLoading(true); setError(null); try { - const query = selectedCategory === "all" ? "" : `?category=${encodeURIComponent(selectedCategory)}`; - const response = await fetch(`/api/compliance${query}`, { + const response = await fetch("/api/compliance/overview", { cache: "no-store", }); if (!response.ok) { - throw new Error("Unable to load compliance data"); + throw new Error("Unable to load compliance overview"); } - const payload = (await response.json()) as ComplianceApiResponse; + const payload = (await response.json()) as ComplianceOverviewResponse; if (!isCancelled) { setData(payload); } } catch { if (!isCancelled) { - setError("Unable to load compliance data right now. Please try again."); + setError("Unable to load compliance overview right now. Please try again."); setData(null); } } finally { @@ -111,169 +137,160 @@ export default function ComplianceHubClient() { } } - void loadComplianceData(); + void loadOverview(); return () => { isCancelled = true; }; - }, [selectedCategory]); - - const passRate = useMemo(() => { - if (!data || data.summary.totalChecks === 0) { - return 0; - } - - return Math.round((data.summary.passingChecks / data.summary.totalChecks) * 100); - }, [data]); + }, []); return (
-
+
-
- Real-time Compliance Tracking +
+ Live Aggregated Compliance Data

Compliance Hub

-

Monitor each framework's readiness with persistent audits and requirement-level checks.

+

+ Unified view across audits and governance policies with framework-level scores, policy status totals, and recent findings. +

-
-
-

Framework filter

-

Filter by compliance framework or requirement category.

-
- -
- - -
-
-
- -
- {loading && ( + {loading ? ( - Loading compliance framework data... + Loading compliance overview... - )} + ) : null} - {error && ( + {error ? ( {error} - )} + ) : null} - {!loading && !error && data && ( -
- + {!loading && !error && data ? ( +
+ Overall compliance score -
-
-

{data.overallComplianceScore}%

-

- {data.summary.passingChecks} of {data.summary.totalChecks} checks passing ({passRate}% pass rate) +

+
+

{data.overallComplianceScore}%

+

+ {data.summary.totalFrameworks} frameworks · {data.summary.totalAudits} audits · {data.policyStatusCounts.total} policies

-
-
+ +
+
+

Weighted from framework scores (75%) and active-policy rate (25%).

- {data.frameworks.length === 0 && ( - - No frameworks matched this filter. - - )} - - {data.frameworks.map((framework) => ( - - -
-
- {framework.name} -

Last audit: {formatDate(framework.lastAudit)}

-
-
- - {framework.status} - - {framework.score}% -
-
+ + + Framework breakdown + + +
+ {data.frameworkBreakdown.map((framework) => ( +
+
+

{framework.framework}

+ {framework.score}% +
+ +
+
+
+ +
+

Pass: {framework.passed}

+

Partial: {framework.partial}

+

Fail: {framework.failed}

+

N/A: {framework.notApplicable}

+
+
+ ))} +
+
+
-
-
-
- - - -
- - Requirements checklist ({framework.requirements.length}) - - - -
- {framework.requirements.map((requirement) => ( -
-
-
-

{requirement.title}

-

{requirement.description}

-

Category: {requirement.category}

-
-
- {requirement.pass ? ( - - ) : ( - - )} -
-
-
- ))} -
-
-
- - ))} + + + Policy status summary + + +
+

Active{data.policyStatusCounts.active}

+

In review{data.policyStatusCounts.review}

+

Draft{data.policyStatusCounts.draft}

+ +

Total{data.policyStatusCounts.total}

+
+
+
+ + + + Recent audits + + +
+ {data.recentAudits.map((audit) => ( +
+
+ {audit.framework} + {audit.status} + {formatDate(audit.auditDate)} +
+

{audit.controlName}

+

{audit.controlId} · Auditor: {audit.auditor}

+
+ ))} +
+
+
+ + + + Compliance sections + + +
+ {SUB_PAGE_LINKS.map((link) => ( + +
+

{link.title}

+ +
+

{link.description}

+ + ))} +
+
+
- )} + ) : null}
); diff --git a/src/app/compliance/page.tsx b/src/app/compliance/page.tsx index 5d72276..4b67a54 100644 --- a/src/app/compliance/page.tsx +++ b/src/app/compliance/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/no-unescaped-entities */ import type { Metadata } from "next"; import ComplianceHubClient from "./compliance-hub-client";