shipwright-cli 2.2.2 → 2.3.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 (151) hide show
  1. package/README.md +12 -11
  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.test.ts +362 -0
  18. package/dashboard/src/core/api.ts +381 -0
  19. package/dashboard/src/core/helpers.ts +118 -0
  20. package/dashboard/src/core/router.test.ts +266 -0
  21. package/dashboard/src/core/router.ts +190 -0
  22. package/dashboard/src/core/sse.ts +38 -0
  23. package/dashboard/src/core/state.test.ts +235 -0
  24. package/dashboard/src/core/state.ts +150 -0
  25. package/dashboard/src/core/ws.test.ts +216 -0
  26. package/dashboard/src/core/ws.ts +143 -0
  27. package/dashboard/src/design/icons.test.ts +105 -0
  28. package/dashboard/src/design/icons.ts +131 -0
  29. package/dashboard/src/design/tokens.test.ts +204 -0
  30. package/dashboard/src/design/tokens.ts +160 -0
  31. package/dashboard/src/main.ts +68 -0
  32. package/dashboard/src/types/api.ts +337 -0
  33. package/dashboard/src/views/activity.ts +185 -0
  34. package/dashboard/src/views/agent-cockpit.ts +236 -0
  35. package/dashboard/src/views/agents.ts +72 -0
  36. package/dashboard/src/views/fleet-map.ts +299 -0
  37. package/dashboard/src/views/insights.ts +298 -0
  38. package/dashboard/src/views/machines.ts +162 -0
  39. package/dashboard/src/views/metrics.ts +420 -0
  40. package/dashboard/src/views/overview.ts +409 -0
  41. package/dashboard/src/views/pipeline-theater.ts +219 -0
  42. package/dashboard/src/views/pipelines.ts +595 -0
  43. package/dashboard/src/views/team.ts +362 -0
  44. package/dashboard/src/views/timeline.ts +389 -0
  45. package/dashboard/tsconfig.json +21 -0
  46. package/dashboard/vitest.config.ts +27 -0
  47. package/docs/AGI-WHATS-NEXT.md +15 -15
  48. package/package.json +16 -2
  49. package/scripts/lib/helpers.sh +30 -0
  50. package/scripts/lib/pipeline-quality-checks.sh +1 -1
  51. package/scripts/lib/pipeline-stages.sh +59 -0
  52. package/scripts/sw +86 -167
  53. package/scripts/sw-activity.sh +1 -1
  54. package/scripts/sw-adaptive.sh +1 -1
  55. package/scripts/sw-adversarial.sh +1 -1
  56. package/scripts/sw-architecture-enforcer.sh +1 -1
  57. package/scripts/sw-auth.sh +14 -6
  58. package/scripts/sw-autonomous.sh +230 -13
  59. package/scripts/sw-changelog.sh +2 -2
  60. package/scripts/sw-checkpoint.sh +1 -1
  61. package/scripts/sw-ci.sh +1 -1
  62. package/scripts/sw-cleanup.sh +1 -1
  63. package/scripts/sw-code-review.sh +1 -1
  64. package/scripts/sw-connect.sh +1 -1
  65. package/scripts/sw-context.sh +1 -1
  66. package/scripts/sw-cost.sh +1 -1
  67. package/scripts/sw-daemon.sh +2 -2
  68. package/scripts/sw-dashboard.sh +1 -1
  69. package/scripts/sw-db.sh +1 -1
  70. package/scripts/sw-decompose.sh +1 -1
  71. package/scripts/sw-deps.sh +1 -1
  72. package/scripts/sw-developer-simulation.sh +1 -1
  73. package/scripts/sw-discovery.sh +1 -1
  74. package/scripts/sw-doc-fleet.sh +1 -1
  75. package/scripts/sw-docs-agent.sh +1 -1
  76. package/scripts/sw-docs.sh +1 -1
  77. package/scripts/sw-doctor.sh +8 -1
  78. package/scripts/sw-dora.sh +1 -1
  79. package/scripts/sw-durable.sh +1 -1
  80. package/scripts/sw-e2e-orchestrator.sh +1 -1
  81. package/scripts/sw-eventbus.sh +1 -1
  82. package/scripts/sw-feedback.sh +1 -1
  83. package/scripts/sw-fix.sh +6 -5
  84. package/scripts/sw-fleet-discover.sh +1 -1
  85. package/scripts/sw-fleet-viz.sh +1 -1
  86. package/scripts/sw-fleet.sh +1 -1
  87. package/scripts/sw-github-app.sh +5 -2
  88. package/scripts/sw-github-checks.sh +1 -1
  89. package/scripts/sw-github-deploy.sh +1 -1
  90. package/scripts/sw-github-graphql.sh +1 -1
  91. package/scripts/sw-guild.sh +1 -1
  92. package/scripts/sw-heartbeat.sh +1 -1
  93. package/scripts/sw-hygiene.sh +1 -1
  94. package/scripts/sw-incident.sh +1 -1
  95. package/scripts/sw-init.sh +112 -9
  96. package/scripts/sw-instrument.sh +6 -1
  97. package/scripts/sw-intelligence.sh +5 -1
  98. package/scripts/sw-jira.sh +1 -1
  99. package/scripts/sw-launchd.sh +1 -1
  100. package/scripts/sw-linear.sh +20 -9
  101. package/scripts/sw-logs.sh +1 -1
  102. package/scripts/sw-loop.sh +2 -1
  103. package/scripts/sw-memory.sh +10 -1
  104. package/scripts/sw-mission-control.sh +1 -1
  105. package/scripts/sw-model-router.sh +4 -1
  106. package/scripts/sw-otel.sh +1 -1
  107. package/scripts/sw-oversight.sh +1 -1
  108. package/scripts/sw-pipeline-composer.sh +3 -1
  109. package/scripts/sw-pipeline-vitals.sh +4 -6
  110. package/scripts/sw-pipeline.sh +4 -1
  111. package/scripts/sw-pm.sh +5 -2
  112. package/scripts/sw-pr-lifecycle.sh +1 -1
  113. package/scripts/sw-predictive.sh +4 -1
  114. package/scripts/sw-prep.sh +3 -2
  115. package/scripts/sw-ps.sh +1 -1
  116. package/scripts/sw-public-dashboard.sh +10 -4
  117. package/scripts/sw-quality.sh +1 -1
  118. package/scripts/sw-reaper.sh +1 -1
  119. package/scripts/sw-recruit.sh +16 -0
  120. package/scripts/sw-regression.sh +2 -1
  121. package/scripts/sw-release-manager.sh +1 -1
  122. package/scripts/sw-release.sh +7 -5
  123. package/scripts/sw-remote.sh +1 -1
  124. package/scripts/sw-replay.sh +1 -1
  125. package/scripts/sw-retro.sh +4 -1
  126. package/scripts/sw-scale.sh +4 -1
  127. package/scripts/sw-security-audit.sh +1 -1
  128. package/scripts/sw-self-optimize.sh +113 -1
  129. package/scripts/sw-session.sh +1 -1
  130. package/scripts/sw-setup.sh +1 -1
  131. package/scripts/sw-standup.sh +2 -1
  132. package/scripts/sw-status.sh +1 -1
  133. package/scripts/sw-strategic.sh +2 -1
  134. package/scripts/sw-stream.sh +1 -1
  135. package/scripts/sw-swarm.sh +6 -1
  136. package/scripts/sw-team-stages.sh +1 -1
  137. package/scripts/sw-templates.sh +1 -1
  138. package/scripts/sw-testgen.sh +3 -2
  139. package/scripts/sw-tmux-pipeline.sh +2 -1
  140. package/scripts/sw-tmux.sh +1 -1
  141. package/scripts/sw-trace.sh +1 -1
  142. package/scripts/sw-tracker-jira.sh +1 -0
  143. package/scripts/sw-tracker-linear.sh +1 -0
  144. package/scripts/sw-tracker.sh +1 -1
  145. package/scripts/sw-triage.sh +198 -11
  146. package/scripts/sw-upgrade.sh +1 -1
  147. package/scripts/sw-ux.sh +1 -1
  148. package/scripts/sw-webhook.sh +1 -1
  149. package/scripts/sw-widgets.sh +2 -2
  150. package/scripts/sw-worktree.sh +1 -1
  151. package/dashboard/public/app.js +0 -4422
