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,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
|
+
}
|