speexor 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/API-REFERENCE.md +96 -1
  2. package/ARCHITECTURE.md +83 -32
  3. package/BENCHMARKS.md +73 -0
  4. package/CHANGELOG.md +59 -4
  5. package/CODE-OF-CONDUCT.md +83 -83
  6. package/CONTRIBUTING.md +92 -97
  7. package/FAQ.md +132 -105
  8. package/GLOSSARY.md +34 -0
  9. package/LICENSE.md +21 -21
  10. package/PUBLISH.md +82 -77
  11. package/README.md +220 -6
  12. package/REFACTOR-LOG.md +40 -40
  13. package/ROADMAP.md +31 -42
  14. package/SECURITY-DEFAULTS.md +118 -0
  15. package/SECURITY.md +80 -79
  16. package/SUMMARY.md +31 -8
  17. package/TESTING.md +140 -140
  18. package/dist/{agent-5D3BVWNK.js → agent-C64T66XT.js} +4 -4
  19. package/dist/agent-C64T66XT.js.map +1 -0
  20. package/dist/{chunk-B7WLHC4W.js → chunk-5OD5UWB5.js} +322 -121
  21. package/dist/chunk-5OD5UWB5.js.map +1 -0
  22. package/dist/chunk-GOGI3JQD.js +1637 -0
  23. package/dist/chunk-GOGI3JQD.js.map +1 -0
  24. package/dist/{chunk-2F66BZYJ.js → chunk-VEZQT5SX.js} +80 -8
  25. package/dist/chunk-VEZQT5SX.js.map +1 -0
  26. package/dist/cli/index.js +2058 -18
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/core/index.d.ts +682 -3
  29. package/dist/core/index.js +1 -1
  30. package/dist/index.d.ts +102 -14
  31. package/dist/index.js +55 -29
  32. package/dist/index.js.map +1 -1
  33. package/dist/plugins/index.d.ts +1 -1
  34. package/dist/plugins/index.js +1 -1
  35. package/dist/types-BOMap-tI.d.ts +389 -0
  36. package/docs/PRD03.md +119 -0
  37. package/docs/PRD06.md +125 -0
  38. package/docs/SETUP.md +94 -94
  39. package/docs/TROUBLESHOOTING.md +113 -113
  40. package/docs/adr/0001-record-architecture-decisions.md +44 -0
  41. package/docs/adr/0002-plugin-architecture.md +53 -0
  42. package/docs/adr/0003-recursive-task-decomposition.md +57 -0
  43. package/docs/adr/0004-local-first-security.md +58 -0
  44. package/docs/adr/0005-data-directory-layout.md +69 -0
  45. package/examples/basic.yaml +61 -61
  46. package/package.json +103 -102
  47. package/schema/config.schema.json +119 -119
  48. package/speexor.config.yaml.example +30 -30
  49. package/dist/agent-5D3BVWNK.js.map +0 -1
  50. package/dist/chunk-2F66BZYJ.js.map +0 -1
  51. package/dist/chunk-B7WLHC4W.js.map +0 -1
  52. package/dist/chunk-SXALZEOJ.js +0 -345
  53. package/dist/chunk-SXALZEOJ.js.map +0 -1
  54. package/dist/types-0q_okI2g.d.ts +0 -205
