@greenarmor/ges-web-dashboard 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as http from "node:http";
2
- import type { ScoreFile, Control, FixHistoryEntry, ActivityLogEntry } from "@greenarmor/ges-core";
2
+ import type { ScoreFile, Control, FixHistoryEntry, ActivityLogEntry, GovernanceRecord, GovernanceVerificationResult } from "@greenarmor/ges-core";
3
3
  import type { Finding } from "@greenarmor/ges-audit-engine";
4
4
  export interface DashboardOptions {
5
5
  port?: number;
@@ -78,9 +78,26 @@ export interface DashboardData {
78
78
  packs: PackSummary[];
79
79
  fixHistory: FixHistoryEntry[];
80
80
  activityLog: ActivityLogEntry[];
81
+ governance: GovernanceData;
81
82
  lastAudit: string;
82
83
  }
84
+ export interface GovernanceData {
85
+ records: GovernanceRecord[];
86
+ verifications: GovernanceVerificationResult[];
87
+ summary: {
88
+ total: number;
89
+ approved: number;
90
+ pending: number;
91
+ rejected: number;
92
+ expired: number;
93
+ validWithIssues: number;
94
+ criticalRisk: number;
95
+ highRisk: number;
96
+ totalEvidence: number;
97
+ };
98
+ }
83
99
  export declare function collectDashboardData(projectPath: string): DashboardData;
84
100
  export declare function collectPackDetail(projectPath: string, packId: string): PackDetailReport | null;
85
101
  export declare function collectControlDetail(projectPath: string, controlId: string): ControlDetail | null;
102
+ export declare function collectGovernanceData(projectPath: string): GovernanceData;
86
103
  export declare function startDashboard(options: DashboardOptions): http.Server;
package/dist/index.js CHANGED
@@ -5,7 +5,9 @@ import { runAudit, deduplicateFindings } from "@greenarmor/ges-audit-engine";
5
5
  import { getAllPacks, getPack } from "@greenarmor/ges-policy-engine";
6
6
  import { generateScoreFile } from "@greenarmor/ges-scoring-engine";
7
7
  import { loadFixHistory, loadActivityLog, loadControlsFromDisk, loadControlOverrides, applyOverridesToControls } from "@greenarmor/ges-core";
8
+ import { loadGovernanceRecords, verifyGovernanceRecord, verifyAllGovernanceRecords } from "@greenarmor/ges-core";
8
9
  import { getInstalledPackIds as getInstalledPackIdsFromDisk } from "@greenarmor/ges-core";
10
+ import { generateMarkdownReport, generateHtmlReport } from "@greenarmor/ges-report-generator";
9
11
  import { renderDashboard } from "./template.js";
10
12
  function loadConfig(projectPath) {
11
13
  const configPath = path.join(projectPath, ".ges", "config.json");
@@ -176,6 +178,7 @@ export function collectDashboardData(projectPath) {
176
178
  const packs = allPacks.map(p => buildPackSummary(p, controls, findings, installedPacks));
177
179
  const fixHistory = loadFixHistory(projectPath);
178
180
  const activityLog = loadActivityLog(projectPath);
181
+ const governance = collectGovernanceData(projectPath);
179
182
  const metadataPath = path.join(projectPath, ".ges", "metadata.json");
180
183
  let lastAudit = "";
181
184
  try {
@@ -190,13 +193,14 @@ export function collectDashboardData(projectPath) {
190
193
  projectName: config?.project_name || "Unknown Project",
191
194
  projectType: config?.project_type || "unknown",
192
195
  frameworks: allFrameworks,
193
- gesfVersion: "1.3.0",
196
+ gesfVersion: "1.4.1",
194
197
  score,
195
198
  controls,
196
199
  findings,
197
200
  packs,
198
201
  fixHistory,
199
202
  activityLog,
203
+ governance,
200
204
  lastAudit,
201
205
  };
202
206
  }
@@ -317,6 +321,22 @@ export function collectControlDetail(projectPath, controlId) {
317
321
  packName: matchingPack?.name || "",
318
322
  };
319
323
  }
324
+ export function collectGovernanceData(projectPath) {
325
+ const records = loadGovernanceRecords(projectPath);
326
+ const verifications = verifyAllGovernanceRecords(projectPath);
327
+ const summary = {
328
+ total: records.length,
329
+ approved: records.filter(r => r.status === "approved").length,
330
+ pending: records.filter(r => r.status === "draft" || r.status === "pending-review").length,
331
+ rejected: records.filter(r => r.status === "rejected" || r.status === "revoked").length,
332
+ expired: records.filter(r => r.status === "expired").length,
333
+ validWithIssues: verifications.filter(v => !v.valid && v.completeness.has_approval).length,
334
+ criticalRisk: records.filter(r => r.risk_level === "critical").length,
335
+ highRisk: records.filter(r => r.risk_level === "high").length,
336
+ totalEvidence: records.reduce((sum, r) => sum + r.evidence.length, 0),
337
+ };
338
+ return { records, verifications, summary };
339
+ }
320
340
  function jsonResponse(res, data, status = 200) {
321
341
  res.writeHead(status, { "Content-Type": "application/json" });
322
342
  res.end(JSON.stringify(data));
@@ -394,6 +414,33 @@ export function startDashboard(options) {
394
414
  }
395
415
  return;
396
416
  }
417
+ if (pathname === "/api/governance") {
418
+ try {
419
+ const data = collectDashboardData(options.projectPath);
420
+ jsonResponse(res, data.governance);
421
+ }
422
+ catch (err) {
423
+ jsonError(res, err instanceof Error ? err.message : String(err));
424
+ }
425
+ return;
426
+ }
427
+ const governanceMatch = pathname.match(/^\/api\/governance\/(.+)$/);
428
+ if (governanceMatch) {
429
+ try {
430
+ const govData = collectGovernanceData(options.projectPath);
431
+ const record = govData.records.find(r => r.id === governanceMatch[1] || r.system_name.toLowerCase() === decodeURIComponent(governanceMatch[1]).toLowerCase());
432
+ if (!record) {
433
+ jsonError(res, `Governance record not found: ${governanceMatch[1]}`, 404);
434
+ return;
435
+ }
436
+ const verification = verifyGovernanceRecord(record);
437
+ jsonResponse(res, { record, verification });
438
+ }
439
+ catch (err) {
440
+ jsonError(res, err instanceof Error ? err.message : String(err));
441
+ }
442
+ return;
443
+ }
397
444
  const packMatch = pathname.match(/^\/api\/packs\/([a-z0-9-]+)$/);
398
445
  if (packMatch) {
399
446
  try {
@@ -454,6 +501,63 @@ export function startDashboard(options) {
454
501
  }
455
502
  return;
456
503
  }
504
+ const governanceDetailMatch = pathname.match(/^\/api\/governance\/(.+)$/);
505
+ if (governanceDetailMatch) {
506
+ try {
507
+ const govData = collectGovernanceData(options.projectPath);
508
+ const record = govData.records.find(r => r.id === governanceDetailMatch[1] || r.system_name.toLowerCase() === decodeURIComponent(governanceDetailMatch[1]).toLowerCase());
509
+ if (!record) {
510
+ jsonError(res, `Governance record not found: ${governanceDetailMatch[1]}`, 404);
511
+ return;
512
+ }
513
+ const verification = verifyGovernanceRecord(record);
514
+ jsonResponse(res, { record, verification });
515
+ }
516
+ catch (err) {
517
+ jsonError(res, err instanceof Error ? err.message : String(err));
518
+ }
519
+ return;
520
+ }
521
+ if (pathname === "/api/report/compliance") {
522
+ try {
523
+ const data = collectDashboardData(options.projectPath);
524
+ const format = url.searchParams.get("format") || "markdown";
525
+ const reportOptions = {
526
+ format,
527
+ title: `Compliance Report - ${data.projectName}`,
528
+ include_executive_summary: true,
529
+ include_risk_assessment: true,
530
+ include_compliance: true,
531
+ include_security: true,
532
+ };
533
+ if (format === "html") {
534
+ const html = generateHtmlReport(reportOptions, data.score, data.controls, data.findings);
535
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Disposition": `attachment; filename="compliance-report.html"` });
536
+ res.end(html);
537
+ }
538
+ else {
539
+ const md = generateMarkdownReport(reportOptions, data.score, data.controls, data.findings);
540
+ res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="compliance-report.md"` });
541
+ res.end(md);
542
+ }
543
+ }
544
+ catch (err) {
545
+ jsonError(res, err instanceof Error ? err.message : String(err));
546
+ }
547
+ return;
548
+ }
549
+ if (pathname === "/api/report/governance") {
550
+ try {
551
+ const govData = collectGovernanceData(options.projectPath);
552
+ const md = generateGovernanceMarkdownReport(govData, collectDashboardData(options.projectPath).projectName);
553
+ res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="governance-provenance-report.md"` });
554
+ res.end(md);
555
+ }
556
+ catch (err) {
557
+ jsonError(res, err instanceof Error ? err.message : String(err));
558
+ }
559
+ return;
560
+ }
457
561
  if (pathname === "/health") {
458
562
  jsonResponse(res, { status: "ok", timestamp: new Date().toISOString() });
459
563
  return;
@@ -464,3 +568,114 @@ export function startDashboard(options) {
464
568
  server.listen(port, host);
465
569
  return server;
466
570
  }
571
+ function generateGovernanceMarkdownReport(data, projectName) {
572
+ const lines = [];
573
+ lines.push(`# Governance Provenance Report`);
574
+ lines.push(`\n**Project**: ${projectName}`);
575
+ lines.push(`**Generated**: ${new Date().toISOString()}\n`);
576
+ const s = data.summary;
577
+ lines.push(`## Summary\n`);
578
+ lines.push(`| Metric | Value |`);
579
+ lines.push(`|--------|-------|`);
580
+ lines.push(`| Total Systems | ${s.total} |`);
581
+ lines.push(`| Approved | ${s.approved} |`);
582
+ lines.push(`| Pending | ${s.pending} |`);
583
+ lines.push(`| Expired / With Issues | ${s.expired + s.validWithIssues} |`);
584
+ lines.push(`| Critical Risk | ${s.criticalRisk} |`);
585
+ lines.push(`| High Risk | ${s.highRisk} |`);
586
+ lines.push(`| Total Evidence References | ${s.totalEvidence} |`);
587
+ if (data.records.length === 0) {
588
+ lines.push(`\n_No governance records found._`);
589
+ return lines.join("\n");
590
+ }
591
+ for (let i = 0; i < data.records.length; i++) {
592
+ const r = data.records[i];
593
+ const v = data.verifications[i] || data.verifications.find(vv => vv.record_id === r.id);
594
+ lines.push(`\n---\n`);
595
+ lines.push(`## ${r.system_name}\n`);
596
+ lines.push(`| Field | Value |`);
597
+ lines.push(`|-------|-------|`);
598
+ lines.push(`| ID | ${r.id} |`);
599
+ lines.push(`| Type | ${r.system_type} |`);
600
+ lines.push(`| Version | ${r.system_version || "(none)"} |`);
601
+ lines.push(`| Status | ${r.status} |`);
602
+ lines.push(`| Risk Level | ${r.risk_level} |`);
603
+ if (v) {
604
+ lines.push(`| Verification | ${v.valid ? "VALID" : "ISSUES"} |`);
605
+ lines.push(`| Approval Status | ${v.approval_status} |`);
606
+ if (v.days_until_expiry !== null) {
607
+ lines.push(`| Days Until Expiry | ${v.days_until_expiry} |`);
608
+ }
609
+ }
610
+ if (r.approval) {
611
+ const a = r.approval;
612
+ lines.push(`\n### Approval Decision\n`);
613
+ lines.push(`- **Approver**: ${a.approver_name} (${a.approver_role})`);
614
+ lines.push(`- **Authority**: ${a.approval_authority}`);
615
+ lines.push(`- **Decision**: ${a.decision.toUpperCase()}`);
616
+ lines.push(`- **Date**: ${a.decision_date}`);
617
+ lines.push(`- **Validity**: ${a.valid_from} → ${a.valid_until || "indefinite"}`);
618
+ if (a.conditions.length > 0)
619
+ lines.push(`- **Conditions**: ${a.conditions.join("; ")}`);
620
+ if (a.rationale)
621
+ lines.push(`- **Rationale**: ${a.rationale}`);
622
+ }
623
+ if (r.risk_assessment) {
624
+ const ra = r.risk_assessment;
625
+ lines.push(`\n### Risk Assessment\n`);
626
+ lines.push(`- **Assessor**: ${ra.assessor}`);
627
+ lines.push(`- **Methodology**: ${ra.methodology}`);
628
+ lines.push(`- **Risk Score**: ${ra.risk_score}`);
629
+ lines.push(`- **Residual Risk**: ${ra.residual_risk}`);
630
+ lines.push(`- **Date**: ${ra.assessment_date}`);
631
+ if (ra.identified_risks.length > 0)
632
+ lines.push(`- **Identified Risks**: ${ra.identified_risks.join(", ")}`);
633
+ }
634
+ if (r.policy_basis) {
635
+ const pb = r.policy_basis;
636
+ lines.push(`\n### Policy Basis\n`);
637
+ lines.push(`- **Policy**: ${pb.policy_name} (${pb.policy_id} v${pb.version})`);
638
+ lines.push(`- **Standard**: ${pb.standard}`);
639
+ if (pb.clauses.length > 0)
640
+ lines.push(`- **Clauses**: ${pb.clauses.join(", ")}`);
641
+ }
642
+ lines.push(`\n### Evidence Chain (${r.evidence.length})\n`);
643
+ if (r.evidence.length === 0) {
644
+ lines.push(`_No evidence references._`);
645
+ }
646
+ else {
647
+ lines.push(`| # | Title | Type | Source | Reference |`);
648
+ lines.push(`|---|-------|------|--------|-----------|`);
649
+ r.evidence.forEach((e, j) => {
650
+ lines.push(`| ${j + 1} | ${e.title} | ${e.type} | ${e.source_system} | ${e.reference} |`);
651
+ });
652
+ }
653
+ if (r.review_cycle) {
654
+ const rc = r.review_cycle;
655
+ lines.push(`\n### Review Cycle\n`);
656
+ lines.push(`- **Frequency**: ${rc.frequency}`);
657
+ lines.push(`- **Last Review**: ${rc.last_review}`);
658
+ lines.push(`- **Next Review**: ${rc.next_review}`);
659
+ }
660
+ if (r.committee) {
661
+ const c = r.committee;
662
+ lines.push(`\n### Committee Approval\n`);
663
+ lines.push(`- **Committee**: ${c.committee_name}`);
664
+ lines.push(`- **Meeting**: ${c.meeting_date} (${c.meeting_reference})`);
665
+ if (c.attendees.length > 0)
666
+ lines.push(`- **Attendees**: ${c.attendees.join(", ")}`);
667
+ }
668
+ if (v && v.issues.length > 0) {
669
+ lines.push(`\n### Blocking Issues\n`);
670
+ for (const iss of v.issues)
671
+ lines.push(`- ${iss}`);
672
+ }
673
+ if (v && v.warnings.length > 0) {
674
+ lines.push(`\n### Warnings\n`);
675
+ for (const w of v.warnings)
676
+ lines.push(`- ${w}`);
677
+ }
678
+ lines.push(`\n_Created: ${r.created_at} by ${r.created_by} | Updated: ${r.updated_at} by ${r.updated_by} (v${r.record_version})_`);
679
+ }
680
+ return lines.join("\n");
681
+ }
package/dist/template.js CHANGED
@@ -15,6 +15,8 @@ function matchPackForControl(controlId, packs) {
15
15
  return p;
16
16
  if (p.id === "government" && idUpper.startsWith("GOV-"))
17
17
  return p;
18
+ if (p.id === "governance" && idUpper.startsWith("GOVP-"))
19
+ return p;
18
20
  if (p.id === "iso27001" && idUpper.startsWith("ISO27K-"))
19
21
  return p;
20
22
  if (p.id === "iso27701" && idUpper.startsWith("ISO277-"))
@@ -71,6 +73,7 @@ export function renderDashboard(data) {
71
73
  const findings = data.findings;
72
74
  const controls = data.controls;
73
75
  const packs = data.packs;
76
+ const governance = data.governance;
74
77
  const findingsBySeverity = {
75
78
  critical: findings.filter(f => f.severity === "critical").length,
76
79
  high: findings.filter(f => f.severity === "high").length,
@@ -119,6 +122,8 @@ export function renderDashboard(data) {
119
122
  return idUpper.startsWith("BC-");
120
123
  if (packId === "government")
121
124
  return idUpper.startsWith("GOV-");
125
+ if (packId === "governance")
126
+ return idUpper.startsWith("GOVP-");
122
127
  if (packId === "iso27001")
123
128
  return idUpper.startsWith("ISO27K-");
124
129
  if (packId === "iso27701")
@@ -327,12 +332,18 @@ export function renderDashboard(data) {
327
332
  <button class="nav-tab" onclick="showPage('findings', this)">Findings</button>
328
333
  <button class="nav-tab" onclick="showPage('traceability', this)">Traceability</button>
329
334
  <button class="nav-tab" onclick="showPage('activity', this)">Activity Log</button>
335
+ <button class="nav-tab" onclick="showPage('governance', this)">Governance</button>
330
336
  </div>
331
337
  </div>
332
338
 
333
339
  <div class="container">
334
340
 
335
341
  <div id="page-overview" class="page active">
342
+ <div style="display:flex;justify-content:flex-end;gap:8px;margin-bottom:12px;">
343
+ <a href="/api/report/compliance?format=markdown" class="report-btn" style="background:#0f766e;color:white;padding:6px 14px;border-radius:6px;text-decoration:none;font-size:12px;font-weight:600;">📄 Compliance Report (MD)</a>
344
+ <a href="/api/report/compliance?format=html" class="report-btn" style="background:#0f766e;color:white;padding:6px 14px;border-radius:6px;text-decoration:none;font-size:12px;font-weight:600;">📄 Compliance Report (HTML)</a>
345
+ <a href="/api/report/governance" class="report-btn" style="background:#6366f1;color:white;padding:6px 14px;border-radius:6px;text-decoration:none;font-size:12px;font-weight:600;">📋 Governance Report</a>
346
+ </div>
336
347
  <div class="grid">
337
348
  <div class="grid grid-3">
338
349
  <div class="card stat">
@@ -637,12 +648,16 @@ export function renderDashboard(data) {
637
648
  ${renderActivityLogSection(data.activityLog || [])}
638
649
  </div>
639
650
 
651
+ <div id="page-governance" class="page">
652
+ ${renderGovernanceSection(governance)}
653
+ </div>
654
+
640
655
  <div id="control-detail-modal" style="display:none;"></div>
641
656
 
642
657
  </div>
643
658
 
644
659
  <div class="footer">
645
- Generated by GESF v${escapeHtml(data.gesfVersion)} | Last audit: ${escapeHtml(new Date(data.lastAudit).toLocaleString())} | <a href="/api/data">JSON API</a> | <a href="/api/packs">Packs API</a> | <a href="/api/fix-history">Fix History API</a> | <a href="/api/activity-log">Activity Log API</a>
660
+ Generated by GESF v${escapeHtml(data.gesfVersion)} | Last audit: ${escapeHtml(new Date(data.lastAudit).toLocaleString())} | <a href="/api/data">JSON API</a> | <a href="/api/packs">Packs API</a> | <a href="/api/fix-history">Fix History API</a> | <a href="/api/activity-log">Activity Log API</a> | <a href="/api/governance">Governance API</a>
646
661
  </div>
647
662
 
648
663
  <script>
@@ -663,7 +678,7 @@ export function renderDashboard(data) {
663
678
  }
664
679
  };
665
680
 
666
- var navTabMap = { overview: 0, packs: 1, fixes: 2, findings: 3, traceability: 4, activity: 5 };
681
+ var navTabMap = { overview: 0, packs: 1, fixes: 2, findings: 3, traceability: 4, activity: 5, governance: 6 };
667
682
 
668
683
  window.navigateToPage = function(page) {
669
684
  var tabs = document.querySelectorAll('.nav-tab');
@@ -1065,6 +1080,8 @@ function renderDetailedFixesList(findings, controls, packs) {
1065
1080
  return idUpper.startsWith("BC-");
1066
1081
  if (p.id === "government")
1067
1082
  return idUpper.startsWith("GOV-");
1083
+ if (p.id === "governance")
1084
+ return idUpper.startsWith("GOVP-");
1068
1085
  if (p.id === "iso27001")
1069
1086
  return idUpper.startsWith("ISO27K-");
1070
1087
  if (p.id === "iso27701")
@@ -1288,6 +1305,9 @@ function renderFixHistorySection(entries, complianceIssues = []) {
1288
1305
  html += `<tr><td style="font-weight:600;">Description</td><td>${escapeHtml(entry.fix.description)}</td></tr>`;
1289
1306
  html += `<tr><td style="font-weight:600;">Status</td><td>${entry.fix.applied ? '<span style="color:#22c55e;font-weight:600;">Applied successfully</span>' : `<span style="color:#ef4444;font-weight:600;">Failed: ${escapeHtml(entry.fix.error || 'Unknown error')}</span>`}</td></tr>`;
1290
1307
  html += `<tr><td style="font-weight:600;">Source</td><td>${entry.source === 'mcp' ? 'MCP auto_fix tool' : 'CLI ges fix command'}</td></tr>`;
1308
+ if (entry.actor_name) {
1309
+ html += `<tr><td style="font-weight:600;">Actor</td><td>${escapeHtml(entry.actor_name)}${entry.actor_role ? ' (' + escapeHtml(entry.actor_role) + ')' : ''}</td></tr>`;
1310
+ }
1291
1311
  html += `<tr><td style="font-weight:600;">Timestamp</td><td>${new Date(entry.timestamp).toLocaleString()}</td></tr>`;
1292
1312
  if (entry.dry_run) {
1293
1313
  html += `<tr><td style="font-weight:600;">Mode</td><td><span class="badge" style="background:#eab308;color:white;">DRY RUN</span></td></tr>`;
@@ -1501,12 +1521,15 @@ function renderActivityLogSection(entries) {
1501
1521
  html += `</div></div>`;
1502
1522
  html += `</div>`;
1503
1523
  html += `<div class="card"><div class="card-title">Timeline (newest first)</div>`;
1504
- html += `<table><thead><tr><th>Time</th><th>Source</th><th>Action</th><th>Status</th><th>Description</th><th>Impact</th></tr></thead><tbody>`;
1524
+ html += `<table><thead><tr><th>Time</th><th>Source</th><th>Actor</th><th>Action</th><th>Status</th><th>Description</th><th>Impact</th></tr></thead><tbody>`;
1505
1525
  for (const entry of sorted) {
1506
1526
  const time = new Date(entry.timestamp).toLocaleString();
1507
1527
  const sourceBadge = entry.source === "mcp"
1508
1528
  ? '<span class="badge" style="background:#7c3aed;font-size:10px;">MCP</span>'
1509
1529
  : '<span class="badge" style="background:#0f766e;font-size:10px;">CLI</span>';
1530
+ const actorHtml = entry.actor_name
1531
+ ? `<div style="font-size:12px;font-weight:600;">${escapeHtml(entry.actor_name)}</div>${entry.actor_role ? '<div style="font-size:11px;color:#6b7280;">' + escapeHtml(entry.actor_role) + '</div>' : ''}`
1532
+ : '<span style="color:#9ca3af;font-size:11px;">-</span>';
1510
1533
  const actionLabel = actionLabels[entry.action] || entry.action;
1511
1534
  const actionColor = actionColors[entry.action] || "#6b7280";
1512
1535
  const actionBadge = `<span class="badge" style="background:${actionColor};font-size:10px;">${escapeHtml(actionLabel)}</span>`;
@@ -1532,6 +1555,7 @@ function renderActivityLogSection(entries) {
1532
1555
  html += `<tr>
1533
1556
  <td style="font-size:11px;white-space:nowrap;">${time}</td>
1534
1557
  <td>${sourceBadge}</td>
1558
+ <td>${actorHtml}</td>
1535
1559
  <td>${actionBadge}</td>
1536
1560
  <td>${statusBadge}</td>
1537
1561
  <td>
@@ -1544,6 +1568,223 @@ function renderActivityLogSection(entries) {
1544
1568
  html += `</tbody></table></div>`;
1545
1569
  return html;
1546
1570
  }
1571
+ function govStatusColor(status) {
1572
+ const m = {
1573
+ approved: "#22c55e",
1574
+ conditional: "#eab308",
1575
+ "pending-review": "#3b82f6",
1576
+ draft: "#6b7280",
1577
+ rejected: "#ef4444",
1578
+ revoked: "#ef4444",
1579
+ expired: "#f97316",
1580
+ };
1581
+ return m[status] || "#6b7280";
1582
+ }
1583
+ function govRiskColor(level) {
1584
+ const m = {
1585
+ low: "#22c55e",
1586
+ medium: "#eab308",
1587
+ high: "#f97316",
1588
+ critical: "#ef4444",
1589
+ };
1590
+ return m[level] || "#6b7280";
1591
+ }
1592
+ function renderGovernanceSection(data) {
1593
+ const { records, verifications, summary } = data;
1594
+ if (records.length === 0) {
1595
+ return `<div class="card">
1596
+ <div class="card-title">Governance Provenance Chain</div>
1597
+ <div class="empty-state">
1598
+ <div class="icon">&#128203;</div>
1599
+ <div class="msg">No governance records found</div>
1600
+ <div class="sub">Create records using <code>ges governance add</code> or the MCP <code>create_governance_record</code> tool</div>
1601
+ <div class="sub" style="margin-top:12px;">The governance tab provides end-to-end traceability for auditors:<br>
1602
+ System &rarr; Risk Assessment &rarr; Policy &rarr; Approval &rarr; Evidence &rarr; Review Cycle</div>
1603
+ </div>
1604
+ </div>`;
1605
+ }
1606
+ let html = "";
1607
+ html += `<div style="margin-bottom:20px;">`;
1608
+ html += `<h2 style="font-size:20px;font-weight:700;margin-bottom:4px;">Governance Provenance Chain</h2>`;
1609
+ html += `<p style="color:#6b7280;font-size:14px;margin-bottom:16px;">End-to-end approval traceability for auditors and regulators. Each record links: System &rarr; Risk Assessment &rarr; Policy &rarr; Approval &rarr; Evidence &rarr; Review Cycle.</p>`;
1610
+ html += `<div style="margin-bottom:12px;"><a href="/api/report/governance" style="background:#6366f1;color:white;padding:6px 14px;border-radius:6px;text-decoration:none;font-size:12px;font-weight:600;">&#128203; Export Provenance Report</a></div>`;
1611
+ html += `<div class="grid grid-4" style="margin-bottom:20px;">`;
1612
+ html += `<div class="card stat"><div class="num">${summary.total}</div><div class="label">Total Systems</div></div>`;
1613
+ html += `<div class="card stat"><div class="num" style="color:#22c55e;">${summary.approved}</div><div class="label">Approved</div></div>`;
1614
+ html += `<div class="card stat"><div class="num" style="color:#3b82f6;">${summary.pending}</div><div class="label">Pending</div></div>`;
1615
+ html += `<div class="card stat"><div class="num" style="color:#f97316;">${summary.expired + summary.validWithIssues}</div><div class="label">Expired / Issues</div></div>`;
1616
+ html += `</div>`;
1617
+ html += `</div>`;
1618
+ if (summary.criticalRisk > 0 || summary.highRisk > 0) {
1619
+ html += `<div class="card" style="margin-bottom:20px;border-left:4px solid #ef4444;">`;
1620
+ html += `<div class="card-title" style="color:#ef4444;">High-Risk Systems</div>`;
1621
+ html += `<div style="display:flex;gap:16px;font-size:14px;">`;
1622
+ html += `<span><span style="color:#ef4444;font-weight:700;">${summary.criticalRisk}</span> critical risk</span>`;
1623
+ html += `<span><span style="color:#f97316;font-weight:700;">${summary.highRisk}</span> high risk</span>`;
1624
+ html += `</div></div>`;
1625
+ }
1626
+ html += `<div id="governance-records-list">`;
1627
+ for (let i = 0; i < records.length; i++) {
1628
+ const r = records[i];
1629
+ const v = verifications[i] || verifications.find(vv => vv.record_id === r.id);
1630
+ const statusBg = govStatusColor(r.status);
1631
+ const riskBg = govRiskColor(r.risk_level);
1632
+ html += `<div class="fix-detail-card" style="margin-bottom:12px;">`;
1633
+ html += `<div class="fix-detail-header" onclick="toggleFix('gov-${i}')" style="cursor:pointer;">`;
1634
+ html += `<div class="fix-detail-info" style="flex:1;">`;
1635
+ html += `<div class="fix-detail-title">${escapeHtml(r.system_name)}</div>`;
1636
+ html += `<div class="fix-detail-meta">${escapeHtml(r.system_type)} | ${escapeHtml(r.id)}${r.system_version ? ' | v' + escapeHtml(r.system_version) : ''}</div>`;
1637
+ html += `</div>`;
1638
+ html += `<div class="fix-detail-badges">`;
1639
+ html += `<span class="badge badge-sev" style="background:${statusBg};font-size:10px;">${r.status.toUpperCase()}</span>`;
1640
+ html += `<span class="badge badge-sev" style="background:${riskBg};font-size:10px;">${r.risk_level.toUpperCase()} RISK</span>`;
1641
+ if (v) {
1642
+ html += `<span class="badge badge-sev" style="background:${v.valid ? '#22c55e' : '#ef4444'};font-size:10px;">${v.valid ? '&#10003; VALID' : '&#10007; ISSUES'}</span>`;
1643
+ }
1644
+ html += `<span class="fix-toggle" id="gov-${i}-toggle">Expand</span>`;
1645
+ html += `</div>`;
1646
+ html += `</div>`;
1647
+ html += `<div class="fix-detail-body" id="gov-${i}">`;
1648
+ if (v) {
1649
+ html += `<div class="fix-section"><div class="fix-section-title">Verification Checklist</div>`;
1650
+ html += `<ul class="check-list">`;
1651
+ const checks = [
1652
+ [v.completeness.has_approval, "Approval Decision"],
1653
+ [v.completeness.has_risk_assessment, "Risk Assessment"],
1654
+ [v.completeness.has_policy_basis, "Policy Basis"],
1655
+ [v.completeness.has_evidence, `Evidence Chain (${v.completeness.evidence_count} refs)`],
1656
+ [v.completeness.has_review_cycle, "Review Cycle"],
1657
+ [v.completeness.has_data_inventory, "Data Inventory"],
1658
+ [v.completeness.has_compliance_links, "Compliance Links"],
1659
+ [v.completeness.is_current, "Currently Valid"],
1660
+ ];
1661
+ for (const [ok, label] of checks) {
1662
+ const icon = ok ? "&#10003;" : "&#10007;";
1663
+ const bg = ok ? "#22c55e" : "#ef4444";
1664
+ html += `<li><span class="check-icon" style="background:${bg};">${icon}</span> <span>${label}</span></li>`;
1665
+ }
1666
+ html += `</ul>`;
1667
+ if (v.approval_status !== "none") {
1668
+ html += `<div style="margin-top:8px;font-size:13px;">`;
1669
+ html += `<strong>Approval Status:</strong> `;
1670
+ const apColor = v.approval_status === "valid" ? "#22c55e" : v.approval_status === "expired" ? "#ef4444" : "#eab308";
1671
+ html += `<span style="color:${apColor};font-weight:600;">${v.approval_status.toUpperCase()}</span>`;
1672
+ if (v.days_until_expiry !== null) {
1673
+ const dayText = v.days_until_expiry < 0
1674
+ ? `${Math.abs(v.days_until_expiry)} days ago (EXPIRED)`
1675
+ : `${v.days_until_expiry} days remaining`;
1676
+ html += ` &mdash; ${dayText}`;
1677
+ }
1678
+ html += `</div>`;
1679
+ }
1680
+ if (v.issues.length > 0) {
1681
+ html += `<div style="margin-top:8px;"><strong style="color:#ef4444;">Blocking Issues:</strong><ul style="margin-top:4px;">`;
1682
+ for (const iss of v.issues) {
1683
+ html += `<li style="font-size:13px;color:#ef4444;">${escapeHtml(iss)}</li>`;
1684
+ }
1685
+ html += `</ul></div>`;
1686
+ }
1687
+ if (v.warnings.length > 0) {
1688
+ html += `<div style="margin-top:4px;"><strong style="color:#eab308;">Warnings:</strong><ul style="margin-top:4px;">`;
1689
+ for (const w of v.warnings) {
1690
+ html += `<li style="font-size:13px;color:#92400e;">${escapeHtml(w)}</li>`;
1691
+ }
1692
+ html += `</ul></div>`;
1693
+ }
1694
+ html += `</div>`;
1695
+ }
1696
+ html += `<div class="fix-section"><div class="fix-section-title">Approval Decision</div>`;
1697
+ if (r.approval) {
1698
+ const a = r.approval;
1699
+ html += `<div style="font-size:13px;line-height:1.8;">`;
1700
+ html += `<div><strong>Approver:</strong> ${escapeHtml(a.approver_name)} (${escapeHtml(a.approver_role)})</div>`;
1701
+ html += `<div><strong>Authority:</strong> ${escapeHtml(a.approval_authority)}</div>`;
1702
+ html += `<div><strong>Decision:</strong> <span style="color:${a.decision === "approved" ? "#22c55e" : "#ef4444"};font-weight:600;">${a.decision.toUpperCase()}</span></div>`;
1703
+ html += `<div><strong>Date:</strong> ${escapeHtml(a.decision_date)}</div>`;
1704
+ html += `<div><strong>Validity:</strong> ${escapeHtml(a.valid_from)} &rarr; ${escapeHtml(a.valid_until || "indefinite")}</div>`;
1705
+ if (a.conditions.length > 0) {
1706
+ html += `<div><strong>Conditions:</strong> ${a.conditions.map(c => escapeHtml(c)).join("; ")}</div>`;
1707
+ }
1708
+ if (a.rationale) {
1709
+ html += `<div><strong>Rationale:</strong> ${escapeHtml(a.rationale)}</div>`;
1710
+ }
1711
+ html += `</div>`;
1712
+ }
1713
+ else {
1714
+ html += `<div style="color:#ef4444;font-size:13px;">&#9888; NOT RECORDED</div>`;
1715
+ }
1716
+ html += `</div>`;
1717
+ if (r.risk_assessment) {
1718
+ const ra = r.risk_assessment;
1719
+ html += `<div class="fix-section"><div class="fix-section-title">Risk Assessment</div>`;
1720
+ html += `<div style="font-size:13px;line-height:1.8;">`;
1721
+ html += `<div><strong>Assessor:</strong> ${escapeHtml(ra.assessor)}</div>`;
1722
+ html += `<div><strong>Methodology:</strong> ${escapeHtml(ra.methodology)}</div>`;
1723
+ html += `<div><strong>Risk Score:</strong> ${escapeHtml(ra.risk_score)} &mdash; <strong>Residual:</strong> ${escapeHtml(ra.residual_risk)}</div>`;
1724
+ html += `<div><strong>Date:</strong> ${escapeHtml(ra.assessment_date)}</div>`;
1725
+ if (ra.identified_risks.length > 0) {
1726
+ html += `<div><strong>Identified Risks:</strong> ${ra.identified_risks.map(r => escapeHtml(r)).join(", ")}</div>`;
1727
+ }
1728
+ html += `</div>`;
1729
+ html += `</div>`;
1730
+ }
1731
+ if (r.policy_basis) {
1732
+ const pb = r.policy_basis;
1733
+ html += `<div class="fix-section"><div class="fix-section-title">Policy Basis</div>`;
1734
+ html += `<div style="font-size:13px;line-height:1.8;">`;
1735
+ html += `<div><strong>Policy:</strong> ${escapeHtml(pb.policy_name)} (${escapeHtml(pb.policy_id)} v${escapeHtml(pb.version)})</div>`;
1736
+ html += `<div><strong>Standard:</strong> ${escapeHtml(pb.standard)}</div>`;
1737
+ if (pb.clauses.length > 0) {
1738
+ html += `<div><strong>Clauses:</strong> ${pb.clauses.map(c => escapeHtml(c)).join(", ")}</div>`;
1739
+ }
1740
+ html += `</div>`;
1741
+ html += `</div>`;
1742
+ }
1743
+ html += `<div class="fix-section"><div class="fix-section-title">Evidence Chain (${r.evidence.length})</div>`;
1744
+ if (r.evidence.length === 0) {
1745
+ html += `<div style="color:#ef4444;font-size:13px;">&#9888; NO EVIDENCE REFERENCES</div>`;
1746
+ }
1747
+ else {
1748
+ html += `<table><thead><tr><th>Title</th><th>Type</th><th>Source</th><th>Reference</th></tr></thead><tbody>`;
1749
+ for (const e of r.evidence) {
1750
+ html += `<tr>`;
1751
+ html += `<td>${escapeHtml(e.title)}</td>`;
1752
+ html += `<td style="font-size:12px;">${escapeHtml(e.type)}</td>`;
1753
+ html += `<td><span class="badge" style="background:#6b7280;color:white;font-size:10px;padding:2px 8px;">${escapeHtml(e.source_system)}</span></td>`;
1754
+ html += `<td style="font-family:monospace;font-size:11px;">${escapeHtml(e.reference)}</td>`;
1755
+ html += `</tr>`;
1756
+ }
1757
+ html += `</tbody></table>`;
1758
+ }
1759
+ html += `</div>`;
1760
+ if (r.review_cycle) {
1761
+ const rc = r.review_cycle;
1762
+ html += `<div class="fix-section"><div class="fix-section-title">Review Cycle</div>`;
1763
+ html += `<div style="font-size:13px;line-height:1.8;">`;
1764
+ html += `<div><strong>Frequency:</strong> ${escapeHtml(rc.frequency)}</div>`;
1765
+ html += `<div><strong>Last Review:</strong> ${escapeHtml(rc.last_review)} | <strong>Next:</strong> ${escapeHtml(rc.next_review)}</div>`;
1766
+ html += `</div>`;
1767
+ html += `</div>`;
1768
+ }
1769
+ if (r.committee) {
1770
+ const c = r.committee;
1771
+ html += `<div class="fix-section"><div class="fix-section-title">Committee Approval</div>`;
1772
+ html += `<div style="font-size:13px;line-height:1.8;">`;
1773
+ html += `<div><strong>Committee:</strong> ${escapeHtml(c.committee_name)}</div>`;
1774
+ html += `<div><strong>Meeting:</strong> ${escapeHtml(c.meeting_date)} (${escapeHtml(c.meeting_reference)})</div>`;
1775
+ if (c.attendees.length > 0) {
1776
+ html += `<div><strong>Attendees:</strong> ${c.attendees.map(a => escapeHtml(a)).join(", ")}</div>`;
1777
+ }
1778
+ html += `</div>`;
1779
+ html += `</div>`;
1780
+ }
1781
+ html += `<div style="margin-top:8px;font-size:11px;color:#9ca3af;">Created: ${escapeHtml(r.created_at)} by ${escapeHtml(r.created_by)} | Updated: ${escapeHtml(r.updated_at)} (v${r.record_version})</div>`;
1782
+ html += `</div>`;
1783
+ html += `</div>`;
1784
+ }
1785
+ html += `</div>`;
1786
+ return html;
1787
+ }
1547
1788
  function escapeHtml(str) {
1548
1789
  return str
1549
1790
  .replace(/&/g, "&amp;")
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "dependencies": {
3
- "@greenarmor/ges-audit-engine": "1.3.0",
4
- "@greenarmor/ges-core": "1.3.0",
5
- "@greenarmor/ges-policy-engine": "1.3.0",
6
- "@greenarmor/ges-scoring-engine": "1.3.0"
3
+ "@greenarmor/ges-audit-engine": "1.4.1",
4
+ "@greenarmor/ges-core": "1.4.1",
5
+ "@greenarmor/ges-policy-engine": "1.4.1",
6
+ "@greenarmor/ges-report-generator": "1.4.1",
7
+ "@greenarmor/ges-scoring-engine": "1.4.1"
7
8
  },
8
9
  "description": "GESF Web Dashboard - Visual compliance dashboard for teams",
9
10
  "devDependencies": {
@@ -40,7 +41,7 @@
40
41
  },
41
42
  "type": "module",
42
43
  "types": "./dist/index.d.ts",
43
- "version": "1.3.0",
44
+ "version": "1.4.1",
44
45
  "scripts": {
45
46
  "build": "tsc",
46
47
  "clean": "rm -rf dist tsconfig.tsbuildinfo",