mindforge-cc 2.0.0-alpha.4 → 2.0.0-alpha.7
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/.agent/CLAUDE.md +37 -7
- package/.agent/mindforge/dashboard.md +98 -0
- package/.agent/mindforge/init-project.md +12 -0
- package/.claude/CLAUDE.md +37 -7
- package/.claude/commands/mindforge/dashboard.md +98 -0
- package/.mindforge/dashboard/api-reference.md +122 -0
- package/.mindforge/dashboard/dashboard-spec.md +96 -0
- package/.planning/approvals/v2-architecture-approval.json +15 -0
- package/CHANGELOG.md +12 -2
- package/README.md +18 -2
- package/RELEASENOTES.md +1 -1
- package/bin/change-classifier.js +86 -0
- package/bin/dashboard/api-router.js +198 -0
- package/bin/dashboard/approval-handler.js +134 -0
- package/bin/dashboard/frontend/index.html +511 -0
- package/bin/dashboard/metrics-aggregator.js +296 -0
- package/bin/dashboard/server.js +135 -0
- package/bin/dashboard/sse-bridge.js +178 -0
- package/bin/dashboard/team-tracker.js +0 -0
- package/bin/governance/approve.js +60 -0
- package/bin/installer-core.js +68 -12
- package/bin/mindforge-cli.js +87 -0
- package/bin/wizard/setup-wizard.js +5 -1
- package/docs/Context/Master-Context.md +11 -11
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/decision-records-index.md +20 -20
- package/docs/ci-cd.md +92 -0
- package/docs/commands-reference.md +1 -0
- package/docs/enterprise-setup.md +1 -1
- package/docs/feature-dashboard.md +52 -0
- package/docs/publishing-guide.md +16 -51
- package/docs/reference/commands.md +42 -42
- package/docs/reference/config-reference.md +2 -2
- package/docs/reference/sdk-api.md +1 -1
- package/docs/testing-current-version.md +130 -0
- package/docs/user-guide.md +24 -2
- package/docs/usp-features.md +15 -1
- package/docs/workflow-atlas.md +57 -0
- package/package.json +5 -2
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>MindForge v2 | Real-time Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0d1117;
|
|
10
|
+
--surface: #161b22;
|
|
11
|
+
--surface-hover: #1c2128;
|
|
12
|
+
--border: #30363d;
|
|
13
|
+
--text: #e6edf3;
|
|
14
|
+
--muted: #8b949e;
|
|
15
|
+
--accent: #58a6ff;
|
|
16
|
+
--green: #3fb950;
|
|
17
|
+
--yellow: #d29922;
|
|
18
|
+
--red: #f85149;
|
|
19
|
+
--orange: #db6d28;
|
|
20
|
+
--purple: #bc8cff;
|
|
21
|
+
--cyan: #39c5bb;
|
|
22
|
+
--font-mono: 'JetBrains Mono', 'Fira Code', 'Ubuntu Mono', monospace;
|
|
23
|
+
}
|
|
24
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
25
|
+
body {
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
29
|
+
font-size: 13px;
|
|
30
|
+
height: 100vh;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ── Header & Nav ────────────────────────────────────────────────────────── */
|
|
37
|
+
header {
|
|
38
|
+
padding: 12px 24px;
|
|
39
|
+
background: var(--surface);
|
|
40
|
+
border-bottom: 1px solid var(--border);
|
|
41
|
+
display: flex;
|
|
42
|
+
justify-content: space-between;
|
|
43
|
+
align-items: center;
|
|
44
|
+
z-index: 100;
|
|
45
|
+
}
|
|
46
|
+
.logo-area { display: flex; align-items: center; gap: 12px; }
|
|
47
|
+
.logo-text { font-size: 16px; font-weight: 700; letter-spacing: -0.5px; }
|
|
48
|
+
.badge { padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; background: var(--border); }
|
|
49
|
+
.badge-live { background: rgba(63, 185, 80, 0.15); color: var(--green); border: 1px solid rgba(63, 185, 80, 0.3); }
|
|
50
|
+
|
|
51
|
+
nav {
|
|
52
|
+
padding: 0 24px;
|
|
53
|
+
background: var(--surface);
|
|
54
|
+
border-bottom: 1px solid var(--border);
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 4px;
|
|
57
|
+
height: 48px;
|
|
58
|
+
}
|
|
59
|
+
.tab {
|
|
60
|
+
padding: 0 16px;
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 8px;
|
|
64
|
+
color: var(--muted);
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
border: none;
|
|
67
|
+
background: none;
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
border-bottom: 2px solid transparent;
|
|
70
|
+
transition: all 0.2s;
|
|
71
|
+
outline: none;
|
|
72
|
+
}
|
|
73
|
+
.tab:hover { color: var(--text); background: var(--surface-hover); }
|
|
74
|
+
.tab.active { color: var(--accent); border-bottom-color: var(--accent); background: rgba(88, 166, 255, 0.05); }
|
|
75
|
+
|
|
76
|
+
/* ── Layout ──────────────────────────────────────────────────────────────── */
|
|
77
|
+
main { flex: 1; padding: 24px; overflow-y: auto; display: flex; flex-direction: column; gap: 24px; }
|
|
78
|
+
.page { display: none; opacity: 0; transform: translateY(10px); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
79
|
+
.page.active { display: flex; flex-direction: column; gap: 24px; opacity: 1; transform: translateY(0); }
|
|
80
|
+
|
|
81
|
+
.grid-4 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 24px; }
|
|
82
|
+
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px; }
|
|
83
|
+
|
|
84
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; }
|
|
85
|
+
.card-header { padding: 12px 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: rgba(255,255,255,0.02); }
|
|
86
|
+
.card-title { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
87
|
+
.card-body { padding: 16px; flex: 1; }
|
|
88
|
+
|
|
89
|
+
/* ── Components ──────────────────────────────────────────────────────────── */
|
|
90
|
+
.stat-value { font-size: 28px; font-weight: 700; margin-top: 4px; display: flex; align-items: baseline; gap: 8px; }
|
|
91
|
+
.stat-unit { font-size: 14px; font-weight: 400; color: var(--muted); }
|
|
92
|
+
.stat-trend { font-size: 12px; font-weight: 600; padding: 2px 6px; border-radius: 4px; }
|
|
93
|
+
.trend-up { color: var(--green); background: rgba(63, 185, 80, 0.1); }
|
|
94
|
+
.trend-down { color: var(--red); background: rgba(248, 81, 73, 0.1); }
|
|
95
|
+
|
|
96
|
+
#audit-feed {
|
|
97
|
+
font-family: var(--font-mono); font-size: 12px; line-height: 1.6;
|
|
98
|
+
background: #000; padding: 12px; height: 400px; overflow-y: auto;
|
|
99
|
+
scroll-behavior: smooth; color: #d1d5db;
|
|
100
|
+
}
|
|
101
|
+
.event { padding: 4px 8px; border-left: 2px solid transparent; transition: background 0.2s; }
|
|
102
|
+
.event:hover { background: rgba(255,255,255,0.05); }
|
|
103
|
+
.event-time { color: var(--muted); margin-right: 12px; min-width: 80px; display: inline-block; }
|
|
104
|
+
.event-type { font-weight: 700; margin-right: 12px; min-width: 120px; display: inline-block; text-transform: uppercase; font-size: 10px; }
|
|
105
|
+
.event-msg { color: #eee; }
|
|
106
|
+
|
|
107
|
+
.type-task_completed { border-left-color: var(--green); color: var(--green); }
|
|
108
|
+
.type-task_failed { border-left-color: var(--red); color: var(--red); }
|
|
109
|
+
.type-security_finding { border-left-color: var(--yellow); color: var(--yellow); }
|
|
110
|
+
.type-approval_pending { border-left-color: var(--purple); color: var(--purple); }
|
|
111
|
+
.type-approval_granted { border-left-color: var(--green); color: var(--green); }
|
|
112
|
+
.type-approval_rejected { border-left-color: var(--red); color: var(--red); }
|
|
113
|
+
|
|
114
|
+
/* ── Approvals UI ────────────────────────────────────────────────────────── */
|
|
115
|
+
.approval-card { margin-bottom: 12px; }
|
|
116
|
+
.tier-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 700; margin-right: 8px; }
|
|
117
|
+
.tier-1 { background: rgba(88, 166, 255, 0.2); color: var(--accent); }
|
|
118
|
+
.tier-2 { background: rgba(210, 153, 34, 0.2); color: var(--yellow); }
|
|
119
|
+
.tier-3 { background: rgba(248, 81, 73, 0.2); color: var(--red); }
|
|
120
|
+
|
|
121
|
+
.btn { padding: 8px 16px; border-radius: 6px; font-weight: 600; cursor: pointer; border: none; transition: 0.2s; outline: none; display: inline-flex; align-items: center; gap: 8px; }
|
|
122
|
+
.btn-approve { background: var(--green); color: white; }
|
|
123
|
+
.btn-approve:hover { filter: brightness(1.1); }
|
|
124
|
+
.btn-reject { background: var(--red); color: white; }
|
|
125
|
+
.btn-reject:hover { filter: brightness(1.1); }
|
|
126
|
+
.btn-disabled { opacity: 0.5; cursor: not-allowed; }
|
|
127
|
+
|
|
128
|
+
.confirm-overlay {
|
|
129
|
+
position: fixed; inset: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px);
|
|
130
|
+
display: none; align-items: center; justify-content: center; z-index: 1000;
|
|
131
|
+
}
|
|
132
|
+
.confirm-modal { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 24px; width: 400px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); }
|
|
133
|
+
.confirm-modal h3 { margin-bottom: 12px; }
|
|
134
|
+
.confirm-modal p { color: var(--muted); margin-bottom: 20px; line-height: 1.5; }
|
|
135
|
+
.confirm-input { width: 100%; background: var(--bg); border: 1px solid var(--border); padding: 10px; border-radius: 6px; color: var(--text); font-family: var(--font-mono); margin-bottom: 20px; outline: none; }
|
|
136
|
+
.confirm-input:focus { border-color: var(--accent); }
|
|
137
|
+
|
|
138
|
+
/* ── Charts ──────────────────────────────────────────────────────────────── */
|
|
139
|
+
canvas { width: 100%; height: 200px; image-rendering: -webkit-optimize-contrast; }
|
|
140
|
+
|
|
141
|
+
/* ── Team Feed ───────────────────────────────────────────────────────────── */
|
|
142
|
+
.activity-row { display: flex; gap: 12px; padding: 12px; border-bottom: 1px solid var(--border); }
|
|
143
|
+
.avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--border); border: 1px solid var(--muted); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; }
|
|
144
|
+
.activity-info { flex: 1; }
|
|
145
|
+
.activity-user { font-weight: 600; margin-right: 8px; }
|
|
146
|
+
.activity-action { color: var(--muted); }
|
|
147
|
+
.activity-time { font-size: 11px; color: var(--muted); margin-top: 4px; }
|
|
148
|
+
</style>
|
|
149
|
+
</head>
|
|
150
|
+
<body>
|
|
151
|
+
<header>
|
|
152
|
+
<div class="logo-area">
|
|
153
|
+
<div style="background:var(--accent); width:28px; height:28px; border-radius:6px; display:flex; align-items:center; justify-content:center; color:var(--bg); font-weight:900;">M</div>
|
|
154
|
+
<div class="logo-text">MindForge <span style="color:var(--muted); font-weight:400">Dash</span></div>
|
|
155
|
+
<div id="project-name" class="badge">PROJ: UNSET</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div id="connection-status" class="badge badge-live">● Connected</div>
|
|
158
|
+
</header>
|
|
159
|
+
|
|
160
|
+
<nav>
|
|
161
|
+
<button class="tab active" onclick="showPage('activity')">Activity</button>
|
|
162
|
+
<button class="tab" onclick="showPage('metrics')">Metrics</button>
|
|
163
|
+
<button class="tab" onclick="showPage('approvals')">Approvals</button>
|
|
164
|
+
<button class="tab" onclick="showPage('memory')">Memory</button>
|
|
165
|
+
<button class="tab" onclick="showPage('team')">Team</button>
|
|
166
|
+
</nav>
|
|
167
|
+
|
|
168
|
+
<main>
|
|
169
|
+
<!-- PAGE: ACTIVITY -->
|
|
170
|
+
<div id="activity" class="page active">
|
|
171
|
+
<div class="grid-4">
|
|
172
|
+
<div class="card"><div class="card-body"><div class="stat-label">System Phase</div><div id="stat-phase" class="stat-value">-</div></div></div>
|
|
173
|
+
<div class="card"><div class="card-body"><div class="stat-label">Agent Status</div><div id="stat-status" class="stat-value" style="color:var(--green)">IDLE</div></div></div>
|
|
174
|
+
<div class="card"><div class="card-body"><div class="stat-label">Task Progress</div><div id="stat-tasks" class="stat-value">-</div></div></div>
|
|
175
|
+
<div class="card"><div class="card-body"><div class="stat-label">Elapsed Time</div><div id="stat-elapsed" class="stat-value">-</div></div></div>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="card">
|
|
178
|
+
<div class="card-header">
|
|
179
|
+
<div class="card-title">Live Audit Feed</div>
|
|
180
|
+
<div style="font-size:10px; color:var(--muted)">SSE STREAMING ACTIVE</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div id="audit-feed"></div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<!-- PAGE: METRICS -->
|
|
187
|
+
<div id="metrics" class="page">
|
|
188
|
+
<div class="grid-2">
|
|
189
|
+
<div class="card">
|
|
190
|
+
<div class="card-header"><div class="card-title">Token Costs (USD)</div></div>
|
|
191
|
+
<div class="card-body"><canvas id="chart-costs"></canvas></div>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="card">
|
|
194
|
+
<div class="card-header"><div class="card-title">Session Quality Score</div></div>
|
|
195
|
+
<div class="card-body"><canvas id="chart-quality"></canvas></div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="grid-4">
|
|
199
|
+
<div class="card"><div class="card-body"><div class="stat-label">Cumulative Cost</div><div id="stat-total-cost" class="stat-value">$0.00</div></div></div>
|
|
200
|
+
<div class="card"><div class="card-body"><div class="stat-label">Avg Quality</div><div id="stat-avg-quality" class="stat-value">0.0</div></div></div>
|
|
201
|
+
<div class="card"><div class="card-body"><div class="stat-label">LSPs Registered</div><div id="stat-lsp-count" class="stat-value">0</div></div></div>
|
|
202
|
+
<div class="card"><div class="card-body"><div class="stat-label">Token Velocity</div><div id="stat-velocity" class="stat-value">0 <span class="stat-unit">tx/m</span></div></div></div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<!-- PAGE: APPROVALS -->
|
|
207
|
+
<div id="approvals" class="page">
|
|
208
|
+
<div class="card" style="max-width: 800px; margin: 0 auto; width: 100%;">
|
|
209
|
+
<div class="card-header"><div class="card-title">Pending Governance Decisions</div></div>
|
|
210
|
+
<div id="approval-list" class="card-body">
|
|
211
|
+
<div style="text-align:center; padding: 40px; color:var(--muted)">No pending approvals</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<!-- PAGE: MEMORY -->
|
|
217
|
+
<div id="memory" class="page">
|
|
218
|
+
<div class="grid-2">
|
|
219
|
+
<div class="card">
|
|
220
|
+
<div class="card-header"><div class="card-title">Knowledge Base Map</div></div>
|
|
221
|
+
<div class="card-body" id="memory-map" style="font-family:var(--font-mono); font-size:11px; color:var(--cyan)">
|
|
222
|
+
Loading graph...
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="card">
|
|
226
|
+
<div class="card-header"><div class="card-title">Persistent Engine Stats</div></div>
|
|
227
|
+
<div class="card-body">
|
|
228
|
+
<div id="memory-stats-list"></div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<!-- PAGE: TEAM -->
|
|
235
|
+
<div id="team" class="page">
|
|
236
|
+
<div class="card" style="max-width: 1000px; margin: 0 auto; width: 100%;">
|
|
237
|
+
<div class="card-header"><div class="card-title">Real-time Team Activity</div></div>
|
|
238
|
+
<div id="team-activity-list" class="card-body" style="padding:0">
|
|
239
|
+
<div style="text-align:center; padding: 40px; color:var(--muted)">No recent team activity</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</main>
|
|
244
|
+
|
|
245
|
+
<!-- CONFIRMATION MODAL -->
|
|
246
|
+
<div id="confirm-overlay" class="confirm-overlay">
|
|
247
|
+
<div class="confirm-modal">
|
|
248
|
+
<h3 id="confirm-title">Confirm Action</h3>
|
|
249
|
+
<p id="confirm-desc">This is a Tier 3 change. Please confirm.</p>
|
|
250
|
+
<div id="tier3-input-group" style="display:none">
|
|
251
|
+
<label style="display:block; font-size:11px; margin-bottom:8px; color:var(--red); font-weight:700;">TYPE PLAN ID TO CONFIRM:</label>
|
|
252
|
+
<input type="text" id="confirm-input" class="confirm-input" placeholder="e.g. 1.1-auth_fix">
|
|
253
|
+
</div>
|
|
254
|
+
<div style="display:flex; gap:12px; justify-content:flex-end;">
|
|
255
|
+
<button class="btn" style="background:var(--border)" onclick="closeConfirm()">Cancel</button>
|
|
256
|
+
<button id="confirm-btn" class="btn btn-approve">Proceed</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<script>
|
|
262
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
263
|
+
let currentApprovals = [];
|
|
264
|
+
let state = { costs: [], quality: [] };
|
|
265
|
+
let currentTier3Approval = null;
|
|
266
|
+
|
|
267
|
+
function showPage(id) {
|
|
268
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
269
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
270
|
+
document.getElementById(id).classList.add('active');
|
|
271
|
+
const tab = Array.from(document.querySelectorAll('.tab')).find(t => t.innerText.toLowerCase() === id.toLowerCase());
|
|
272
|
+
if (tab) tab.classList.add('active');
|
|
273
|
+
|
|
274
|
+
if (id === 'metrics') drawCharts();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function escapeHtml(str) {
|
|
278
|
+
if (!str) return '';
|
|
279
|
+
return String(str).replace(/[&<>"']/g, m => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[m]));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── WebSocket / SSE ───────────────────────────────────────────────────────
|
|
283
|
+
const es = new EventSource('/events');
|
|
284
|
+
const feed = document.getElementById('audit-feed');
|
|
285
|
+
|
|
286
|
+
es.onopen = () => {
|
|
287
|
+
const el = document.getElementById('connection-status');
|
|
288
|
+
el.textContent = '● Connected';
|
|
289
|
+
el.style.color = 'var(--green)';
|
|
290
|
+
refreshData();
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
es.onerror = () => {
|
|
294
|
+
const el = document.getElementById('connection-status');
|
|
295
|
+
el.textContent = '● Disconnected';
|
|
296
|
+
el.style.color = 'var(--red)';
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
es.addEventListener('status:update', (e) => {
|
|
300
|
+
const data = JSON.parse(e.data);
|
|
301
|
+
updateStatusUI(data);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
es.addEventListener('audit:new', (e) => {
|
|
305
|
+
const data = JSON.parse(e.data);
|
|
306
|
+
appendAuditEvent(data);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
es.addEventListener('approval:new', (e) => {
|
|
310
|
+
const data = JSON.parse(e.data);
|
|
311
|
+
refreshApprovals();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ── API Interactions ──────────────────────────────────────────────────────
|
|
315
|
+
async function refreshData() {
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch('/api/metrics');
|
|
318
|
+
const data = await res.json();
|
|
319
|
+
state = data;
|
|
320
|
+
updateMetricsUI(data);
|
|
321
|
+
refreshApprovals();
|
|
322
|
+
refreshMemory();
|
|
323
|
+
refreshTeam();
|
|
324
|
+
} catch(e) { console.error('Refresh fail', e); }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function refreshApprovals() {
|
|
328
|
+
try {
|
|
329
|
+
const res = await fetch('/api/approvals');
|
|
330
|
+
const data = await res.json();
|
|
331
|
+
currentApprovals = data;
|
|
332
|
+
renderApprovals();
|
|
333
|
+
} catch(e) {}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function refreshMemory() {
|
|
337
|
+
try {
|
|
338
|
+
const res = await fetch('/api/memory');
|
|
339
|
+
const data = await res.json();
|
|
340
|
+
document.getElementById('memory-map').innerHTML = `<pre>${escapeHtml(JSON.stringify(data.graph || {}, null, 2))}</pre>`;
|
|
341
|
+
document.getElementById('memory-stats-list').innerHTML = `<div class="stat-value">${data.count || 0} <span class="stat-unit">Items Indexed</span></div>`;
|
|
342
|
+
} catch(e) {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function refreshTeam() {
|
|
346
|
+
try {
|
|
347
|
+
const res = await fetch('/api/team');
|
|
348
|
+
const data = await res.json();
|
|
349
|
+
renderTeam(data);
|
|
350
|
+
} catch(e) {}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function decide(id, decision) {
|
|
354
|
+
const approval = currentApprovals.find(a => a.id === id);
|
|
355
|
+
if (!approval) return;
|
|
356
|
+
|
|
357
|
+
if (decision === 'approve' && approval.tier === 3) {
|
|
358
|
+
currentTier3Approval = approval;
|
|
359
|
+
const expected = `${approval.phase}-${approval.plan}`;
|
|
360
|
+
document.getElementById('confirm-title').innerText = 'Critical Tier 3 Approval';
|
|
361
|
+
document.getElementById('confirm-desc').innerText = `Danger: You are approving a sensitive change (Phase ${approval.phase}, Plan ${approval.plan}). This requires manual confirmation.`;
|
|
362
|
+
document.getElementById('tier3-input-group').style.display = 'block';
|
|
363
|
+
document.getElementById('confirm-input').value = '';
|
|
364
|
+
document.getElementById('confirm-overlay').style.display = 'flex';
|
|
365
|
+
document.getElementById('confirm-btn').onclick = () => submitDecision(id, decision, document.getElementById('confirm-input').value);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
submitDecision(id, decision);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function submitDecision(id, decision, confirmationId = null) {
|
|
373
|
+
try {
|
|
374
|
+
const res = await fetch(`/api/approve/${id}`, {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
headers: { 'Content-Type': 'application/json' },
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
decision,
|
|
379
|
+
comment: 'Decided via Dashboard',
|
|
380
|
+
approver: 'admin-dash',
|
|
381
|
+
confirmation_id: confirmationId
|
|
382
|
+
})
|
|
383
|
+
});
|
|
384
|
+
const result = await res.json();
|
|
385
|
+
if (!result.success) alert(result.error);
|
|
386
|
+
closeConfirm();
|
|
387
|
+
refreshApprovals();
|
|
388
|
+
} catch(e) { alert('Action failed'); }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function closeConfirm() {
|
|
392
|
+
document.getElementById('confirm-overlay').style.display = 'none';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── UI Rendering ──────────────────────────────────────────────────────────
|
|
396
|
+
function updateStatusUI(data) {
|
|
397
|
+
document.getElementById('project-name').textContent = `PROJ: ${data.project_name || 'UNSET'}`;
|
|
398
|
+
document.getElementById('stat-phase').textContent = data.phase || '0';
|
|
399
|
+
document.getElementById('stat-status').textContent = (data.auto_status || 'IDLE').toUpperCase();
|
|
400
|
+
document.getElementById('stat-tasks').textContent = `${data.tasks_completed || 0}/${data.tasks_total || 0}`;
|
|
401
|
+
document.getElementById('stat-elapsed').textContent = data.elapsed_ms ? (data.elapsed_ms / 1000).toFixed(1) + 's' : '0s';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function updateMetricsUI(data) {
|
|
405
|
+
document.getElementById('stat-total-cost').textContent = `$${(data.costs?.reduce((a,b) => a + b.cost, 0) || 0).toFixed(4)}`;
|
|
406
|
+
document.getElementById('stat-avg-quality').textContent = (data.quality?.reduce((a,b) => a + b.score, 0) / (data.quality?.length || 1)).toFixed(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function appendAuditEvent(data) {
|
|
410
|
+
const div = document.createElement('div');
|
|
411
|
+
div.className = `event type-${data.event}`;
|
|
412
|
+
const time = new Date(data.timestamp).toLocaleTimeString([], { hour12:false, hour:'2-digit', minute:'2-digit', second:'2-digit' });
|
|
413
|
+
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>`;
|
|
414
|
+
feed.prepend(div);
|
|
415
|
+
if (feed.children.length > 200) feed.lastChild.remove();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function renderApprovals() {
|
|
419
|
+
const list = document.getElementById('approval-list');
|
|
420
|
+
if (currentApprovals.length === 0) {
|
|
421
|
+
list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">No pending approvals</div>';
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
list.innerHTML = currentApprovals.map(a => `
|
|
426
|
+
<div class="card approval-card">
|
|
427
|
+
<div class="card-body">
|
|
428
|
+
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px;">
|
|
429
|
+
<div>
|
|
430
|
+
<span class="tier-badge tier-${a.tier}">Tier ${a.tier}</span>
|
|
431
|
+
<span style="font-weight:700">Phase ${a.phase} | ${a.plan}</span>
|
|
432
|
+
</div>
|
|
433
|
+
<div style="font-size:11px; color:var(--muted)">Expires: ${new Date(a.expires_at).toLocaleTimeString()}</div>
|
|
434
|
+
</div>
|
|
435
|
+
<p style="margin-bottom:16px; font-size:12px; line-height:1.4">${escapeHtml(a.summary || 'No summary provided')}</p>
|
|
436
|
+
<div style="display:flex; gap:12px;">
|
|
437
|
+
<button class="btn btn-approve" onclick="decide('${a.id}', 'approve')">Approve Selection</button>
|
|
438
|
+
<button class="btn btn-reject" onclick="decide('${a.id}', 'reject')">Reject</button>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
`).join('');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function renderTeam(data) {
|
|
446
|
+
const list = document.getElementById('team-activity-list');
|
|
447
|
+
if (!data || data.length === 0) {
|
|
448
|
+
list.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--muted)">No recent team activity</div>';
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
list.innerHTML = data.map(ev => `
|
|
452
|
+
<div class="activity-row">
|
|
453
|
+
<div class="avatar">${(ev.user || '?').charAt(0).toUpperCase()}</div>
|
|
454
|
+
<div class="activity-info">
|
|
455
|
+
<div><span class="activity-user">${escapeHtml(ev.user)}</span> <span class="activity-action">${escapeHtml(ev.action)}</span></div>
|
|
456
|
+
<div class="activity-time">${new Date(ev.timestamp).toLocaleString()}</div>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
`).join('');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Simple Charts Implementation ──────────────────────────────────────────
|
|
463
|
+
function drawCharts() {
|
|
464
|
+
drawCanvasChart('chart-costs', state.costs?.map(c => c.cost) || [], varColor('--accent'));
|
|
465
|
+
drawCanvasChart('chart-quality', state.quality?.map(q => q.score) || [], varColor('--green'), 100);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function varColor(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
|
|
469
|
+
|
|
470
|
+
function drawCanvasChart(id, data, color, maxVal = null) {
|
|
471
|
+
const canvas = document.getElementById(id);
|
|
472
|
+
if (!canvas) return;
|
|
473
|
+
const ctx = canvas.getContext('2d');
|
|
474
|
+
const dpr = window.devicePixelRatio || 1;
|
|
475
|
+
canvas.width = canvas.offsetWidth * dpr;
|
|
476
|
+
canvas.height = canvas.offsetHeight * dpr;
|
|
477
|
+
ctx.scale(dpr, dpr);
|
|
478
|
+
|
|
479
|
+
const w = canvas.offsetWidth;
|
|
480
|
+
const h = canvas.offsetHeight;
|
|
481
|
+
ctx.clearRect(0,0,w,h);
|
|
482
|
+
|
|
483
|
+
if (data.length < 2) return;
|
|
484
|
+
|
|
485
|
+
const max = maxVal || Math.max(...data) * 1.2 || 1;
|
|
486
|
+
const step = w / (data.length - 1);
|
|
487
|
+
|
|
488
|
+
ctx.beginPath();
|
|
489
|
+
ctx.strokeStyle = color;
|
|
490
|
+
ctx.lineWidth = 2;
|
|
491
|
+
ctx.lineJoin = 'round';
|
|
492
|
+
|
|
493
|
+
data.forEach((val, i) => {
|
|
494
|
+
const x = i * step;
|
|
495
|
+
const y = h - (val / max) * h;
|
|
496
|
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
497
|
+
});
|
|
498
|
+
ctx.stroke();
|
|
499
|
+
|
|
500
|
+
// Area fill
|
|
501
|
+
ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
|
|
502
|
+
ctx.fillStyle = color.replace(')', ', 0.1)').replace('rgb', 'rgba');
|
|
503
|
+
ctx.fill();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
window.onresize = drawCharts;
|
|
507
|
+
setInterval(refreshData, 30000); // Periodic full refresh
|
|
508
|
+
showPage('activity');
|
|
509
|
+
</script>
|
|
510
|
+
</body>
|
|
511
|
+
</html>
|