@greenarmor/ges-web-dashboard 1.2.4 → 1.2.6

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 } from "@greenarmor/ges-core";
2
+ import type { ScoreFile, Control, FixHistoryEntry, ActivityLogEntry } from "@greenarmor/ges-core";
3
3
  import type { Finding } from "@greenarmor/ges-audit-engine";
4
4
  export interface DashboardOptions {
5
5
  port?: number;
@@ -77,6 +77,7 @@ export interface DashboardData {
77
77
  findings: Finding[];
78
78
  packs: PackSummary[];
79
79
  fixHistory: FixHistoryEntry[];
80
+ activityLog: ActivityLogEntry[];
80
81
  lastAudit: string;
81
82
  }
82
83
  export declare function collectDashboardData(projectPath: string): DashboardData;
package/dist/index.js CHANGED
@@ -4,7 +4,8 @@ import * as path from "node:path";
4
4
  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
- import { loadFixHistory } from "@greenarmor/ges-core";
7
+ import { loadFixHistory, loadActivityLog, loadControlsFromDisk, loadControlOverrides, applyOverridesToControls } from "@greenarmor/ges-core";
8
+ import { getInstalledPackIds as getInstalledPackIdsFromDisk } from "@greenarmor/ges-core";
8
9
  import { renderDashboard } from "./template.js";
9
10
  function loadConfig(projectPath) {
10
11
  const configPath = path.join(projectPath, ".ges", "config.json");
@@ -31,21 +32,13 @@ function loadControlsForConfig(projectPath, config) {
31
32
  const fwLower = new Set(config.frameworks.map(f => f.toLowerCase()));
32
33
  const allPacks = getAllPacks();
33
34
  const packs = allPacks.filter(pack => fwLower.has(pack.id.toLowerCase()));
34
- const controls = packs.flatMap(p => p.controls);
35
- const overridesPath = path.join(projectPath, ".ges", "control-overrides.json");
36
- if (fs.existsSync(overridesPath)) {
37
- const overrides = JSON.parse(fs.readFileSync(overridesPath, "utf-8"));
38
- for (const override of overrides) {
39
- const control = controls.find((c) => c.id === override.control_id);
40
- if (control) {
41
- control.status = override.status;
42
- for (const check of control.checks) {
43
- check.status = override.status;
44
- }
45
- }
46
- }
47
- }
48
- return controls;
35
+ const inMemoryControls = packs.flatMap(p => p.controls);
36
+ const diskControls = loadControlsFromDisk(projectPath);
37
+ const seenIds = new Set(inMemoryControls.map((c) => c.id));
38
+ const extraFromDisk = diskControls.filter(c => !seenIds.has(c.id));
39
+ const controls = [...inMemoryControls, ...extraFromDisk];
40
+ const overrides = loadControlOverrides(projectPath);
41
+ return applyOverridesToControls(controls, overrides);
49
42
  }
50
43
  catch {
51
44
  return [];
@@ -139,32 +132,38 @@ function getInstalledPackIds(projectPath, config) {
139
132
  }
140
133
  }
141
134
  }
142
- const controlsDir = path.join(projectPath, "controls");
143
- try {
144
- const entries = fs.readdirSync(controlsDir, { withFileTypes: true });
145
- for (const entry of entries) {
146
- if (entry.isDirectory()) {
147
- const ctrlFile = path.join(controlsDir, entry.name, "controls.json");
148
- if (fs.existsSync(ctrlFile)) {
149
- ids.add(entry.name);
150
- }
151
- }
152
- }
153
- }
154
- catch {
155
- // controls dir may not exist
135
+ for (const id of getInstalledPackIdsFromDisk(projectPath)) {
136
+ ids.add(id);
156
137
  }
157
138
  return ids;
158
139
  }
140
+ function getFrameworksFromControls(controls) {
141
+ const fwSet = new Set();
142
+ for (const c of controls) {
143
+ if (c.framework)
144
+ fwSet.add(c.framework);
145
+ }
146
+ return [...fwSet];
147
+ }
159
148
  export function collectDashboardData(projectPath) {
160
149
  const config = loadConfig(projectPath);
161
150
  let score = loadScore(projectPath);
162
- const baseControls = config ? loadControlsForConfig(projectPath, config) : [];
151
+ let baseControls;
152
+ let frameworks;
153
+ if (config) {
154
+ baseControls = loadControlsForConfig(projectPath, config);
155
+ frameworks = config.frameworks;
156
+ }
157
+ else {
158
+ baseControls = loadControlsFromDisk(projectPath);
159
+ frameworks = [];
160
+ }
163
161
  const findings = loadFindings(projectPath);
164
162
  const controls = updateControlsFromFindings(baseControls, findings);
165
- if (config) {
163
+ if (config || controls.length > 0) {
166
164
  try {
167
- const freshScore = generateScoreFile(controls, config.frameworks, findings);
165
+ const scoreFrameworks = getFrameworksFromControls(controls);
166
+ const freshScore = generateScoreFile(controls, scoreFrameworks, findings);
168
167
  score = freshScore;
169
168
  }
170
169
  catch {
@@ -176,6 +175,7 @@ export function collectDashboardData(projectPath) {
176
175
  const installedPacks = getInstalledPackIds(projectPath, config || undefined);
177
176
  const packs = allPacks.map(p => buildPackSummary(p, controls, findings, installedPacks));
178
177
  const fixHistory = loadFixHistory(projectPath);
178
+ const activityLog = loadActivityLog(projectPath);
179
179
  const metadataPath = path.join(projectPath, ".ges", "metadata.json");
180
180
  let lastAudit = "";
181
181
  try {
@@ -185,16 +185,18 @@ export function collectDashboardData(projectPath) {
185
185
  catch {
186
186
  lastAudit = new Date().toISOString();
187
187
  }
188
+ const allFrameworks = getFrameworksFromControls(controls);
188
189
  return {
189
190
  projectName: config?.project_name || "Unknown Project",
190
191
  projectType: config?.project_type || "unknown",
191
- frameworks: config?.frameworks || [],
192
- gesfVersion: "1.2.4",
192
+ frameworks: allFrameworks,
193
+ gesfVersion: "1.2.6",
193
194
  score,
194
195
  controls,
195
196
  findings,
196
197
  packs,
197
198
  fixHistory,
199
+ activityLog,
198
200
  lastAudit,
199
201
  };
200
202
  }
@@ -203,7 +205,9 @@ export function collectPackDetail(projectPath, packId) {
203
205
  if (!pack)
204
206
  return null;
205
207
  const config = loadConfig(projectPath);
206
- const baseControls = config ? loadControlsForConfig(projectPath, config) : [];
208
+ const baseControls = config
209
+ ? loadControlsForConfig(projectPath, config)
210
+ : loadControlsFromDisk(projectPath);
207
211
  const findings = loadFindings(projectPath);
208
212
  const controls = updateControlsFromFindings(baseControls, findings);
209
213
  const packControlIds = new Set(pack.controls.map(c => c.id));
@@ -281,9 +285,9 @@ export function collectPackDetail(projectPath, packId) {
281
285
  }
282
286
  export function collectControlDetail(projectPath, controlId) {
283
287
  const config = loadConfig(projectPath);
284
- if (!config)
285
- return null;
286
- const baseControls = loadControlsForConfig(projectPath, config);
288
+ const baseControls = config
289
+ ? loadControlsForConfig(projectPath, config)
290
+ : loadControlsFromDisk(projectPath);
287
291
  const findings = loadFindings(projectPath);
288
292
  const controls = updateControlsFromFindings(baseControls, findings);
289
293
  const control = controls.find(c => c.id === controlId);
@@ -380,6 +384,16 @@ export function startDashboard(options) {
380
384
  }
381
385
  return;
382
386
  }
387
+ if (pathname === "/api/activity-log") {
388
+ try {
389
+ const data = collectDashboardData(options.projectPath);
390
+ jsonResponse(res, data.activityLog);
391
+ }
392
+ catch (err) {
393
+ jsonError(res, err instanceof Error ? err.message : String(err));
394
+ }
395
+ return;
396
+ }
383
397
  const packMatch = pathname.match(/^\/api\/packs\/([a-z0-9-]+)$/);
384
398
  if (packMatch) {
385
399
  try {
package/dist/template.js CHANGED
@@ -317,8 +317,8 @@ export function renderDashboard(data) {
317
317
 
318
318
  <div class="header">
319
319
  <div>
320
- <h1>GESF Compliance Dashboard</h1>
321
- <div class="subtitle">${escapeHtml(data.projectName)} | ${escapeHtml(data.projectType)} | GESF v${escapeHtml(data.gesfVersion)}</div>
320
+ <h1>${escapeHtml(data.projectName)}</h1>
321
+ <div class="subtitle">GESF v${escapeHtml(data.gesfVersion)}</div>
322
322
  </div>
323
323
  <div class="nav-tabs">
324
324
  <button class="nav-tab active" onclick="showPage('overview', this)">Overview</button>
@@ -326,6 +326,7 @@ export function renderDashboard(data) {
326
326
  <button class="nav-tab" onclick="showPage('fixes', this)">Fixes Detail</button>
327
327
  <button class="nav-tab" onclick="showPage('findings', this)">Findings</button>
328
328
  <button class="nav-tab" onclick="showPage('traceability', this)">Traceability</button>
329
+ <button class="nav-tab" onclick="showPage('activity', this)">Activity Log</button>
329
330
  </div>
330
331
  </div>
331
332
 
@@ -589,12 +590,16 @@ export function renderDashboard(data) {
589
590
  </div>
590
591
  </div>
591
592
 
593
+ <div id="page-activity" class="page">
594
+ ${renderActivityLogSection(data.activityLog || [])}
595
+ </div>
596
+
592
597
  <div id="control-detail-modal" style="display:none;"></div>
593
598
 
594
599
  </div>
595
600
 
596
601
  <div class="footer">
597
- 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>
602
+ 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>
598
603
  </div>
599
604
 
600
605
  <script>
@@ -615,7 +620,7 @@ export function renderDashboard(data) {
615
620
  }
616
621
  };
617
622
 
618
- var navTabMap = { overview: 0, packs: 1, fixes: 2, findings: 3, traceability: 4 };
623
+ var navTabMap = { overview: 0, packs: 1, fixes: 2, findings: 3, traceability: 4, activity: 5 };
619
624
 
620
625
  window.navigateToPage = function(page) {
621
626
  var tabs = document.querySelectorAll('.nav-tab');
@@ -1363,6 +1368,139 @@ function renderComplianceFixCards(issues, idPrefix) {
1363
1368
  }
1364
1369
  return html;
1365
1370
  }
1371
+ function renderActivityLogSection(entries) {
1372
+ if (!entries || entries.length === 0) {
1373
+ return `<div class="card">
1374
+ <h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Activity Log</h2>
1375
+ <p style="color:#6b7280;font-size:14px;margin-bottom:16px;">Every GESF operation performed via CLI or MCP is recorded here &mdash; the single source of truth for what GESF did to your project.</p>
1376
+ <div class="empty-state">
1377
+ <div class="icon">&#128203;</div>
1378
+ <div class="msg">No activity recorded yet</div>
1379
+ <div class="sub">Operations will appear here as you use GESF commands (<code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:12px;">ges init</code>, <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:12px;">ges audit</code>, <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:12px;">ges fix</code>, MCP tools, etc.)</div>
1380
+ </div>
1381
+ </div>`;
1382
+ }
1383
+ const sorted = [...entries].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1384
+ const bySource = { cli: entries.filter(e => e.source === "cli").length, mcp: entries.filter(e => e.source === "mcp").length };
1385
+ const byStatus = {
1386
+ success: entries.filter(e => e.status === "success").length,
1387
+ partial: entries.filter(e => e.status === "partial").length,
1388
+ failed: entries.filter(e => e.status === "failed").length,
1389
+ info: entries.filter(e => e.status === "info").length,
1390
+ };
1391
+ const byAction = {};
1392
+ for (const e of entries) {
1393
+ byAction[e.action] = (byAction[e.action] || 0) + 1;
1394
+ }
1395
+ const actionLabels = {
1396
+ init: "Project Init",
1397
+ audit: "Audit Run",
1398
+ fix: "Auto-Fix",
1399
+ policy_install: "Pack Installed",
1400
+ policy_remove: "Pack Removed",
1401
+ control_override: "Control Override",
1402
+ implement_control: "Control Implemented",
1403
+ score: "Score Generated",
1404
+ scan: "Scanners Run",
1405
+ validate: "Validation",
1406
+ generate: "Docs Generated",
1407
+ hooks_install: "Hooks Installed",
1408
+ hooks_uninstall: "Hooks Removed",
1409
+ dashboard_start: "Dashboard Started",
1410
+ badge_generate: "Badge Generated",
1411
+ };
1412
+ const actionColors = {
1413
+ init: "#0f766e",
1414
+ audit: "#3b82f6",
1415
+ fix: "#22c55e",
1416
+ policy_install: "#8b5cf6",
1417
+ policy_remove: "#ef4444",
1418
+ control_override: "#eab308",
1419
+ implement_control: "#22c55e",
1420
+ score: "#3b82f6",
1421
+ scan: "#f97316",
1422
+ validate: "#6b7280",
1423
+ generate: "#0f766e",
1424
+ hooks_install: "#6b7280",
1425
+ hooks_uninstall: "#6b7280",
1426
+ dashboard_start: "#8b5cf6",
1427
+ badge_generate: "#0f766e",
1428
+ };
1429
+ const statusColors = {
1430
+ success: "#22c55e",
1431
+ partial: "#eab308",
1432
+ failed: "#ef4444",
1433
+ info: "#3b82f6",
1434
+ };
1435
+ let html = '';
1436
+ html += `<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Activity Log</h2>`;
1437
+ html += `<p style="color:#6b7280;font-size:14px;margin-bottom:16px;">Every GESF operation performed via CLI or MCP is recorded here &mdash; the single source of truth for what GESF did to your project.</p>`;
1438
+ html += `<div class="grid grid-4" style="margin-bottom:20px;">`;
1439
+ html += `<div class="card stat"><div class="num">${entries.length}</div><div class="label">Total Operations</div></div>`;
1440
+ html += `<div class="card stat"><div class="num" style="color:#22c55e;">${byStatus.success}</div><div class="label">Successful</div></div>`;
1441
+ html += `<div class="card stat"><div class="num" style="color:#ef4444;">${byStatus.failed + byStatus.partial}</div><div class="label">Failed/Partial</div></div>`;
1442
+ html += `<div class="card stat"><div class="num" style="color:#0f766e;">${bySource.cli}</div><div class="label">CLI Source</div></div>`;
1443
+ html += `</div>`;
1444
+ html += `<div class="grid grid-2" style="margin-bottom:20px;">`;
1445
+ html += `<div class="card"><div class="card-title">Operations by Type</div>`;
1446
+ for (const [action, count] of Object.entries(byAction).sort((a, b) => b[1] - a[1])) {
1447
+ const label = actionLabels[action] || action;
1448
+ const color = actionColors[action] || "#6b7280";
1449
+ html += `<div class="framework-row"><div class="framework-name" style="min-width:160px;font-size:13px;"><span class="badge" style="background:${color};font-size:10px;margin-right:6px;">${label}</span></div><div style="flex:1;"></div><div class="pct-text">${count}</div></div>`;
1450
+ }
1451
+ html += `</div>`;
1452
+ html += `<div class="card"><div class="card-title">Sources & Status</div>`;
1453
+ html += `<div style="display:flex;flex-wrap:wrap;gap:16px;margin-top:8px;">`;
1454
+ html += `<div class="stat"><div class="num" style="font-size:20px;">${bySource.cli}</div><div class="label">CLI</div></div>`;
1455
+ html += `<div class="stat"><div class="num" style="font-size:20px;">${bySource.mcp}</div><div class="label">MCP</div></div>`;
1456
+ html += `<div class="stat"><div class="num" style="font-size:20px;color:#22c55e;">${byStatus.success}</div><div class="label">Success</div></div>`;
1457
+ html += `<div class="stat"><div class="num" style="font-size:20px;color:#ef4444;">${byStatus.failed}</div><div class="label">Failed</div></div>`;
1458
+ html += `</div></div>`;
1459
+ html += `</div>`;
1460
+ html += `<div class="card"><div class="card-title">Timeline (newest first)</div>`;
1461
+ 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>`;
1462
+ for (const entry of sorted) {
1463
+ const time = new Date(entry.timestamp).toLocaleString();
1464
+ const sourceBadge = entry.source === "mcp"
1465
+ ? '<span class="badge" style="background:#7c3aed;font-size:10px;">MCP</span>'
1466
+ : '<span class="badge" style="background:#0f766e;font-size:10px;">CLI</span>';
1467
+ const actionLabel = actionLabels[entry.action] || entry.action;
1468
+ const actionColor = actionColors[entry.action] || "#6b7280";
1469
+ const actionBadge = `<span class="badge" style="background:${actionColor};font-size:10px;">${escapeHtml(actionLabel)}</span>`;
1470
+ const statusBadge = `<span class="badge badge-status" style="background:${statusColors[entry.status] || '#6b7280'};font-size:10px;">${entry.status.toUpperCase()}</span>`;
1471
+ const impactParts = [];
1472
+ if (entry.details.findings_count !== undefined)
1473
+ impactParts.push(`${entry.details.findings_count} findings`);
1474
+ if (entry.details.fixes_applied !== undefined)
1475
+ impactParts.push(`${entry.details.fixes_applied} fixes`);
1476
+ if (entry.details.packs_affected && entry.details.packs_affected.length > 0)
1477
+ impactParts.push(`Packs: ${entry.details.packs_affected.join(", ")}`);
1478
+ if (entry.details.controls_affected && entry.details.controls_affected.length > 0)
1479
+ impactParts.push(`Controls: ${entry.details.controls_affected.length}`);
1480
+ if (entry.details.files_created && entry.details.files_created.length > 0)
1481
+ impactParts.push(`${entry.details.files_created.length} files created`);
1482
+ if (entry.details.frameworks_added && entry.details.frameworks_added.length > 0)
1483
+ impactParts.push(`Added: ${entry.details.frameworks_added.join(", ")}`);
1484
+ if (entry.details.score !== undefined)
1485
+ impactParts.push(`Score: ${entry.details.score}%`);
1486
+ const impactHtml = impactParts.length > 0
1487
+ ? impactParts.map(p => `<div style="font-size:11px;color:#6b7280;margin-bottom:2px;">${escapeHtml(p)}</div>`).join('')
1488
+ : '<span style="color:#9ca3af;">-</span>';
1489
+ html += `<tr>
1490
+ <td style="font-size:11px;white-space:nowrap;">${time}</td>
1491
+ <td>${sourceBadge}</td>
1492
+ <td>${actionBadge}</td>
1493
+ <td>${statusBadge}</td>
1494
+ <td>
1495
+ <div style="font-weight:600;font-size:13px;">${escapeHtml(entry.title)}</div>
1496
+ <div style="font-size:12px;color:#6b7280;">${escapeHtml(entry.description)}</div>
1497
+ </td>
1498
+ <td style="max-width:200px;">${impactHtml}</td>
1499
+ </tr>`;
1500
+ }
1501
+ html += `</tbody></table></div>`;
1502
+ return html;
1503
+ }
1366
1504
  function escapeHtml(str) {
1367
1505
  return str
1368
1506
  .replace(/&/g, "&amp;")
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "dependencies": {
3
- "@greenarmor/ges-audit-engine": "1.2.4",
4
- "@greenarmor/ges-core": "1.2.4",
5
- "@greenarmor/ges-policy-engine": "1.2.4",
6
- "@greenarmor/ges-scoring-engine": "1.2.4"
3
+ "@greenarmor/ges-audit-engine": "1.2.6",
4
+ "@greenarmor/ges-core": "1.2.6",
5
+ "@greenarmor/ges-policy-engine": "1.2.6",
6
+ "@greenarmor/ges-scoring-engine": "1.2.6"
7
7
  },
8
8
  "description": "GESF Web Dashboard - Visual compliance dashboard for teams",
9
9
  "devDependencies": {
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "type": "module",
42
42
  "types": "./dist/index.d.ts",
43
- "version": "1.2.4",
43
+ "version": "1.2.6",
44
44
  "scripts": {
45
45
  "build": "tsc",
46
46
  "clean": "rm -rf dist tsconfig.tsbuildinfo",