@greenarmor/ges-web-dashboard 1.4.2 → 1.5.0

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, GovernanceRecord, GovernanceVerificationResult } from "@greenarmor/ges-core";
2
+ import type { ScoreFile, Control, FixHistoryEntry, ActivityLogEntry, GovernanceRecord, GovernanceVerificationResult, FixAssignment } from "@greenarmor/ges-core";
3
3
  import type { Finding } from "@greenarmor/ges-audit-engine";
4
4
  export interface DashboardOptions {
5
5
  port?: number;
@@ -79,6 +79,7 @@ export interface DashboardData {
79
79
  fixHistory: FixHistoryEntry[];
80
80
  activityLog: ActivityLogEntry[];
81
81
  governance: GovernanceData;
82
+ fixAssignments: FixAssignment[];
82
83
  lastAudit: string;
83
84
  }
84
85
  export interface GovernanceData {
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ 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
8
  import { loadGovernanceRecords, verifyGovernanceRecord, verifyAllGovernanceRecords, createGovernanceRecord, addGovernanceRecord, findGovernanceRecord, setGovernanceApproval, addGovernanceEvidence, createEvidenceRef, deleteGovernanceRecord, setGovernanceRiskAssessment, setGovernancePolicyBasis, setGovernanceReviewCycle, setGovernanceDataInventory, setGovernanceComplianceLinks, setGovernanceCommittee, recordActivity, } from "@greenarmor/ges-core";
9
+ import { loadFixAssignments, createFixAssignment, addFixAssignment, resolveFixAssignment, unassignFix, } from "@greenarmor/ges-core";
9
10
  import { getInstalledPackIds as getInstalledPackIdsFromDisk } from "@greenarmor/ges-core";
10
11
  import { generateMarkdownReport, generateHtmlReport } from "@greenarmor/ges-report-generator";
11
12
  import { renderDashboard } from "./template.js";
@@ -179,6 +180,7 @@ export function collectDashboardData(projectPath) {
179
180
  const fixHistory = loadFixHistory(projectPath);
180
181
  const activityLog = loadActivityLog(projectPath);
181
182
  const governance = collectGovernanceData(projectPath);
183
+ const fixAssignments = loadFixAssignments(projectPath);
182
184
  const metadataPath = path.join(projectPath, ".ges", "metadata.json");
183
185
  let lastAudit = "";
184
186
  try {
@@ -193,7 +195,7 @@ export function collectDashboardData(projectPath) {
193
195
  projectName: config?.project_name || "Unknown Project",
194
196
  projectType: config?.project_type || "unknown",
195
197
  frameworks: allFrameworks,
196
- gesfVersion: "1.4.2",
198
+ gesfVersion: "1.5.0",
197
199
  score,
198
200
  controls,
199
201
  findings,
@@ -201,6 +203,7 @@ export function collectDashboardData(projectPath) {
201
203
  fixHistory,
202
204
  activityLog,
203
205
  governance,
206
+ fixAssignments,
204
207
  lastAudit,
205
208
  };
206
209
  }
@@ -543,6 +546,121 @@ export function startDashboard(options) {
543
546
  }
544
547
  return;
545
548
  }
549
+ if (req.method === "POST" && pathname === "/api/fix-assignments/assign") {
550
+ try {
551
+ const body = await readBody(req);
552
+ const fk = String(body.finding_key || "").trim();
553
+ const recordId = String(body.governance_record_id || "").trim();
554
+ const assignee = String(body.assignee || "").trim();
555
+ if (!fk || !recordId || !assignee) {
556
+ jsonError(res, "finding_key, governance_record_id, and assignee are required", 400);
557
+ return;
558
+ }
559
+ const record = findGovernanceRecord(options.projectPath, recordId);
560
+ if (!record) {
561
+ jsonError(res, `Governance record not found: ${recordId}`, 404);
562
+ return;
563
+ }
564
+ const assignment = createFixAssignment({
565
+ finding_key: fk,
566
+ finding_rule_id: String(body.finding_rule_id || ""),
567
+ finding_title: String(body.finding_title || ""),
568
+ finding_file: String(body.finding_file || ""),
569
+ finding_line: body.finding_line ? Number(body.finding_line) : undefined,
570
+ finding_severity: body.finding_severity || "medium",
571
+ finding_control_ids: parseList(body.finding_control_ids),
572
+ governance_record_id: record.id,
573
+ governance_system_name: record.system_name,
574
+ assignee,
575
+ assignee_role: String(body.assignee_role || ""),
576
+ assigned_by: String(body.assigned_by || body.actor_name || "dashboard"),
577
+ notes: String(body.notes || ""),
578
+ });
579
+ addFixAssignment(options.projectPath, assignment);
580
+ recordActivity(options.projectPath, {
581
+ source: "cli",
582
+ action: "fix_assign",
583
+ title: `Fix assigned: ${assignment.finding_rule_id} → ${record.system_name}`,
584
+ description: `Assigned ${assignment.finding_rule_id} (${assignment.finding_title}) to ${assignee} (${assignment.assignee_role || "unspecified role"}), linked to governance record ${record.system_name}.`,
585
+ details: {
586
+ finding_key: fk,
587
+ governance_record_id: record.id,
588
+ assignee,
589
+ governance_system_name: record.system_name,
590
+ },
591
+ actor_name: body.actor_name ? String(body.actor_name) : undefined,
592
+ actor_role: body.actor_role ? String(body.actor_role) : undefined,
593
+ });
594
+ jsonResponse(res, { success: true, assignment });
595
+ }
596
+ catch (err) {
597
+ jsonError(res, err instanceof Error ? err.message : String(err));
598
+ }
599
+ return;
600
+ }
601
+ if (req.method === "POST" && pathname === "/api/fix-assignments/resolve") {
602
+ try {
603
+ const body = await readBody(req);
604
+ const fk = String(body.finding_key || "").trim();
605
+ if (!fk) {
606
+ jsonError(res, "finding_key is required", 400);
607
+ return;
608
+ }
609
+ const resolved = resolveFixAssignment(options.projectPath, fk, {
610
+ resolved_by: String(body.resolved_by || body.actor_name || "dashboard"),
611
+ resolved_by_role: String(body.resolved_by_role || body.actor_role || ""),
612
+ method: body.method || "manual",
613
+ resolution_notes: String(body.resolution_notes || ""),
614
+ });
615
+ if (!resolved) {
616
+ jsonError(res, `Fix assignment not found for finding_key: ${fk}`, 404);
617
+ return;
618
+ }
619
+ recordActivity(options.projectPath, {
620
+ source: "cli",
621
+ action: "fix_resolve",
622
+ title: `Fix resolved: ${resolved.finding_rule_id}`,
623
+ description: `Resolved ${resolved.finding_rule_id} via ${body.method || "manual"} by ${body.resolved_by || body.actor_name || "dashboard"}.`,
624
+ details: {
625
+ finding_key: fk,
626
+ governance_record_id: resolved.governance_record_id,
627
+ method: body.method || "manual",
628
+ },
629
+ actor_name: body.actor_name ? String(body.actor_name) : undefined,
630
+ actor_role: body.actor_role ? String(body.actor_role) : undefined,
631
+ });
632
+ jsonResponse(res, { success: true, assignment: resolved });
633
+ }
634
+ catch (err) {
635
+ jsonError(res, err instanceof Error ? err.message : String(err));
636
+ }
637
+ return;
638
+ }
639
+ const fixAssignDeleteMatch = req.method === "POST" ? pathname.match(/^\/api\/fix-assignments\/([^/]+)\/unassign$/) : null;
640
+ if (fixAssignDeleteMatch) {
641
+ try {
642
+ const fkey = decodeURIComponent(fixAssignDeleteMatch[1]);
643
+ const deleted = unassignFix(options.projectPath, fkey);
644
+ if (!deleted) {
645
+ jsonError(res, `Fix assignment not found for finding: ${fkey}`, 404);
646
+ return;
647
+ }
648
+ recordActivity(options.projectPath, {
649
+ source: "cli",
650
+ action: "fix_assign",
651
+ title: `Fix assignment removed`,
652
+ description: `Removed fix assignment for finding ${fkey}.`,
653
+ details: { finding_key: fkey },
654
+ actor_name: undefined,
655
+ actor_role: undefined,
656
+ });
657
+ jsonResponse(res, { success: true });
658
+ }
659
+ catch (err) {
660
+ jsonError(res, err instanceof Error ? err.message : String(err));
661
+ }
662
+ return;
663
+ }
546
664
  if (req.method !== "GET") {
547
665
  jsonError(res, "Method not allowed", 405);
548
666
  return;
@@ -590,6 +708,16 @@ export function startDashboard(options) {
590
708
  }
591
709
  return;
592
710
  }
711
+ if (pathname === "/api/fix-assignments") {
712
+ try {
713
+ const data = collectDashboardData(options.projectPath);
714
+ jsonResponse(res, data.fixAssignments);
715
+ }
716
+ catch (err) {
717
+ jsonError(res, err instanceof Error ? err.message : String(err));
718
+ }
719
+ return;
720
+ }
593
721
  if (pathname === "/api/activity-log") {
594
722
  try {
595
723
  const data = collectDashboardData(options.projectPath);
package/dist/template.js CHANGED
@@ -74,6 +74,10 @@ export function renderDashboard(data) {
74
74
  const controls = data.controls;
75
75
  const packs = data.packs;
76
76
  const governance = data.governance;
77
+ const fixAssignmentMap = new Map();
78
+ for (const a of data.fixAssignments) {
79
+ fixAssignmentMap.set(a.finding_key, a);
80
+ }
77
81
  const findingsBySeverity = {
78
82
  critical: findings.filter(f => f.severity === "critical").length,
79
83
  high: findings.filter(f => f.severity === "high").length,
@@ -574,7 +578,7 @@ export function renderDashboard(data) {
574
578
  </div>
575
579
 
576
580
  <div id="fixes-tab-pending" class="tab-panel">
577
- ${renderComplianceFixCards(complianceIssues, "fix")}
581
+ ${renderComplianceFixCards(complianceIssues, "fix", fixAssignmentMap, governance.records)}
578
582
  </div>
579
583
  </div>
580
584
 
@@ -656,7 +660,7 @@ export function renderDashboard(data) {
656
660
  </div>
657
661
 
658
662
  <div id="trace-tab-fixes" class="tab-panel">
659
- ${renderComplianceFixCards(complianceIssues, "trace")}
663
+ ${renderComplianceFixCards(complianceIssues, "trace", fixAssignmentMap, governance.records)}
660
664
  </div>
661
665
 
662
666
  <div id="trace-tab-controls" class="tab-panel">
@@ -695,10 +699,14 @@ export function renderDashboard(data) {
695
699
 
696
700
  <div id="control-detail-modal" style="display:none;"></div>
697
701
 
702
+ <div class="gov-modal-overlay" id="assign-modal-overlay" onclick="if(event.target===this)closeAssignModal()">
703
+ <div class="gov-modal" id="assign-modal"></div>
704
+ </div>
705
+
698
706
  </div>
699
707
 
700
708
  <div class="footer">
701
- 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>
709
+ 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/fix-assignments">Fix Assignments API</a> | <a href="/api/activity-log">Activity Log API</a> | <a href="/api/governance">Governance API</a>
702
710
  </div>
703
711
 
704
712
  <script>
@@ -1149,6 +1157,130 @@ export function renderDashboard(data) {
1149
1157
  c.appendChild(t);
1150
1158
  setTimeout(function() { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(function() { if(t.parentNode) t.remove(); }, 300); }, 3000);
1151
1159
  };
1160
+
1161
+ var govRecordsForAssign = ${JSON.stringify(governance.records.map(r => ({ id: r.id, name: r.system_name, status: r.status, risk: r.risk_level })))};
1162
+
1163
+ window.openAssignModal = function(fkey, ruleId, title, file, line, severity, controlIds) {
1164
+ var overlay = document.getElementById('assign-modal-overlay');
1165
+ var modal = document.getElementById('assign-modal');
1166
+ if (!overlay || !modal) return;
1167
+ if (govRecordsForAssign.length === 0) {
1168
+ showToast('No governance records found. Create one first on the Governance tab.', 'error');
1169
+ return;
1170
+ }
1171
+ var recordOptions = govRecordsForAssign.map(function(r) {
1172
+ return '<option value="' + r.id + '">' + r.name + ' (' + r.status + ', ' + r.risk + ' risk)</option>';
1173
+ }).join('');
1174
+ modal.innerHTML =
1175
+ '<div class="gov-modal-header"><h3>Assign Fix to Governance Record</h3><button class="gov-modal-close" onclick="closeAssignModal()">&times;</button></div>' +
1176
+ '<div class="gov-modal-body">' +
1177
+ '<div style="margin-bottom:12px;padding:8px 10px;background:#f9fafb;border-radius:6px;font-size:12px;">' +
1178
+ '<strong>' + ruleId + '</strong> &mdash; ' + title + '<br>' +
1179
+ '<span style="color:#6b7280;font-family:monospace;">' + file + (line ? ':' + line : '') + '</span>' +
1180
+ '</div>' +
1181
+ '<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Governance Record *</label>' +
1182
+ '<select name="governance_record_id" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;">' + recordOptions + '</select>' +
1183
+ '<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Assignee Name *</label>' +
1184
+ '<input name="assignee" type="text" placeholder="e.g., Jane Doe" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;" />' +
1185
+ '<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Assignee Role</label>' +
1186
+ '<input name="assignee_role" type="text" placeholder="e.g., Security Engineer" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;" />' +
1187
+ '<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Notes</label>' +
1188
+ '<textarea name="notes" rows="2" placeholder="Optional context for this assignment..." style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;"></textarea>' +
1189
+ '<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Your Name (for audit log)</label>' +
1190
+ '<input name="actor_name" type="text" placeholder="Who is making this assignment" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;" />' +
1191
+ '<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Your Role</label>' +
1192
+ '<input name="actor_role" type="text" placeholder="e.g., Tech Lead" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;" />' +
1193
+ '</div>' +
1194
+ '<div class="gov-modal-footer"><button class="gov-btn gov-btn-outline" onclick="closeAssignModal()">Cancel</button><button class="gov-btn gov-btn-primary" onclick="submitAssignForm()">Assign</button></div>';
1195
+ modal.dataset.fkey = fkey;
1196
+ modal.dataset.ruleId = ruleId;
1197
+ modal.dataset.title = title;
1198
+ modal.dataset.file = file;
1199
+ modal.dataset.line = String(line || 0);
1200
+ modal.dataset.severity = severity;
1201
+ modal.dataset.controlIds = controlIds;
1202
+ overlay.classList.add('active');
1203
+ };
1204
+
1205
+ window.closeAssignModal = function() {
1206
+ var overlay = document.getElementById('assign-modal-overlay');
1207
+ var modal = document.getElementById('assign-modal');
1208
+ if (overlay) overlay.classList.remove('active');
1209
+ if (modal) modal.innerHTML = '';
1210
+ };
1211
+
1212
+ window.submitAssignForm = function() {
1213
+ var modal = document.getElementById('assign-modal');
1214
+ if (!modal) return;
1215
+ var inputs = modal.querySelectorAll('[name]');
1216
+ var body = {
1217
+ finding_key: modal.dataset.fkey,
1218
+ finding_rule_id: modal.dataset.ruleId,
1219
+ finding_title: modal.dataset.title,
1220
+ finding_file: modal.dataset.file,
1221
+ finding_line: parseInt(modal.dataset.line, 10),
1222
+ finding_severity: modal.dataset.severity,
1223
+ finding_control_ids: (modal.dataset.controlIds || '').split(',').filter(Boolean),
1224
+ };
1225
+ for (var i = 0; i < inputs.length; i++) {
1226
+ body[inputs[i].name] = inputs[i].value;
1227
+ }
1228
+ var btn = modal.querySelector('.gov-btn-primary');
1229
+ if (btn) { btn.textContent = 'Saving...'; btn.disabled = true; }
1230
+ fetch('/api/fix-assignments/assign', {
1231
+ method: 'POST',
1232
+ headers: { 'Content-Type': 'application/json' },
1233
+ body: JSON.stringify(body),
1234
+ })
1235
+ .then(function(r) { return r.json(); })
1236
+ .then(function(d) {
1237
+ if (d.success) { closeAssignModal(); showToast('Fix assigned! Reloading...', 'success'); setTimeout(function() { location.reload(); }, 800); }
1238
+ else { showToast(d.error || 'Failed', 'error'); if (btn) { btn.textContent = 'Assign'; btn.disabled = false; } }
1239
+ })
1240
+ .catch(function(e) { showToast('Error: ' + (e.message||'network'), 'error'); if (btn) { btn.textContent = 'Assign'; btn.disabled = false; } });
1241
+ };
1242
+
1243
+ window.resolveFindingFix = function(fkey) {
1244
+ if (!confirm('Mark this fix as resolved?')) return;
1245
+ var resolver = prompt('Your name:', '') || 'dashboard';
1246
+ var resolverRole = prompt('Your role:', '') || '';
1247
+ var method = prompt('Method (auto-fix / manual / not-applicable):', 'manual') || 'manual';
1248
+ var notes = prompt('Resolution notes (optional):', '') || '';
1249
+ fetch('/api/fix-assignments/resolve', {
1250
+ method: 'POST',
1251
+ headers: { 'Content-Type': 'application/json' },
1252
+ body: JSON.stringify({
1253
+ finding_key: fkey,
1254
+ resolved_by: resolver,
1255
+ resolved_by_role: resolverRole,
1256
+ method: method,
1257
+ resolution_notes: notes,
1258
+ actor_name: resolver,
1259
+ actor_role: resolverRole,
1260
+ }),
1261
+ })
1262
+ .then(function(r) { return r.json(); })
1263
+ .then(function(d) {
1264
+ if (d.success) { showToast('Fix resolved! Reloading...', 'success'); setTimeout(function() { location.reload(); }, 800); }
1265
+ else { showToast(d.error || 'Failed', 'error'); }
1266
+ })
1267
+ .catch(function(e) { showToast('Error: ' + (e.message||'network'), 'error'); });
1268
+ };
1269
+
1270
+ window.unassignFix = function(fkey) {
1271
+ if (!confirm('Remove this fix assignment?')) return;
1272
+ fetch('/api/fix-assignments/' + encodeURIComponent(fkey) + '/unassign', {
1273
+ method: 'POST',
1274
+ headers: { 'Content-Type': 'application/json' },
1275
+ body: '{}',
1276
+ })
1277
+ .then(function(r) { return r.json(); })
1278
+ .then(function(d) {
1279
+ if (d.success) { showToast('Unassigned! Reloading...', 'success'); setTimeout(function() { location.reload(); }, 800); }
1280
+ else { showToast(d.error || 'Failed', 'error'); }
1281
+ })
1282
+ .catch(function(e) { showToast('Error: ' + (e.message||'network'), 'error'); });
1283
+ };
1152
1284
  })();
1153
1285
  </script>
1154
1286
 
@@ -1531,13 +1663,14 @@ function renderComplianceIssuesTable(issues) {
1531
1663
  </tbody>
1532
1664
  </table>`;
1533
1665
  }
1534
- function renderComplianceFixCards(issues, idPrefix) {
1666
+ function renderComplianceFixCards(issues, idPrefix, assignmentMap, govRecords) {
1535
1667
  if (issues.length === 0) {
1536
1668
  return '<div class="card"><div class="empty-state"><div class="icon">&#10003;</div><div class="msg" style="color:#22c55e;">All controls passing</div><div class="sub">No fixes needed</div></div></div>';
1537
1669
  }
1538
1670
  const totalAuditFindings = issues.reduce((sum, i) => sum + i.auditFindings.length, 0);
1539
1671
  const criticalCount = issues.filter(i => i.severity === "critical").length;
1540
1672
  const highCount = issues.filter(i => i.severity === "high").length;
1673
+ const assignedCount = assignmentMap ? assignmentMap.size : 0;
1541
1674
  let html = '';
1542
1675
  html += `<div style="margin-bottom:20px;">`;
1543
1676
  html += `<div class="grid grid-4" style="margin-bottom:20px;">`;
@@ -1545,6 +1678,7 @@ function renderComplianceFixCards(issues, idPrefix) {
1545
1678
  html += `<div class="card stat"><div class="num" style="color:#ef4444;">${criticalCount}</div><div class="label">Critical</div></div>`;
1546
1679
  html += `<div class="card stat"><div class="num" style="color:#f97316;">${highCount}</div><div class="label">High</div></div>`;
1547
1680
  html += `<div class="card stat"><div class="num">${totalAuditFindings}</div><div class="label">Audit Findings</div></div>`;
1681
+ html += `<div class="card stat"><div class="num" style="color:${assignedCount > 0 ? "#3b82f6" : "#9ca3af"};">${assignedCount}</div><div class="label">Assigned</div></div>`;
1548
1682
  html += `</div>`;
1549
1683
  html += `</div>`;
1550
1684
  for (let i = 0; i < issues.length; i++) {
@@ -1571,8 +1705,10 @@ function renderComplianceFixCards(issues, idPrefix) {
1571
1705
  html += `<div style="font-size:13px;color:#4b5563;line-height:1.6;">${escapeHtml(issue.description)}</div>`;
1572
1706
  html += `</div>`;
1573
1707
  if (issue.auditFindings.length > 0) {
1574
- html += `<div class="fix-section"><div class="fix-section-title">Audit Evidence (${issue.auditFindings.length})</div>`;
1708
+ html += `<div class="fix-section"><div class="fix-section-title">Audit Evidence &amp; Fix Assignments (${issue.auditFindings.length})</div>`;
1575
1709
  for (const f of issue.auditFindings) {
1710
+ const fkey = `${f.ruleId}:${f.file}:${f.line || 0}`;
1711
+ const assignment = assignmentMap?.get(fkey);
1576
1712
  html += `<div class="fix-finding-item ${f.severity}">`;
1577
1713
  html += `<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">`;
1578
1714
  html += `<span class="badge badge-sev" style="background:${severityColor(f.severity)};font-size:10px;">${f.severity.toUpperCase()}</span>`;
@@ -1582,6 +1718,30 @@ function renderComplianceFixCards(issues, idPrefix) {
1582
1718
  html += `<div style="font-size:12px;color:#4b5563;margin-top:4px;">${escapeHtml(f.description)}</div>`;
1583
1719
  if (f.evidence)
1584
1720
  html += `<div class="fix-evidence">${escapeHtml(f.evidence)}</div>`;
1721
+ if (assignment) {
1722
+ const statusColorAssign = assignment.status === "fixed" || assignment.status === "verified" ? "#22c55e" : assignment.status === "in-progress" ? "#3b82f6" : "#eab308";
1723
+ html += `<div class="fix-assign-box" style="margin-top:8px;padding:10px 12px;border-radius:8px;background:#f0fdf4;border:1px solid #bbf7d0;">`;
1724
+ html += `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">`;
1725
+ html += `<span class="badge" style="background:${statusColorAssign};color:#fff;font-size:10px;text-transform:uppercase;">${escapeHtml(assignment.status)}</span>`;
1726
+ html += `<span style="font-size:12px;font-weight:600;color:#166534;">Linked: ${escapeHtml(assignment.governance_system_name)}</span>`;
1727
+ html += `<span style="font-size:11px;color:#4b5563;">Assignee: ${escapeHtml(assignment.assignee)}${assignment.assignee_role ? ' (' + escapeHtml(assignment.assignee_role) + ')' : ''}</span>`;
1728
+ html += `</div>`;
1729
+ if (assignment.resolution) {
1730
+ html += `<div style="font-size:11px;color:#4b5563;margin-top:4px;">Resolved by ${escapeHtml(assignment.resolution.resolved_by)} via ${escapeHtml(assignment.resolution.method)} on ${escapeHtml(new Date(assignment.resolution.resolved_at).toLocaleDateString())}</div>`;
1731
+ }
1732
+ html += `<div style="margin-top:6px;display:flex;gap:6px;">`;
1733
+ if (assignment.status !== "fixed" && assignment.status !== "verified") {
1734
+ html += `<button class="gov-action-btn" style="background:#22c55e;color:#fff;border:none;padding:4px 10px;border-radius:4px;font-size:11px;cursor:pointer;" onclick="event.stopPropagation();resolveFindingFix('${escapeHtml(fkey)}')">Mark Fixed</button>`;
1735
+ }
1736
+ html += `<button class="gov-action-btn" style="background:#fee2e2;color:#991b1b;border:none;padding:4px 10px;border-radius:4px;font-size:11px;cursor:pointer;" onclick="event.stopPropagation();unassignFix('${escapeHtml(fkey)}')">Unassign</button>`;
1737
+ html += `</div>`;
1738
+ html += `</div>`;
1739
+ }
1740
+ else {
1741
+ html += `<div style="margin-top:8px;">`;
1742
+ html += `<button class="gov-action-btn" style="background:#3b82f6;color:#fff;border:none;padding:4px 12px;border-radius:4px;font-size:11px;cursor:pointer;" onclick="event.stopPropagation();openAssignModal('${escapeHtml(fkey)}','${escapeHtml(f.ruleId)}','${escapeHtml(f.title.replace(/'/g, "\\'"))}','${escapeHtml(f.file)}',${f.line || 0},'${escapeHtml(f.severity)}','${escapeHtml(f.controlIds.join(","))}')">+ Assign to Governance Record</button>`;
1743
+ html += `</div>`;
1744
+ }
1585
1745
  html += `</div>`;
1586
1746
  }
1587
1747
  html += `</div>`;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "dependencies": {
3
- "@greenarmor/ges-audit-engine": "1.4.2",
4
- "@greenarmor/ges-core": "1.4.2",
5
- "@greenarmor/ges-policy-engine": "1.4.2",
6
- "@greenarmor/ges-report-generator": "1.4.2",
7
- "@greenarmor/ges-scoring-engine": "1.4.2"
3
+ "@greenarmor/ges-audit-engine": "1.5.0",
4
+ "@greenarmor/ges-core": "1.5.0",
5
+ "@greenarmor/ges-policy-engine": "1.5.0",
6
+ "@greenarmor/ges-report-generator": "1.5.0",
7
+ "@greenarmor/ges-scoring-engine": "1.5.0"
8
8
  },
9
9
  "description": "GESF Web Dashboard - Visual compliance dashboard for teams",
10
10
  "devDependencies": {
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "type": "module",
43
43
  "types": "./dist/index.d.ts",
44
- "version": "1.4.2",
44
+ "version": "1.5.0",
45
45
  "scripts": {
46
46
  "build": "tsc",
47
47
  "clean": "rm -rf dist tsconfig.tsbuildinfo",