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,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, "&amp;")
24
+ .replace(/</g, "&lt;")
25
+ .replace(/>/g, "&gt;")
26
+ .replace(/"/g, "&quot;");
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
+ }