shipwright-cli 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +19 -19
  2. package/dashboard/public/index.html +224 -8
  3. package/dashboard/public/styles.css +1078 -4
  4. package/dashboard/server.ts +1100 -15
  5. package/dashboard/src/canvas/interactions.ts +74 -0
  6. package/dashboard/src/canvas/layout.ts +85 -0
  7. package/dashboard/src/canvas/overlays.ts +117 -0
  8. package/dashboard/src/canvas/particles.ts +105 -0
  9. package/dashboard/src/canvas/renderer.ts +191 -0
  10. package/dashboard/src/components/charts/bar.ts +54 -0
  11. package/dashboard/src/components/charts/donut.ts +25 -0
  12. package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
  13. package/dashboard/src/components/charts/sparkline.ts +82 -0
  14. package/dashboard/src/components/header.ts +616 -0
  15. package/dashboard/src/components/modal.ts +413 -0
  16. package/dashboard/src/components/terminal.ts +144 -0
  17. package/dashboard/src/core/api.ts +381 -0
  18. package/dashboard/src/core/helpers.ts +118 -0
  19. package/dashboard/src/core/router.ts +190 -0
  20. package/dashboard/src/core/sse.ts +38 -0
  21. package/dashboard/src/core/state.ts +150 -0
  22. package/dashboard/src/core/ws.ts +143 -0
  23. package/dashboard/src/design/icons.ts +131 -0
  24. package/dashboard/src/design/tokens.ts +160 -0
  25. package/dashboard/src/main.ts +68 -0
  26. package/dashboard/src/types/api.ts +337 -0
  27. package/dashboard/src/views/activity.ts +185 -0
  28. package/dashboard/src/views/agent-cockpit.ts +236 -0
  29. package/dashboard/src/views/agents.ts +72 -0
  30. package/dashboard/src/views/fleet-map.ts +299 -0
  31. package/dashboard/src/views/insights.ts +298 -0
  32. package/dashboard/src/views/machines.ts +162 -0
  33. package/dashboard/src/views/metrics.ts +420 -0
  34. package/dashboard/src/views/overview.ts +409 -0
  35. package/dashboard/src/views/pipeline-theater.ts +219 -0
  36. package/dashboard/src/views/pipelines.ts +595 -0
  37. package/dashboard/src/views/team.ts +362 -0
  38. package/dashboard/src/views/timeline.ts +389 -0
  39. package/dashboard/tsconfig.json +21 -0
  40. package/docs/AGI-PLATFORM-PLAN.md +5 -5
  41. package/docs/AGI-WHATS-NEXT.md +19 -16
  42. package/docs/README.md +2 -0
  43. package/package.json +8 -1
  44. package/scripts/check-version-consistency.sh +72 -0
  45. package/scripts/lib/daemon-adaptive.sh +610 -0
  46. package/scripts/lib/daemon-dispatch.sh +489 -0
  47. package/scripts/lib/daemon-failure.sh +387 -0
  48. package/scripts/lib/daemon-patrol.sh +1113 -0
  49. package/scripts/lib/daemon-poll.sh +1202 -0
  50. package/scripts/lib/daemon-state.sh +550 -0
  51. package/scripts/lib/daemon-triage.sh +490 -0
  52. package/scripts/lib/helpers.sh +81 -0
  53. package/scripts/lib/pipeline-intelligence.sh +0 -6
  54. package/scripts/lib/pipeline-quality-checks.sh +3 -1
  55. package/scripts/lib/pipeline-stages.sh +20 -0
  56. package/scripts/sw +109 -168
  57. package/scripts/sw-activity.sh +1 -1
  58. package/scripts/sw-adaptive.sh +2 -2
  59. package/scripts/sw-adversarial.sh +1 -1
  60. package/scripts/sw-architecture-enforcer.sh +1 -1
  61. package/scripts/sw-auth.sh +14 -6
  62. package/scripts/sw-autonomous.sh +1 -1
  63. package/scripts/sw-changelog.sh +2 -2
  64. package/scripts/sw-checkpoint.sh +1 -1
  65. package/scripts/sw-ci.sh +1 -1
  66. package/scripts/sw-cleanup.sh +1 -1
  67. package/scripts/sw-code-review.sh +1 -1
  68. package/scripts/sw-connect.sh +1 -1
  69. package/scripts/sw-context.sh +1 -1
  70. package/scripts/sw-cost.sh +1 -1
  71. package/scripts/sw-daemon.sh +53 -4817
  72. package/scripts/sw-dashboard.sh +1 -1
  73. package/scripts/sw-db.sh +1 -1
  74. package/scripts/sw-decompose.sh +1 -1
  75. package/scripts/sw-deps.sh +1 -1
  76. package/scripts/sw-developer-simulation.sh +1 -1
  77. package/scripts/sw-discovery.sh +1 -1
  78. package/scripts/sw-doc-fleet.sh +1 -1
  79. package/scripts/sw-docs-agent.sh +1 -1
  80. package/scripts/sw-docs.sh +1 -1
  81. package/scripts/sw-doctor.sh +49 -1
  82. package/scripts/sw-dora.sh +1 -1
  83. package/scripts/sw-durable.sh +1 -1
  84. package/scripts/sw-e2e-orchestrator.sh +1 -1
  85. package/scripts/sw-eventbus.sh +1 -1
  86. package/scripts/sw-feedback.sh +1 -1
  87. package/scripts/sw-fix.sh +6 -5
  88. package/scripts/sw-fleet-discover.sh +1 -1
  89. package/scripts/sw-fleet-viz.sh +3 -3
  90. package/scripts/sw-fleet.sh +1 -1
  91. package/scripts/sw-github-app.sh +5 -2
  92. package/scripts/sw-github-checks.sh +1 -1
  93. package/scripts/sw-github-deploy.sh +1 -1
  94. package/scripts/sw-github-graphql.sh +1 -1
  95. package/scripts/sw-guild.sh +1 -1
  96. package/scripts/sw-heartbeat.sh +1 -1
  97. package/scripts/sw-hygiene.sh +1 -1
  98. package/scripts/sw-incident.sh +1 -1
  99. package/scripts/sw-init.sh +112 -9
  100. package/scripts/sw-instrument.sh +6 -1
  101. package/scripts/sw-intelligence.sh +5 -1
  102. package/scripts/sw-jira.sh +1 -1
  103. package/scripts/sw-launchd.sh +1 -1
  104. package/scripts/sw-linear.sh +20 -9
  105. package/scripts/sw-logs.sh +1 -1
  106. package/scripts/sw-loop.sh +2 -1
  107. package/scripts/sw-memory.sh +10 -1
  108. package/scripts/sw-mission-control.sh +1 -1
  109. package/scripts/sw-model-router.sh +4 -1
  110. package/scripts/sw-otel.sh +4 -4
  111. package/scripts/sw-oversight.sh +1 -1
  112. package/scripts/sw-pipeline-composer.sh +3 -1
  113. package/scripts/sw-pipeline-vitals.sh +4 -6
  114. package/scripts/sw-pipeline.sh +19 -56
  115. package/scripts/sw-pipeline.sh.mock +7 -0
  116. package/scripts/sw-pm.sh +5 -2
  117. package/scripts/sw-pr-lifecycle.sh +1 -1
  118. package/scripts/sw-predictive.sh +4 -1
  119. package/scripts/sw-prep.sh +3 -2
  120. package/scripts/sw-ps.sh +1 -1
  121. package/scripts/sw-public-dashboard.sh +10 -4
  122. package/scripts/sw-quality.sh +1 -1
  123. package/scripts/sw-reaper.sh +1 -1
  124. package/scripts/sw-recruit.sh +25 -1
  125. package/scripts/sw-regression.sh +2 -1
  126. package/scripts/sw-release-manager.sh +1 -1
  127. package/scripts/sw-release.sh +7 -5
  128. package/scripts/sw-remote.sh +1 -1
  129. package/scripts/sw-replay.sh +1 -1
  130. package/scripts/sw-retro.sh +1 -1
  131. package/scripts/sw-scale.sh +11 -5
  132. package/scripts/sw-security-audit.sh +1 -1
  133. package/scripts/sw-self-optimize.sh +172 -7
  134. package/scripts/sw-session.sh +1 -1
  135. package/scripts/sw-setup.sh +1 -1
  136. package/scripts/sw-standup.sh +4 -3
  137. package/scripts/sw-status.sh +1 -1
  138. package/scripts/sw-strategic.sh +2 -1
  139. package/scripts/sw-stream.sh +8 -2
  140. package/scripts/sw-swarm.sh +12 -10
  141. package/scripts/sw-team-stages.sh +1 -1
  142. package/scripts/sw-templates.sh +1 -1
  143. package/scripts/sw-testgen.sh +3 -2
  144. package/scripts/sw-tmux-pipeline.sh +2 -1
  145. package/scripts/sw-tmux.sh +1 -1
  146. package/scripts/sw-trace.sh +1 -1
  147. package/scripts/sw-tracker-jira.sh +1 -0
  148. package/scripts/sw-tracker-linear.sh +1 -0
  149. package/scripts/sw-tracker.sh +24 -6
  150. package/scripts/sw-triage.sh +1 -1
  151. package/scripts/sw-upgrade.sh +1 -1
  152. package/scripts/sw-ux.sh +1 -1
  153. package/scripts/sw-webhook.sh +1 -1
  154. package/scripts/sw-widgets.sh +2 -2
  155. package/scripts/sw-worktree.sh +1 -1
  156. package/dashboard/public/app.js +0 -4422
