shipwright-cli 2.2.2 → 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 +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.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-WHATS-NEXT.md +15 -15
- package/package.json +8 -1
- package/scripts/lib/helpers.sh +30 -0
- package/scripts/lib/pipeline-quality-checks.sh +1 -1
- 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 +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 +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 +1 -1
- package/scripts/sw-scale.sh +4 -1
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +15 -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 +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,381 @@
|
|
|
1
|
+
// Typed REST client for all dashboard API endpoints
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
PipelineDetail,
|
|
5
|
+
MetricsData,
|
|
6
|
+
TimelineEntry,
|
|
7
|
+
MachineInfo,
|
|
8
|
+
JoinToken,
|
|
9
|
+
CostBreakdown,
|
|
10
|
+
DaemonConfig,
|
|
11
|
+
AlertInfo,
|
|
12
|
+
InsightsData,
|
|
13
|
+
HeatmapData,
|
|
14
|
+
TeamData,
|
|
15
|
+
TeamActivityEvent,
|
|
16
|
+
StagePerformance,
|
|
17
|
+
UserInfo,
|
|
18
|
+
} from "../types/api";
|
|
19
|
+
|
|
20
|
+
async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
|
21
|
+
const resp = await fetch(url, options);
|
|
22
|
+
if (!resp.ok) {
|
|
23
|
+
const body = await resp
|
|
24
|
+
.json()
|
|
25
|
+
.catch(() => ({ error: `HTTP ${resp.status}` }));
|
|
26
|
+
throw new Error(body.error || `HTTP ${resp.status}`);
|
|
27
|
+
}
|
|
28
|
+
return resp.json();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function post<T>(url: string, body?: unknown): Promise<T> {
|
|
32
|
+
const opts: RequestInit = {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
};
|
|
36
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
37
|
+
return request<T>(url, opts);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function patch<T>(url: string, body: unknown): Promise<T> {
|
|
41
|
+
return request<T>(url, {
|
|
42
|
+
method: "PATCH",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: JSON.stringify(body),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function del<T>(url: string): Promise<T> {
|
|
49
|
+
return request<T>(url, {
|
|
50
|
+
method: "DELETE",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// User
|
|
56
|
+
export const fetchMe = () => request<UserInfo>("/api/me");
|
|
57
|
+
|
|
58
|
+
// Pipeline detail
|
|
59
|
+
export const fetchPipelineDetail = (issue: number | string) =>
|
|
60
|
+
request<PipelineDetail>(`/api/pipeline/${encodeURIComponent(issue)}`);
|
|
61
|
+
|
|
62
|
+
// Metrics
|
|
63
|
+
export const fetchMetricsHistory = (period = 30) =>
|
|
64
|
+
request<MetricsData>(`/api/metrics/history?period=${period}`);
|
|
65
|
+
|
|
66
|
+
// Timeline — server returns bare array
|
|
67
|
+
export const fetchTimeline = (range = "24h") =>
|
|
68
|
+
request<TimelineEntry[]>(`/api/timeline?range=${range}`);
|
|
69
|
+
|
|
70
|
+
// Activity
|
|
71
|
+
export const fetchActivity = (params: {
|
|
72
|
+
limit?: number;
|
|
73
|
+
offset?: number;
|
|
74
|
+
type?: string;
|
|
75
|
+
issue?: string;
|
|
76
|
+
}) => {
|
|
77
|
+
const qs = new URLSearchParams();
|
|
78
|
+
if (params.limit) qs.set("limit", String(params.limit));
|
|
79
|
+
if (params.offset) qs.set("offset", String(params.offset));
|
|
80
|
+
if (params.type && params.type !== "all") qs.set("type", params.type);
|
|
81
|
+
if (params.issue) qs.set("issue", params.issue);
|
|
82
|
+
return request<{ events: Array<Record<string, unknown>>; hasMore: boolean }>(
|
|
83
|
+
`/api/activity?${qs}`,
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Machines — server returns bare array
|
|
88
|
+
export const fetchMachines = () => request<MachineInfo[]>("/api/machines");
|
|
89
|
+
export const addMachine = (body: Record<string, unknown>) =>
|
|
90
|
+
post<MachineInfo>("/api/machines", body);
|
|
91
|
+
export const updateMachine = (name: string, body: Record<string, unknown>) =>
|
|
92
|
+
patch<MachineInfo>(`/api/machines/${encodeURIComponent(name)}`, body);
|
|
93
|
+
export const removeMachine = (name: string) =>
|
|
94
|
+
del<{ ok: boolean }>(`/api/machines/${encodeURIComponent(name)}`);
|
|
95
|
+
export const machineHealthCheck = (name: string) =>
|
|
96
|
+
post<{ machine: MachineInfo }>(
|
|
97
|
+
`/api/machines/${encodeURIComponent(name)}/health-check`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Join tokens
|
|
101
|
+
export const fetchJoinTokens = () =>
|
|
102
|
+
request<{ tokens: JoinToken[] }>("/api/join-token");
|
|
103
|
+
export const generateJoinToken = (body: {
|
|
104
|
+
label: string;
|
|
105
|
+
max_workers: number;
|
|
106
|
+
}) => post<{ join_cmd: string }>("/api/join-token", body);
|
|
107
|
+
|
|
108
|
+
// Costs
|
|
109
|
+
export const fetchCostBreakdown = (period = 7) =>
|
|
110
|
+
request<CostBreakdown>(`/api/costs/breakdown?period=${period}`);
|
|
111
|
+
export const fetchCostTrend = (period = 30) =>
|
|
112
|
+
request<{ points: Array<Record<string, number>> }>(
|
|
113
|
+
`/api/costs/trend?period=${period}`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Daemon
|
|
117
|
+
export const fetchDaemonConfig = () =>
|
|
118
|
+
request<DaemonConfig>("/api/daemon/config");
|
|
119
|
+
export const daemonControl = (action: string) =>
|
|
120
|
+
post<{ ok: boolean }>(`/api/daemon/${action}`);
|
|
121
|
+
|
|
122
|
+
// Alerts
|
|
123
|
+
export const fetchAlerts = () =>
|
|
124
|
+
request<{ alerts: AlertInfo[] }>("/api/alerts");
|
|
125
|
+
|
|
126
|
+
// Emergency brake
|
|
127
|
+
export const emergencyBrake = () =>
|
|
128
|
+
post<{ ok: boolean }>("/api/emergency-brake");
|
|
129
|
+
|
|
130
|
+
// Intervention
|
|
131
|
+
export const sendIntervention = (
|
|
132
|
+
issue: number | string,
|
|
133
|
+
action: string,
|
|
134
|
+
body?: unknown,
|
|
135
|
+
) => post<{ ok: boolean }>(`/api/intervention/${issue}/${action}`, body);
|
|
136
|
+
|
|
137
|
+
// Insights
|
|
138
|
+
export const fetchPatterns = () =>
|
|
139
|
+
request<{ patterns: InsightsData["patterns"] }>("/api/memory/patterns").catch(
|
|
140
|
+
() => ({ patterns: [] }),
|
|
141
|
+
);
|
|
142
|
+
export const fetchDecisions = () =>
|
|
143
|
+
request<{ decisions: InsightsData["decisions"] }>(
|
|
144
|
+
"/api/memory/decisions",
|
|
145
|
+
).catch(() => ({ decisions: [] }));
|
|
146
|
+
export const fetchPatrol = () =>
|
|
147
|
+
request<{ findings: InsightsData["patrol"] }>("/api/patrol/recent").catch(
|
|
148
|
+
() => ({ findings: [] }),
|
|
149
|
+
);
|
|
150
|
+
export const fetchHeatmap = () =>
|
|
151
|
+
request<HeatmapData>("/api/metrics/failure-heatmap").catch(() => null);
|
|
152
|
+
|
|
153
|
+
// Artifacts
|
|
154
|
+
export const fetchArtifact = (issue: number | string, type: string) =>
|
|
155
|
+
request<{ content: string }>(
|
|
156
|
+
`/api/artifacts/${encodeURIComponent(issue)}/${encodeURIComponent(type)}`,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// GitHub
|
|
160
|
+
export const fetchGitHubStatus = (issue: number | string) =>
|
|
161
|
+
request<Record<string, unknown>>(`/api/github/${encodeURIComponent(issue)}`);
|
|
162
|
+
|
|
163
|
+
// Logs
|
|
164
|
+
export const fetchLogs = (issue: number | string) =>
|
|
165
|
+
request<{ content: string }>(`/api/logs/${encodeURIComponent(issue)}`);
|
|
166
|
+
|
|
167
|
+
// Metrics detail
|
|
168
|
+
export const fetchStagePerformance = (period = 7) =>
|
|
169
|
+
request<{ stages: StagePerformance[] }>(
|
|
170
|
+
`/api/metrics/stage-performance?period=${period}`,
|
|
171
|
+
);
|
|
172
|
+
export const fetchBottlenecks = () =>
|
|
173
|
+
request<{
|
|
174
|
+
bottlenecks: Array<{
|
|
175
|
+
stage: string;
|
|
176
|
+
avgDuration: number;
|
|
177
|
+
impact: string;
|
|
178
|
+
suggestion: string;
|
|
179
|
+
}>;
|
|
180
|
+
}>("/api/metrics/bottlenecks");
|
|
181
|
+
export const fetchThroughputTrend = (period = 30) =>
|
|
182
|
+
request<{ points: Array<Record<string, number>> }>(
|
|
183
|
+
`/api/metrics/throughput-trend?period=${period}`,
|
|
184
|
+
);
|
|
185
|
+
export const fetchCapacity = () =>
|
|
186
|
+
request<{ rate: number; queue_clear_hours: number }>("/api/metrics/capacity");
|
|
187
|
+
export const fetchDoraTrend = (period = 30) =>
|
|
188
|
+
request<Record<string, Array<Record<string, number>>>>(
|
|
189
|
+
`/api/metrics/dora-trend?period=${period}`,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Queue detailed
|
|
193
|
+
export const fetchQueueDetailed = () =>
|
|
194
|
+
request<{ queue: Array<Record<string, unknown>> }>(
|
|
195
|
+
"/api/queue/detailed",
|
|
196
|
+
).then((d) => ({ items: d.queue || [] }));
|
|
197
|
+
|
|
198
|
+
// Team
|
|
199
|
+
export const fetchTeam = () => request<TeamData>("/api/team");
|
|
200
|
+
export const fetchTeamActivity = () =>
|
|
201
|
+
request<{ events: TeamActivityEvent[] }>("/api/team/activity")
|
|
202
|
+
.then((d) => d.events)
|
|
203
|
+
.catch(() => [] as TeamActivityEvent[]);
|
|
204
|
+
|
|
205
|
+
// Pipeline live changes
|
|
206
|
+
export const fetchPipelineDiff = (issue: number | string) =>
|
|
207
|
+
request<{
|
|
208
|
+
diff: string;
|
|
209
|
+
stats: { files_changed: number; insertions: number; deletions: number };
|
|
210
|
+
worktree: string;
|
|
211
|
+
}>(`/api/pipeline/${encodeURIComponent(issue)}/diff`);
|
|
212
|
+
|
|
213
|
+
export const fetchPipelineFiles = (issue: number | string) =>
|
|
214
|
+
request<{ files: Array<{ path: string; status: string }> }>(
|
|
215
|
+
`/api/pipeline/${encodeURIComponent(issue)}/files`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
export const fetchPipelineTestResults = (issue: number | string) =>
|
|
219
|
+
request<Record<string, unknown>>(
|
|
220
|
+
`/api/pipeline/${encodeURIComponent(issue)}/test-results`,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Pipeline reasoning and failures
|
|
224
|
+
export const fetchPipelineReasoning = (issue: number | string) =>
|
|
225
|
+
request<{ reasoning: Array<Record<string, unknown>> }>(
|
|
226
|
+
`/api/pipeline/${encodeURIComponent(issue)}/reasoning`,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
export const fetchPipelineFailures = (issue: number | string) =>
|
|
230
|
+
request<{ failures: Array<Record<string, unknown>> }>(
|
|
231
|
+
`/api/pipeline/${encodeURIComponent(issue)}/failures`,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Global learnings
|
|
235
|
+
export const fetchGlobalLearnings = () =>
|
|
236
|
+
request<{ learnings: Array<Record<string, unknown>> }>("/api/memory/global");
|
|
237
|
+
|
|
238
|
+
// Team invites
|
|
239
|
+
export const createTeamInvite = (options?: {
|
|
240
|
+
expires_hours?: number;
|
|
241
|
+
max_uses?: number;
|
|
242
|
+
}) =>
|
|
243
|
+
request<{ token: string; url: string; expires_at: string }>(
|
|
244
|
+
"/api/team/invite",
|
|
245
|
+
{ method: "POST", body: JSON.stringify(options || {}) },
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Linear integration status
|
|
249
|
+
export const fetchLinearStatus = () =>
|
|
250
|
+
request<Record<string, unknown>>("/api/linear/status");
|
|
251
|
+
|
|
252
|
+
// DB debug endpoints
|
|
253
|
+
export const fetchDbEvents = (since = 0, limit = 200) =>
|
|
254
|
+
request<{ events: Array<Record<string, unknown>>; source: string }>(
|
|
255
|
+
`/api/db/events?since=${since}&limit=${limit}`,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
export const fetchDbJobs = (status?: string) =>
|
|
259
|
+
request<{ jobs: Array<Record<string, unknown>>; source: string }>(
|
|
260
|
+
`/api/db/jobs${status ? `?status=${status}` : ""}`,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
export const fetchDbCostsToday = () =>
|
|
264
|
+
request<Record<string, unknown>>("/api/db/costs/today");
|
|
265
|
+
|
|
266
|
+
export const fetchDbHeartbeats = () =>
|
|
267
|
+
request<{ heartbeats: Array<Record<string, unknown>>; source: string }>(
|
|
268
|
+
"/api/db/heartbeats",
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
export const fetchDbHealth = () =>
|
|
272
|
+
request<Record<string, unknown>>("/api/db/health");
|
|
273
|
+
|
|
274
|
+
// Machine claim/release
|
|
275
|
+
export const claimIssue = (issue: number, machine: string, repo?: string) =>
|
|
276
|
+
request<{ approved: boolean; claimed_by?: string; error?: string }>(
|
|
277
|
+
"/api/claim",
|
|
278
|
+
{
|
|
279
|
+
method: "POST",
|
|
280
|
+
body: JSON.stringify({ issue, machine, repo }),
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
export const releaseIssue = (issue: number, machine?: string, repo?: string) =>
|
|
285
|
+
request<{ ok: boolean }>("/api/claim/release", {
|
|
286
|
+
method: "POST",
|
|
287
|
+
body: JSON.stringify({ issue, machine, repo }),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Audit log
|
|
291
|
+
export const fetchAuditLog = () =>
|
|
292
|
+
request<{ entries: Array<Record<string, unknown>> }>("/api/audit-log");
|
|
293
|
+
|
|
294
|
+
// Quality gates
|
|
295
|
+
export const fetchQualityGates = () =>
|
|
296
|
+
request<{
|
|
297
|
+
enabled: boolean;
|
|
298
|
+
rules: Array<{
|
|
299
|
+
name: string;
|
|
300
|
+
operator: string;
|
|
301
|
+
threshold: number;
|
|
302
|
+
unit: string;
|
|
303
|
+
}>;
|
|
304
|
+
}>("/api/quality-gates");
|
|
305
|
+
|
|
306
|
+
export const fetchPipelineQuality = (issue: number | string) =>
|
|
307
|
+
request<{
|
|
308
|
+
quality: Record<string, unknown>;
|
|
309
|
+
gates: Record<string, unknown>;
|
|
310
|
+
results: Array<{
|
|
311
|
+
name: string;
|
|
312
|
+
operator: string;
|
|
313
|
+
threshold: number;
|
|
314
|
+
value: unknown;
|
|
315
|
+
passed: boolean;
|
|
316
|
+
}>;
|
|
317
|
+
}>(`/api/pipeline/${encodeURIComponent(issue)}/quality`);
|
|
318
|
+
|
|
319
|
+
// Approval gates
|
|
320
|
+
export const fetchApprovalGates = () =>
|
|
321
|
+
request<{
|
|
322
|
+
enabled: boolean;
|
|
323
|
+
stages: string[];
|
|
324
|
+
pending: Array<{ issue: number; stage: string; requested_at: string }>;
|
|
325
|
+
}>("/api/approval-gates");
|
|
326
|
+
|
|
327
|
+
export const updateApprovalGates = (config: {
|
|
328
|
+
enabled?: boolean;
|
|
329
|
+
stages?: string[];
|
|
330
|
+
}) =>
|
|
331
|
+
request<{ ok: boolean }>("/api/approval-gates", {
|
|
332
|
+
method: "POST",
|
|
333
|
+
body: JSON.stringify(config),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
export const approveGate = (issue: number, stage?: string) =>
|
|
337
|
+
request<{ ok: boolean }>(`/api/approval-gates/${issue}/approve`, {
|
|
338
|
+
method: "POST",
|
|
339
|
+
body: JSON.stringify({ stage }),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
export const rejectGate = (issue: number, stage?: string, reason?: string) =>
|
|
343
|
+
request<{ ok: boolean }>(`/api/approval-gates/${issue}/reject`, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
body: JSON.stringify({ stage, reason }),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Notifications
|
|
349
|
+
export const fetchNotificationConfig = () =>
|
|
350
|
+
request<{
|
|
351
|
+
enabled: boolean;
|
|
352
|
+
webhooks: Array<{
|
|
353
|
+
url: string;
|
|
354
|
+
label: string;
|
|
355
|
+
events: string[];
|
|
356
|
+
created_at: string;
|
|
357
|
+
}>;
|
|
358
|
+
}>("/api/notifications/config");
|
|
359
|
+
|
|
360
|
+
export const addWebhook = (url: string, label?: string, events?: string[]) =>
|
|
361
|
+
request<{ ok: boolean }>("/api/notifications/webhook", {
|
|
362
|
+
method: "POST",
|
|
363
|
+
body: JSON.stringify({ url, label, events }),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
export const removeWebhook = (url: string) =>
|
|
367
|
+
request<{ ok: boolean }>("/api/notifications/webhook", {
|
|
368
|
+
method: "DELETE",
|
|
369
|
+
body: JSON.stringify({ url }),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
export const testNotification = () =>
|
|
373
|
+
request<{ ok: boolean }>("/api/notifications/test", { method: "POST" });
|
|
374
|
+
|
|
375
|
+
// Predictions (new endpoint, returns graceful defaults if not yet implemented)
|
|
376
|
+
export const fetchPredictions = (issue: number | string) =>
|
|
377
|
+
request<{
|
|
378
|
+
eta_s?: number;
|
|
379
|
+
success_probability?: number;
|
|
380
|
+
estimated_cost?: number;
|
|
381
|
+
}>(`/api/predictions/${encodeURIComponent(issue)}`).catch(() => ({}));
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Shared utility helpers
|
|
2
|
+
|
|
3
|
+
export function formatDuration(s: number | null | undefined): string {
|
|
4
|
+
if (s == null) return "\u2014";
|
|
5
|
+
s = Math.floor(s);
|
|
6
|
+
if (s < 60) return s + "s";
|
|
7
|
+
if (s < 3600) return Math.floor(s / 60) + "m " + (s % 60) + "s";
|
|
8
|
+
return Math.floor(s / 3600) + "h " + Math.floor((s % 3600) / 60) + "m";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatTime(iso: string | null | undefined): string {
|
|
12
|
+
if (!iso) return "\u2014";
|
|
13
|
+
const d = new Date(iso);
|
|
14
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
15
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
16
|
+
const s = String(d.getSeconds()).padStart(2, "0");
|
|
17
|
+
return `${h}:${m}:${s}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function escapeHtml(str: string | null | undefined): string {
|
|
21
|
+
if (!str) return "";
|
|
22
|
+
return str
|
|
23
|
+
.replace(/&/g, "&")
|
|
24
|
+
.replace(/</g, "<")
|
|
25
|
+
.replace(/>/g, ">")
|
|
26
|
+
.replace(/"/g, """);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function fmtNum(n: number | null | undefined): string {
|
|
30
|
+
if (n == null) return "0";
|
|
31
|
+
return Number(n).toLocaleString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function truncate(
|
|
35
|
+
str: string | null | undefined,
|
|
36
|
+
maxLen: number,
|
|
37
|
+
): string {
|
|
38
|
+
if (!str) return "";
|
|
39
|
+
return str.length > maxLen ? str.substring(0, maxLen) + "\u2026" : str;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function padZero(n: number): string {
|
|
43
|
+
return n < 10 ? "0" + n : "" + n;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getBadgeClass(typeRaw: string): string {
|
|
47
|
+
if (typeRaw.includes("intervention")) return "intervention";
|
|
48
|
+
if (typeRaw.includes("heartbeat")) return "heartbeat";
|
|
49
|
+
if (typeRaw.includes("recovery") || typeRaw.includes("checkpoint"))
|
|
50
|
+
return "recovery";
|
|
51
|
+
if (typeRaw.includes("remote") || typeRaw.includes("distributed"))
|
|
52
|
+
return "remote";
|
|
53
|
+
if (typeRaw.includes("poll")) return "poll";
|
|
54
|
+
if (typeRaw.includes("spawn")) return "spawn";
|
|
55
|
+
if (typeRaw.includes("started")) return "started";
|
|
56
|
+
if (typeRaw.includes("completed") || typeRaw.includes("reap"))
|
|
57
|
+
return "completed";
|
|
58
|
+
if (typeRaw.includes("failed")) return "failed";
|
|
59
|
+
if (typeRaw.includes("stage")) return "stage";
|
|
60
|
+
if (typeRaw.includes("scale")) return "scale";
|
|
61
|
+
return "default";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getTypeShort(typeRaw: string): string {
|
|
65
|
+
const parts = String(typeRaw || "unknown").split(".");
|
|
66
|
+
return parts[parts.length - 1];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function animateValue(
|
|
70
|
+
el: HTMLElement | null,
|
|
71
|
+
start: number,
|
|
72
|
+
end: number,
|
|
73
|
+
duration: number,
|
|
74
|
+
suffix = "",
|
|
75
|
+
): void {
|
|
76
|
+
if (!el) return;
|
|
77
|
+
const diff = end - start;
|
|
78
|
+
if (diff === 0) {
|
|
79
|
+
el.textContent = fmtNum(end) + suffix;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let startTime: number | null = null;
|
|
83
|
+
function step(timestamp: number) {
|
|
84
|
+
if (!startTime) startTime = timestamp;
|
|
85
|
+
const progress = Math.min((timestamp - startTime) / duration, 1);
|
|
86
|
+
const current = Math.floor(start + diff * progress);
|
|
87
|
+
el!.textContent = fmtNum(current) + suffix;
|
|
88
|
+
if (progress < 1) requestAnimationFrame(step);
|
|
89
|
+
}
|
|
90
|
+
requestAnimationFrame(step);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function timeAgo(date: Date): string {
|
|
94
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
95
|
+
if (seconds < 60) return seconds + "s ago";
|
|
96
|
+
const minutes = Math.floor(seconds / 60);
|
|
97
|
+
if (minutes < 60) return minutes + "m ago";
|
|
98
|
+
const hours = Math.floor(minutes / 60);
|
|
99
|
+
if (hours < 24) return hours + "h ago";
|
|
100
|
+
return Math.floor(hours / 24) + "d ago";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatMarkdown(text: string | null | undefined): string {
|
|
104
|
+
if (!text) return "";
|
|
105
|
+
let escaped = escapeHtml(text);
|
|
106
|
+
escaped = escaped.replace(
|
|
107
|
+
/^#{1,3}\s+(.+)$/gm,
|
|
108
|
+
(_m, content) => "<strong>" + content + "</strong>",
|
|
109
|
+
);
|
|
110
|
+
escaped = escaped.replace(/```[\s\S]*?```/g, (block) => {
|
|
111
|
+
const inner = block.replace(/^```\w*\n?/, "").replace(/\n?```$/, "");
|
|
112
|
+
return '<pre class="artifact-code">' + inner + "</pre>";
|
|
113
|
+
});
|
|
114
|
+
escaped = escaped.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
115
|
+
escaped = escaped.replace(/^[-*]\s+(.+)$/gm, "<li>$1</li>");
|
|
116
|
+
escaped = escaped.replace(/\n/g, "<br>");
|
|
117
|
+
return escaped;
|
|
118
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Tab navigation with hash routing and view lifecycle
|
|
2
|
+
|
|
3
|
+
import { store } from "./state";
|
|
4
|
+
import type { TabId, View, FleetState } from "../types/api";
|
|
5
|
+
|
|
6
|
+
const views = new Map<TabId, View>();
|
|
7
|
+
const initializedViews = new Set<TabId>();
|
|
8
|
+
|
|
9
|
+
const VALID_TABS: TabId[] = [
|
|
10
|
+
"overview",
|
|
11
|
+
"agents",
|
|
12
|
+
"pipelines",
|
|
13
|
+
"timeline",
|
|
14
|
+
"activity",
|
|
15
|
+
"metrics",
|
|
16
|
+
"machines",
|
|
17
|
+
"insights",
|
|
18
|
+
"team",
|
|
19
|
+
"fleet-map",
|
|
20
|
+
"pipeline-theater",
|
|
21
|
+
"agent-cockpit",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
let teamRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
25
|
+
|
|
26
|
+
export function registerView(tabId: TabId, view: View): void {
|
|
27
|
+
views.set(tabId, view);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function switchTab(tab: TabId): void {
|
|
31
|
+
const prev = store.get("activeTab");
|
|
32
|
+
if (prev === tab) return;
|
|
33
|
+
|
|
34
|
+
// Destroy previous view
|
|
35
|
+
const prevView = views.get(prev);
|
|
36
|
+
if (prevView && initializedViews.has(prev)) {
|
|
37
|
+
prevView.destroy();
|
|
38
|
+
initializedViews.delete(prev);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Clear team refresh timer if leaving team tab
|
|
42
|
+
if (prev === "team" && teamRefreshTimer) {
|
|
43
|
+
clearInterval(teamRefreshTimer);
|
|
44
|
+
teamRefreshTimer = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
store.set("activeTab", tab);
|
|
48
|
+
location.hash = "#" + tab;
|
|
49
|
+
|
|
50
|
+
// Update tab button classes
|
|
51
|
+
const btns = document.querySelectorAll(".tab-btn");
|
|
52
|
+
btns.forEach((btn) => {
|
|
53
|
+
if (btn.getAttribute("data-tab") === tab) {
|
|
54
|
+
btn.classList.add("active");
|
|
55
|
+
} else {
|
|
56
|
+
btn.classList.remove("active");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Update panel visibility
|
|
61
|
+
const panels = document.querySelectorAll(".tab-panel");
|
|
62
|
+
panels.forEach((panel) => {
|
|
63
|
+
if (panel.id === "panel-" + tab) {
|
|
64
|
+
panel.classList.add("active");
|
|
65
|
+
} else {
|
|
66
|
+
panel.classList.remove("active");
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Initialize the new view with error boundary
|
|
71
|
+
const view = views.get(tab);
|
|
72
|
+
if (view && !initializedViews.has(tab)) {
|
|
73
|
+
try {
|
|
74
|
+
view.init();
|
|
75
|
+
initializedViews.add(tab);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(`[Error Boundary] Tab "${tab}" init failed:`, err);
|
|
78
|
+
showTabError(tab, err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Render with current data
|
|
83
|
+
const fleetState = store.get("fleetState");
|
|
84
|
+
if (fleetState && view) {
|
|
85
|
+
try {
|
|
86
|
+
view.render(fleetState);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(`[Error Boundary] Tab "${tab}" render failed:`, err);
|
|
89
|
+
showTabError(tab, err);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function renderActiveView(): void {
|
|
95
|
+
const tab = store.get("activeTab");
|
|
96
|
+
const view = views.get(tab);
|
|
97
|
+
const fleetState = store.get("fleetState");
|
|
98
|
+
if (!view || !fleetState) return;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
if (!initializedViews.has(tab)) {
|
|
102
|
+
view.init();
|
|
103
|
+
initializedViews.add(tab);
|
|
104
|
+
}
|
|
105
|
+
view.render(fleetState);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(`[Error Boundary] Tab "${tab}" render failed:`, err);
|
|
108
|
+
showTabError(tab, err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function showTabError(tab: TabId, err: unknown): void {
|
|
113
|
+
const panel = document.getElementById("panel-" + tab);
|
|
114
|
+
if (!panel) return;
|
|
115
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
+
const existing = panel.querySelector(".tab-error-boundary");
|
|
117
|
+
if (existing) return; // don't stack errors
|
|
118
|
+
const div = document.createElement("div");
|
|
119
|
+
div.className = "tab-error-boundary";
|
|
120
|
+
div.innerHTML =
|
|
121
|
+
`<div class="error-boundary-content">` +
|
|
122
|
+
`<span class="error-boundary-icon">\u26A0</span>` +
|
|
123
|
+
`<div><strong>This tab encountered an error</strong>` +
|
|
124
|
+
`<pre class="error-boundary-msg">${msg.replace(/</g, "<")}</pre></div>` +
|
|
125
|
+
`<button class="btn-sm error-boundary-retry">Retry</button></div>`;
|
|
126
|
+
panel.prepend(div);
|
|
127
|
+
const retryBtn = div.querySelector(".error-boundary-retry");
|
|
128
|
+
if (retryBtn) {
|
|
129
|
+
retryBtn.addEventListener("click", () => {
|
|
130
|
+
div.remove();
|
|
131
|
+
initializedViews.delete(tab);
|
|
132
|
+
const v = views.get(tab);
|
|
133
|
+
if (v) {
|
|
134
|
+
try {
|
|
135
|
+
v.init();
|
|
136
|
+
initializedViews.add(tab);
|
|
137
|
+
const state = store.get("fleetState");
|
|
138
|
+
if (state) v.render(state);
|
|
139
|
+
} catch (retryErr) {
|
|
140
|
+
console.error(
|
|
141
|
+
`[Error Boundary] Retry failed for "${tab}":`,
|
|
142
|
+
retryErr,
|
|
143
|
+
);
|
|
144
|
+
showTabError(tab, retryErr);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function setupRouter(): void {
|
|
152
|
+
// Tab button click handlers
|
|
153
|
+
const btns = document.querySelectorAll(".tab-btn");
|
|
154
|
+
btns.forEach((btn) => {
|
|
155
|
+
btn.addEventListener("click", () => {
|
|
156
|
+
const tab = btn.getAttribute("data-tab") as TabId;
|
|
157
|
+
if (tab) switchTab(tab);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Hash-based routing
|
|
162
|
+
const hash = location.hash.replace("#", "") as TabId;
|
|
163
|
+
if (VALID_TABS.includes(hash)) {
|
|
164
|
+
switchTab(hash);
|
|
165
|
+
} else {
|
|
166
|
+
// Default to overview
|
|
167
|
+
const activeTab = store.get("activeTab");
|
|
168
|
+
const view = views.get(activeTab);
|
|
169
|
+
if (view && !initializedViews.has(activeTab)) {
|
|
170
|
+
view.init();
|
|
171
|
+
initializedViews.add(activeTab);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
window.addEventListener("hashchange", () => {
|
|
176
|
+
const h = location.hash.replace("#", "") as TabId;
|
|
177
|
+
if (VALID_TABS.includes(h) && h !== store.get("activeTab")) {
|
|
178
|
+
switchTab(h);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Subscribe to fleet state changes to re-render active view
|
|
183
|
+
store.subscribe("fleetState", () => {
|
|
184
|
+
renderActiveView();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function getRegisteredViews(): Map<TabId, View> {
|
|
189
|
+
return views;
|
|
190
|
+
}
|