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.
- package/README.md +12 -11
- package/dashboard/public/index.html +224 -8
- package/dashboard/public/styles.css +1078 -4
- package/dashboard/server.ts +1100 -15
- package/dashboard/src/canvas/interactions.ts +74 -0
- package/dashboard/src/canvas/layout.ts +85 -0
- package/dashboard/src/canvas/overlays.ts +117 -0
- package/dashboard/src/canvas/particles.ts +105 -0
- package/dashboard/src/canvas/renderer.ts +191 -0
- package/dashboard/src/components/charts/bar.ts +54 -0
- package/dashboard/src/components/charts/donut.ts +25 -0
- package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
- package/dashboard/src/components/charts/sparkline.ts +82 -0
- package/dashboard/src/components/header.ts +616 -0
- package/dashboard/src/components/modal.ts +413 -0
- package/dashboard/src/components/terminal.ts +144 -0
- package/dashboard/src/core/api.test.ts +362 -0
- package/dashboard/src/core/api.ts +381 -0
- package/dashboard/src/core/helpers.ts +118 -0
- package/dashboard/src/core/router.test.ts +266 -0
- package/dashboard/src/core/router.ts +190 -0
- package/dashboard/src/core/sse.ts +38 -0
- package/dashboard/src/core/state.test.ts +235 -0
- package/dashboard/src/core/state.ts +150 -0
- package/dashboard/src/core/ws.test.ts +216 -0
- package/dashboard/src/core/ws.ts +143 -0
- package/dashboard/src/design/icons.test.ts +105 -0
- package/dashboard/src/design/icons.ts +131 -0
- package/dashboard/src/design/tokens.test.ts +204 -0
- package/dashboard/src/design/tokens.ts +160 -0
- package/dashboard/src/main.ts +68 -0
- package/dashboard/src/types/api.ts +337 -0
- package/dashboard/src/views/activity.ts +185 -0
- package/dashboard/src/views/agent-cockpit.ts +236 -0
- package/dashboard/src/views/agents.ts +72 -0
- package/dashboard/src/views/fleet-map.ts +299 -0
- package/dashboard/src/views/insights.ts +298 -0
- package/dashboard/src/views/machines.ts +162 -0
- package/dashboard/src/views/metrics.ts +420 -0
- package/dashboard/src/views/overview.ts +409 -0
- package/dashboard/src/views/pipeline-theater.ts +219 -0
- package/dashboard/src/views/pipelines.ts +595 -0
- package/dashboard/src/views/team.ts +362 -0
- package/dashboard/src/views/timeline.ts +389 -0
- package/dashboard/tsconfig.json +21 -0
- package/dashboard/vitest.config.ts +27 -0
- package/docs/AGI-WHATS-NEXT.md +15 -15
- package/package.json +16 -2
- package/scripts/lib/helpers.sh +30 -0
- package/scripts/lib/pipeline-quality-checks.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +59 -0
- package/scripts/sw +86 -167
- package/scripts/sw-activity.sh +1 -1
- package/scripts/sw-adaptive.sh +1 -1
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +14 -6
- package/scripts/sw-autonomous.sh +230 -13
- package/scripts/sw-changelog.sh +2 -2
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +1 -1
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +1 -1
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +1 -1
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +2 -2
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +1 -1
- package/scripts/sw-decompose.sh +1 -1
- package/scripts/sw-deps.sh +1 -1
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +1 -1
- package/scripts/sw-doc-fleet.sh +1 -1
- package/scripts/sw-docs-agent.sh +1 -1
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +8 -1
- package/scripts/sw-dora.sh +1 -1
- package/scripts/sw-durable.sh +1 -1
- package/scripts/sw-e2e-orchestrator.sh +1 -1
- package/scripts/sw-eventbus.sh +1 -1
- package/scripts/sw-feedback.sh +1 -1
- package/scripts/sw-fix.sh +6 -5
- package/scripts/sw-fleet-discover.sh +1 -1
- package/scripts/sw-fleet-viz.sh +1 -1
- package/scripts/sw-fleet.sh +1 -1
- package/scripts/sw-github-app.sh +5 -2
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +1 -1
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +1 -1
- package/scripts/sw-incident.sh +1 -1
- package/scripts/sw-init.sh +112 -9
- package/scripts/sw-instrument.sh +6 -1
- package/scripts/sw-intelligence.sh +5 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +1 -1
- package/scripts/sw-linear.sh +20 -9
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +2 -1
- package/scripts/sw-memory.sh +10 -1
- package/scripts/sw-mission-control.sh +1 -1
- package/scripts/sw-model-router.sh +4 -1
- package/scripts/sw-otel.sh +1 -1
- package/scripts/sw-oversight.sh +1 -1
- package/scripts/sw-pipeline-composer.sh +3 -1
- package/scripts/sw-pipeline-vitals.sh +4 -6
- package/scripts/sw-pipeline.sh +4 -1
- package/scripts/sw-pm.sh +5 -2
- package/scripts/sw-pr-lifecycle.sh +1 -1
- package/scripts/sw-predictive.sh +4 -1
- package/scripts/sw-prep.sh +3 -2
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +10 -4
- package/scripts/sw-quality.sh +1 -1
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +16 -0
- package/scripts/sw-regression.sh +2 -1
- package/scripts/sw-release-manager.sh +1 -1
- package/scripts/sw-release.sh +7 -5
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +1 -1
- package/scripts/sw-retro.sh +4 -1
- package/scripts/sw-scale.sh +4 -1
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +113 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +2 -1
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +2 -1
- package/scripts/sw-stream.sh +1 -1
- package/scripts/sw-swarm.sh +6 -1
- package/scripts/sw-team-stages.sh +1 -1
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +3 -2
- package/scripts/sw-tmux-pipeline.sh +2 -1
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +1 -1
- package/scripts/sw-tracker-jira.sh +1 -0
- package/scripts/sw-tracker-linear.sh +1 -0
- package/scripts/sw-tracker.sh +1 -1
- package/scripts/sw-triage.sh +198 -11
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +1 -1
- package/scripts/sw-webhook.sh +1 -1
- package/scripts/sw-widgets.sh +2 -2
- package/scripts/sw-worktree.sh +1 -1
- 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
|
+
};
|