|
| 1 | +import { readFileSync } from 'fs'; |
| 2 | +import path from 'path'; |
| 3 | + |
| 4 | +export const metadata = { title: 'Governance Capability Matrix' } as const; |
| 5 | +export const dynamic = 'force-static'; |
| 6 | + |
| 7 | +type Dimension = { |
| 8 | + id: string; |
| 9 | + name: string; |
| 10 | + phase: string; |
| 11 | + score: number; // 0-5 |
| 12 | + evidence: string[]; |
| 13 | + gaps: string[]; |
| 14 | + remediation: string[]; |
| 15 | + links?: Record<string, string>; |
| 16 | +}; |
| 17 | + |
| 18 | +type Maturity = { dimensions: Dimension[] }; |
| 19 | + |
| 20 | +function gateText(score: number) { |
| 21 | + if (score < 2) return { label: 'Do not advance', color: '#dc2626', note: 'Address gaps before proceeding' }; |
| 22 | + if (score < 4) return { label: 'Proceed with guardrails', color: '#f59e0b', note: 'Monitor and document mitigations' }; |
| 23 | + return { label: 'Clear to advance', color: '#16a34a', note: 'Maintain controls and evidence' }; |
| 24 | +} |
| 25 | + |
| 26 | +function scoreColor(score: number) { |
| 27 | + if (score <= 1) return '#b91c1c'; |
| 28 | + if (score === 2) return '#e11d48'; |
| 29 | + if (score === 3) return '#f59e0b'; |
| 30 | + if (score === 4) return '#10b981'; |
| 31 | + return '#059669'; |
| 32 | +} |
| 33 | + |
| 34 | +export default function Page() { |
| 35 | + const file = path.join(process.cwd(), 'next-app', 'data', 'maturity.json'); |
| 36 | + const data: Maturity = JSON.parse(readFileSync(file, 'utf8')); |
| 37 | + return ( |
| 38 | + <main className="space-y-4"> |
| 39 | + <h1 className="text-2xl font-semibold">Governance Capability Matrix</h1> |
| 40 | + <p className="text-sm text-slate-600">Scores (0–5), evidence, gaps, remediation and gating guidance per dimension.</p> |
| 41 | + |
| 42 | + <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> |
| 43 | + {data.dimensions.map((d) => { |
| 44 | + const gate = gateText(d.score); |
| 45 | + return ( |
| 46 | + <section key={d.id} className="rounded border bg-white p-4 shadow-sm"> |
| 47 | + <header className="mb-2 flex items-center justify-between"> |
| 48 | + <div> |
| 49 | + <div className="text-base font-semibold text-slate-800">{d.name}</div> |
| 50 | + <div className="text-xs text-slate-500">Phase: {d.phase}</div> |
| 51 | + </div> |
| 52 | + <div className="text-right"> |
| 53 | + <span className="inline-flex items-center gap-1 rounded border px-2 py-0.5 text-xs" style={{ borderColor: scoreColor(d.score), color: scoreColor(d.score) }}> |
| 54 | + Score <strong className="ml-1">{d.score}</strong> |
| 55 | + </span> |
| 56 | + <div className="mt-1 text-xs" style={{ color: gate.color }}>{gate.label}</div> |
| 57 | + </div> |
| 58 | + </header> |
| 59 | + |
| 60 | + {d.evidence?.length ? ( |
| 61 | + <div className="mb-2"> |
| 62 | + <div className="mb-1 text-xs font-semibold text-slate-700">Evidence</div> |
| 63 | + <ul className="list-disc pl-5 text-sm text-slate-700"> |
| 64 | + {d.evidence.map((e, i) => (<li key={i}>{e}</li>))} |
| 65 | + </ul> |
| 66 | + </div> |
| 67 | + ) : null} |
| 68 | + |
| 69 | + {d.gaps?.length ? ( |
| 70 | + <div className="mb-2"> |
| 71 | + <div className="mb-1 text-xs font-semibold text-slate-700">Gaps</div> |
| 72 | + <ul className="list-disc pl-5 text-sm text-slate-700"> |
| 73 | + {d.gaps.map((g, i) => (<li key={i}>{g}</li>))} |
| 74 | + </ul> |
| 75 | + <div className="mt-2 rounded bg-red-50 p-2 text-xs text-red-700">{gate.note}</div> |
| 76 | + </div> |
| 77 | + ) : null} |
| 78 | + |
| 79 | + {d.remediation?.length ? ( |
| 80 | + <div className="mb-2"> |
| 81 | + <div className="mb-1 text-xs font-semibold text-slate-700">Remediation</div> |
| 82 | + <ul className="list-disc pl-5 text-sm text-slate-700"> |
| 83 | + {d.remediation.map((r, i) => (<li key={i}>{r}</li>))} |
| 84 | + </ul> |
| 85 | + </div> |
| 86 | + ) : null} |
| 87 | + |
| 88 | + {d.links && Object.keys(d.links).length > 0 ? ( |
| 89 | + <div className="mt-3 flex flex-wrap gap-2 text-xs"> |
| 90 | + {Object.entries(d.links).map(([k, v]) => ( |
| 91 | + <a key={k} href={v} className="rounded border border-amber-300 bg-amber-50 px-2 py-1 text-amber-800 underline"> |
| 92 | + {k} |
| 93 | + </a> |
| 94 | + ))} |
| 95 | + </div> |
| 96 | + ) : null} |
| 97 | + </section> |
| 98 | + ); |
| 99 | + })} |
| 100 | + </div> |
| 101 | + </main> |
| 102 | + ); |
| 103 | +} |
0 commit comments