mindforge-cc 11.9.2 → 11.9.4

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.
Files changed (95) hide show
  1. package/.agent/CLAUDE.md +37 -13
  2. package/.agent/hooks/mindforge-block-no-verify.js +61 -13
  3. package/.agent/hooks/mindforge-config-protection.js +82 -3
  4. package/.agent/hooks/mindforge-context-monitor.js +1 -1
  5. package/.agent/hooks/mindforge-workflow-guard.js +2 -2
  6. package/.agent/hooks/run-with-flags.js +190 -20
  7. package/.agent/mindforge/browse.md +2 -2
  8. package/.agent/mindforge/checkpoint.md +1 -1
  9. package/.agent/mindforge/harness-audit.md +1 -1
  10. package/.agent/mindforge/orch-add-feature.md +1 -1
  11. package/.agent/mindforge/orch-build-mvp.md +1 -1
  12. package/.agent/mindforge/orch-change-feature.md +1 -1
  13. package/.agent/mindforge/orch-fix-defect.md +1 -1
  14. package/.agent/mindforge/orch-refine-code.md +1 -1
  15. package/.agent/mindforge/qa.md +2 -2
  16. package/.claude/CLAUDE.md +37 -13
  17. package/.claude/commands/mindforge/browse.md +2 -2
  18. package/.claude/commands/mindforge/checkpoint.md +1 -1
  19. package/.claude/commands/mindforge/harness-audit.md +1 -1
  20. package/.claude/commands/mindforge/orch-add-feature.md +1 -1
  21. package/.claude/commands/mindforge/orch-build-mvp.md +1 -1
  22. package/.claude/commands/mindforge/orch-change-feature.md +1 -1
  23. package/.claude/commands/mindforge/orch-fix-defect.md +1 -1
  24. package/.claude/commands/mindforge/orch-refine-code.md +1 -1
  25. package/.claude/commands/mindforge/qa.md +2 -2
  26. package/.mindforge/MINDFORGE-SCHEMA.json +1 -1
  27. package/.mindforge/config.json +3 -3
  28. package/.mindforge/engine/autonomous/headless-adapter.md +9 -2
  29. package/.mindforge/engine/temporal-protocol.md +2 -2
  30. package/.mindforge/governance/change-classifier.md +20 -4
  31. package/.mindforge/skills/agent-architecture-audit/SKILL.md +2 -2
  32. package/.mindforge/skills/orch-pipeline/SKILL.md +4 -4
  33. package/CHANGELOG.md +357 -0
  34. package/MINDFORGE.md +13 -6
  35. package/README.md +49 -30
  36. package/RELEASENOTES.md +65 -2
  37. package/SECURITY.md +22 -3
  38. package/bin/autonomous/auto-runner.js +65 -2
  39. package/bin/change-classifier.js +151 -16
  40. package/bin/dashboard/api-router.js +18 -38
  41. package/bin/dashboard/frontend/app.js +429 -0
  42. package/bin/dashboard/frontend/index.html +13 -406
  43. package/bin/dashboard/metrics-aggregator.js +52 -23
  44. package/bin/dashboard/server.js +160 -1
  45. package/bin/dashboard/sse-bridge.js +11 -8
  46. package/bin/engine/sre-manager.js +1 -1
  47. package/bin/engine/temporal-cli.js +56 -6
  48. package/bin/engine/verification-runner.js +134 -17
  49. package/bin/engine/verify-cli.js +25 -7
  50. package/bin/governance/approval-record.js +147 -0
  51. package/bin/governance/approve.js +24 -8
  52. package/bin/governance/policy-engine.js +33 -3
  53. package/bin/governance/policy-gate-hardened.js +36 -1
  54. package/bin/governance/verify-approvals.js +175 -0
  55. package/bin/harness-audit.js +224 -10
  56. package/bin/hooks/instinct-capture-hook.js +12 -4
  57. package/bin/install.js +63 -3
  58. package/bin/installer/harness-adapter-compliance.js +339 -28
  59. package/bin/installer/hook-registration.js +547 -0
  60. package/bin/installer-core.js +481 -65
  61. package/bin/learning/instinct-cli.js +7 -0
  62. package/bin/memory/vector-hub.js +196 -13
  63. package/bin/migrations/0.6.0-to-1.0.0.js +30 -25
  64. package/bin/migrations/1.0.0-to-2.0.0.js +22 -23
  65. package/bin/mindforge-cli.js +67 -6
  66. package/bin/models/cost-tracker.js +104 -6
  67. package/bin/models/model-client.js +6 -1
  68. package/bin/revops/debt-monitor.js +57 -13
  69. package/bin/security/trust-gate-hook.js +50 -6
  70. package/bin/skill-validator.js +6 -1
  71. package/bin/skills-builder/skill-scorer.js +46 -6
  72. package/bin/updater/self-update.js +6 -1
  73. package/bin/updater/version-comparator.js +21 -1
  74. package/bin/utils/mindforge-version.js +99 -0
  75. package/bin/utils/redact-secrets.js +106 -0
  76. package/bin/validate-config.js +42 -2
  77. package/bin/wizard/setup-wizard.js +4 -1
  78. package/bin/wizard/theme.js +9 -1
  79. package/changelogs/index.json +11 -9
  80. package/changelogs/v11.9.3.md +204 -0
  81. package/changelogs/v11.9.4.md +155 -0
  82. package/docs/References/config-reference.md +5 -2
  83. package/docs/References/sdk-api.md +1 -1
  84. package/docs/Templates/Codebase/architecture.md +1 -1
  85. package/docs/commands-reference.md +4 -5
  86. package/docs/faq.md +25 -5
  87. package/docs/getting-started.md +10 -4
  88. package/docs/sdk-reference.md +15 -7
  89. package/docs/troubleshooting.md +65 -6
  90. package/docs/user-guide.md +14 -14
  91. package/examples/sdk-integration/README.md +1 -1
  92. package/package.json +8 -3
  93. package/subagents/.claude-plugin/marketplace.json +1 -1
  94. package/.mindforge/memory/sync-manifest.json +0 -6
  95. package/bin/dashboard/approval-handler.js +0 -136