@@ -0,0 +1,409 @@
1
+ // Overview tab - stats, pipelines, queue, activity, resources, cost, machines
2
+
3
+ import { store } from "../core/state";
4
+ import {
5
+ escapeHtml,
6
+ fmtNum,
7
+ formatDuration,
8
+ formatTime,
9
+ animateValue,
10
+ getBadgeClass,
11
+ getTypeShort,
12
+ } from "../core/helpers";
13
+ import { renderPipelineSVG } from "../components/charts/pipeline-rail";
14
+ import { renderCostTicker } from "../components/header";
15
+ import { switchTab } from "../core/router";
16
+ import * as api from "../core/api";
17
+ import { fetchPipelineDetail } from "./pipelines";
18
+ import { icon } from "../design/icons";
19
+ import type {
20
+ FleetState,
21
+ View,
22
+ PipelineInfo,
23
+ QueueItem,
24
+ EventItem,
25
+ } from "../types/api";
26
+
27
+ function renderStats(data: FleetState): void {
28
+ const d = data.daemon || ({} as any);
29
+ const m = data.metrics || ({} as any);
30
+ const firstRender = store.get("firstRender");
31
+
32
+ const statusEl = document.getElementById("stat-status");
33
+ const statusDot = document.getElementById("status-dot");
34
+ if (statusEl && statusDot) {
35
+ if (d.running) {
36
+ statusEl.textContent = "OPERATIONAL";
37
+ statusEl.className = "stat-value status-green";
38
+ statusDot.className = "pulse-dot operational";
39
+ } else {
40
+ statusEl.textContent = "OFFLINE";
41
+ statusEl.className = "stat-value status-rose";
42
+ statusDot.className = "pulse-dot offline";
43
+ }
44
+ }
45
+
46
+ const active = data.pipelines ? data.pipelines.length : 0;
47
+ const max = d.maxParallel || 0;
48
+ const activeEl = document.getElementById("stat-active");
49
+ if (activeEl) {
50
+ if (firstRender && active > 0) {
51
+ animateValue(activeEl, 0, active, 600, " / " + fmtNum(max));
52
+ } else {
53
+ activeEl.textContent = fmtNum(active) + " / " + fmtNum(max);
54
+ }
55
+ }
56
+ const barPct = max > 0 ? Math.min((active / max) * 100, 100) : 0;
57
+ const bar = document.getElementById("stat-active-bar");
58
+ if (bar) bar.style.width = barPct + "%";
59
+
60
+ const queued = data.queue ? data.queue.length : 0;
61
+ const queueEl = document.getElementById("stat-queue");
62
+ if (queueEl) {
63
+ queueEl.textContent = fmtNum(queued);
64
+ queueEl.className =
65
+ queued > 0 ? "stat-value status-amber" : "stat-value status-green";
66
+ }
67
+ const queueSub = document.getElementById("stat-queue-sub");
68
+ if (queueSub)
69
+ queueSub.textContent = queued === 1 ? "issue waiting" : "issues waiting";
70
+
71
+ const completed = m.completed ?? 0;
72
+ const completedEl = document.getElementById("stat-completed");
73
+ if (completedEl) {
74
+ if (firstRender && completed > 0) {
75
+ animateValue(completedEl, 0, completed, 800, "");
76
+ } else {
77
+ completedEl.textContent = fmtNum(completed);
78
+ }
79
+ }
80
+ const failed = m.failed ?? 0;
81
+ const failedSub = document.getElementById("stat-failed-sub");
82
+ if (failedSub) {
83
+ failedSub.textContent = fmtNum(failed) + " failed";
84
+ failedSub.className =
85
+ failed > 0 ? "stat-subtitle failed-some" : "stat-subtitle failed-none";
86
+ }
87
+ }
88
+
89
+ function renderOverviewPipelines(data: FleetState): void {
90
+ const container = document.getElementById("active-pipelines");
91
+ if (!container) return;
92
+ const firstRender = store.get("firstRender");
93
+
94
+ if (!data.pipelines || data.pipelines.length === 0) {
95
+ container.innerHTML = `<div class="empty-state">${icon("clock", 32)}<p>No active pipelines</p></div>`;
96
+ return;
97
+ }
98
+
99
+ let html = "";
100
+ for (let idx = 0; idx < data.pipelines.length; idx++) {
101
+ const p = data.pipelines[idx];
102
+ const maxIter = p.maxIterations || 20;
103
+ const curIter = p.iteration || 0;
104
+ const iterPct = maxIter > 0 ? Math.min((curIter / maxIter) * 100, 100) : 0;
105
+
106
+ const linesText =
107
+ p.linesWritten != null ? fmtNum(p.linesWritten) + " lines" : "";
108
+ const testsText =
109
+ p.testsPassing === true
110
+ ? '<span class="tests-pass">Tests \u2713</span>'
111
+ : p.testsPassing === false
112
+ ? '<span class="tests-fail">Tests \u2717</span>'
113
+ : "";
114
+ const metaParts = [linesText, testsText].filter(Boolean);
115
+ const animDelay = firstRender
116
+ ? ` style="animation-delay:${idx * 0.05}s"`
117
+ : "";
118
+
119
+ html +=
120
+ `<div class="pipeline-card" data-issue="${p.issue}"${animDelay}>` +
121
+ `<div class="pipeline-header">` +
122
+ `<span class="pipeline-issue">#${p.issue}</span>` +
123
+ `<span class="pipeline-title">${escapeHtml(p.title)}</span>` +
124
+ `<span class="pipeline-elapsed">${formatDuration(p.elapsed_s)}</span></div>` +
125
+ `<div class="pipeline-svg-wrap">${renderPipelineSVG(p)}</div>` +
126
+ `<div class="pipeline-iter">` +
127
+ `<span class="pipeline-iter-label">Iteration ${curIter}/${maxIter}</span>` +
128
+ `<div class="iter-bar-track"><div class="iter-bar-fill" style="width:${iterPct}%"></div></div></div>` +
129
+ `<div class="pipeline-meta">${metaParts.join(" <span>\u00b7</span> ")}</div>` +
130
+ (p.worktree
131
+ ? `<div class="pipeline-worktree">WORKTREE: ${escapeHtml(p.worktree)}</div>`
132
+ : "") +
133
+ `</div>`;
134
+ }
135
+ container.innerHTML = html;
136
+
137
+ container.querySelectorAll(".pipeline-card").forEach((card) => {
138
+ card.addEventListener("click", () => {
139
+ const issue = card.getAttribute("data-issue");
140
+ if (issue) {
141
+ switchTab("pipelines");
142
+ fetchPipelineDetail(Number(issue));
143
+ }
144
+ });
145
+ });
146
+ }
147
+
148
+ function renderQueue(data: FleetState): void {
149
+ const container = document.getElementById("queue-list");
150
+ if (!container) return;
151
+
152
+ if (!data.queue || data.queue.length === 0) {
153
+ container.innerHTML = '<div class="empty-state"><p>Queue clear</p></div>';
154
+ return;
155
+ }
156
+
157
+ let html = "";
158
+ for (let i = 0; i < data.queue.length; i++) {
159
+ const q = data.queue[i];
160
+ const costEst =
161
+ q.estimated_cost != null
162
+ ? ` <span class="queue-cost-est">~$${q.estimated_cost.toFixed(2)}</span>`
163
+ : "";
164
+ html +=
165
+ `<div class="queue-row" data-queue-idx="${i}" data-issue="${q.issue}">` +
166
+ `<span class="queue-issue">#${q.issue}</span>` +
167
+ `<span class="queue-title-text">${escapeHtml(q.title)}</span>` +
168
+ `<span class="queue-score">${q.score != null ? q.score : "\u2014"}</span>${costEst}</div>`;
169
+ html += `<div class="queue-scoring-detail" id="queue-detail-${i}" style="display:none">`;
170
+ if (q.factors) {
171
+ html += renderScoringFactors(
172
+ q.factors as unknown as Record<string, unknown>,
173
+ );
174
+ }
175
+ html += `<div class="queue-triage-reasoning" id="queue-reasoning-${i}"></div>`;
176
+ html += `</div>`;
177
+ }
178
+ container.innerHTML = html;
179
+
180
+ // Fetch detailed queue data for triage reasoning
181
+ let detailedData: Array<Record<string, unknown>> | null = null;
182
+ api
183
+ .fetchQueueDetailed()
184
+ .then((d) => {
185
+ detailedData = d.items || [];
186
+ })
187
+ .catch(() => {});
188
+
189
+ container.querySelectorAll(".queue-row").forEach((row) => {
190
+ row.addEventListener("click", () => {
191
+ const idx = row.getAttribute("data-queue-idx");
192
+ const detail = document.getElementById("queue-detail-" + idx);
193
+ if (!detail) return;
194
+ const isHidden = detail.style.display === "none";
195
+ detail.style.display = isHidden ? "" : "none";
196
+
197
+ if (isHidden && detailedData) {
198
+ const issue = row.getAttribute("data-issue");
199
+ const reasoningEl = document.getElementById("queue-reasoning-" + idx);
200
+ if (reasoningEl && !reasoningEl.innerHTML) {
201
+ const match = detailedData.find((d) => String(d.issue) === issue);
202
+ if (match) {
203
+ let rHtml = "";
204
+ if (match.triage_reason || match.reason)
205
+ rHtml += `<div class="triage-reason"><strong>Triage:</strong> ${escapeHtml(String(match.triage_reason || match.reason))}</div>`;
206
+ if (match.complexity_estimate)
207
+ rHtml += `<div class="triage-detail"><strong>Complexity:</strong> ${escapeHtml(String(match.complexity_estimate))}</div>`;
208
+ if (match.labels)
209
+ rHtml += `<div class="triage-detail"><strong>Labels:</strong> ${escapeHtml(String(match.labels))}</div>`;
210
+ if (match.age_hours)
211
+ rHtml += `<div class="triage-detail"><strong>Age:</strong> ${Number(match.age_hours).toFixed(1)}h</div>`;
212
+ reasoningEl.innerHTML = rHtml;
213
+ }
214
+ }
215
+ }
216
+ });
217
+ });
218
+ }
219
+
220
+ function renderScoringFactors(factors: Record<string, unknown>): string {
221
+ const keys = [
222
+ "complexity",
223
+ "impact",
224
+ "priority",
225
+ "age",
226
+ "dependency",
227
+ "memory",
228
+ ];
229
+ let html = '<div class="scoring-factors">';
230
+ for (const k of keys) {
231
+ const val = Number(factors[k] ?? 0);
232
+ const pct = Math.max(0, Math.min(100, val));
233
+ html +=
234
+ `<div class="scoring-factor-row">` +
235
+ `<span class="scoring-factor-label">${escapeHtml(k)}</span>` +
236
+ `<div class="scoring-factor-track"><div class="scoring-factor-fill" style="width:${pct}%"></div></div>` +
237
+ `<span class="scoring-factor-val">${pct}</span></div>`;
238
+ }
239
+ html += "</div>";
240
+ return html;
241
+ }
242
+
243
+ function renderOverviewActivity(data: FleetState): void {
244
+ const container = document.getElementById("activity-feed");
245
+ if (!container) return;
246
+
247
+ if (!data.events || data.events.length === 0) {
248
+ container.innerHTML =
249
+ '<div class="empty-state"><p>Awaiting events...</p></div>';
250
+ return;
251
+ }
252
+
253
+ const events = data.events.slice(-10).reverse();
254
+ let html = "";
255
+ for (const ev of events) {
256
+ const typeRaw = String(ev.type || "unknown");
257
+ const typeShort = getTypeShort(typeRaw);
258
+ const badgeClass = getBadgeClass(typeRaw);
259
+
260
+ const skip: Record<string, boolean> = {
261
+ ts: true,
262
+ type: true,
263
+ timestamp: true,
264
+ };
265
+ const dparts: string[] = [];
266
+ for (const [key, val] of Object.entries(ev)) {
267
+ if (!skip[key]) dparts.push(key + "=" + val);
268
+ }
269
+ const detail = dparts.join(" ");
270
+
271
+ html +=
272
+ `<div class="activity-row">` +
273
+ `<span class="activity-ts">${formatTime(ev.ts || ev.timestamp)}</span>` +
274
+ `<span class="activity-badge ${badgeClass}">${escapeHtml(typeShort)}</span>` +
275
+ `<span class="activity-detail">${escapeHtml(detail)}</span></div>`;
276
+ }
277
+ container.innerHTML = html;
278
+ }
279
+
280
+ function renderResources(data: FleetState): void {
281
+ const s = data.scale || ({} as any);
282
+ const m = data.metrics || ({} as any);
283
+ const active = data.pipelines ? data.pipelines.length : 0;
284
+
285
+ const cores = m.cpuCores || s.cpuCores || 0;
286
+ const maxByCpu = s.maxByCpu ?? null;
287
+ const maxByMem = s.maxByMem ?? null;
288
+ const maxByBudget = s.maxByBudget ?? null;
289
+
290
+ const cpuBar = document.getElementById("res-cpu-bar");
291
+ const cpuInfo = document.getElementById("res-cpu-info");
292
+ if (cpuBar && cpuInfo) {
293
+ if (maxByCpu != null) {
294
+ const pct = maxByCpu > 0 ? Math.min((active / maxByCpu) * 100, 100) : 0;
295
+ cpuBar.style.width = pct + "%";
296
+ cpuBar.className = "resource-bar-fill";
297
+ cpuInfo.textContent = maxByCpu + " max (" + cores + " cores)";
298
+ } else {
299
+ cpuBar.style.width = "0%";
300
+ cpuInfo.textContent = "\u2014";
301
+ }
302
+ }
303
+
304
+ const memBar = document.getElementById("res-mem-bar");
305
+ const memInfo = document.getElementById("res-mem-info");
306
+ if (memBar && memInfo) {
307
+ if (maxByMem != null) {
308
+ const pct = maxByMem > 0 ? Math.min((active / maxByMem) * 100, 100) : 0;
309
+ memBar.style.width = pct + "%";
310
+ memBar.className =
311
+ maxByMem <= 1
312
+ ? "resource-bar-fill critical"
313
+ : maxByMem <= 2
314
+ ? "resource-bar-fill warning"
315
+ : "resource-bar-fill";
316
+ const memGb = s.availMemGb != null ? s.availMemGb + "GB free" : "";
317
+ memInfo.textContent =
318
+ maxByMem + " max" + (memGb ? " (" + memGb + ")" : "");
319
+ } else {
320
+ memBar.style.width = "0%";
321
+ memInfo.textContent = "\u2014";
322
+ }
323
+ }
324
+
325
+ const budgetBar = document.getElementById("res-budget-bar");
326
+ const budgetInfo = document.getElementById("res-budget-info");
327
+ if (budgetBar && budgetInfo) {
328
+ if (maxByBudget != null) {
329
+ const pct =
330
+ maxByBudget > 0 ? Math.min((active / maxByBudget) * 100, 100) : 0;
331
+ budgetBar.style.width = pct + "%";
332
+ budgetBar.className = "resource-bar-fill";
333
+ budgetInfo.textContent = maxByBudget + " max";
334
+ } else {
335
+ budgetBar.style.width = "0%";
336
+ budgetInfo.textContent = "unlimited";
337
+ }
338
+ }
339
+
340
+ const constraintEl = document.getElementById("resource-constraint");
341
+ if (constraintEl) {
342
+ if (maxByMem != null && maxByCpu != null) {
343
+ const minFactor = Math.min(
344
+ maxByCpu || Infinity,
345
+ maxByMem || Infinity,
346
+ maxByBudget ?? Infinity,
347
+ );
348
+ if (minFactor === maxByMem && maxByMem <= 2) {
349
+ constraintEl.innerHTML =
350
+ '<span class="constraint-badge warning">MEM-BOUND</span>';
351
+ } else if (maxByBudget != null && minFactor === maxByBudget) {
352
+ constraintEl.innerHTML =
353
+ '<span class="constraint-badge warning">BUDGET-BOUND</span>';
354
+ } else {
355
+ constraintEl.innerHTML =
356
+ '<span class="constraint-badge nominal">NOMINAL</span>';
357
+ }
358
+ } else {
359
+ constraintEl.innerHTML =
360
+ '<span class="constraint-badge nominal">NOMINAL</span>';
361
+ }
362
+ }
363
+ }
364
+
365
+ function renderMachines(data: FleetState): void {
366
+ const section = document.getElementById("machines-section");
367
+ if (!section) return;
368
+ const machines = data.machines || [];
369
+ if (machines.length === 0) {
370
+ section.style.display = "none";
371
+ return;
372
+ }
373
+ section.style.display = "";
374
+ const grid = document.getElementById("machines-grid");
375
+ if (!grid) return;
376
+ let html = "";
377
+ for (const m of machines) {
378
+ const statusCls =
379
+ m.status === "online"
380
+ ? "machine-online"
381
+ : m.status === "degraded"
382
+ ? "machine-degraded"
383
+ : "machine-offline";
384
+ html +=
385
+ `<div class="machine-card ${statusCls}">` +
386
+ `<div class="machine-card-header">` +
387
+ `<span class="machine-name">${escapeHtml(m.name)}</span>` +
388
+ `<span class="machine-status-dot"></span></div>` +
389
+ `<div class="machine-card-body">` +
390
+ `<span class="machine-host">${escapeHtml(m.host)}</span>` +
391
+ `<span class="machine-workers">${m.active_workers}/${m.max_workers} workers</span>` +
392
+ `</div></div>`;
393
+ }
394
+ grid.innerHTML = html;
395
+ }
396
+
397
+ export const overviewView: View = {
398
+ init() {},
399
+ render(data: FleetState) {
400
+ renderStats(data);
401
+ renderOverviewPipelines(data);
402
+ renderQueue(data);
403
+ renderOverviewActivity(data);
404
+ renderResources(data);
405
+ renderCostTicker(data);
406
+ renderMachines(data);
407
+ },
408
+ destroy() {},
409
+ };
@@ -0,0 +1,219 @@
1
+ // Pipeline Theater - live terminal + animated stage rail + token burn meter + file accumulator
2
+
3
+ import { store } from "../core/state";
4
+ import { escapeHtml, formatDuration, fmtNum } from "../core/helpers";
5
+ import { icon } from "../design/icons";
6
+ import { colors, STAGES, STAGE_HEX, STAGE_SHORT } from "../design/tokens";
7
+ import { LiveTerminal } from "../components/terminal";
8
+ import { SSEClient } from "../core/sse";
9
+ import { renderPipelineSVG } from "../components/charts/pipeline-rail";
10
+ import * as api from "../core/api";
11
+ import type { FleetState, View, PipelineInfo } from "../types/api";
12
+
13
+ let terminal: LiveTerminal | null = null;
14
+ let sseClient: SSEClient | null = null;
15
+ let selectedIssue: number | null = null;
16
+
17
+ function renderTheater(data: FleetState): void {
18
+ const container = document.getElementById("panel-pipeline-theater");
19
+ if (!container) return;
20
+
21
+ const pipelines = data.pipelines || [];
22
+ if (pipelines.length === 0 && !selectedIssue) {
23
+ container.innerHTML = `<div class="empty-state">${icon("eye", 48)}<p>No active pipelines to observe</p></div>`;
24
+ return;
25
+ }
26
+
27
+ // Pipeline selector
28
+ let html = '<div class="theater-layout">';
29
+ html += '<div class="theater-sidebar">';
30
+ html += '<div class="theater-sidebar-header">Active Pipelines</div>';
31
+ for (const p of pipelines) {
32
+ const isSelected = selectedIssue === p.issue;
33
+ html +=
34
+ `<div class="theater-pipeline-item${isSelected ? " selected" : ""}" data-issue="${p.issue}">` +
35
+ `<span class="theater-issue">#${p.issue}</span>` +
36
+ `<span class="theater-stage">${escapeHtml(p.stage)}</span>` +
37
+ `<span class="theater-elapsed">${formatDuration(p.elapsed_s)}</span></div>`;
38
+ }
39
+ html += "</div>";
40
+
41
+ // Main theater area
42
+ html += '<div class="theater-main">';
43
+ if (selectedIssue) {
44
+ const pipeline = pipelines.find((p) => p.issue === selectedIssue);
45
+ if (pipeline) {
46
+ // Stage rail
47
+ html += `<div class="theater-stage-rail">${renderPipelineSVG(pipeline)}</div>`;
48
+
49
+ // Token burn + file accumulator
50
+ html += '<div class="theater-metrics-bar">';
51
+ html +=
52
+ `<div class="theater-metric"><span class="theater-metric-label">${icon("zap", 14)} Iteration</span>` +
53
+ `<span class="theater-metric-value">${pipeline.iteration || 0}/${pipeline.maxIterations || 20}</span></div>`;
54
+ if (pipeline.linesWritten != null) {
55
+ html +=
56
+ `<div class="theater-metric"><span class="theater-metric-label">${icon("file-diff", 14)} Lines</span>` +
57
+ `<span class="theater-metric-value">${fmtNum(pipeline.linesWritten)}</span></div>`;
58
+ }
59
+ if (pipeline.cost != null) {
60
+ html +=
61
+ `<div class="theater-metric"><span class="theater-metric-label">${icon("dollar-sign", 14)} Cost</span>` +
62
+ `<span class="theater-metric-value">$${pipeline.cost.toFixed(2)}</span></div>`;
63
+ }
64
+ html += "</div>";
65
+
66
+ // Live Changes panel (diff + files)
67
+ html += '<div class="theater-changes" id="theater-changes">';
68
+ html += `<div class="theater-changes-header">${icon("file-diff", 16)} Live Changes <button class="btn-sm" id="theater-refresh-diff">Refresh</button></div>`;
69
+ html +=
70
+ '<div class="theater-changes-body" id="theater-changes-body"><div class="empty-state"><p>Loading changes...</p></div></div>';
71
+ html += "</div>";
72
+
73
+ // Live terminal
74
+ html +=
75
+ '<div class="theater-terminal" id="theater-terminal-container"></div>';
76
+ } else {
77
+ html += `<div class="empty-state"><p>Pipeline #${selectedIssue} no longer active</p></div>`;
78
+ }
79
+ } else {
80
+ html += `<div class="empty-state">${icon("terminal", 32)}<p>Select a pipeline to observe</p></div>`;
81
+ }
82
+ html += "</div></div>";
83
+ container.innerHTML = html;
84
+
85
+ // Wire up pipeline selection
86
+ container.querySelectorAll(".theater-pipeline-item").forEach((item) => {
87
+ item.addEventListener("click", () => {
88
+ const issue = parseInt(item.getAttribute("data-issue") || "0", 10);
89
+ if (issue) selectPipeline(issue, data);
90
+ });
91
+ });
92
+
93
+ // Initialize terminal and live changes if pipeline is selected
94
+ if (selectedIssue) {
95
+ const termContainer = document.getElementById("theater-terminal-container");
96
+ if (termContainer) {
97
+ terminal = new LiveTerminal(termContainer);
98
+ connectLogStream(selectedIssue);
99
+ }
100
+ loadLiveChanges(selectedIssue);
101
+ const refreshBtn = document.getElementById("theater-refresh-diff");
102
+ if (refreshBtn) {
103
+ refreshBtn.addEventListener("click", () => {
104
+ if (selectedIssue) loadLiveChanges(selectedIssue);
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ function selectPipeline(issue: number, data: FleetState): void {
111
+ if (sseClient) sseClient.close();
112
+ if (terminal) terminal.destroy();
113
+ selectedIssue = issue;
114
+ renderTheater(data);
115
+ }
116
+
117
+ function loadLiveChanges(issue: number): void {
118
+ const body = document.getElementById("theater-changes-body");
119
+ if (!body) return;
120
+
121
+ Promise.all([
122
+ api.fetchPipelineFiles(issue).catch(() => ({ files: [] })),
123
+ api.fetchPipelineDiff(issue).catch(() => ({
124
+ diff: "",
125
+ stats: { files_changed: 0, insertions: 0, deletions: 0 },
126
+ worktree: "",
127
+ })),
128
+ ]).then(([filesData, diffData]) => {
129
+ const files = filesData.files || [];
130
+ const stats = diffData.stats;
131
+ let html = "";
132
+
133
+ // Stats summary
134
+ if (stats.files_changed > 0) {
135
+ html +=
136
+ `<div class="changes-stats">` +
137
+ `<span class="stat-files">${stats.files_changed} file${stats.files_changed !== 1 ? "s" : ""}</span>` +
138
+ `<span class="stat-add">+${stats.insertions}</span>` +
139
+ `<span class="stat-del">-${stats.deletions}</span></div>`;
140
+ }
141
+
142
+ // File list
143
+ if (files.length > 0) {
144
+ html += '<div class="changes-file-list">';
145
+ for (const f of files) {
146
+ const statusCls =
147
+ f.status === "added"
148
+ ? "file-added"
149
+ : f.status === "deleted"
150
+ ? "file-deleted"
151
+ : "file-modified";
152
+ const statusChar =
153
+ f.status === "added" ? "A" : f.status === "deleted" ? "D" : "M";
154
+ html += `<div class="changes-file-item ${statusCls}"><span class="file-status">${statusChar}</span><span class="file-path">${escapeHtml(f.path)}</span></div>`;
155
+ }
156
+ html += "</div>";
157
+ }
158
+
159
+ // Diff preview (truncated)
160
+ if (diffData.diff) {
161
+ const truncatedDiff =
162
+ diffData.diff.length > 5000
163
+ ? diffData.diff.substring(0, 5000) + "\n... (truncated)"
164
+ : diffData.diff;
165
+ html += `<details class="changes-diff-details"><summary>Show Diff</summary><pre class="changes-diff">${escapeHtml(truncatedDiff)}</pre></details>`;
166
+ }
167
+
168
+ if (!html) {
169
+ html =
170
+ '<div class="empty-state"><p>No changes detected (worktree may not exist yet)</p></div>';
171
+ }
172
+
173
+ body.innerHTML = html;
174
+ });
175
+ }
176
+
177
+ function connectLogStream(issue: number): void {
178
+ if (sseClient) sseClient.close();
179
+
180
+ // Try SSE endpoint first, fall back to static logs
181
+ sseClient = new SSEClient(
182
+ `/api/logs/${issue}/stream`,
183
+ (data) => {
184
+ if (terminal) terminal.append(data);
185
+ },
186
+ () => {
187
+ // SSE not available, load static logs
188
+ api
189
+ .fetchLogs(issue)
190
+ .then((data) => {
191
+ if (terminal) terminal.append(data.content || "No logs available");
192
+ })
193
+ .catch(() => {
194
+ if (terminal) terminal.append("Failed to load logs");
195
+ });
196
+ },
197
+ );
198
+ sseClient.connect();
199
+ }
200
+
201
+ export const pipelineTheaterView: View = {
202
+ init() {},
203
+
204
+ render(data: FleetState) {
205
+ renderTheater(data);
206
+ },
207
+
208
+ destroy() {
209
+ if (sseClient) {
210
+ sseClient.close();
211
+ sseClient = null;
212
+ }
213
+ if (terminal) {
214
+ terminal.destroy();
215
+ terminal = null;
216
+ }
217
+ selectedIssue = null;
218
+ },
219
+ };