@@ -0,0 +1,1637 @@
1
+ import { createServer } from 'http';
2
+ import Debug from 'debug';
3
+
4
+ // src/dashboard/state.ts
5
+ var DashboardState = class {
6
+ state = {
7
+ sessions: [],
8
+ worktrees: [],
9
+ runtimes: [],
10
+ projects: []
11
+ };
12
+ taskGraphs = /* @__PURE__ */ new Map();
13
+ agentEvents = /* @__PURE__ */ new Map();
14
+ approvals = [];
15
+ activityFeed = [];
16
+ costData = null;
17
+ decisionLogEntries = [];
18
+ listeners = [];
19
+ getState() {
20
+ return { ...this.state };
21
+ }
22
+ setProjects(projects) {
23
+ this.state.projects = projects;
24
+ this.notify();
25
+ }
26
+ addSession(session) {
27
+ this.state.sessions.push(session);
28
+ this.notify();
29
+ }
30
+ updateSession(sessionId, updates) {
31
+ const idx = this.state.sessions.findIndex((s) => s.id === sessionId);
32
+ if (idx !== -1) {
33
+ const entry = this.state.sessions[idx];
34
+ if (entry) {
35
+ if (updates.id !== void 0) entry.id = updates.id;
36
+ if (updates.taskId !== void 0) entry.taskId = updates.taskId;
37
+ if (updates.provider !== void 0) entry.provider = updates.provider;
38
+ if (updates.status !== void 0) entry.status = updates.status;
39
+ if (updates.startedAt !== void 0) entry.startedAt = updates.startedAt;
40
+ if (updates.runtimeSessionId !== void 0) entry.runtimeSessionId = updates.runtimeSessionId;
41
+ this.notify();
42
+ }
43
+ }
44
+ }
45
+ removeSession(sessionId) {
46
+ this.state.sessions = this.state.sessions.filter((s) => s.id !== sessionId);
47
+ this.notify();
48
+ }
49
+ addWorktree(worktree) {
50
+ this.state.worktrees.push(worktree);
51
+ this.notify();
52
+ }
53
+ removeWorktree(sessionId) {
54
+ this.state.worktrees = this.state.worktrees.filter((w) => w.id !== sessionId);
55
+ this.notify();
56
+ }
57
+ addRuntime(runtime) {
58
+ this.state.runtimes.push(runtime);
59
+ this.notify();
60
+ }
61
+ removeRuntime(sessionId) {
62
+ this.state.runtimes = this.state.runtimes.filter((r) => r.id !== sessionId);
63
+ this.notify();
64
+ }
65
+ updateTaskGraph(graph) {
66
+ this.taskGraphs.set(graph.id, graph);
67
+ this.notify();
68
+ }
69
+ getTaskGraph(id) {
70
+ return this.taskGraphs.get(id);
71
+ }
72
+ getTaskGraphs() {
73
+ return Array.from(this.taskGraphs.values());
74
+ }
75
+ addAgentEvent(event) {
76
+ const events = this.agentEvents.get(event.agentId) ?? [];
77
+ events.push(event);
78
+ if (events.length > 500) events.shift();
79
+ this.agentEvents.set(event.agentId, events);
80
+ this.notify();
81
+ }
82
+ getAgentEvents(agentId) {
83
+ if (agentId) return this.agentEvents.get(agentId) ?? [];
84
+ const all = [];
85
+ for (const events of this.agentEvents.values()) {
86
+ all.push(...events);
87
+ }
88
+ return all.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
89
+ }
90
+ getAgentEventsByTask(taskId) {
91
+ const result = [];
92
+ for (const events of this.agentEvents.values()) {
93
+ for (const e of events) {
94
+ if (e.taskId === taskId) result.push(e);
95
+ }
96
+ }
97
+ return result.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
98
+ }
99
+ addApproval(item) {
100
+ this.approvals.push(item);
101
+ this.notify();
102
+ }
103
+ resolveApproval(id, status) {
104
+ const item = this.approvals.find((a) => a.id === id);
105
+ if (item) {
106
+ item.status = status;
107
+ this.notify();
108
+ }
109
+ }
110
+ getApprovals() {
111
+ return [...this.approvals];
112
+ }
113
+ getPendingApprovals() {
114
+ return this.approvals.filter((a) => a.status === "pending");
115
+ }
116
+ updateActivityFeed(entry) {
117
+ this.activityFeed.push(entry);
118
+ if (this.activityFeed.length > 1e3) this.activityFeed.shift();
119
+ this.notify();
120
+ }
121
+ getActivityFeed(limit) {
122
+ const feed = [...this.activityFeed].reverse();
123
+ return limit ? feed.slice(0, limit) : feed;
124
+ }
125
+ updateCostData(data) {
126
+ this.costData = data;
127
+ this.notify();
128
+ }
129
+ getCostData() {
130
+ return this.costData;
131
+ }
132
+ addDecisionLogEntry(entry) {
133
+ this.decisionLogEntries.push(entry);
134
+ this.notify();
135
+ }
136
+ getDecisionLogEntries() {
137
+ return [...this.decisionLogEntries];
138
+ }
139
+ getDecisionLog() {
140
+ return [...this.decisionLogEntries].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
141
+ }
142
+ onUpdate(listener) {
143
+ this.listeners.push(listener);
144
+ return () => {
145
+ this.listeners = this.listeners.filter((l) => l !== listener);
146
+ };
147
+ }
148
+ notify() {
149
+ for (const listener of this.listeners) {
150
+ try {
151
+ listener();
152
+ } catch {
153
+ }
154
+ }
155
+ }
156
+ toJSON() {
157
+ return {
158
+ ...this.state,
159
+ taskGraphs: Array.from(this.taskGraphs.entries()).map(([, graph]) => ({
160
+ ...graph,
161
+ nodes: Array.from(graph.nodes.values())
162
+ })),
163
+ agentEvents: Array.from(this.agentEvents.entries()).map(([agentId, events]) => ({
164
+ agentId,
165
+ events: events.map((e) => ({
166
+ ...e,
167
+ timestamp: e.timestamp.toISOString()
168
+ }))
169
+ })),
170
+ approvals: this.approvals.map((a) => ({
171
+ ...a,
172
+ createdAt: a.createdAt.toISOString(),
173
+ expiresAt: a.expiresAt?.toISOString()
174
+ })),
175
+ activityFeed: this.activityFeed.map((e) => ({
176
+ ...e,
177
+ timestamp: e.timestamp.toISOString()
178
+ })),
179
+ costData: this.costData,
180
+ decisionLogEntries: this.decisionLogEntries.map((e) => ({
181
+ ...e,
182
+ createdAt: e.createdAt.toISOString()
183
+ }))
184
+ };
185
+ }
186
+ };
187
+ var debug = Debug("speexor:dashboard");
188
+ var DashboardServer = class {
189
+ server;
190
+ lifecycle;
191
+ state;
192
+ port;
193
+ routes = [];
194
+ sseClients = [];
195
+ heartbeatTimer = null;
196
+ constructor(lifecycle, port = 7777) {
197
+ this.lifecycle = lifecycle;
198
+ this.state = new DashboardState();
199
+ this.port = port;
200
+ this.state.setProjects(lifecycle.getConfig().projects);
201
+ lifecycle.eventBus.on("session:created", (data) => {
202
+ const { session } = data;
203
+ this.state.addSession(session);
204
+ this.broadcastSSE("agent-update", { session });
205
+ });
206
+ lifecycle.eventBus.on("session:completed", (data) => {
207
+ const { sessionId } = data;
208
+ this.state.removeSession(sessionId);
209
+ this.broadcastSSE("agent-update", { sessionId, status: "completed" });
210
+ });
211
+ lifecycle.eventBus.on("session:status-changed", (data) => {
212
+ const { sessionId, status } = data;
213
+ this.state.updateSession(sessionId, {
214
+ status
215
+ });
216
+ this.broadcastSSE("agent-update", { sessionId, status });
217
+ });
218
+ lifecycle.eventBus.on("task:created", (data) => {
219
+ this.broadcastSSE("task-update", data);
220
+ });
221
+ lifecycle.eventBus.on("task:status-changed", (data) => {
222
+ this.broadcastSSE("task-update", data);
223
+ });
224
+ lifecycle.eventBus.on("task:completed", (data) => {
225
+ this.broadcastSSE("task-update", data);
226
+ });
227
+ lifecycle.eventBus.on("approval:created", (data) => {
228
+ this.broadcastSSE("approval-update", data);
229
+ });
230
+ lifecycle.eventBus.on("approval:resolved", (data) => {
231
+ this.broadcastSSE("approval-update", data);
232
+ });
233
+ lifecycle.eventBus.on("cost:recorded", (data) => {
234
+ this.broadcastSSE("cost-update", data);
235
+ });
236
+ lifecycle.eventBus.on("agent:event", (data) => {
237
+ this.broadcastSSE("activity", data);
238
+ });
239
+ this.setupRoutes();
240
+ this.server = createServer((req, res) => this.handleRequest(req, res));
241
+ }
242
+ setupRoutes() {
243
+ this.routes = [
244
+ {
245
+ method: "GET",
246
+ path: "/api/status",
247
+ handler: this.handleStatus.bind(this)
248
+ },
249
+ {
250
+ method: "GET",
251
+ path: "/api/projects",
252
+ handler: this.handleProjects.bind(this)
253
+ },
254
+ {
255
+ method: "GET",
256
+ path: "/api/sessions",
257
+ handler: this.handleSessions.bind(this)
258
+ },
259
+ {
260
+ method: "GET",
261
+ path: "/api/health",
262
+ handler: this.handleHealth.bind(this)
263
+ },
264
+ {
265
+ method: "GET",
266
+ path: "/api/events",
267
+ handler: this.handleSSE.bind(this)
268
+ },
269
+ {
270
+ method: "GET",
271
+ path: "/api/task-graphs",
272
+ handler: this.handleTaskGraphs.bind(this)
273
+ },
274
+ {
275
+ method: "GET",
276
+ path: "/api/task-graphs/:id",
277
+ handler: this.handleTaskGraphById.bind(this)
278
+ },
279
+ {
280
+ method: "GET",
281
+ path: "/api/approvals",
282
+ handler: this.handleApprovals.bind(this)
283
+ },
284
+ {
285
+ method: "POST",
286
+ path: "/api/approvals/:id/approve",
287
+ handler: this.handleApproveApproval.bind(this)
288
+ },
289
+ {
290
+ method: "POST",
291
+ path: "/api/approvals/:id/reject",
292
+ handler: this.handleRejectApproval.bind(this)
293
+ },
294
+ {
295
+ method: "GET",
296
+ path: "/api/activity-feed",
297
+ handler: this.handleActivityFeed.bind(this)
298
+ },
299
+ { method: "GET", path: "/api/cost", handler: this.handleCost.bind(this) },
300
+ {
301
+ method: "GET",
302
+ path: "/api/eval/calibration",
303
+ handler: this.handleEvalCalibration.bind(this)
304
+ },
305
+ {
306
+ method: "GET",
307
+ path: "/api/task-graphs/:id/nodes/:nodeId",
308
+ handler: this.handleTaskGraphNode.bind(this)
309
+ },
310
+ {
311
+ method: "GET",
312
+ path: "/api/decision-log",
313
+ handler: this.handleDecisionLog.bind(this)
314
+ },
315
+ {
316
+ method: "POST",
317
+ path: "/api/approvals/:id/rollback",
318
+ handler: this.handleRollbackApproval.bind(this)
319
+ }
320
+ ];
321
+ }
322
+ async handleRequest(req, res) {
323
+ res.setHeader("Access-Control-Allow-Origin", "*");
324
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
325
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
326
+ if (req.method === "OPTIONS") {
327
+ res.writeHead(204);
328
+ res.end();
329
+ return;
330
+ }
331
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
332
+ const pathname = url.pathname;
333
+ for (const route of this.routes) {
334
+ if (req.method === route.method) {
335
+ const params = this.matchPath(route.path, pathname);
336
+ if (params !== null) {
337
+ try {
338
+ await route.handler(req, res, params);
339
+ } catch (error) {
340
+ this.sendJSON(res, 500, { error: String(error) });
341
+ }
342
+ return;
343
+ }
344
+ }
345
+ }
346
+ if (pathname === "/" || pathname === "/dashboard") {
347
+ this.serveDashboardHTML(res);
348
+ return;
349
+ }
350
+ this.sendJSON(res, 404, { error: "Not found" });
351
+ }
352
+ matchPath(pattern, pathname) {
353
+ const patternParts = pattern.split("/").filter(Boolean);
354
+ const pathParts = pathname.split("/").filter(Boolean);
355
+ if (patternParts.length !== pathParts.length) return null;
356
+ const params = {};
357
+ for (let i = 0; i < patternParts.length; i++) {
358
+ const pp = patternParts[i];
359
+ if (pp && pp.startsWith(":")) {
360
+ params[pp.slice(1)] = decodeURIComponent(pathParts[i] ?? "");
361
+ } else if (pp !== pathParts[i]) {
362
+ return null;
363
+ }
364
+ }
365
+ return params;
366
+ }
367
+ sendJSON(res, status, data) {
368
+ res.writeHead(status, { "Content-Type": "application/json" });
369
+ res.end(JSON.stringify(data));
370
+ }
371
+ broadcastSSE(type, payload) {
372
+ const data = JSON.stringify({ type, payload });
373
+ const message = `data: ${data}
374
+
375
+ `;
376
+ for (const client of this.sseClients) {
377
+ try {
378
+ client.write(message);
379
+ } catch {
380
+ }
381
+ }
382
+ }
383
+ handleStatus(_req, res) {
384
+ this.sendJSON(res, 200, {
385
+ status: this.lifecycle.getStatus(),
386
+ projectCount: this.lifecycle.getConfig().projects.length,
387
+ activeSessions: this.lifecycle.listSessions().length,
388
+ uptime: process.uptime()
389
+ });
390
+ }
391
+ handleProjects(_req, res) {
392
+ this.sendJSON(res, 200, {
393
+ projects: this.lifecycle.getConfig().projects
394
+ });
395
+ }
396
+ handleSessions(_req, res) {
397
+ this.sendJSON(res, 200, {
398
+ sessions: this.lifecycle.listSessions(),
399
+ worktrees: [],
400
+ runtimes: []
401
+ });
402
+ }
403
+ handleHealth(_req, res) {
404
+ this.sendJSON(res, 200, {
405
+ status: "healthy",
406
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
407
+ memory: process.memoryUsage()
408
+ });
409
+ }
410
+ handleSSE(req, res) {
411
+ res.writeHead(200, {
412
+ "Content-Type": "text/event-stream",
413
+ "Cache-Control": "no-cache",
414
+ Connection: "keep-alive",
415
+ "Access-Control-Allow-Origin": "*"
416
+ });
417
+ const costData = this.state.getCostData();
418
+ const initPayload = {
419
+ status: this.lifecycle.getStatus(),
420
+ projects: this.lifecycle.getConfig().projects,
421
+ sessions: this.lifecycle.listSessions(),
422
+ taskGraphs: this.state.getTaskGraphs().map((g) => ({
423
+ ...g,
424
+ nodes: Array.from(g.nodes.values())
425
+ })),
426
+ approvals: this.state.getPendingApprovals(),
427
+ allApprovals: this.state.getApprovals(),
428
+ activityFeed: this.state.getActivityFeed(50),
429
+ costData,
430
+ decisionLog: this.state.getDecisionLog().slice(0, 50)
431
+ };
432
+ res.write(`data: ${JSON.stringify({ type: "init", payload: initPayload })}
433
+
434
+ `);
435
+ this.sseClients.push(res);
436
+ req.on("close", () => {
437
+ this.sseClients = this.sseClients.filter((c) => c !== res);
438
+ });
439
+ }
440
+ handleTaskGraphs(_req, res) {
441
+ const graphs = this.state.getTaskGraphs().map((g) => ({
442
+ ...g,
443
+ nodes: Array.from(g.nodes.values())
444
+ }));
445
+ this.sendJSON(res, 200, { taskGraphs: graphs });
446
+ }
447
+ handleTaskGraphById(_req, res, params) {
448
+ const graph = this.state.getTaskGraph(params["id"] ?? "");
449
+ if (!graph) {
450
+ this.sendJSON(res, 404, { error: "Task graph not found" });
451
+ return;
452
+ }
453
+ this.sendJSON(res, 200, {
454
+ ...graph,
455
+ nodes: Array.from(graph.nodes.values()).sort((a, b) => a.depth - b.depth || a.createdAt.getTime() - b.createdAt.getTime())
456
+ });
457
+ }
458
+ handleApprovals(_req, res) {
459
+ this.sendJSON(res, 200, {
460
+ pending: this.state.getPendingApprovals(),
461
+ all: this.state.getApprovals()
462
+ });
463
+ }
464
+ handleApproveApproval(_req, res, params) {
465
+ const id = params["id"] ?? "";
466
+ this.state.resolveApproval(id, "approved");
467
+ this.broadcastSSE("approval-update", {
468
+ approvalId: id,
469
+ status: "approved"
470
+ });
471
+ this.sendJSON(res, 200, { status: "approved", id });
472
+ }
473
+ handleRejectApproval(_req, res, params) {
474
+ const id = params["id"] ?? "";
475
+ this.state.resolveApproval(id, "rejected");
476
+ this.broadcastSSE("approval-update", {
477
+ approvalId: id,
478
+ status: "rejected"
479
+ });
480
+ this.sendJSON(res, 200, { status: "rejected", id });
481
+ }
482
+ handleActivityFeed(_req, res) {
483
+ const feed = this.state.getActivityFeed(100);
484
+ this.sendJSON(res, 200, { entries: feed });
485
+ }
486
+ handleCost(_req, res) {
487
+ const data = this.state.getCostData();
488
+ this.sendJSON(
489
+ res,
490
+ 200,
491
+ data ?? {
492
+ totalCost: 0,
493
+ byProvider: [],
494
+ byProject: [],
495
+ recentEntries: [],
496
+ budgetExceeded: false
497
+ }
498
+ );
499
+ }
500
+ handleTaskGraphNode(_req, res, params) {
501
+ const graphId = params["id"] ?? "";
502
+ const nodeId = params["nodeId"] ?? "";
503
+ const graph = this.state.getTaskGraph(graphId);
504
+ if (!graph) {
505
+ this.sendJSON(res, 404, { error: "Task graph not found" });
506
+ return;
507
+ }
508
+ const node = graph.nodes.get(nodeId);
509
+ if (!node) {
510
+ this.sendJSON(res, 404, { error: "Task node not found" });
511
+ return;
512
+ }
513
+ this.sendJSON(res, 200, {
514
+ ...node,
515
+ createdAt: node.createdAt.toISOString(),
516
+ updatedAt: node.updatedAt.toISOString(),
517
+ result: node.result ? {
518
+ ...node.result
519
+ } : void 0
520
+ });
521
+ }
522
+ handleDecisionLog(_req, res) {
523
+ const entries = this.state.getDecisionLog();
524
+ this.sendJSON(res, 200, { entries });
525
+ }
526
+ handleRollbackApproval(_req, res, params) {
527
+ const id = params["id"] ?? "";
528
+ try {
529
+ this.state.resolveApproval(id, "pending");
530
+ this.broadcastSSE("approval-update", { approvalId: id, status: "pending" });
531
+ this.sendJSON(res, 200, { status: "rolled-back", id });
532
+ } catch (e) {
533
+ this.sendJSON(res, 400, { error: String(e) });
534
+ }
535
+ }
536
+ handleEvalCalibration(_req, res) {
537
+ const entries = this.state.getDecisionLogEntries();
538
+ const total = entries.length;
539
+ const labeled = entries.filter((e) => e.outcome && e.outcome !== "unknown");
540
+ const autoExecuted = entries.filter((e) => e.wasAutoExecuted);
541
+ const byConfidence = {};
542
+ for (const e of entries) {
543
+ const bucket = e.confidence < 0.3 ? "low" : e.confidence < 0.7 ? "medium" : "high";
544
+ byConfidence[bucket] = (byConfidence[bucket] ?? 0) + 1;
545
+ }
546
+ this.sendJSON(res, 200, {
547
+ totalDecisions: total,
548
+ labeledCount: labeled.length,
549
+ autoExecutedCount: autoExecuted.length,
550
+ byConfidence,
551
+ unlabeledCount: entries.filter((e) => !e.outcome || e.outcome === "unknown").length
552
+ });
553
+ }
554
+ serveDashboardHTML(res) {
555
+ const html = `<!DOCTYPE html>
556
+ <html lang="en">
557
+ <head>
558
+ <meta charset="UTF-8" />
559
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
560
+ <title>Speexor Dashboard v2</title>
561
+ <style>
562
+ :root {
563
+ --bg-primary: #0d1117;
564
+ --bg-secondary: #161b22;
565
+ --bg-tertiary: #1c2333;
566
+ --bg-hover: #1c2128;
567
+ --border: #30363d;
568
+ --border-light: #21262d;
569
+ --text-primary: #e6edf3;
570
+ --text-secondary: #8b949e;
571
+ --text-muted: #484f58;
572
+ --accent-blue: #58a6ff;
573
+ --accent-green: #3fb950;
574
+ --accent-red: #f85149;
575
+ --accent-yellow: #d29922;
576
+ --accent-orange: #db6d28;
577
+ --accent-purple: #bc8cff;
578
+ --accent-cyan: #39d2c0;
579
+ --accent-gray: #8b949e;
580
+ --accent-darkred: #da3633;
581
+ --status-pending: #8b949e;
582
+ --status-decomposing: #d29922;
583
+ --status-ready: #58a6ff;
584
+ --status-in-progress: #3fb950;
585
+ --status-blocked: #f85149;
586
+ --status-review: #bc8cff;
587
+ --status-done: #484f58;
588
+ --status-failed: #da3633;
589
+ --radius: 8px;
590
+ --radius-sm: 4px;
591
+ --font-mono: 'SF Mono','Fira Code','Cascadia Code',Consolas,monospace;
592
+ --transition: 150ms ease;
593
+ }
594
+ * { margin: 0; padding: 0; box-sizing: border-box; }
595
+ body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,sans-serif; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; }
596
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
597
+ ::-webkit-scrollbar-track { background: var(--bg-primary); }
598
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
599
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
600
+
601
+ .app-header { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1.5rem; background: var(--bg-secondary); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 100; }
602
+ .app-header .brand { display: flex; align-items: center; gap: 0.75rem; }
603
+ .app-header .brand h1 { font-size: 1.15rem; font-weight: 600; color: var(--accent-blue); letter-spacing: -0.02em; }
604
+ .app-header .brand h1 span { color: var(--text-secondary); font-weight: 400; }
605
+ .app-header .brand .version { font-size: 0.65rem; color: var(--text-muted); background: var(--bg-tertiary); padding: 0.125rem 0.4rem; border-radius: 3px; font-family: var(--font-mono); }
606
+ .header-meta { display: flex; align-items: center; gap: 1rem; font-size: 0.8rem; }
607
+ .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.35rem; }
608
+ .status-dot.online { background: var(--accent-green); box-shadow: 0 0 6px rgba(63,185,80,0.4); }
609
+ .status-dot.offline { background: var(--accent-red); }
610
+ .header-time { color: var(--text-secondary); font-family: var(--font-mono); font-size: 0.75rem; }
611
+
612
+ .tab-bar { display: flex; gap: 0; background: var(--bg-secondary); border-bottom: 1px solid var(--border); padding: 0 1.5rem; overflow-x: auto; }
613
+ .tab-btn { padding: 0.65rem 1.15rem; font-size: 0.82rem; font-weight: 500; color: var(--text-secondary); background: transparent; border: none; cursor: pointer; border-bottom: 2px solid transparent; transition: var(--transition); white-space: nowrap; }
614
+ .tab-btn:hover { color: var(--text-primary); background: var(--bg-hover); }
615
+ .tab-btn.active { color: var(--accent-blue); border-bottom-color: var(--accent-blue); }
616
+ .tab-btn .badge { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; font-size: 0.65rem; font-weight: 600; border-radius: 9px; background: var(--accent-red); color: #fff; margin-left: 0.4rem; vertical-align: middle; }
617
+
618
+ .panel { display: none; padding: 1.5rem; max-width: 1400px; margin: 0 auto; }
619
+ .panel.active { display: block; }
620
+
621
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
622
+ .stat-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.25rem; transition: var(--transition); }
623
+ .stat-card:hover { border-color: var(--text-muted); }
624
+ .stat-card .stat-label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-secondary); margin-bottom: 0.4rem; }
625
+ .stat-card .stat-value { font-size: 1.75rem; font-weight: 700; font-variant-numeric: tabular-nums; }
626
+ .stat-card .stat-value.blue { color: var(--accent-blue); }
627
+ .stat-card .stat-value.green { color: var(--accent-green); }
628
+ .stat-card .stat-value.yellow { color: var(--accent-yellow); }
629
+ .stat-card .stat-value.red { color: var(--accent-red); }
630
+ .stat-card .stat-value.purple { color: var(--accent-purple); }
631
+ .stat-card .stat-sub { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem; }
632
+
633
+ .section-title { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-light); display: flex; align-items: center; gap: 0.5rem; }
634
+ .section-title .count { font-size: 0.7rem; color: var(--text-secondary); background: var(--bg-tertiary); padding: 0.1rem 0.45rem; border-radius: 3px; font-weight: 400; }
635
+
636
+ table { width: 100%; border-collapse: collapse; }
637
+ th, td { text-align: left; padding: 0.7rem 0.75rem; border-bottom: 1px solid var(--border-light); font-size: 0.85rem; }
638
+ th { color: var(--text-secondary); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; }
639
+ td { color: var(--text-primary); }
640
+ tr:hover td { background: var(--bg-hover); }
641
+ tr:last-child td { border-bottom: none; }
642
+
643
+ .status-badge { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.75rem; font-weight: 500; padding: 0.15rem 0.5rem; border-radius: 999px; }
644
+ .status-badge .dot { width: 6px; height: 6px; border-radius: 50%; }
645
+ .status-pending { color: var(--status-pending); }
646
+ .status-pending .dot { background: var(--status-pending); }
647
+ .status-decomposing { color: var(--status-decomposing); }
648
+ .status-decomposing .dot { background: var(--status-decomposing); }
649
+ .status-ready { color: var(--status-ready); }
650
+ .status-ready .dot { background: var(--status-ready); }
651
+ .status-in_progress, .status-in-progress, .status-running { color: var(--status-in-progress); }
652
+ .status-in_progress .dot, .status-running .dot { background: var(--status-in-progress); }
653
+ .status-blocked { color: var(--status-blocked); }
654
+ .status-blocked .dot { background: var(--status-blocked); }
655
+ .status-review { color: var(--status-review); }
656
+ .status-review .dot { background: var(--status-review); }
657
+ .status-done, .status-completed { color: var(--status-done); }
658
+ .status-done .dot, .status-completed .dot { background: var(--status-done); }
659
+ .status-failed { color: var(--status-failed); }
660
+ .status-failed .dot { background: var(--status-failed); }
661
+
662
+ .agent-tag { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 3px; font-size: 0.72rem; font-weight: 500; background: var(--bg-tertiary); color: var(--accent-blue); font-family: var(--font-mono); }
663
+ .agent-tag.claude-code { color: var(--accent-purple); }
664
+ .agent-tag.aider { color: var(--accent-green); }
665
+ .agent-tag.codex { color: var(--accent-yellow); }
666
+
667
+ .task-tree { font-family: var(--font-mono); font-size: 0.82rem; }
668
+ .task-node { padding: 0.45rem 0.75rem; border-left: 2px solid var(--border); margin: 0.15rem 0; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; transition: var(--transition); cursor: pointer; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
669
+ .task-node:hover { background: var(--bg-hover); border-left-color: var(--accent-blue); }
670
+ .task-node .node-depth { color: var(--text-muted); font-size: 0.7rem; min-width: 1.5rem; }
671
+ .task-node .node-title { flex: 1; }
672
+ .task-node .node-meta { display: flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; color: var(--text-secondary); }
673
+ .task-node .node-skills { display: flex; gap: 0.25rem; flex-wrap: wrap; }
674
+ .task-node .node-skills .skill-chip { font-size: 0.65rem; padding: 0.05rem 0.35rem; border-radius: 3px; background: var(--bg-tertiary); color: var(--accent-cyan); }
675
+ .task-deps { font-size: 0.7rem; color: var(--text-muted); padding: 0.15rem 0 0.15rem 1.5rem; }
676
+ .task-deps::before { content: 'depends on: '; color: var(--text-muted); }
677
+
678
+ .graph-selector { margin-bottom: 1rem; display: flex; align-items: center; gap: 0.75rem; }
679
+ .graph-selector select { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border); padding: 0.4rem 0.75rem; border-radius: var(--radius-sm); font-size: 0.85rem; cursor: pointer; }
680
+ .graph-selector select:focus { outline: none; border-color: var(--accent-blue); }
681
+ .graph-status { font-size: 0.8rem; color: var(--text-secondary); }
682
+
683
+ .fleet-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
684
+ .agent-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; transition: var(--transition); }
685
+ .agent-card:hover { border-color: var(--text-muted); }
686
+ .agent-card .card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem; }
687
+ .agent-card .card-header .agent-id { font-family: var(--font-mono); font-size: 0.82rem; font-weight: 600; color: var(--accent-blue); word-break: break-all; }
688
+ .agent-card .card-detail { display: flex; justify-content: space-between; font-size: 0.8rem; padding: 0.25rem 0; }
689
+ .agent-card .card-detail .label { color: var(--text-secondary); }
690
+ .agent-card .card-detail .value { color: var(--text-primary); font-family: var(--font-mono); font-size: 0.78rem; }
691
+
692
+ .approval-actions { display: flex; gap: 0.5rem; }
693
+ .btn { padding: 0.35rem 0.85rem; border: none; border-radius: var(--radius-sm); font-size: 0.78rem; font-weight: 500; cursor: pointer; transition: var(--transition); display: inline-flex; align-items: center; gap: 0.25rem; }
694
+ .btn-approve { background: #1a3a2a; color: var(--accent-green); border: 1px solid var(--accent-green); }
695
+ .btn-approve:hover { background: #238636; color: #fff; }
696
+ .btn-reject { background: #3d1c1c; color: var(--accent-red); border: 1px solid var(--accent-red); }
697
+ .btn-reject:hover { background: #da3633; color: #fff; }
698
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
699
+
700
+ .risk-low { color: var(--accent-green); }
701
+ .risk-medium { color: var(--accent-yellow); }
702
+ .risk-high { color: var(--accent-red); }
703
+
704
+ .activity-feed { max-height: 600px; overflow-y: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-secondary); }
705
+ .feed-entry { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border-light); font-size: 0.8rem; display: flex; align-items: center; gap: 0.75rem; font-family: var(--font-mono); }
706
+ .feed-entry:last-child { border-bottom: none; }
707
+ .feed-entry .feed-time { color: var(--text-muted); font-size: 0.7rem; min-width: 7ch; flex-shrink: 0; }
708
+ .feed-entry .feed-agent { color: var(--accent-blue); min-width: 12ch; }
709
+ .feed-entry .feed-type { color: var(--accent-purple); min-width: 10ch; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; }
710
+ .feed-entry .feed-summary { color: var(--text-primary); flex: 1; }
711
+ .feed-entry .feed-skills { color: var(--accent-cyan); font-size: 0.7rem; }
712
+ .feed-entry .feed-file { color: var(--accent-yellow); font-size: 0.7rem; }
713
+
714
+ .cost-hero { text-align: center; padding: 2rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 1.5rem; }
715
+ .cost-hero .amount { font-size: 3rem; font-weight: 800; font-variant-numeric: tabular-nums; }
716
+ .cost-hero .amount.under { color: var(--accent-green); }
717
+ .cost-hero .amount.over { color: var(--accent-red); }
718
+ .cost-hero .sub { font-size: 0.85rem; color: var(--text-secondary); margin-top: 0.25rem; }
719
+ .budget-bar { max-width: 400px; margin: 1rem auto 0; height: 6px; background: var(--bg-tertiary); border-radius: 3px; overflow: hidden; }
720
+ .budget-bar .fill { height: 100%; border-radius: 3px; transition: width var(--transition); }
721
+ .budget-bar .fill.safe { background: var(--accent-green); }
722
+ .budget-bar .fill.warn { background: var(--accent-yellow); }
723
+ .budget-bar .fill.danger { background: var(--accent-red); }
724
+
725
+ .cost-breakdowns { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem; }
726
+ .cost-section { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; }
727
+ .cost-section h3 { font-size: 0.82rem; color: var(--text-secondary); margin-bottom: 0.75rem; }
728
+ .bar-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
729
+ .bar-row .bar-label { font-size: 0.78rem; min-width: 12ch; color: var(--text-primary); }
730
+ .bar-row .bar-track { flex: 1; height: 8px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; }
731
+ .bar-row .bar-fill { height: 100%; border-radius: 4px; background: var(--accent-blue); transition: width 0.5s ease; }
732
+ .bar-row .bar-fill.green { background: var(--accent-green); }
733
+ .bar-row .bar-fill.purple { background: var(--accent-purple); }
734
+ .bar-row .bar-fill.yellow { background: var(--accent-yellow); }
735
+ .bar-row .bar-fill.orange { background: var(--accent-orange); }
736
+ .bar-row .bar-value { font-size: 0.78rem; font-family: var(--font-mono); min-width: 8ch; text-align: right; color: var(--text-primary); }
737
+
738
+ .cost-entries { max-height: 300px; overflow-y: auto; }
739
+ .cost-entry { padding: 0.35rem 0; border-bottom: 1px solid var(--border-light); font-size: 0.78rem; display: flex; justify-content: space-between; font-family: var(--font-mono); }
740
+ .cost-entry:last-child { border-bottom: none; }
741
+ .cost-entry .ce-provider { color: var(--accent-blue); }
742
+ .cost-entry .ce-task { color: var(--text-muted); }
743
+ .cost-entry .ce-amount { color: var(--text-primary); }
744
+
745
+ .empty-state { text-align: center; padding: 3rem 1rem; color: var(--text-muted); }
746
+ .empty-state .icon { font-size: 2rem; margin-bottom: 0.5rem; opacity: 0.5; }
747
+ .empty-state p { font-size: 0.9rem; }
748
+
749
+ .footer { margin-top: 2rem; padding: 1rem 0; text-align: center; color: var(--text-muted); font-size: 0.75rem; border-top: 1px solid var(--border-light); }
750
+
751
+ /* FR-80: Keyboard navigation focus */
752
+ .tab-btn:focus-visible, .btn:focus-visible, select:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
753
+ .btn:focus-visible { outline: 2px solid var(--accent-blue); }
754
+ a:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
755
+
756
+ /* FR-81: Simple mode toggle */
757
+ .simple-toggle { display: flex; align-items: center; gap: 0.5rem; font-size: 0.78rem; color: var(--text-secondary); cursor: pointer; user-select: none; }
758
+ .simple-toggle input { appearance: none; width: 36px; height: 20px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 10px; position: relative; cursor: pointer; transition: var(--transition); }
759
+ .simple-toggle input:checked { background: var(--accent-blue); border-color: var(--accent-blue); }
760
+ .simple-toggle input::before { content: ''; position: absolute; width: 16px; height: 16px; border-radius: 50%; background: var(--text-primary); top: 1px; left: 1px; transition: var(--transition); }
761
+ .simple-toggle input:checked::before { left: 17px; }
762
+ .panel.simple-hidden { display: none !important; }
763
+ .simple-summary { display: none; text-align: center; padding: 2rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 1.5rem; }
764
+ .simple-summary.visible { display: block; }
765
+ .simple-summary .big-count { font-size: 3rem; font-weight: 800; color: var(--accent-blue); }
766
+ .simple-summary .big-label { font-size: 1.1rem; color: var(--text-secondary); margin-top: 0.25rem; }
767
+
768
+ /* FR-82: Status badges with Unicode icons */
769
+ .status-badge-icon { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.78rem; font-weight: 500; padding: 0.15rem 0.5rem; border-radius: 999px; }
770
+
771
+ /* FR-35: Collapsible task tree */
772
+ .task-tree { font-family: var(--font-mono); font-size: 0.82rem; }
773
+ .task-node-row { display: flex; align-items: stretch; cursor: pointer; transition: var(--transition); }
774
+ .task-node-row:hover { background: var(--bg-hover); }
775
+ .task-node-content { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.5rem; flex: 1; flex-wrap: wrap; border-left: 3px solid transparent; }
776
+ .task-node-content.selected { border-left-color: var(--accent-blue); background: var(--bg-tertiary); }
777
+ .task-node-content .node-toggle { color: var(--text-muted); font-size: 0.65rem; min-width: 1.2rem; text-align: center; user-select: none; }
778
+ .task-node-content .node-title { flex: 1; word-break: break-word; }
779
+ .task-node-content .node-meta { display: flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; color: var(--text-secondary); }
780
+ .task-node-content .node-deps { font-size: 0.7rem; color: var(--text-muted); width: 100%; padding-left: 1.5rem; }
781
+ .task-node-content .node-deps::before { content: '\u21B3 depends: '; color: var(--text-muted); }
782
+
783
+ /* FR-38: Detail side panel */
784
+ .graph-layout { display: flex; gap: 1rem; }
785
+ .graph-tree-panel { flex: 1; min-width: 0; }
786
+ .graph-detail-panel { width: 380px; flex-shrink: 0; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; display: none; position: sticky; top: 120px; max-height: calc(100vh - 140px); overflow-y: auto; }
787
+ .graph-detail-panel.visible { display: block; }
788
+ .detail-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem; }
789
+ .detail-header h3 { font-size: 0.95rem; color: var(--accent-blue); word-break: break-word; }
790
+ .detail-close { background: none; border: 1px solid var(--border); color: var(--text-secondary); cursor: pointer; font-size: 0.85rem; padding: 0.15rem 0.45rem; border-radius: 3px; }
791
+ .detail-close:hover { color: var(--text-primary); border-color: var(--text-muted); }
792
+ .detail-field { margin-bottom: 0.6rem; font-size: 0.8rem; }
793
+ .detail-field .df-label { color: var(--text-secondary); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.15rem; }
794
+ .detail-field .df-value { color: var(--text-primary); word-break: break-word; font-family: var(--font-mono); font-size: 0.78rem; }
795
+ .detail-field .df-value.multiline { white-space: pre-wrap; max-height: 120px; overflow-y: auto; }
796
+ .detail-skills { display: flex; gap: 0.25rem; flex-wrap: wrap; }
797
+ .detail-skills .skill-chip { font-size: 0.65rem; padding: 0.05rem 0.35rem; border-radius: 3px; background: var(--bg-tertiary); color: var(--accent-cyan); }
798
+ .detail-commands { max-height: 150px; overflow-y: auto; }
799
+ .detail-commands .cmd-entry { font-size: 0.72rem; padding: 0.2rem 0; border-bottom: 1px solid var(--border-light); display: flex; gap: 0.5rem; }
800
+ .detail-commands .cmd-entry:last-child { border-bottom: none; }
801
+ .detail-commands .cmd-code { color: var(--accent-yellow); font-family: var(--font-mono); }
802
+
803
+ /* Decision Log table */
804
+ .decision-log-table { max-height: 500px; overflow-y: auto; }
805
+ .decision-log-table td { font-size: 0.78rem; }
806
+
807
+ @media (max-width: 768px) {
808
+ .app-header { flex-direction: column; gap: 0.5rem; align-items: flex-start; }
809
+ .tab-bar { padding: 0 0.5rem; }
810
+ .tab-btn { padding: 0.5rem 0.75rem; font-size: 0.78rem; }
811
+ .panel { padding: 1rem; }
812
+ .stats-grid { grid-template-columns: 1fr 1fr; }
813
+ .cost-breakdowns { grid-template-columns: 1fr; }
814
+ .fleet-grid { grid-template-columns: 1fr; }
815
+ }
816
+ </style>
817
+ </head>
818
+ <body>
819
+
820
+ <header class="app-header">
821
+ <div class="brand">
822
+ <h1>Speexor <span>Dashboard</span></h1>
823
+ <span class="version">v2</span>
824
+ </div>
825
+ <div class="header-meta">
826
+ <label class="simple-toggle" title="Toggle simple mode (WCAG)">
827
+ <input type="checkbox" id="simpleToggle" onchange="toggleSimpleMode(this.checked)" />
828
+ <span>Simple Mode</span>
829
+ </label>
830
+ <span><span class="status-dot online" id="headerStatusDot"></span><span id="headerStatus">Running</span></span>
831
+ <span class="header-time" id="headerTime"></span>
832
+ </div>
833
+ </header>
834
+
835
+ <div class="simple-summary" id="simpleSummary">
836
+ <div class="big-count" id="simpleAgentCount">0</div>
837
+ <div class="big-label" id="simpleSummaryLabel">agents running</div>
838
+ <div style="margin-top:1rem;color:var(--text-muted);font-size:0.9rem;" id="simpleProjectCount"></div>
839
+ </div>
840
+
841
+ <nav class="tab-bar" id="tabBar">
842
+ <button class="tab-btn active" data-panel="mission">Mission Control</button>
843
+ <button class="tab-btn" data-panel="graph">Task Graph</button>
844
+ <button class="tab-btn" data-panel="fleet">Fleet</button>
845
+ <button class="tab-btn" data-panel="approvals">Approvals <span class="badge" id="approvalBadge">0</span></button>
846
+ <button class="tab-btn" data-panel="cost">Cost</button>
847
+ <button class="tab-btn" data-panel="extensions">Extensions</button>
848
+ <button class="tab-btn" data-panel="decisionlog">Decision Log</button>
849
+ </nav>
850
+
851
+ <!-- Panel: Mission Control -->
852
+ <div class="panel active" id="panel-mission">
853
+ <div class="stats-grid" id="statsGrid">
854
+ <div class="stat-card">
855
+ <div class="stat-label">Projects</div>
856
+ <div class="stat-value blue" id="statProjects">-</div>
857
+ <div class="stat-sub" id="statProjectsSub">configured</div>
858
+ </div>
859
+ <div class="stat-card">
860
+ <div class="stat-label">Active Agents</div>
861
+ <div class="stat-value green" id="statAgents">0</div>
862
+ <div class="stat-sub">currently running</div>
863
+ </div>
864
+ <div class="stat-card">
865
+ <div class="stat-label">Task Graphs</div>
866
+ <div class="stat-value purple" id="statGraphs">0</div>
867
+ <div class="stat-sub">active decompositions</div>
868
+ </div>
869
+ <div class="stat-card">
870
+ <div class="stat-label">Pending Approvals</div>
871
+ <div class="stat-value yellow" id="statApprovals">0</div>
872
+ <div class="stat-sub">awaiting decision</div>
873
+ </div>
874
+ <div class="stat-card">
875
+ <div class="stat-label">Uptime</div>
876
+ <div class="stat-value" id="statUptime">0s</div>
877
+ <div class="stat-sub" id="statUptimeSub">since start</div>
878
+ </div>
879
+ <div class="stat-card">
880
+ <div class="stat-label">System</div>
881
+ <div class="stat-value" id="statMemory">-</div>
882
+ <div class="stat-sub">memory used</div>
883
+ </div>
884
+ </div>
885
+
886
+ <div class="section-title">Recent Activity</div>
887
+ <div class="activity-feed" id="missionFeed">
888
+ <div class="empty-state"><p>Waiting for activity...</p></div>
889
+ </div>
890
+ </div>
891
+
892
+ <!-- Panel: Task Graph -->
893
+ <div class="panel" id="panel-graph">
894
+ <div class="graph-selector">
895
+ <label style="color:var(--text-secondary);font-size:0.82rem;">Task Graph:</label>
896
+ <select id="graphSelector" onchange="selectGraph(this.value)">
897
+ <option value="">-- No graphs --</option>
898
+ </select>
899
+ <span class="graph-status" id="graphStatus"></span>
900
+ </div>
901
+ <div class="section-title">Task Tree <span class="count" id="graphNodeCount">0 nodes</span></div>
902
+ <div class="graph-layout">
903
+ <div class="graph-tree-panel">
904
+ <div id="graphContainer">
905
+ <div class="empty-state"><p>No task graph selected</p></div>
906
+ </div>
907
+ </div>
908
+ <div class="graph-detail-panel" id="graphDetailPanel">
909
+ <div class="detail-header">
910
+ <h3 id="detailTitle">Task Details</h3>
911
+ <button class="detail-close" onclick="closeDetail()">&#10005;</button>
912
+ </div>
913
+ <div id="detailBody">
914
+ <div class="empty-state"><p>Select a task node to see details</p></div>
915
+ </div>
916
+ </div>
917
+ </div>
918
+ </div>
919
+
920
+ <!-- Panel: Fleet -->
921
+ <div class="panel" id="panel-fleet">
922
+ <div class="section-title">Agent Fleet <span class="count" id="fleetCount">0</span></div>
923
+ <div class="fleet-grid" id="fleetGrid">
924
+ <div class="empty-state"><p>No active agents</p></div>
925
+ </div>
926
+ </div>
927
+
928
+ <!-- Panel: Approvals -->
929
+ <div class="panel" id="panel-approvals">
930
+ <div class="section-title">Approval Inbox <span class="count" id="approvalCount">0</span></div>
931
+ <table>
932
+ <thead>
933
+ <tr><th>Title</th><th>Axis</th><th>Risk</th><th>Proposed By</th><th>Created</th><th>Actions</th></tr>
934
+ </thead>
935
+ <tbody id="approvalsBody">
936
+ <tr><td colspan="6" class="empty-state"><p>No pending approvals</p></td></tr>
937
+ </tbody>
938
+ </table>
939
+
940
+ <div class="section-title" style="margin-top:2rem;">All Approvals</div>
941
+ <table>
942
+ <thead>
943
+ <tr><th>Title</th><th>Status</th><th>Axis</th><th>Risk</th><th>Created</th><th>Actions</th></tr>
944
+ </thead>
945
+ <tbody id="allApprovalsBody">
946
+ <tr><td colspan="6" class="empty-state"><p>No approvals yet</p></td></tr>
947
+ </tbody>
948
+ </table>
949
+ </div>
950
+
951
+ <!-- Panel: Cost -->
952
+ <div class="panel" id="panel-cost">
953
+ <div class="cost-hero" id="costHero">
954
+ <div class="amount under" id="costTotal">$0.00</div>
955
+ <div class="sub">total estimated cost</div>
956
+ <div class="budget-bar" id="budgetBarContainer" style="display:none;">
957
+ <div class="fill safe" id="budgetFill" style="width:0%"></div>
958
+ </div>
959
+ <div class="sub" id="budgetLabel"></div>
960
+ </div>
961
+
962
+ <div class="cost-breakdowns">
963
+ <div class="cost-section">
964
+ <h3>By Provider</h3>
965
+ <div id="costByProvider"><div class="empty-state"><p>No cost data</p></div></div>
966
+ </div>
967
+ <div class="cost-section">
968
+ <h3>By Project</h3>
969
+ <div id="costByProject"><div class="empty-state"><p>No cost data</p></div></div>
970
+ </div>
971
+ </div>
972
+
973
+ <div class="section-title">Cost by Task Node <span class="count" id="costNodeCount">0 nodes</span></div>
974
+ <div id="costByTaskNode">
975
+ <div class="empty-state"><p>No per-node cost data</p></div>
976
+ </div>
977
+
978
+ <div class="section-title">Recent Cost Entries</div>
979
+ <div class="cost-entries" id="costEntries">
980
+ <div class="empty-state"><p>No entries yet</p></div>
981
+ </div>
982
+ </div>
983
+
984
+ <!-- Panel: Extensions -->
985
+ <div class="panel" id="panel-extensions">
986
+ <div class="section-title">Extensions</div>
987
+ <div class="empty-state">
988
+ <div class="icon">&#9881;</div>
989
+ <p>Extension marketplace coming soon</p>
990
+ </div>
991
+ </div>
992
+
993
+ <!-- Panel: Decision Log -->
994
+ <div class="panel" id="panel-decisionlog">
995
+ <div class="section-title">Decision Log <span class="count" id="decisionLogCount">0</span></div>
996
+ <div class="decision-log-table">
997
+ <table>
998
+ <thead>
999
+ <tr><th>Time</th><th>Agent</th><th>Action</th><th>Confidence</th><th>Auto-Exec</th><th>Outcome</th></tr>
1000
+ </thead>
1001
+ <tbody id="decisionLogBody">
1002
+ <tr><td colspan="6" class="empty-state"><p>No decision log entries yet</p></td></tr>
1003
+ </tbody>
1004
+ </table>
1005
+ </div>
1006
+ </div>
1007
+
1008
+ <footer class="footer">
1009
+ Speexor Agent Orchestrator v2 &mdash; part of SpeexJS &mdash; <span id="footerTime"></span>
1010
+ </footer>
1011
+
1012
+ <script>
1013
+ const sseUrl = '/api/events'
1014
+
1015
+ function $(id) { return document.getElementById(id) }
1016
+
1017
+ function fmtTime(d) {
1018
+ if (typeof d === 'string') d = new Date(d)
1019
+ return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })
1020
+ }
1021
+
1022
+ function fmtCurrency(n) { return '$' + Number(n).toFixed(4) }
1023
+
1024
+ function fmtDuration(seconds) {
1025
+ if (seconds < 60) return Math.floor(seconds) + 's'
1026
+ if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + Math.floor(seconds % 60) + 's'
1027
+ const h = Math.floor(seconds / 3600)
1028
+ const m = Math.floor((seconds % 3600) / 60)
1029
+ return h + 'h ' + m + 'm'
1030
+ }
1031
+
1032
+ function statBadgeHTML(status) {
1033
+ const cls = status.replace(/_/g, '-')
1034
+ return '<span class="status-badge status-' + cls + '"><span class="dot"></span>' + status + '</span>'
1035
+ }
1036
+
1037
+ function statusIconHTML(status) {
1038
+ var icons = { pending: '\u26AA', in_progress: '\u25B6', running: '\u25B6', done: '\u2B1B', failed: '\u2715', blocked: '\u2298', decomposing: '\u2699', ready: '\u25B6', review: '\u1F50D', completed: '\u2B1B', active: '\u25B6', initializing: '\u26AA', cancelled: '\u2715' }
1039
+ var icon = icons[status] || '\u26AA'
1040
+ var label = status.replace(/_/g, ' ')
1041
+ return '<span class="status-badge-icon status-' + status.replace(/_/g, '-') + '">' + icon + ' ' + label + '</span>'
1042
+ }
1043
+
1044
+ function agentTagHTML(provider) {
1045
+ const cls = (provider || '').toLowerCase().replace(/[^a-z0-9]/g, '-')
1046
+ return '<span class="agent-tag ' + cls + '">' + (provider || 'unknown') + '</span>'
1047
+ }
1048
+
1049
+ function riskHTML(level) {
1050
+ if (!level) return '<span class="risk-low">-</span>'
1051
+ return '<span class="risk-' + level + '">' + level + '</span>'
1052
+ }
1053
+
1054
+ // Tab switching
1055
+ document.querySelectorAll('.tab-btn').forEach(function(btn) {
1056
+ btn.addEventListener('click', function() {
1057
+ document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active') })
1058
+ document.querySelectorAll('.panel').forEach(function(p) { p.classList.remove('active') })
1059
+ btn.classList.add('active')
1060
+ var panel = document.getElementById('panel-' + btn.dataset.panel)
1061
+ if (panel) panel.classList.add('active')
1062
+ })
1063
+ })
1064
+
1065
+ // SSE
1066
+ function connectSSE() {
1067
+ var source = new EventSource(sseUrl)
1068
+ source.onmessage = function(e) {
1069
+ try {
1070
+ var msg = JSON.parse(e.data)
1071
+ handleSSEMessage(msg)
1072
+ } catch (err) {
1073
+ console.error('SSE parse error:', err)
1074
+ }
1075
+ }
1076
+ source.onerror = function() {
1077
+ source.close()
1078
+ setTimeout(connectSSE, 2000)
1079
+ }
1080
+ }
1081
+
1082
+ function handleSSEMessage(msg) {
1083
+ switch (msg.type) {
1084
+ case 'init':
1085
+ renderAll(msg.payload)
1086
+ break
1087
+ case 'task-update':
1088
+ refreshTaskGraphs()
1089
+ refreshStats()
1090
+ break
1091
+ case 'agent-update':
1092
+ refreshFleet()
1093
+ refreshStats()
1094
+ break
1095
+ case 'approval-update':
1096
+ refreshApprovals()
1097
+ refreshStats()
1098
+ break
1099
+ case 'cost-update':
1100
+ refreshCost()
1101
+ break
1102
+ case 'activity':
1103
+ appendActivity(msg.payload)
1104
+ break
1105
+ case 'decisionlog-update':
1106
+ refreshDecisionLog()
1107
+ break
1108
+ }
1109
+ }
1110
+
1111
+ // Render all from initial state
1112
+ function renderAll(payload) {
1113
+ renderStats(payload)
1114
+ renderTaskGraphs(payload.taskGraphs || [])
1115
+ renderFleet(payload.sessions || [])
1116
+ renderApprovals(payload.approvals || [], payload.allApprovals || [])
1117
+ renderCost(payload.costData)
1118
+ renderActivityFeed(payload.activityFeed || [])
1119
+ renderDecisionLog(payload.decisionLog || [])
1120
+ }
1121
+
1122
+ function renderStats(payload) {
1123
+ var status = payload.status || 'active'
1124
+ $('headerStatus').textContent = status.charAt(0).toUpperCase() + status.slice(1)
1125
+ $('headerStatusDot').className = 'status-dot ' + (status === 'active' ? 'online' : 'offline')
1126
+
1127
+ var pCount = payload.projects ? payload.projects.length : 0
1128
+ var aCount = payload.sessions ? payload.sessions.length : 0
1129
+ var gCount = payload.taskGraphs ? payload.taskGraphs.length : 0
1130
+ var appCount = payload.approvals ? payload.approvals.length : 0
1131
+ $('statProjects').textContent = pCount
1132
+ $('statAgents').textContent = aCount
1133
+ $('statGraphs').textContent = gCount
1134
+ $('statApprovals').textContent = appCount
1135
+ $('statUptime').textContent = fmtDuration(payload.uptime || process.uptime ? Math.floor(process.uptime) : 0)
1136
+ }
1137
+
1138
+ function refreshStats() {
1139
+ fetch('/api/status').then(function(r) { return r.json() }).then(function(data) {
1140
+ $('statProjects').textContent = data.projectCount
1141
+ $('statAgents').textContent = data.activeSessions
1142
+ $('statUptime').textContent = fmtDuration(data.uptime)
1143
+ }).catch(function() {})
1144
+ }
1145
+
1146
+ // Task Graphs
1147
+ var currentGraphs = []
1148
+ var currentGraphId = null
1149
+ var expandedNodes = {}
1150
+ var selectedNodeId = null
1151
+
1152
+ function renderTaskGraphs(graphs) {
1153
+ currentGraphs = graphs || []
1154
+ var sel = $('graphSelector')
1155
+ sel.innerHTML = ''
1156
+ if (graphs.length === 0) {
1157
+ sel.innerHTML = '<option value="">-- No graphs --</option>'
1158
+ $('graphContainer').innerHTML = '<div class="empty-state"><p>No task graphs yet</p></div>'
1159
+ $('graphNodeCount').textContent = '0 nodes'
1160
+ $('graphStatus').textContent = ''
1161
+ return
1162
+ }
1163
+ graphs.forEach(function(g, i) {
1164
+ var opt = document.createElement('option')
1165
+ opt.value = g.id
1166
+ opt.textContent = g.projectName + ' (' + g.status + ')'
1167
+ sel.appendChild(opt)
1168
+ })
1169
+ sel.value = graphs[0].id
1170
+ currentGraphId = graphs[0].id
1171
+ displayGraph(graphs[0])
1172
+ }
1173
+
1174
+ function refreshTaskGraphs() {
1175
+ fetch('/api/task-graphs').then(function(r) { return r.json() }).then(function(data) {
1176
+ renderTaskGraphs(data.taskGraphs || [])
1177
+ }).catch(function() {})
1178
+ }
1179
+
1180
+ function selectGraph(id) {
1181
+ var graph = currentGraphs.find(function(g) { return g.id === id })
1182
+ if (graph) {
1183
+ currentGraphId = graph.id
1184
+ displayGraph(graph)
1185
+ }
1186
+ }
1187
+
1188
+ function toggleNode(nodeId) {
1189
+ expandedNodes[nodeId] = !expandedNodes[nodeId]
1190
+ var graph = currentGraphs.find(function(g) { return g.id === currentGraphId })
1191
+ if (graph) displayGraph(graph)
1192
+ }
1193
+
1194
+ function selectNode(nodeId, graphId) {
1195
+ selectedNodeId = nodeId
1196
+ var graph = currentGraphs.find(function(g) { return g.id === (graphId || currentGraphId) })
1197
+ if (!graph) return
1198
+ displayGraph(graph)
1199
+ fetch('/api/task-graphs/' + (graphId || currentGraphId) + '/nodes/' + nodeId).then(function(r) { return r.json() }).then(function(node) {
1200
+ var body = $('detailBody')
1201
+ var fields = '<div class="detail-field"><div class="df-label">ID</div><div class="df-value">' + node.id + '</div></div>'
1202
+ fields += '<div class="detail-field"><div class="df-label">Status</div><div class="df-value">' + statusIconHTML(node.status) + '</div></div>'
1203
+ fields += '<div class="detail-field"><div class="df-label">Description</div><div class="df-value multiline">' + (node.description || '-') + '</div></div>'
1204
+ fields += '<div class="detail-field"><div class="df-label">Agent</div><div class="df-value">' + (node.assignedAgentId || 'Unassigned') + '</div></div>'
1205
+ fields += '<div class="detail-field"><div class="df-label">Created</div><div class="df-value">' + fmtTime(node.createdAt) + '</div></div>'
1206
+ fields += '<div class="detail-field"><div class="df-label">Updated</div><div class="df-value">' + fmtTime(node.updatedAt) + '</div></div>'
1207
+ fields += '<div class="detail-field"><div class="df-label">Depth</div><div class="df-value">' + node.depth + '</div></div>'
1208
+ fields += '<div class="detail-field"><div class="df-label">Dependencies</div><div class="df-value">' + ((node.dependsOn && node.dependsOn.length) ? node.dependsOn.join(', ') : 'None') + '</div></div>'
1209
+ if (node.skillsUsed && node.skillsUsed.length) {
1210
+ fields += '<div class="detail-field"><div class="df-label">Skills Used</div><div class="df-value"><div class="detail-skills">' + node.skillsUsed.map(function(s) { return '<span class="skill-chip">' + s + '</span>' }).join('') + '</div></div></div>'
1211
+ }
1212
+ if (node.commandsExecuted && node.commandsExecuted.length) {
1213
+ fields += '<div class="detail-field"><div class="df-label">Commands Executed</div><div class="df-value"><div class="detail-commands">' + node.commandsExecuted.map(function(c) { return '<div class="cmd-entry"><span class="cmd-code">$ ' + c.command + '</span><span style="color:var(--text-muted)">exit:' + c.exitCode + '</span></div>' }).join('') + '</div></div></div>'
1214
+ }
1215
+ if (node.result && node.result.summary) {
1216
+ fields += '<div class="detail-field"><div class="df-label">Result</div><div class="df-value multiline">' + node.result.summary + '</div></div>'
1217
+ }
1218
+ if (node.result && node.result.prUrl) {
1219
+ fields += '<div class="detail-field"><div class="df-label">PR</div><div class="df-value"><a href="' + node.result.prUrl + '" target="_blank" style="color:var(--accent-blue)">' + node.result.prUrl + '</a></div></div>'
1220
+ }
1221
+ $('detailTitle').textContent = node.title
1222
+ $('detailBody').innerHTML = fields
1223
+ $('graphDetailPanel').classList.add('visible')
1224
+ }).catch(function() {
1225
+ $('detailBody').innerHTML = '<div class="empty-state"><p>Failed to load details</p></div>'
1226
+ })
1227
+ }
1228
+
1229
+ function closeDetail() {
1230
+ selectedNodeId = null
1231
+ $('graphDetailPanel').classList.remove('visible')
1232
+ var graph = currentGraphs.find(function(g) { return g.id === currentGraphId })
1233
+ if (graph) displayGraph(graph)
1234
+ }
1235
+
1236
+ function displayGraph(graph) {
1237
+ if (!graph) return
1238
+ $('graphNodeCount').textContent = graph.nodes.length + ' nodes'
1239
+ $('graphStatus').textContent = 'Status: ' + graph.status
1240
+ var nodes = graph.nodes.slice().sort(function(a, b) { return a.depth - b.depth || (a.createdAt < b.createdAt ? -1 : 1) })
1241
+ var parentMap = {}
1242
+ nodes.forEach(function(n) {
1243
+ if (n.parentId) {
1244
+ if (!parentMap[n.parentId]) parentMap[n.parentId] = []
1245
+ parentMap[n.parentId].push(n.id)
1246
+ }
1247
+ })
1248
+ var rootNodes = nodes.filter(function(n) { return !n.parentId })
1249
+ var html = '<div class="task-tree">'
1250
+ function renderSubtree(nodeIds, depth) {
1251
+ nodeIds.forEach(function(id) {
1252
+ var n = nodes.find(function(x) { return x.id === id })
1253
+ if (!n) return
1254
+ var hasChildren = parentMap[n.id] && parentMap[n.id].length
1255
+ var isExpanded = expandedNodes[n.id] !== false
1256
+ var isSelected = selectedNodeId === n.id
1257
+ var indent = n.depth * 24
1258
+ var deps = (n.dependsOn && n.dependsOn.length) ? '<div class="node-deps">' + n.dependsOn.join(', \u2192 ') + '</div>' : ''
1259
+ var skills = (n.skillsUsed && n.skillsUsed.length) ? n.skillsUsed.map(function(s) { return '<span class="skill-chip">' + s + '</span>' }).join('') : ''
1260
+ var agent = n.assignedAgentId ? '<span class="agent-tag">' + n.assignedAgentId + '</span>' : ''
1261
+ var toggle = hasChildren ? '<span class="node-toggle" onclick="event.stopPropagation();toggleNode('' + n.id + '')">' + (isExpanded ? '\u25BC' : '\u25B6') + '</span>' : '<span class="node-toggle" style="visibility:hidden">\u25CB</span>'
1262
+ html += '<div class="task-node-row" style="padding-left:' + indent + 'px">'
1263
+ html += '<div class="task-node-content' + (isSelected ? ' selected' : '') + '" onclick="selectNode('' + n.id + '','' + graph.id + '')" role="treeitem" tabindex="0" onkeydown="if(event.key==='Enter')selectNode('' + n.id + '','' + graph.id + '')">'
1264
+ html += toggle
1265
+ html += statusIconHTML(n.status)
1266
+ html += '<span class="node-title">' + n.title + '</span>'
1267
+ html += '<span class="node-meta">' + agent + (skills ? '<span class="node-skills">' + skills + '</span>' : '') + '</span>'
1268
+ if (deps) html += deps
1269
+ html += '</div></div>'
1270
+ if (hasChildren && isExpanded && parentMap[n.id]) {
1271
+ renderSubtree(parentMap[n.id], depth + 1)
1272
+ }
1273
+ })
1274
+ }
1275
+ renderSubtree(rootNodes.map(function(n) { return n.id }), 0)
1276
+ html += '</div>'
1277
+ $('graphContainer').innerHTML = html
1278
+ }
1279
+
1280
+ // Fleet
1281
+ function renderFleet(sessions) {
1282
+ var grid = $('fleetGrid')
1283
+ $('fleetCount').textContent = (sessions || []).length
1284
+ if (!sessions || sessions.length === 0) {
1285
+ grid.innerHTML = '<div class="empty-state"><p>No active agents</p></div>'
1286
+ return
1287
+ }
1288
+ var html = ''
1289
+ sessions.forEach(function(s) {
1290
+ var uptime = s.startedAt ? fmtDuration((Date.now() - new Date(s.startedAt).getTime()) / 1000) : '-'
1291
+ html += '<div class="agent-card">'
1292
+ html += '<div class="card-header">'
1293
+ html += '<span class="agent-id">' + (s.id || '-') + '</span>'
1294
+ html += statBadgeHTML(s.status || 'pending')
1295
+ html += '</div>'
1296
+ html += '<div class="card-detail"><span class="label">Task</span><span class="value">' + (s.taskId || '-') + '</span></div>'
1297
+ html += '<div class="card-detail"><span class="label">Provider</span><span class="value">' + agentTagHTML(s.provider) + '</span></div>'
1298
+ html += '<div class="card-detail"><span class="label">Status</span><span class="value">' + statBadgeHTML(s.status || 'unknown') + '</span></div>'
1299
+ html += '<div class="card-detail"><span class="label">Uptime</span><span class="value">' + uptime + '</span></div>'
1300
+ html += '</div>'
1301
+ })
1302
+ grid.innerHTML = html
1303
+ }
1304
+
1305
+ function refreshFleet() {
1306
+ fetch('/api/sessions').then(function(r) { return r.json() }).then(function(data) {
1307
+ renderFleet(data.sessions || [])
1308
+ $('statAgents').textContent = (data.sessions || []).length
1309
+ }).catch(function() {})
1310
+ }
1311
+
1312
+ // Approvals
1313
+ function renderApprovals(pending, all) {
1314
+ var badge = $('approvalBadge')
1315
+ pending = pending || []
1316
+ all = all || []
1317
+ badge.textContent = pending.length
1318
+ badge.style.display = pending.length ? 'inline-flex' : 'none'
1319
+ $('approvalCount').textContent = pending.length
1320
+
1321
+ var body = $('approvalsBody')
1322
+ if (pending.length === 0) {
1323
+ body.innerHTML = '<tr><td colspan="6" class="empty-state"><p>No pending approvals</p></td></tr>'
1324
+ } else {
1325
+ body.innerHTML = pending.map(function(a) {
1326
+ return '<tr>' +
1327
+ '<td><strong>' + a.title + '</strong><br><span style="font-size:0.75rem;color:var(--text-muted)">' + (a.description || '') + '</span></td>' +
1328
+ '<td>' + (a.axis || '-') + '</td>' +
1329
+ '<td>' + riskHTML(a.riskLevel) + '</td>' +
1330
+ '<td>' + (a.proposedBy || '-') + '</td>' +
1331
+ '<td>' + fmtTime(a.createdAt) + '</td>' +
1332
+ '<td><div class="approval-actions">' +
1333
+ '<button class="btn btn-approve" onclick="approveAction('' + a.id + '')">&#10003; Approve</button>' +
1334
+ '<button class="btn btn-reject" onclick="rejectAction('' + a.id + '')">&#10007; Reject</button>' +
1335
+ '</div></td></tr>'
1336
+ }).join('')
1337
+ }
1338
+
1339
+ var allBody = $('allApprovalsBody')
1340
+ if (all.length === 0) {
1341
+ allBody.innerHTML = '<tr><td colspan="6" class="empty-state"><p>No approvals yet</p></td></tr>'
1342
+ } else {
1343
+ allBody.innerHTML = all.map(function(a) {
1344
+ var rollbackBtn = a.status === 'approved' ? '<button class="btn" style="background:#3d1c1c;color:var(--accent-red);border:1px solid var(--accent-red);" onclick="rollbackAction('' + a.id + '')">\u21A9 Rollback</button>' : ''
1345
+ return '<tr>' +
1346
+ '<td><strong>' + a.title + '</strong></td>' +
1347
+ '<td>' + statusIconHTML(a.status) + '</td>' +
1348
+ '<td>' + (a.axis || '-') + '</td>' +
1349
+ '<td>' + riskHTML(a.riskLevel) + '</td>' +
1350
+ '<td>' + fmtTime(a.createdAt) + '</td>' +
1351
+ '<td>' + rollbackBtn + '</td></tr>'
1352
+ }).join('')
1353
+ }
1354
+ }
1355
+
1356
+ function refreshApprovals() {
1357
+ fetch('/api/approvals').then(function(r) { return r.json() }).then(function(data) {
1358
+ renderApprovals(data.pending || [], data.all || [])
1359
+ $('statApprovals').textContent = (data.pending || []).length
1360
+ }).catch(function() {})
1361
+ }
1362
+
1363
+ function approveAction(id) {
1364
+ fetch('/api/approvals/' + id + '/approve', { method: 'POST' }).then(function(r) { return r.json() }).then(function() {
1365
+ refreshApprovals()
1366
+ }).catch(function(e) { console.error(e) })
1367
+ }
1368
+
1369
+ function rejectAction(id) {
1370
+ fetch('/api/approvals/' + id + '/reject', { method: 'POST' }).then(function(r) { return r.json() }).then(function() {
1371
+ refreshApprovals()
1372
+ }).catch(function(e) { console.error(e) })
1373
+ }
1374
+
1375
+ function rollbackAction(id) {
1376
+ if (!confirm('Rollback this approval? The action will be reverted to pending state.')) return
1377
+ fetch('/api/approvals/' + id + '/rollback', { method: 'POST' }).then(function(r) { return r.json() }).then(function() {
1378
+ refreshApprovals()
1379
+ }).catch(function(e) { console.error(e) })
1380
+ }
1381
+
1382
+ // Decision Log
1383
+ function renderDecisionLog(entries) {
1384
+ var body = $('decisionLogBody')
1385
+ $('decisionLogCount').textContent = (entries || []).length
1386
+ if (!entries || entries.length === 0) {
1387
+ body.innerHTML = '<tr><td colspan="6" class="empty-state"><p>No decision log entries yet</p></td></tr>'
1388
+ return
1389
+ }
1390
+ body.innerHTML = entries.slice(0, 50).map(function(e) {
1391
+ return '<tr>' +
1392
+ '<td>' + fmtTime(e.createdAt) + '</td>' +
1393
+ '<td>' + (e.agentId || '-') + '</td>' +
1394
+ '<td>' + (e.action || '-') + '</td>' +
1395
+ '<td>' + (Math.round(e.confidence * 100) || '-') + '%</td>' +
1396
+ '<td>' + (e.wasAutoExecuted ? '\u2705' : '\u274C') + '</td>' +
1397
+ '<td>' + (e.outcome || e.outcome === 'unknown' ? (e.outcome || 'unknown') : '-') + '</td></tr>'
1398
+ }).join('')
1399
+ }
1400
+
1401
+ function refreshDecisionLog() {
1402
+ fetch('/api/decision-log').then(function(r) { return r.json() }).then(function(data) {
1403
+ renderDecisionLog(data.entries || [])
1404
+ }).catch(function() {})
1405
+ }
1406
+
1407
+ // Simple Mode
1408
+ function toggleSimpleMode(enabled) {
1409
+ var hiddenPanels = ['graph', 'extensions']
1410
+ if (enabled) {
1411
+ hiddenPanels.forEach(function(p) {
1412
+ var panel = document.getElementById('panel-' + p)
1413
+ if (panel) panel.classList.add('simple-hidden')
1414
+ var btn = document.querySelector('.tab-btn[data-panel="' + p + '"]')
1415
+ if (btn) btn.style.display = 'none'
1416
+ })
1417
+ document.getElementById('simpleSummary').classList.add('visible')
1418
+ var agentCount = parseInt($('statAgents').textContent) || 0
1419
+ var projectCount = parseInt($('statProjects').textContent) || 0
1420
+ $('simpleAgentCount').textContent = agentCount
1421
+ $('simpleSummaryLabel').textContent = agentCount === 1 ? 'agent running' : 'agents running'
1422
+ $('simpleProjectCount').textContent = 'across ' + projectCount + ' project' + (projectCount !== 1 ? 's' : '')
1423
+ } else {
1424
+ hiddenPanels.forEach(function(p) {
1425
+ var panel = document.getElementById('panel-' + p)
1426
+ if (panel) panel.classList.remove('simple-hidden')
1427
+ var btn = document.querySelector('.tab-btn[data-panel="' + p + '"]')
1428
+ if (btn) btn.style.display = ''
1429
+ })
1430
+ document.getElementById('simpleSummary').classList.remove('visible')
1431
+ }
1432
+ }
1433
+
1434
+ // Cost
1435
+ function renderCost(data) {
1436
+ if (!data) {
1437
+ $('costTotal').textContent = '$0.00'
1438
+ $('costTotal').className = 'amount under'
1439
+ return
1440
+ }
1441
+
1442
+ var total = data.totalCost || 0
1443
+ var exceeded = data.budgetExceeded || false
1444
+ $('costTotal').textContent = '$' + total.toFixed(4)
1445
+ $('costTotal').className = 'amount ' + (exceeded ? 'over' : 'under')
1446
+
1447
+ var barContainer = $('budgetBarContainer')
1448
+ barContainer.style.display = 'none'
1449
+
1450
+ var providers = data.byProvider || []
1451
+ var projects = data.byProject || []
1452
+ var entries = data.recentEntries || []
1453
+
1454
+ var maxProvider = providers.length ? Math.max.apply(null, providers.map(function(p) { return p.cost })) : 0
1455
+ var maxProject = projects.length ? Math.max.apply(null, projects.map(function(p) { return p.cost })) : 0
1456
+
1457
+ var colors = ['', 'green', 'purple', 'yellow', 'orange']
1458
+
1459
+ if (providers.length) {
1460
+ $('costByProvider').innerHTML = providers.map(function(p, i) {
1461
+ var pct = maxProvider > 0 ? (p.cost / maxProvider * 100) : 0
1462
+ return '<div class="bar-row"><span class="bar-label">' + p.name + '</span><div class="bar-track"><div class="bar-fill ' + (colors[i] || '') + '" style="width:' + pct + '%"></div></div><span class="bar-value">$' + p.cost.toFixed(4) + '</span></div>'
1463
+ }).join('')
1464
+ } else {
1465
+ $('costByProvider').innerHTML = '<div class="empty-state"><p>No cost data</p></div>'
1466
+ }
1467
+
1468
+ if (projects.length) {
1469
+ $('costByProject').innerHTML = projects.map(function(p, i) {
1470
+ var pct = maxProject > 0 ? (p.cost / maxProject * 100) : 0
1471
+ return '<div class="bar-row"><span class="bar-label">' + p.name + '</span><div class="bar-track"><div class="bar-fill ' + (colors[i] || '') + '" style="width:' + pct + '%"></div></div><span class="bar-value">$' + p.cost.toFixed(4) + '</span></div>'
1472
+ }).join('')
1473
+ } else {
1474
+ $('costByProject').innerHTML = '<div class="empty-state"><p>No cost data</p></div>'
1475
+ }
1476
+
1477
+ if (entries.length) {
1478
+ $('costEntries').innerHTML = entries.map(function(e) {
1479
+ return '<div class="cost-entry"><span class="ce-provider">' + (e.provider || '-') + '</span><span class="ce-task">' + (e.taskId || '-') + '</span><span class="ce-amount">$' + (e.estimatedCostUSD || 0).toFixed(4) + '</span></div>'
1480
+ }).join('')
1481
+ } else {
1482
+ $('costEntries').innerHTML = '<div class="empty-state"><p>No entries yet</p></div>'
1483
+ }
1484
+
1485
+ var taskNodes = data.byTaskNode || []
1486
+ $('costNodeCount').textContent = taskNodes.length + ' nodes'
1487
+ if (taskNodes.length) {
1488
+ var maxNodeCost = Math.max.apply(null, taskNodes.map(function(n) { return n.cost }))
1489
+ var taskColors = ['', 'green', 'purple', 'yellow', 'orange', 'blue']
1490
+ $('costByTaskNode').innerHTML = taskNodes.slice(0, 15).map(function(n, i) {
1491
+ var pct = maxNodeCost > 0 ? (n.cost / maxNodeCost * 100) : 0
1492
+ return '<div class="bar-row"><span class="bar-label" title="' + n.taskId + '">' + n.title.substring(0, 30) + (n.title.length > 30 ? '...' : '') + '</span><div class="bar-track"><div class="bar-fill ' + (taskColors[i % taskColors.length] || '') + '" style="width:' + pct + '%"></div></div><span class="bar-value">$' + n.cost.toFixed(4) + '</span></div>'
1493
+ }).join('')
1494
+ } else {
1495
+ $('costByTaskNode').innerHTML = '<div class="empty-state"><p>No per-node cost data</p></div>'
1496
+ }
1497
+ }
1498
+
1499
+ function refreshCost() {
1500
+ fetch('/api/cost').then(function(r) { return r.json() }).then(function(data) {
1501
+ renderCost(data)
1502
+ }).catch(function() {})
1503
+ }
1504
+
1505
+ // Activity Feed
1506
+ function renderActivityFeed(entries) {
1507
+ var feed = $('missionFeed')
1508
+ if (!entries || entries.length === 0) {
1509
+ feed.innerHTML = '<div class="empty-state"><p>Waiting for activity...</p></div>'
1510
+ return
1511
+ }
1512
+ feed.innerHTML = entries.slice(0, 100).map(function(e) {
1513
+ return '<div class="feed-entry"><span class="feed-time">' + fmtTime(e.timestamp) + '</span><span class="feed-agent">' + (e.agentId || '') + '</span><span class="feed-type">' + (e.type || e.eventType || '') + '</span><span class="feed-summary">' + (e.summary || e.title || e.description || '') + '</span></div>'
1514
+ }).join('')
1515
+ }
1516
+
1517
+ function appendActivity(event) {
1518
+ if (!event) return
1519
+ var feed = $('missionFeed')
1520
+ var entry = document.createElement('div')
1521
+ entry.className = 'feed-entry'
1522
+ entry.innerHTML = '<span class="feed-time">' + fmtTime(event.timestamp || new Date()) + '</span><span class="feed-agent">' + (event.agentId || event.agentId || '') + '</span><span class="feed-type">' + (event.type || event.eventType || 'event') + '</span><span class="feed-summary">' + (event.summary || event.title || event.description || JSON.stringify(event.payload || '')) + '</span>'
1523
+
1524
+ // Remove empty state
1525
+ var empty = feed.querySelector('.empty-state')
1526
+ if (empty) feed.innerHTML = ''
1527
+
1528
+ feed.insertBefore(entry, feed.firstChild)
1529
+ if (feed.children.length > 200) feed.removeChild(feed.lastChild)
1530
+ }
1531
+
1532
+ // Clock
1533
+ function updateClock() {
1534
+ var now = new Date()
1535
+ $('headerTime').textContent = now.toLocaleTimeString('en-US', { hour12: false })
1536
+ $('footerTime').textContent = now.toISOString()
1537
+ }
1538
+ setInterval(updateClock, 1000)
1539
+ updateClock()
1540
+
1541
+ // Init health refresh
1542
+ fetch('/api/health').then(function(r) { return r.json() }).then(function(data) {
1543
+ if (data.memory) {
1544
+ var mb = (data.memory.heapUsed / 1024 / 1024).toFixed(1)
1545
+ $('statMemory').textContent = mb + ' MB'
1546
+ }
1547
+ }).catch(function() {})
1548
+
1549
+ // Startup: fetch initial data
1550
+ async function initDashboard() {
1551
+ try {
1552
+ var [status, projects, sessions, graphs, approvals, cost, decisionLog] = await Promise.all([
1553
+ fetch('/api/status').then(function(r) { return r.json() }),
1554
+ fetch('/api/projects').then(function(r) { return r.json() }),
1555
+ fetch('/api/sessions').then(function(r) { return r.json() }),
1556
+ fetch('/api/task-graphs').then(function(r) { return r.json() }),
1557
+ fetch('/api/approvals').then(function(r) { return r.json() }),
1558
+ fetch('/api/cost').then(function(r) { return r.json() }),
1559
+ fetch('/api/decision-log').then(function(r) { return r.json() }),
1560
+ ])
1561
+
1562
+ renderStats({
1563
+ status: status.status,
1564
+ projects: projects.projects,
1565
+ sessions: sessions.sessions,
1566
+ taskGraphs: graphs.taskGraphs,
1567
+ approvals: approvals.pending,
1568
+ uptime: status.uptime,
1569
+ })
1570
+
1571
+ var uptime = status.uptime || 0
1572
+ $('statUptime').textContent = fmtDuration(uptime)
1573
+
1574
+ renderTaskGraphs(graphs.taskGraphs || [])
1575
+ renderFleet(sessions.sessions || [])
1576
+ renderApprovals(approvals.pending || [], approvals.all || [])
1577
+ renderCost(cost)
1578
+ renderDecisionLog(decisionLog.entries || [])
1579
+ } catch (e) {
1580
+ console.error('Init error:', e)
1581
+ }
1582
+ }
1583
+
1584
+ initDashboard()
1585
+ connectSSE()
1586
+
1587
+ // Periodic refresh fallback in case SSE drops
1588
+ setInterval(function() {
1589
+ refreshStats()
1590
+ refreshTaskGraphs()
1591
+ refreshFleet()
1592
+ refreshApprovals()
1593
+ refreshCost()
1594
+ refreshDecisionLog()
1595
+ }, 15000)
1596
+ </script>
1597
+ </body>
1598
+ </html>`;
1599
+ res.writeHead(200, { "Content-Type": "text/html" });
1600
+ res.end(html);
1601
+ }
1602
+ start() {
1603
+ this.server.listen(this.port, () => {
1604
+ debug(`Dashboard v2 listening on http://localhost:${this.port}`);
1605
+ });
1606
+ this.heartbeatTimer = setInterval(() => {
1607
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1608
+ for (const client of this.sseClients) {
1609
+ try {
1610
+ client.write(`: heartbeat ${now}
1611
+
1612
+ `);
1613
+ } catch {
1614
+ }
1615
+ }
1616
+ }, 3e4);
1617
+ }
1618
+ stop() {
1619
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
1620
+ for (const client of this.sseClients) {
1621
+ try {
1622
+ client.end();
1623
+ } catch {
1624
+ }
1625
+ }
1626
+ this.sseClients = [];
1627
+ this.server.close();
1628
+ debug("Dashboard v2 stopped");
1629
+ }
1630
+ };
1631
+ function createDashboardServer(lifecycle, port = 7777) {
1632
+ return new DashboardServer(lifecycle, port);
1633
+ }
1634
+
1635
+ export { DashboardServer, DashboardState, createDashboardServer };
1636
+ //# sourceMappingURL=chunk-GOGI3JQD.js.map
1637
+ //# sourceMappingURL=chunk-GOGI3JQD.js.map