@@ -0,0 +1,299 @@
1
+ // Fleet Map - Canvas2D topology with stage columns, animated pipeline particles
2
+
3
+ import { store } from "../core/state";
4
+ import { colors, STAGES, STAGE_HEX, STAGE_SHORT } from "../design/tokens";
5
+ import { formatDuration } from "../core/helpers";
6
+ import {
7
+ CanvasRenderer,
8
+ drawText,
9
+ drawCircle,
10
+ drawRoundRect,
11
+ type CanvasScene,
12
+ } from "../canvas/renderer";
13
+ import {
14
+ computeLayout,
15
+ type LayoutNode,
16
+ type StageColumn,
17
+ } from "../canvas/layout";
18
+ import { ParticleSystem } from "../canvas/particles";
19
+ import { hitTestNode, ZoomPan } from "../canvas/interactions";
20
+ import { drawTooltip, drawStageLabel } from "../canvas/overlays";
21
+ import * as api from "../core/api";
22
+ import type { FleetState, View } from "../types/api";
23
+
24
+ let renderer: CanvasRenderer | null = null;
25
+ let scene: FleetMapScene | null = null;
26
+
27
+ class FleetMapScene implements CanvasScene {
28
+ nodes: LayoutNode[] = [];
29
+ columns: StageColumn[] = [];
30
+ particles = new ParticleSystem();
31
+ zoomPan = new ZoomPan();
32
+ hoveredNode: LayoutNode | null = null;
33
+ predictions: Record<
34
+ number,
35
+ { eta_s?: number; success_probability?: number; estimated_cost?: number }
36
+ > = {};
37
+ time = 0;
38
+ width = 0;
39
+ height = 0;
40
+
41
+ updateData(data: FleetState): void {
42
+ if (!data.pipelines) return;
43
+ const { nodes, columns } = computeLayout(
44
+ data.pipelines,
45
+ this.width,
46
+ this.height,
47
+ );
48
+
49
+ // Smooth transition: find matching nodes and lerp
50
+ for (const newNode of nodes) {
51
+ const existing = this.nodes.find((n) => n.issue === newNode.issue);
52
+ if (existing) {
53
+ newNode.x = existing.x;
54
+ newNode.y = existing.y;
55
+ }
56
+ }
57
+
58
+ this.nodes = nodes;
59
+ this.columns = columns;
60
+
61
+ // Fetch predictions for active pipelines
62
+ for (const p of data.pipelines) {
63
+ if (!this.predictions[p.issue]) {
64
+ api.fetchPredictions(p.issue).then((pred) => {
65
+ this.predictions[p.issue] = pred;
66
+ });
67
+ }
68
+ }
69
+ }
70
+
71
+ update(dt: number): void {
72
+ this.time += dt;
73
+
74
+ // Smooth node movement
75
+ for (const node of this.nodes) {
76
+ const dx = node.targetX - node.x;
77
+ const dy = node.targetY - node.y;
78
+ node.x += dx * 5 * dt;
79
+ node.y += dy * 5 * dt;
80
+ }
81
+
82
+ // Emit trail particles for active nodes
83
+ for (const node of this.nodes) {
84
+ if (node.status === "active" || node.status === "running") {
85
+ if (Math.random() < 0.3) {
86
+ this.particles.emit(
87
+ node.x + (Math.random() - 0.5) * node.radius,
88
+ node.y + (Math.random() - 0.5) * node.radius,
89
+ "trail",
90
+ node.color,
91
+ 1,
92
+ );
93
+ }
94
+ }
95
+ }
96
+
97
+ // Ambient particles along columns
98
+ if (Math.random() < 0.05) {
99
+ const col = this.columns[Math.floor(Math.random() * this.columns.length)];
100
+ if (col) {
101
+ this.particles.emit(
102
+ col.x + Math.random() * col.width,
103
+ Math.random() * this.height,
104
+ "ambient",
105
+ col.color,
106
+ 1,
107
+ );
108
+ }
109
+ }
110
+
111
+ this.particles.update(dt);
112
+ }
113
+
114
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
115
+ // Background
116
+ ctx.fillStyle = colors.bg.abyss;
117
+ ctx.fillRect(0, 0, width, height);
118
+
119
+ ctx.save();
120
+ this.zoomPan.apply(ctx);
121
+
122
+ // Stage columns
123
+ for (const col of this.columns) {
124
+ // Column separator
125
+ ctx.fillStyle = colors.bg.deep;
126
+ ctx.fillRect(col.x, 0, col.width, height);
127
+
128
+ // Column border
129
+ ctx.strokeStyle = colors.bg.foam + "40";
130
+ ctx.lineWidth = 1;
131
+ ctx.beginPath();
132
+ ctx.moveTo(col.x, 0);
133
+ ctx.lineTo(col.x, height);
134
+ ctx.stroke();
135
+
136
+ // Stage label at top
137
+ drawStageLabel(ctx, col.name, col.x + col.width / 2, 20, col.color);
138
+ }
139
+
140
+ // Connection lines between pipeline stages
141
+ for (const node of this.nodes) {
142
+ if (node.stageIndex > 0) {
143
+ const prevCol = this.columns[node.stageIndex - 1];
144
+ if (prevCol) {
145
+ const fromX = prevCol.x + prevCol.width / 2;
146
+ ctx.globalAlpha = 0.15;
147
+ ctx.strokeStyle = node.color;
148
+ ctx.lineWidth = 1;
149
+ ctx.setLineDash([4, 4]);
150
+ ctx.beginPath();
151
+ ctx.moveTo(fromX, node.y);
152
+ ctx.lineTo(node.x, node.y);
153
+ ctx.stroke();
154
+ ctx.setLineDash([]);
155
+ ctx.globalAlpha = 1;
156
+ }
157
+ }
158
+ }
159
+
160
+ // Particles (behind nodes)
161
+ this.particles.draw(ctx);
162
+
163
+ // Pipeline nodes
164
+ for (const node of this.nodes) {
165
+ const isHovered = this.hoveredNode === node;
166
+ const r = isHovered ? node.radius * 1.2 : node.radius;
167
+
168
+ // Glow
169
+ if (node.status !== "failed") {
170
+ ctx.globalAlpha = 0.2 + 0.1 * Math.sin(this.time * 2 + node.issue);
171
+ drawCircle(ctx, node.x, node.y, r + 6, undefined, node.color, 2);
172
+ ctx.globalAlpha = 1;
173
+ }
174
+
175
+ // Node circle
176
+ drawCircle(ctx, node.x, node.y, r, node.color);
177
+
178
+ // Issue number
179
+ drawText(ctx, "#" + node.issue, node.x, node.y - 4, {
180
+ font: "monoSm",
181
+ color: colors.bg.abyss,
182
+ align: "center",
183
+ baseline: "middle",
184
+ });
185
+
186
+ // Progress arc
187
+ if (node.progress > 0 && node.progress < 1) {
188
+ ctx.beginPath();
189
+ ctx.arc(
190
+ node.x,
191
+ node.y,
192
+ r + 3,
193
+ -Math.PI / 2,
194
+ -Math.PI / 2 + node.progress * Math.PI * 2,
195
+ );
196
+ ctx.strokeStyle = colors.accent.cyan;
197
+ ctx.lineWidth = 2;
198
+ ctx.stroke();
199
+ }
200
+ }
201
+
202
+ ctx.restore();
203
+
204
+ // Tooltip (drawn in screen space)
205
+ if (this.hoveredNode) {
206
+ drawTooltip(
207
+ ctx,
208
+ this.hoveredNode,
209
+ this.predictions[this.hoveredNode.issue],
210
+ );
211
+ }
212
+
213
+ // HUD: pipeline count
214
+ drawText(
215
+ ctx,
216
+ `${this.nodes.length} active pipeline${this.nodes.length !== 1 ? "s" : ""}`,
217
+ 16,
218
+ height - 30,
219
+ {
220
+ font: "caption",
221
+ color: colors.text.muted,
222
+ },
223
+ );
224
+ drawText(ctx, `${this.particles.count} particles`, 16, height - 16, {
225
+ font: "tiny",
226
+ color: colors.text.muted,
227
+ });
228
+ }
229
+
230
+ onResize(width: number, height: number): void {
231
+ this.width = width;
232
+ this.height = height;
233
+ // Recompute layout
234
+ const data = store.get("fleetState");
235
+ if (data?.pipelines) {
236
+ const { nodes, columns } = computeLayout(data.pipelines, width, height);
237
+ this.nodes = nodes;
238
+ this.columns = columns;
239
+ }
240
+ }
241
+
242
+ onMouseMove(x: number, y: number): void {
243
+ const world = this.zoomPan.screenToWorld(x, y);
244
+ this.hoveredNode = hitTestNode(this.nodes, world.x, world.y);
245
+ if (renderer) {
246
+ renderer.getCanvas().style.cursor = this.hoveredNode
247
+ ? "pointer"
248
+ : "default";
249
+ }
250
+ }
251
+
252
+ onMouseClick(x: number, y: number): void {
253
+ const world = this.zoomPan.screenToWorld(x, y);
254
+ const node = hitTestNode(this.nodes, world.x, world.y);
255
+ if (node) {
256
+ this.particles.burstAt(node.x, node.y, node.color);
257
+ import("../core/router").then(({ switchTab }) => {
258
+ switchTab("pipelines");
259
+ import("./pipelines").then(({ fetchPipelineDetail }) => {
260
+ fetchPipelineDetail(node.issue);
261
+ });
262
+ });
263
+ }
264
+ }
265
+
266
+ onMouseWheel(delta: number): void {
267
+ this.zoomPan.zoom(delta, this.width / 2, this.height / 2);
268
+ }
269
+ }
270
+
271
+ export const fleetMapView: View = {
272
+ init() {
273
+ const container = document.getElementById("panel-fleet-map");
274
+ if (!container) return;
275
+
276
+ container.innerHTML =
277
+ '<div class="fleet-map-canvas" style="width:100%;height:calc(100vh - 160px);position:relative;"></div>';
278
+ const canvasContainer = container.querySelector(
279
+ ".fleet-map-canvas",
280
+ ) as HTMLElement;
281
+
282
+ renderer = new CanvasRenderer(canvasContainer);
283
+ scene = new FleetMapScene();
284
+ renderer.setScene(scene);
285
+ renderer.start();
286
+ },
287
+
288
+ render(data: FleetState) {
289
+ if (scene) scene.updateData(data);
290
+ },
291
+
292
+ destroy() {
293
+ if (renderer) {
294
+ renderer.destroy();
295
+ renderer = null;
296
+ }
297
+ scene = null;
298
+ },
299
+ };
@@ -0,0 +1,298 @@
1
+ // Insights tab - failure patterns, patrol findings, decision log, failure heatmap
2
+
3
+ import { store } from "../core/state";
4
+ import { escapeHtml, formatTime } from "../core/helpers";
5
+ import { icon } from "../design/icons";
6
+ import * as api from "../core/api";
7
+ import type {
8
+ FleetState,
9
+ View,
10
+ InsightsData,
11
+ FailurePattern,
12
+ Decision,
13
+ PatrolFinding,
14
+ HeatmapData,
15
+ } from "../types/api";
16
+
17
+ function fetchInsightsData(): void {
18
+ const cache = store.get("insightsCache");
19
+ if (cache) {
20
+ renderInsightsTab(cache);
21
+ return;
22
+ }
23
+
24
+ const panel = document.getElementById("panel-insights");
25
+ if (panel)
26
+ panel.innerHTML =
27
+ '<div class="empty-state"><p>Loading insights...</p></div>';
28
+
29
+ const results: InsightsData = {
30
+ patterns: null,
31
+ decisions: null,
32
+ patrol: null,
33
+ heatmap: null,
34
+ globalLearnings: null,
35
+ };
36
+ let pending = 5;
37
+
38
+ function checkDone() {
39
+ pending--;
40
+ if (pending <= 0) {
41
+ store.set("insightsCache", results);
42
+ renderInsightsTab(results);
43
+ }
44
+ }
45
+
46
+ api
47
+ .fetchPatterns()
48
+ .then((d) => {
49
+ results.patterns = d.patterns || [];
50
+ })
51
+ .catch(() => {
52
+ results.patterns = [];
53
+ })
54
+ .then(checkDone);
55
+ api
56
+ .fetchDecisions()
57
+ .then((d) => {
58
+ results.decisions = d.decisions || [];
59
+ })
60
+ .catch(() => {
61
+ results.decisions = [];
62
+ })
63
+ .then(checkDone);
64
+ api
65
+ .fetchPatrol()
66
+ .then((d) => {
67
+ results.patrol = d.findings || [];
68
+ })
69
+ .catch(() => {
70
+ results.patrol = [];
71
+ })
72
+ .then(checkDone);
73
+ api
74
+ .fetchHeatmap()
75
+ .then((d) => {
76
+ results.heatmap = d;
77
+ })
78
+ .catch(() => {
79
+ results.heatmap = null;
80
+ })
81
+ .then(checkDone);
82
+ api
83
+ .fetchGlobalLearnings()
84
+ .then((d) => {
85
+ results.globalLearnings = d.learnings || [];
86
+ })
87
+ .catch(() => {
88
+ results.globalLearnings = [];
89
+ })
90
+ .then(checkDone);
91
+ }
92
+
93
+ function renderInsightsTab(data: InsightsData): void {
94
+ const panel = document.getElementById("panel-insights");
95
+ if (!panel) return;
96
+
97
+ let html = '<div class="insights-grid">';
98
+ html +=
99
+ `<div class="insights-section"><div class="section-header"><h3>${icon("lightbulb", 18)} Failure Patterns</h3></div>` +
100
+ `<div id="failure-patterns-content">${renderFailurePatterns(data.patterns || [])}</div></div>`;
101
+ html +=
102
+ `<div class="insights-section"><div class="section-header"><h3>${icon("shield-alert", 18)} Patrol Findings</h3></div>` +
103
+ `<div id="patrol-findings-content">${renderPatrolFindings(data.patrol || [])}</div></div>`;
104
+ html +=
105
+ `<div class="insights-section insights-full-width"><div class="section-header"><h3>${icon("git-branch", 18)} Decision Log</h3></div>` +
106
+ `<div id="decision-log-content">${renderDecisionLog(data.decisions || [])}</div></div>`;
107
+ html +=
108
+ `<div class="insights-section insights-full-width"><div class="section-header"><h3>${icon("bar-chart-3", 18)} Failure Heatmap</h3></div>` +
109
+ `<div id="failure-heatmap-content">${renderFailureHeatmap(data.heatmap)}</div></div>`;
110
+ html +=
111
+ `<div class="insights-section insights-full-width"><div class="section-header"><h3>${icon("brain", 18)} Global Learnings</h3></div>` +
112
+ `<div id="global-learnings-content">${renderGlobalLearnings(data.globalLearnings || [])}</div></div>`;
113
+ html +=
114
+ `<div class="insights-section insights-full-width"><div class="section-header"><h3>${icon("clipboard-list", 18)} Audit Log</h3></div>` +
115
+ `<div id="audit-log-content"><div class="empty-state"><p>Loading...</p></div></div></div>`;
116
+ html += "</div>";
117
+ panel.innerHTML = html;
118
+
119
+ // Load audit log asynchronously
120
+ const auditContainer = document.getElementById("audit-log-content");
121
+ if (auditContainer) {
122
+ api
123
+ .fetchAuditLog()
124
+ .then((data) => {
125
+ const entries = data.entries || [];
126
+ if (entries.length === 0) {
127
+ auditContainer.innerHTML =
128
+ '<div class="empty-state"><p>No audit entries. Human interventions will appear here.</p></div>';
129
+ return;
130
+ }
131
+ let html2 = '<div class="audit-list">';
132
+ for (const e of entries.slice(0, 50)) {
133
+ const ts = e.ts ? formatTime(String(e.ts)) : "";
134
+ const action = String(e.action || "unknown");
135
+ const issue = e.issue ? ` #${e.issue}` : "";
136
+ const details = Object.entries(e)
137
+ .filter(([k]) => !["ts", "ts_epoch", "action", "issue"].includes(k))
138
+ .map(([k, v]) => `${k}: ${String(v)}`)
139
+ .join(", ");
140
+ html2 +=
141
+ `<div class="audit-entry"><span class="audit-ts">${ts}</span>` +
142
+ `<span class="audit-action">${escapeHtml(action)}${issue}</span>` +
143
+ (details
144
+ ? `<span class="audit-details">${escapeHtml(details)}</span>`
145
+ : "") +
146
+ "</div>";
147
+ }
148
+ html2 += "</div>";
149
+ auditContainer.innerHTML = html2;
150
+ })
151
+ .catch(() => {
152
+ auditContainer.innerHTML =
153
+ '<div class="empty-state"><p>Could not load audit log</p></div>';
154
+ });
155
+ }
156
+ }
157
+
158
+ function renderFailurePatterns(patterns: FailurePattern[]): string {
159
+ if (!patterns.length)
160
+ return '<div class="empty-state"><p>No failure patterns recorded</p></div>';
161
+ const sorted = [...patterns].sort(
162
+ (a, b) => (b.frequency || b.count || 0) - (a.frequency || a.count || 0),
163
+ );
164
+ let html = "";
165
+ for (const p of sorted) {
166
+ const freq = p.frequency || p.count || 0;
167
+ html +=
168
+ `<div class="pattern-card"><div class="pattern-card-header">` +
169
+ `<span class="pattern-desc">${escapeHtml(p.description || p.pattern || "")}</span>` +
170
+ `<span class="pattern-freq-badge">${freq}x</span></div>`;
171
+ if (p.root_cause)
172
+ html += `<div class="pattern-detail"><span class="pattern-label">Root cause:</span> ${escapeHtml(p.root_cause)}</div>`;
173
+ if (p.fix || p.suggested_fix)
174
+ html += `<div class="pattern-detail pattern-fix"><span class="pattern-label">Fix:</span> ${escapeHtml(p.fix || p.suggested_fix || "")}</div>`;
175
+ html += "</div>";
176
+ }
177
+ return html;
178
+ }
179
+
180
+ function renderPatrolFindings(findings: PatrolFinding[]): string {
181
+ if (!findings.length)
182
+ return '<div class="empty-state"><p>No patrol findings</p></div>';
183
+ let html = "";
184
+ for (const f of findings) {
185
+ const severity = (f.severity || "low").toLowerCase();
186
+ html +=
187
+ `<div class="patrol-card"><div class="patrol-card-header">` +
188
+ `<span class="patrol-severity-badge severity-${escapeHtml(severity)}">${escapeHtml(severity.toUpperCase())}</span>` +
189
+ `<span class="patrol-type">${escapeHtml(f.type || f.category || "")}</span></div>` +
190
+ `<div class="patrol-desc">${escapeHtml(f.description || f.message || "")}</div>` +
191
+ (f.file ? `<div class="patrol-file">${escapeHtml(f.file)}</div>` : "") +
192
+ "</div>";
193
+ }
194
+ return html;
195
+ }
196
+
197
+ function renderDecisionLog(decisions: Decision[]): string {
198
+ if (!decisions.length)
199
+ return '<div class="empty-state"><p>No decisions logged</p></div>';
200
+ let html = '<div class="decision-list">';
201
+ for (const d of decisions) {
202
+ html +=
203
+ `<div class="decision-row">` +
204
+ `<span class="decision-ts">${formatTime(d.timestamp || d.ts)}</span>` +
205
+ `<span class="decision-action">${escapeHtml(d.action || d.decision || "")}</span>` +
206
+ `<span class="decision-outcome">${escapeHtml(d.outcome || d.result || "")}</span>` +
207
+ (d.issue ? `<span class="decision-issue">#${d.issue}</span>` : "") +
208
+ "</div>";
209
+ }
210
+ html += "</div>";
211
+ return html;
212
+ }
213
+
214
+ function renderGlobalLearnings(
215
+ learnings: Array<Record<string, unknown>>,
216
+ ): string {
217
+ if (!learnings.length)
218
+ return '<div class="empty-state"><p>No global learnings yet. Agents accumulate learnings across pipelines.</p></div>';
219
+ let html = '<div class="learnings-list">';
220
+ for (const l of learnings) {
221
+ const category = String(l.category || l.type || "general");
222
+ const content = String(l.content || l.description || l.learning || "");
223
+ const source = l.source
224
+ ? `<span class="learning-source">${escapeHtml(String(l.source))}</span>`
225
+ : "";
226
+ const ts = l.timestamp || l.ts;
227
+ const time = ts
228
+ ? `<span class="learning-time">${formatTime(String(ts))}</span>`
229
+ : "";
230
+ html +=
231
+ `<div class="learning-card">` +
232
+ `<div class="learning-header"><span class="learning-category">${escapeHtml(category)}</span>${source}${time}</div>` +
233
+ `<div class="learning-content">${escapeHtml(content)}</div></div>`;
234
+ }
235
+ html += "</div>";
236
+ return html;
237
+ }
238
+
239
+ function renderFailureHeatmap(data: HeatmapData | null): string {
240
+ if (!data?.heatmap)
241
+ return '<div class="empty-state"><p>No heatmap data</p></div>';
242
+
243
+ const heatmap = data.heatmap;
244
+ const stages = Object.keys(heatmap);
245
+ if (stages.length === 0)
246
+ return '<div class="empty-state"><p>No heatmap data</p></div>';
247
+
248
+ const daysSet = new Set<string>();
249
+ for (const stage of stages) {
250
+ for (const day of Object.keys(heatmap[stage])) daysSet.add(day);
251
+ }
252
+ const days = Array.from(daysSet).sort();
253
+ if (days.length === 0)
254
+ return '<div class="empty-state"><p>No heatmap data</p></div>';
255
+
256
+ let maxCount = 0;
257
+ for (const stage of stages) {
258
+ for (const day of days) {
259
+ const count = heatmap[stage]?.[day] || 0;
260
+ if (count > maxCount) maxCount = count;
261
+ }
262
+ }
263
+ if (maxCount === 0) maxCount = 1;
264
+
265
+ let html = `<div class="heatmap-grid" style="grid-template-columns: 100px repeat(${days.length}, 1fr)">`;
266
+ html += '<div class="heatmap-corner"></div>';
267
+ for (const d of days) {
268
+ const parts = d.split("-");
269
+ const label = parts.length >= 3 ? parts[1] + "/" + parts[2] : d;
270
+ html += `<div class="heatmap-day-label">${escapeHtml(label)}</div>`;
271
+ }
272
+
273
+ for (const s of stages) {
274
+ html += `<div class="heatmap-stage-label">${escapeHtml(s)}</div>`;
275
+ for (const d of days) {
276
+ const count = heatmap[s]?.[d] || 0;
277
+ const intensity = count / maxCount;
278
+ const bgColor =
279
+ count === 0
280
+ ? "transparent"
281
+ : `rgba(244, 63, 94, ${(0.2 + intensity * 0.8).toFixed(2)})`;
282
+ html += `<div class="heatmap-cell" style="background:${bgColor}" title="${escapeHtml(s)} ${escapeHtml(d)}: ${count} failures">${count > 0 ? count : ""}</div>`;
283
+ }
284
+ }
285
+ html += "</div>";
286
+ return html;
287
+ }
288
+
289
+ export const insightsView: View = {
290
+ init() {
291
+ fetchInsightsData();
292
+ },
293
+ render(_data: FleetState) {
294
+ const cache = store.get("insightsCache");
295
+ if (cache) renderInsightsTab(cache);
296
+ },
297
+ destroy() {},
298
+ };