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.
- package/README.md +19 -19
- 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.ts +381 -0
- package/dashboard/src/core/helpers.ts +118 -0
- package/dashboard/src/core/router.ts +190 -0
- package/dashboard/src/core/sse.ts +38 -0
- package/dashboard/src/core/state.ts +150 -0
- package/dashboard/src/core/ws.ts +143 -0
- package/dashboard/src/design/icons.ts +131 -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/docs/AGI-PLATFORM-PLAN.md +5 -5
- package/docs/AGI-WHATS-NEXT.md +19 -16
- package/docs/README.md +2 -0
- package/package.json +8 -1
- package/scripts/check-version-consistency.sh +72 -0
- package/scripts/lib/daemon-adaptive.sh +610 -0
- package/scripts/lib/daemon-dispatch.sh +489 -0
- package/scripts/lib/daemon-failure.sh +387 -0
- package/scripts/lib/daemon-patrol.sh +1113 -0
- package/scripts/lib/daemon-poll.sh +1202 -0
- package/scripts/lib/daemon-state.sh +550 -0
- package/scripts/lib/daemon-triage.sh +490 -0
- package/scripts/lib/helpers.sh +81 -0
- package/scripts/lib/pipeline-intelligence.sh +0 -6
- package/scripts/lib/pipeline-quality-checks.sh +3 -1
- package/scripts/lib/pipeline-stages.sh +20 -0
- package/scripts/sw +109 -168
- package/scripts/sw-activity.sh +1 -1
- package/scripts/sw-adaptive.sh +2 -2
- 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 +1 -1
- 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 +53 -4817
- 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 +49 -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 +3 -3
- 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 +4 -4
- 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 +19 -56
- package/scripts/sw-pipeline.sh.mock +7 -0
- 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 +25 -1
- 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 +1 -1
- package/scripts/sw-scale.sh +11 -5
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +172 -7
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +4 -3
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +2 -1
- package/scripts/sw-stream.sh +8 -2
- package/scripts/sw-swarm.sh +12 -10
- 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 +24 -6
- package/scripts/sw-triage.sh +1 -1
- 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,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
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Machines tab - machine grid, health checks, worker management
|
|
2
|
+
|
|
3
|
+
import { store } from "../core/state";
|
|
4
|
+
import { escapeHtml } from "../core/helpers";
|
|
5
|
+
import { icon } from "../design/icons";
|
|
6
|
+
import {
|
|
7
|
+
setupMachinesModals,
|
|
8
|
+
updateWorkerCount,
|
|
9
|
+
machineHealthCheckAction,
|
|
10
|
+
confirmMachineRemove,
|
|
11
|
+
} from "../components/modal";
|
|
12
|
+
import * as api from "../core/api";
|
|
13
|
+
import type { FleetState, View, MachineInfo, JoinToken } from "../types/api";
|
|
14
|
+
|
|
15
|
+
function fetchMachinesTab(): void {
|
|
16
|
+
api
|
|
17
|
+
.fetchMachines()
|
|
18
|
+
.then((machines) => {
|
|
19
|
+
store.set("machinesCache", machines);
|
|
20
|
+
renderMachinesTab(machines);
|
|
21
|
+
})
|
|
22
|
+
.catch(() => {});
|
|
23
|
+
|
|
24
|
+
api
|
|
25
|
+
.fetchJoinTokens()
|
|
26
|
+
.then(({ tokens }) => {
|
|
27
|
+
store.set("joinTokensCache", tokens);
|
|
28
|
+
renderJoinTokens(tokens);
|
|
29
|
+
})
|
|
30
|
+
.catch(() => {});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderMachinesTab(machines: MachineInfo[]): void {
|
|
34
|
+
const summaryEl = document.getElementById("machines-summary");
|
|
35
|
+
const gridEl = document.getElementById("machines-grid");
|
|
36
|
+
if (!summaryEl || !gridEl) return;
|
|
37
|
+
|
|
38
|
+
summaryEl.innerHTML = renderMachineSummary(machines);
|
|
39
|
+
|
|
40
|
+
if (machines.length === 0) {
|
|
41
|
+
gridEl.innerHTML = `<div class="empty-state">${icon("server", 32)}<p>No machines registered</p></div>`;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
gridEl.innerHTML = machines.map((m) => renderMachineCard(m)).join("");
|
|
46
|
+
|
|
47
|
+
// Wire up action buttons
|
|
48
|
+
gridEl.querySelectorAll(".machine-action-btn").forEach((btn) => {
|
|
49
|
+
btn.addEventListener("click", (e) => {
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
const action = btn.getAttribute("data-machine-action");
|
|
52
|
+
const name = btn.getAttribute("data-machine-name") || "";
|
|
53
|
+
if (action === "check") machineHealthCheckAction(name);
|
|
54
|
+
else if (action === "remove") confirmMachineRemove(name);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Wire up worker sliders
|
|
59
|
+
gridEl.querySelectorAll(".workers-slider").forEach((slider) => {
|
|
60
|
+
slider.addEventListener("input", (e) => {
|
|
61
|
+
const el = e.target as HTMLInputElement;
|
|
62
|
+
const name = el.getAttribute("data-machine-name") || "";
|
|
63
|
+
updateWorkerCount(name, el.value);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderMachineSummary(machines: MachineInfo[]): string {
|
|
69
|
+
let totalMaxWorkers = 0;
|
|
70
|
+
let totalActiveWorkers = 0;
|
|
71
|
+
let onlineCount = 0;
|
|
72
|
+
for (const m of machines) {
|
|
73
|
+
totalMaxWorkers += m.max_workers || 0;
|
|
74
|
+
totalActiveWorkers += m.active_workers || 0;
|
|
75
|
+
if (m.status === "online") onlineCount++;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
`<div class="machines-summary-card"><div class="stat-value">${machines.length}</div><div class="stat-label">Total Machines</div></div>` +
|
|
80
|
+
`<div class="machines-summary-card"><div class="stat-value">${onlineCount}</div><div class="stat-label">Online</div></div>` +
|
|
81
|
+
`<div class="machines-summary-card"><div class="stat-value">${totalActiveWorkers} / ${totalMaxWorkers}</div><div class="stat-label">Active / Max Workers</div></div>`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderMachineCard(machine: MachineInfo): string {
|
|
86
|
+
const name = machine.name || "";
|
|
87
|
+
const host = machine.host || "\u2014";
|
|
88
|
+
const role = machine.role || "worker";
|
|
89
|
+
const status = machine.status || "offline";
|
|
90
|
+
const maxWorkers = machine.max_workers || 4;
|
|
91
|
+
const activeWorkers = machine.active_workers || 0;
|
|
92
|
+
const health = machine.health || {};
|
|
93
|
+
const daemonRunning = health.daemon_running || false;
|
|
94
|
+
const heartbeatCount = health.heartbeat_count || 0;
|
|
95
|
+
const lastHbAge = health.last_heartbeat_s_ago;
|
|
96
|
+
let lastHbText = "\u2014";
|
|
97
|
+
if (typeof lastHbAge === "number" && lastHbAge < 9999) {
|
|
98
|
+
if (lastHbAge < 60) lastHbText = lastHbAge + "s ago";
|
|
99
|
+
else if (lastHbAge < 3600)
|
|
100
|
+
lastHbText = Math.floor(lastHbAge / 60) + "m ago";
|
|
101
|
+
else lastHbText = Math.floor(lastHbAge / 3600) + "h ago";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
`<div class="machine-card" id="machine-card-${escapeHtml(name)}">` +
|
|
106
|
+
`<div class="machine-card-header">` +
|
|
107
|
+
`<span class="presence-dot ${status}"></span>` +
|
|
108
|
+
`<span class="machine-name">${escapeHtml(name)}</span>` +
|
|
109
|
+
`<span class="machine-role">${escapeHtml(role)}</span></div>` +
|
|
110
|
+
`<div class="machine-host">${escapeHtml(host)}</div>` +
|
|
111
|
+
`<div class="machine-workers-section">` +
|
|
112
|
+
`<div class="machine-workers-label-row"><span>Workers</span><span class="workers-count">${activeWorkers} / ${maxWorkers}</span></div>` +
|
|
113
|
+
`<input type="range" class="workers-slider" min="1" max="64" value="${maxWorkers}" data-machine-name="${escapeHtml(name)}" title="Max workers" /></div>` +
|
|
114
|
+
`<div class="machine-health">` +
|
|
115
|
+
`<div class="machine-health-row"><span class="health-label">Daemon</span>` +
|
|
116
|
+
`<span class="health-status ${daemonRunning ? "running" : "stopped"}">${daemonRunning ? "Running" : "Stopped"}</span></div>` +
|
|
117
|
+
`<div class="machine-health-row"><span class="health-label">Heartbeats</span><span class="health-value">${heartbeatCount}</span></div>` +
|
|
118
|
+
`<div class="machine-health-row"><span class="health-label">Last heartbeat</span><span class="health-value">${lastHbText}</span></div></div>` +
|
|
119
|
+
`<div class="machine-card-actions">` +
|
|
120
|
+
`<button class="machine-action-btn" data-machine-action="check" data-machine-name="${escapeHtml(name)}">Check</button>` +
|
|
121
|
+
`<button class="machine-action-btn danger" data-machine-action="remove" data-machine-name="${escapeHtml(name)}">Remove</button></div></div>`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderJoinTokens(tokens: JoinToken[]): void {
|
|
126
|
+
const section = document.getElementById("join-tokens-section");
|
|
127
|
+
const list = document.getElementById("join-tokens-list");
|
|
128
|
+
if (!section || !list) return;
|
|
129
|
+
if (!tokens || tokens.length === 0) {
|
|
130
|
+
section.style.display = "none";
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
section.style.display = "";
|
|
134
|
+
|
|
135
|
+
let html = "";
|
|
136
|
+
for (const t of tokens) {
|
|
137
|
+
const label = t.label || "Unlabeled";
|
|
138
|
+
const created = t.created_at
|
|
139
|
+
? new Date(t.created_at).toLocaleDateString()
|
|
140
|
+
: "\u2014";
|
|
141
|
+
const used = t.used ? "Claimed" : "Active";
|
|
142
|
+
const usedClass = t.used ? "c-amber" : "c-green";
|
|
143
|
+
html +=
|
|
144
|
+
`<div class="join-token-row">` +
|
|
145
|
+
`<span class="join-token-label">${escapeHtml(label)}</span>` +
|
|
146
|
+
`<span class="join-token-created">${created}</span>` +
|
|
147
|
+
`<span class="join-token-status ${usedClass}">${used}</span></div>`;
|
|
148
|
+
}
|
|
149
|
+
list.innerHTML = html;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const machinesView: View = {
|
|
153
|
+
init() {
|
|
154
|
+
setupMachinesModals();
|
|
155
|
+
fetchMachinesTab();
|
|
156
|
+
},
|
|
157
|
+
render(_data: FleetState) {
|
|
158
|
+
const cache = store.get("machinesCache");
|
|
159
|
+
if (cache) renderMachinesTab(cache);
|
|
160
|
+
},
|
|
161
|
+
destroy() {},
|
|
162
|
+
};
|