@@ -0,0 +1,429 @@
1
+ /**
2
+ * MindForge dashboard front end.
3
+ *
4
+ * EXTRACTED FROM an inline <script> in index.html. The server sets its own
5
+ * Content-Security-Policy of `script-src 'self'` (bin/dashboard/server.js:165), which blocks inline
6
+ * script execution — so the entire application never ran. Measured in a real headless browser:
7
+ *
8
+ * [error] Executing inline script violates the following Content Security Policy directive
9
+ * 'script-src 'self''. The action has been blocked.
10
+ * typeof window.showPage -> undefined
11
+ * GET /api/connections -> {"clients":0}
12
+ *
13
+ * while the static HTML displayed "● Connected" and "SSE STREAMING ACTIVE". The page returned HTTP
14
+ * 200 and affirmatively reported health it could not have had, because the only code able to
15
+ * contradict it was the code being blocked.
16
+ *
17
+ * Externalising is the fix that does NOT weaken the policy. Adding 'unsafe-inline', or a hash, or a
18
+ * nonce would each have restored the <script> while leaving the 10 inline on* attribute handlers
19
+ * dead — those are blocked by the same directive and need 'unsafe-hashes' or removal. They are now
20
+ * addEventListener bindings at the end of this file.
21
+ *
22
+ * SECOND LATENT BUG this uncovered: the inline script ended with a top-level `showPage('activity')`,
23
+ * but sat at line 373 of 757 — above the `.page` elements it queries. As an inline script it ran
24
+ * during parsing, found nothing, and silently did nothing. Loaded with `defer` it runs after the
25
+ * document is parsed, so that call now works. The CSP block had been masking it.
26
+ */
27
+ /* global document, window, EventSource, getComputedStyle, alert */
28
+ // Browser globals, declared file-locally. This file is the only browser-targeted source in the repo;
29
+ // eslint.config.mjs configures Node globals and is protected against per-file weakening, which is the
30
+ // right trade — a /* global */ comment scopes the declaration here instead of loosening the project.
31
+ // These 55 no-undef errors existed the whole time the code was inline in index.html; nothing linted it.
32
+ 'use strict';
33
+
34
+ // ── State ─────────────────────────────────────────────────────────────────
35
+ let currentApprovals = [];
36
+ let state = { costs: [], quality: [] };
37
+
38
+ function showPage(id) {
39
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
40
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
41
+ document.getElementById(id).classList.add('active');
42
+ const tab = Array.from(document.querySelectorAll('.tab')).find(t => t.innerText.toLowerCase() === id.toLowerCase());
43
+ if (tab) tab.classList.add('active');
44
+
45
+ if (id === 'metrics') drawCharts();
46
+ }
47
+
48
+ function escapeHtml(str) {
49
+ if (!str) return '';
50
+ return String(str).replace(/[&<>"']/g, m => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', '\'':'&#39;' }[m]));
51
+ }
52
+
53
+ // ── WebSocket / SSE ───────────────────────────────────────────────────────
54
+ const es = new EventSource('/events');
55
+ const feed = document.getElementById('audit-feed');
56
+
57
+ es.onopen = () => {
58
+ const el = document.getElementById('connection-status');
59
+ el.textContent = '● Connected';
60
+ el.style.color = 'var(--green)';
61
+ refreshData();
62
+ };
63
+
64
+ es.onerror = () => {
65
+ const el = document.getElementById('connection-status');
66
+ el.textContent = '● Disconnected';
67
+ el.style.color = 'var(--red)';
68
+ };
69
+
70
+ es.addEventListener('status:update', (e) => {
71
+ const data = JSON.parse(e.data);
72
+ updateStatusUI(data);
73
+ });
74
+
75
+ es.addEventListener('audit:new', (e) => {
76
+ const data = JSON.parse(e.data);
77
+ appendAuditEvent(data);
78
+ });
79
+
80
+ es.addEventListener('approval:new', (e) => {
81
+ const data = JSON.parse(e.data);
82
+ refreshApprovals();
83
+ });
84
+
85
+ // ── API Interactions ──────────────────────────────────────────────────────
86
+ async function refreshData() {
87
+ try {
88
+ const res = await fetch('/api/metrics');
89
+ const data = await res.json();
90
+ state = data;
91
+ updateMetricsUI(data);
92
+ refreshCosts();
93
+ refreshApprovals();
94
+ refreshMemory();
95
+ refreshTeam();
96
+ refreshRevOps();
97
+ if (document.getElementById('temporal').classList.contains('active')) {
98
+ refreshTemporal();
99
+ }
100
+ } catch(e) { console.error('Refresh fail', e); }
101
+ }
102
+
103
+ async function refreshRevOps() {
104
+ try {
105
+ const res = await fetch('/api/revops/overview');
106
+ const data = await res.json();
107
+ if (data.success) {
108
+ updateRevOpsUI(data);
109
+ }
110
+ } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
111
+ }
112
+
113
+ let temporalHistory = [];
114
+ async function refreshTemporal() {
115
+ try {
116
+ const res = await fetch('/api/temporal/history');
117
+ const data = await res.json();
118
+ temporalHistory = data;
119
+ renderTemporalTimeline();
120
+ } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
121
+ }
122
+
123
+ async function refreshApprovals() {
124
+ try {
125
+ const res = await fetch('/api/approvals');
126
+ const data = await res.json();
127
+ currentApprovals = data;
128
+ renderApprovals();
129
+ } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
130
+ }
131
+
132
+ async function refreshMemory() {
133
+ try {
134
+ const res = await fetch('/api/memory');
135
+ const data = await res.json();
136
+ document.getElementById('memory-map').innerHTML = `<pre>${escapeHtml(JSON.stringify(data.graph || {}, null, 2))}</pre>`;
137
+ document.getElementById('memory-stats-list').innerHTML = `<div class="stat-value">${data.count || 0} <span class="stat-unit">Items Indexed</span></div>`;
138
+ } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
139
+ }
140
+
141
+ async function refreshTeam() {
142
+ try {
143
+ const res = await fetch('/api/team');
144
+ const data = await res.json();
145
+ renderTeam(data);
146
+ } catch { /* one panel failing must not abort the rest of the refresh; it keeps its last value */ }
147
+ }
148
+
149
+ // decide() and submitDecision() are removed along with POST /api/approve/:id. They sent
150
+ // `approver: 'admin-dash'` from the client, which the route already had to discard as
151
+ // forgeable. There is now no endpoint that writes an approval record at all, so that
152
+ // attribution problem is gone by construction rather than mitigated. Records are minted only
153
+ // by bin/governance/approve.js, which takes identity from git and fails closed without a GPG
154
+ // key unless MINDFORGE_ALLOW_UNVERIFIED_APPROVAL=1.
155
+
156
+
157
+ function closeConfirm() {
158
+ document.getElementById('confirm-overlay').style.display = 'none';
159
+ }
160
+
161
+ // ── UI Rendering ──────────────────────────────────────────────────────────
162
+ function updateStatusUI(data) {
163
+ document.getElementById('project-name').textContent = `PROJ: ${data.project_name || 'UNSET'}`;
164
+ document.getElementById('stat-phase').textContent = data.phase || '0';
165
+ document.getElementById('stat-status').textContent = (data.auto_status || 'IDLE').toUpperCase();
166
+ document.getElementById('stat-tasks').textContent = `${data.tasks_completed || 0}/${data.tasks_total || 0}`;
167
+ document.getElementById('stat-elapsed').textContent = data.elapsed_ms ? (data.elapsed_ms / 1000).toFixed(1) + 's' : '0s';
168
+ }
169
+
170
+ function updateMetricsUI(data) {
171
+ // /api/metrics returns { sessions, avg_quality, avg_cost_usd, ... } — see
172
+ // bin/dashboard/metrics-aggregator.js getMetrics(). It has never returned
173
+ // `costs` or `quality`, so these tiles used to render "$0.0000" and "NaN".
174
+ document.getElementById('stat-avg-quality').textContent = (data.avg_quality ?? 0).toFixed(1);
175
+ }
176
+
177
+ // Cumulative spend comes from the usage ledger via /api/costs (already wired
178
+ // in bin/dashboard/api-router.js but never fetched until now).
179
+ async function refreshCosts() {
180
+ try {
181
+ const res = await fetch('/api/costs?window=7');
182
+ // sendServerError returns a well-formed JSON body, so res.json() would
183
+ // SUCCEED on a 500 and `total_usd ?? 0` would render "$0.00" — an outage
184
+ // and genuine zero spend look identical under the "Cost (7d)" label.
185
+ if (!res.ok) throw new Error(`/api/costs ${res.status}`);
186
+ const data = await res.json();
187
+ document.getElementById('stat-total-cost').textContent = `${(data.total_usd ?? 0).toFixed(2)}`;
188
+ } catch (e) { console.error('Cost refresh failed', e); }
189
+ }
190
+
191
+ function appendAuditEvent(data) {
192
+ const div = document.createElement('div');
193
+ div.className = `event type-${data.event}`;
194
+ const time = new Date(data.timestamp).toLocaleTimeString([], { hour12:false, hour:'2-digit', minute:'2-digit', second:'2-digit' });
195
+ div.innerHTML = `<span class="event-time">${time}</span><span class="event-type type-${data.event}">${data.event}</span><span class="event-msg">${escapeHtml(data.message || data.task || '')}</span>`;
196
+ feed.prepend(div);
197
+ if (feed.children.length > 200) feed.lastChild.remove();
198
+ }
199
+
200
+ // Read-only. The Approve/Reject buttons that were here posted to /api/approve/:id, an
201
+ // endpoint that could never succeed and has now been removed: it decided on pending
202
+ // REQUESTS, which nothing in MindForge produces. This panel shows the records that exist,
203
+ // with the integrity state the shared verifier reports.
204
+ //
205
+ // It also rendered a.phase, a.plan and a.summary — three fields no producer has ever
206
+ // written — and called .length/.map() on an object, which threw a TypeError.
207
+ function renderApprovals() {
208
+ const list = document.getElementById('approval-list');
209
+ const records = Array.isArray(currentApprovals) ? currentApprovals : [];
210
+ if (records.length === 0) {
211
+ list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">'
212
+ + 'No approval records. Tier-3 review is a recorded human acknowledgement; '
213
+ + 'authorization is enforced by branch protection, not from here.</div>';
214
+ return;
215
+ }
216
+
217
+ const colour = { valid: 'var(--green)', stale: 'var(--purple)', corrupt: 'var(--red)' };
218
+ list.innerHTML = records.map(a => {
219
+ const hrs = a.hours_remaining;
220
+ const life = (hrs === null || hrs === undefined) ? ''
221
+ : hrs > 0 ? `${hrs.toFixed(1)}h remaining` : `expired ${Math.abs(hrs).toFixed(1)}h ago`;
222
+ const problems = (a.problems || []).length
223
+ ? '<ul style="margin:8px 0 0 16px; font-size:11px; color:var(--red)">'
224
+ + a.problems.map(x => `<li>${escapeHtml(x)}</li>`).join('') + '</ul>'
225
+ : '';
226
+ return `
227
+ <div class="card approval-card">
228
+ <div class="card-body">
229
+ <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px;">
230
+ <div>
231
+ <span class="tier-badge tier-${escapeHtml(String(a.tier ?? '?'))}">Tier ${escapeHtml(String(a.tier ?? '?'))}</span>
232
+ <span style="font-weight:700">${escapeHtml(a.id || a.file || 'unknown')}</span>
233
+ <span style="font-size:11px; color:${colour[a.state] || 'var(--muted)'}; margin-left:8px; text-transform:uppercase">${escapeHtml(a.state || '')}</span>
234
+ </div>
235
+ <div style="font-size:11px; color:var(--muted)">${escapeHtml(life)}</div>
236
+ </div>
237
+ <div style="font-size:11px; color:var(--muted); margin-bottom:6px">
238
+ ${escapeHtml(a.approved_by || 'unknown')} &middot; v${escapeHtml(a.version || '?')}
239
+ </div>
240
+ <p style="margin:0; font-size:12px; line-height:1.4">${escapeHtml(a.reason || 'No reason recorded')}</p>
241
+ ${problems}
242
+ </div>
243
+ </div>`;
244
+ }).join('');
245
+ }
246
+
247
+ function renderTeam(data) {
248
+ const list = document.getElementById('team-activity-list');
249
+ if (!data || data.length === 0) {
250
+ list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">No recent team activity</div>';
251
+ return;
252
+ }
253
+ list.innerHTML = data.map(ev => `
254
+ <div class="activity-row">
255
+ <div class="avatar">${(ev.user || '?').charAt(0).toUpperCase()}</div>
256
+ <div class="activity-info">
257
+ <div><span class="activity-user">${escapeHtml(ev.user)}</span> <span class="activity-action">${escapeHtml(ev.action)}</span></div>
258
+ <div class="activity-time">${new Date(ev.timestamp).toLocaleString()}</div>
259
+ </div>
260
+ </div>
261
+ `).join('');
262
+ }
263
+
264
+ // ── Simple Charts Implementation ──────────────────────────────────────────
265
+ function drawCharts() {
266
+ drawCanvasChart('chart-costs', state.costs?.map(c => c.cost) || [], varColor('--accent'));
267
+ drawCanvasChart('chart-quality', state.quality?.map(q => q.score) || [], varColor('--green'), 100);
268
+ }
269
+
270
+ function varColor(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
271
+
272
+ function drawCanvasChart(id, data, color, maxVal = null) {
273
+ const canvas = document.getElementById(id);
274
+ if (!canvas) return;
275
+ const ctx = canvas.getContext('2d');
276
+ const dpr = window.devicePixelRatio || 1;
277
+ canvas.width = canvas.offsetWidth * dpr;
278
+ canvas.height = canvas.offsetHeight * dpr;
279
+ ctx.scale(dpr, dpr);
280
+
281
+ const w = canvas.offsetWidth;
282
+ const h = canvas.offsetHeight;
283
+ ctx.clearRect(0,0,w,h);
284
+
285
+ if (data.length < 2) return;
286
+
287
+ const max = maxVal || Math.max(...data) * 1.2 || 1;
288
+ const step = w / (data.length - 1);
289
+
290
+ ctx.beginPath();
291
+ ctx.strokeStyle = color;
292
+ ctx.lineWidth = 2;
293
+ ctx.lineJoin = 'round';
294
+
295
+ data.forEach((val, i) => {
296
+ const x = i * step;
297
+ const y = h - (val / max) * h;
298
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
299
+ });
300
+ ctx.stroke();
301
+
302
+ // Area fill
303
+ ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
304
+ ctx.fillStyle = color.replace(')', ', 0.1)').replace('rgb', 'rgba');
305
+ ctx.fill();
306
+ }
307
+
308
+ // ── Temporal Steering Logic ────────────────────────────────────────────────
309
+ function renderTemporalTimeline() {
310
+ const slider = document.getElementById('temporal-slider');
311
+ const count = document.getElementById('timeline-count');
312
+
313
+ if (temporalHistory.length === 0) {
314
+ slider.max = 0;
315
+ count.textContent = '0 Snapshots';
316
+ return;
317
+ }
318
+
319
+ slider.max = temporalHistory.length - 1;
320
+ count.textContent = `${temporalHistory.length} Snapshots`;
321
+
322
+ // Default to latest if not touching
323
+ if (slider.value == 0 && temporalHistory.length > 0) {
324
+ onSliderChange(temporalHistory.length - 1);
325
+ slider.value = temporalHistory.length - 1;
326
+ }
327
+ }
328
+
329
+ let selectedSnapshot = null;
330
+ async function onSliderChange(index) {
331
+ const snap = temporalHistory[temporalHistory.length - 1 - index]; // reversed for chronological range
332
+ if (!snap) return;
333
+
334
+ selectedSnapshot = snap;
335
+ document.getElementById('slider-current').textContent = `Point: ${snap.id.slice(0, 8)} (${new Date(snap.timestamp).toLocaleTimeString()})`;
336
+ document.getElementById('inject-btn').disabled = false;
337
+
338
+ // Fetch sample audit file for this snapshot
339
+ try {
340
+ const res = await fetch(`/api/temporal/snapshot/${snap.id}/AUDIT.jsonl`);
341
+ const content = await res.text();
342
+ document.getElementById('snapshot-viewer').textContent = content || 'No audit log available for this point.';
343
+ } catch(e) {
344
+ document.getElementById('snapshot-viewer').textContent = 'Failed to load snapshot details.';
345
+ }
346
+ }
347
+
348
+ async function injectHindsight() {
349
+ if (!selectedSnapshot) return;
350
+ const instruction = document.getElementById('steering-input').value;
351
+ if (!instruction) {
352
+ alert('Please provide a steering instruction for the hindsight injection.');
353
+ return;
354
+ }
355
+
356
+ const btn = document.getElementById('inject-btn');
357
+ btn.disabled = true;
358
+ btn.textContent = 'Injecting Steering Vector...';
359
+
360
+ try {
361
+ const res = await fetch('/api/temporal/inject', {
362
+ method: 'POST',
363
+ headers: { 'Content-Type': 'application/json' },
364
+ body: JSON.stringify({
365
+ auditId: selectedSnapshot.id,
366
+ fixDescription: instruction
367
+ })
368
+ });
369
+ const result = await res.json();
370
+ if (result.success) {
371
+ alert('Hindsight Injection Successful. Agent state rolled back and awaiting re-optimization.');
372
+ showPage('activity');
373
+ } else {
374
+ alert('Injection failed: ' + result.error);
375
+ }
376
+ } catch(e) {
377
+ alert('Failed to connect to temporal engine.');
378
+ } finally {
379
+ btn.disabled = false;
380
+ btn.textContent = 'Rewind & Inject Fix';
381
+ }
382
+ }
383
+
384
+ // ── RevOps Logic ─────────────────────────────────────────────────────────
385
+ function updateRevOpsUI(data) {
386
+ const { roi, velocity, debt } = data;
387
+
388
+ document.getElementById('rev-roi').textContent = `${roi.roi_percentage}%`;
389
+ document.getElementById('rev-net').textContent = `$${roi.net_value}`;
390
+ document.getElementById('rev-velocity').innerHTML = `${velocity.avg_seconds_per_task} <span class="stat-unit">sec/task</span>`;
391
+ document.getElementById('rev-eta').textContent = `ETA: ${velocity.eta}`;
392
+ document.getElementById('rev-health').textContent = debt.security_health_score;
393
+
394
+ const healthStatus = document.getElementById('rev-health-status');
395
+ healthStatus.textContent = `STATUS: ${debt.governance_status.toUpperCase()}`;
396
+ healthStatus.style.background = debt.security_health_score > 80 ? 'rgba(63, 185, 80, 0.15)' : 'rgba(248, 81, 73, 0.15)';
397
+ healthStatus.style.color = debt.security_health_score > 80 ? 'var(--green)' : 'var(--red)';
398
+
399
+ document.getElementById('roi-hours').textContent = `${roi.hours_saved}h`;
400
+ document.getElementById('roi-gross').textContent = `$${roi.gross_value}`;
401
+ document.getElementById('roi-burn').textContent = `$${roi.token_cost}`;
402
+ document.getElementById('roi-total-pct').textContent = `${roi.roi_percentage}%`;
403
+
404
+ document.getElementById('debt-critical').textContent = debt.critical_findings;
405
+ document.getElementById('debt-tier3').textContent = debt.tier3_approvals;
406
+ const riskBadge = document.getElementById('debt-risk');
407
+ riskBadge.textContent = debt.debt_level.toUpperCase();
408
+ riskBadge.className = `badge ${debt.debt_level === 'Minimal' ? 'badge-live' : ''}`;
409
+ if (debt.debt_level !== 'Minimal') riskBadge.style.color = 'var(--yellow)';
410
+ }
411
+
412
+ window.onresize = drawCharts;
413
+ setInterval(refreshData, 10000); // More frequent refresh for live dashboard
414
+ showPage('activity');
415
+
416
+ // ── Event wiring ────────────────────────────────────────────────────────────
417
+ // These listeners replace 10 inline on* attributes. Inline handlers are blocked by the server's own
418
+ // Content-Security-Policy (script-src 'self') exactly as an inline <script> is, so converting the
419
+ // script to an external file without also converting these would have left every control dead.
420
+ document.querySelectorAll('nav .tab[data-page]').forEach((btn) => {
421
+ btn.addEventListener('click', () => showPage(btn.dataset.page));
422
+ });
423
+ const __slider = document.getElementById('temporal-slider');
424
+ if (__slider) __slider.addEventListener('input', (e) => onSliderChange(e.target.value));
425
+ document.querySelectorAll('[data-action]').forEach((el) => {
426
+ const fns = { 'inject-hindsight': () => injectHindsight(), 'close-confirm': () => closeConfirm() };
427
+ const fn = fns[el.dataset.action];
428
+ if (fn) el.addEventListener('click', fn);
429
+ });