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,595 @@
|
|
|
1
|
+
// Pipelines tab - table, filters, detail panel, artifact viewer
|
|
2
|
+
|
|
3
|
+
import { store } from "../core/state";
|
|
4
|
+
import { escapeHtml, formatDuration, formatMarkdown } from "../core/helpers";
|
|
5
|
+
import { renderPipelineSVG } from "../components/charts/pipeline-rail";
|
|
6
|
+
import { renderLogViewer } from "../components/terminal";
|
|
7
|
+
import { updateBulkToolbar } from "../components/modal";
|
|
8
|
+
import { icon } from "../design/icons";
|
|
9
|
+
import { STAGE_SHORT } from "../design/tokens";
|
|
10
|
+
import * as api from "../core/api";
|
|
11
|
+
import type { FleetState, View, PipelineDetail } from "../types/api";
|
|
12
|
+
|
|
13
|
+
let localPipelineDetail: PipelineDetail | null = null;
|
|
14
|
+
|
|
15
|
+
function setupPipelineFilters(): void {
|
|
16
|
+
const chips = document.querySelectorAll("#pipeline-filters .filter-chip");
|
|
17
|
+
chips.forEach((chip) => {
|
|
18
|
+
chip.addEventListener("click", () => {
|
|
19
|
+
const filter = chip.getAttribute("data-filter") || "all";
|
|
20
|
+
store.set("pipelineFilter", filter);
|
|
21
|
+
const siblings = document.querySelectorAll(
|
|
22
|
+
"#pipeline-filters .filter-chip",
|
|
23
|
+
);
|
|
24
|
+
siblings.forEach((s) => s.classList.remove("active"));
|
|
25
|
+
chip.classList.add("active");
|
|
26
|
+
const data = store.get("fleetState");
|
|
27
|
+
if (data) renderPipelinesTab(data);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const closeBtn = document.getElementById("detail-panel-close");
|
|
32
|
+
if (closeBtn) closeBtn.addEventListener("click", closePipelineDetail);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderPipelinesTab(data: FleetState): void {
|
|
36
|
+
const tbody = document.getElementById("pipeline-table-body");
|
|
37
|
+
if (!tbody) return;
|
|
38
|
+
|
|
39
|
+
const pipelines = data.pipelines || [];
|
|
40
|
+
const events = data.events || [];
|
|
41
|
+
const pipelineFilter = store.get("pipelineFilter");
|
|
42
|
+
const selectedPipelineIssue = store.get("selectedPipelineIssue");
|
|
43
|
+
const selectedIssues = store.get("selectedIssues");
|
|
44
|
+
|
|
45
|
+
interface PipelineRow {
|
|
46
|
+
issue: number;
|
|
47
|
+
title: string;
|
|
48
|
+
status: string;
|
|
49
|
+
stage: string;
|
|
50
|
+
elapsed_s: number | null;
|
|
51
|
+
branch: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rows: PipelineRow[] = [];
|
|
55
|
+
for (const p of pipelines) {
|
|
56
|
+
rows.push({
|
|
57
|
+
issue: p.issue,
|
|
58
|
+
title: p.title || "",
|
|
59
|
+
status: "active",
|
|
60
|
+
stage:
|
|
61
|
+
(STAGE_SHORT as Record<string, string>)[p.stage] || p.stage || "\u2014",
|
|
62
|
+
elapsed_s: p.elapsed_s,
|
|
63
|
+
branch: p.worktree || "",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const seen: Record<number, boolean> = {};
|
|
68
|
+
rows.forEach((r) => (seen[r.issue] = true));
|
|
69
|
+
|
|
70
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
71
|
+
const ev = events[i];
|
|
72
|
+
if (!ev.issue || seen[ev.issue]) continue;
|
|
73
|
+
const typeRaw = String(ev.type || "");
|
|
74
|
+
if (typeRaw.includes("completed") || typeRaw.includes("failed")) {
|
|
75
|
+
const st = typeRaw.includes("failed") ? "failed" : "completed";
|
|
76
|
+
rows.push({
|
|
77
|
+
issue: ev.issue,
|
|
78
|
+
title: ev.issueTitle || ev.title || "",
|
|
79
|
+
status: st,
|
|
80
|
+
stage: st === "completed" ? "DONE" : "FAIL",
|
|
81
|
+
elapsed_s: ev.duration_s ?? null,
|
|
82
|
+
branch: "",
|
|
83
|
+
});
|
|
84
|
+
seen[ev.issue] = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let filtered = rows;
|
|
89
|
+
if (pipelineFilter !== "all") {
|
|
90
|
+
filtered = rows.filter((r) => r.status === pipelineFilter);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (filtered.length === 0) {
|
|
94
|
+
tbody.innerHTML =
|
|
95
|
+
'<tr><td colspan="7" class="empty-state"><p>No pipelines match filter</p></td></tr>';
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let html = "";
|
|
100
|
+
for (const r of filtered) {
|
|
101
|
+
const selectedClass =
|
|
102
|
+
selectedPipelineIssue == r.issue ? " row-selected" : "";
|
|
103
|
+
const isChecked = selectedIssues[r.issue] ? " checked" : "";
|
|
104
|
+
html +=
|
|
105
|
+
`<tr class="pipeline-row${selectedClass}" data-issue="${r.issue}">` +
|
|
106
|
+
`<td class="col-checkbox"><input type="checkbox" class="pipeline-checkbox" data-issue="${r.issue}"${isChecked}></td>` +
|
|
107
|
+
`<td class="col-issue">#${r.issue}</td>` +
|
|
108
|
+
`<td class="col-title">${escapeHtml(r.title)}</td>` +
|
|
109
|
+
`<td><span class="status-badge ${r.status}">${r.status.toUpperCase()}</span></td>` +
|
|
110
|
+
`<td class="col-stage">${escapeHtml(r.stage)}</td>` +
|
|
111
|
+
`<td class="col-duration">${formatDuration(r.elapsed_s)}</td>` +
|
|
112
|
+
`<td class="col-branch">${escapeHtml(r.branch)}</td></tr>`;
|
|
113
|
+
}
|
|
114
|
+
tbody.innerHTML = html;
|
|
115
|
+
|
|
116
|
+
// Checkbox handlers
|
|
117
|
+
tbody.querySelectorAll(".pipeline-checkbox").forEach((cb) => {
|
|
118
|
+
cb.addEventListener("change", (e) => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
const el = e.target as HTMLInputElement;
|
|
121
|
+
const iss = el.getAttribute("data-issue") || "";
|
|
122
|
+
const issues = { ...store.get("selectedIssues") };
|
|
123
|
+
if (el.checked) issues[iss] = true;
|
|
124
|
+
else delete issues[iss];
|
|
125
|
+
store.set("selectedIssues", issues);
|
|
126
|
+
updateBulkToolbar();
|
|
127
|
+
});
|
|
128
|
+
cb.addEventListener("click", (e) => e.stopPropagation());
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Select-all
|
|
132
|
+
const selectAll = document.getElementById(
|
|
133
|
+
"pipeline-select-all",
|
|
134
|
+
) as HTMLInputElement;
|
|
135
|
+
if (selectAll) {
|
|
136
|
+
selectAll.addEventListener("change", () => {
|
|
137
|
+
const cbs = tbody.querySelectorAll(
|
|
138
|
+
".pipeline-checkbox",
|
|
139
|
+
) as NodeListOf<HTMLInputElement>;
|
|
140
|
+
const issues: Record<string, boolean> = {};
|
|
141
|
+
cbs.forEach((cb) => {
|
|
142
|
+
cb.checked = selectAll.checked;
|
|
143
|
+
const iss = cb.getAttribute("data-issue") || "";
|
|
144
|
+
if (selectAll.checked) issues[iss] = true;
|
|
145
|
+
});
|
|
146
|
+
store.set("selectedIssues", selectAll.checked ? issues : {});
|
|
147
|
+
updateBulkToolbar();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Row click handlers
|
|
152
|
+
tbody.querySelectorAll(".pipeline-row").forEach((tr) => {
|
|
153
|
+
tr.addEventListener("click", () => {
|
|
154
|
+
const issue = tr.getAttribute("data-issue");
|
|
155
|
+
if (!issue) return;
|
|
156
|
+
if (store.get("selectedPipelineIssue") == Number(issue)) {
|
|
157
|
+
closePipelineDetail();
|
|
158
|
+
} else {
|
|
159
|
+
fetchPipelineDetail(Number(issue));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function fetchPipelineDetail(issue: number): void {
|
|
166
|
+
store.set("selectedPipelineIssue", issue);
|
|
167
|
+
|
|
168
|
+
document
|
|
169
|
+
.querySelectorAll("#pipeline-table-body .pipeline-row")
|
|
170
|
+
.forEach((tr) => {
|
|
171
|
+
if (tr.getAttribute("data-issue") == String(issue))
|
|
172
|
+
tr.classList.add("row-selected");
|
|
173
|
+
else tr.classList.remove("row-selected");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const panel = document.getElementById("pipeline-detail-panel");
|
|
177
|
+
const title = document.getElementById("detail-panel-title");
|
|
178
|
+
const body = document.getElementById("detail-panel-body");
|
|
179
|
+
if (title) title.textContent = "Pipeline #" + issue;
|
|
180
|
+
if (body) body.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
|
|
181
|
+
if (panel) panel.classList.add("open");
|
|
182
|
+
|
|
183
|
+
api
|
|
184
|
+
.fetchPipelineDetail(issue)
|
|
185
|
+
.then((detail) => {
|
|
186
|
+
localPipelineDetail = detail;
|
|
187
|
+
store.set("pipelineDetail", detail);
|
|
188
|
+
renderPipelineDetail(detail);
|
|
189
|
+
})
|
|
190
|
+
.catch((err) => {
|
|
191
|
+
if (body)
|
|
192
|
+
body.innerHTML = `<div class="empty-state"><p>Failed to load: ${escapeHtml(String(err))}</p></div>`;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderPipelineDetail(detail: PipelineDetail): void {
|
|
197
|
+
const body = document.getElementById("detail-panel-body");
|
|
198
|
+
if (!body) return;
|
|
199
|
+
const issue = detail.issue || store.get("selectedPipelineIssue");
|
|
200
|
+
let html = "";
|
|
201
|
+
|
|
202
|
+
html += `<div id="github-status-${issue}" class="github-status-banner"></div>`;
|
|
203
|
+
const stagesDone = detail.stageHistory?.map((h) => h.stage) || [];
|
|
204
|
+
html += `<div class="pipeline-svg-wrap">${renderPipelineSVG({
|
|
205
|
+
stagesDone,
|
|
206
|
+
stage: detail.stage,
|
|
207
|
+
status: "",
|
|
208
|
+
issue: detail.issue,
|
|
209
|
+
title: detail.title,
|
|
210
|
+
elapsed_s: detail.elapsed_s,
|
|
211
|
+
iteration: 0,
|
|
212
|
+
maxIterations: 0,
|
|
213
|
+
})}</div>`;
|
|
214
|
+
|
|
215
|
+
const history = detail.stageHistory || [];
|
|
216
|
+
if (history.length > 0) {
|
|
217
|
+
html += '<div class="stage-timeline">';
|
|
218
|
+
for (const sh of history) {
|
|
219
|
+
const isActive = sh.stage === detail.stage;
|
|
220
|
+
const dotCls = isActive ? "active" : "done";
|
|
221
|
+
html +=
|
|
222
|
+
`<div class="stage-timeline-item">` +
|
|
223
|
+
`<div class="stage-timeline-dot ${dotCls}"></div>` +
|
|
224
|
+
`<span class="stage-timeline-name">${escapeHtml(sh.stage)}</span>` +
|
|
225
|
+
`<span class="stage-timeline-duration">${formatDuration(sh.duration_s)}</span></div>`;
|
|
226
|
+
}
|
|
227
|
+
html += "</div>";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
html += '<div class="detail-meta-row">';
|
|
231
|
+
if (detail.branch)
|
|
232
|
+
html += `<div class="detail-meta-item">Branch: <span>${escapeHtml(detail.branch)}</span></div>`;
|
|
233
|
+
if (detail.elapsed_s != null)
|
|
234
|
+
html += `<div class="detail-meta-item">Elapsed: <span>${formatDuration(detail.elapsed_s)}</span></div>`;
|
|
235
|
+
if (detail.prLink)
|
|
236
|
+
html += `<div class="detail-meta-item">PR: <a href="${escapeHtml(detail.prLink)}" target="_blank">${escapeHtml(detail.prLink)}</a></div>`;
|
|
237
|
+
html += "</div>";
|
|
238
|
+
|
|
239
|
+
// Quality gate display
|
|
240
|
+
html += `<div class="quality-gate-panel" id="quality-gate-${issue}" style="display:none"></div>`;
|
|
241
|
+
|
|
242
|
+
// Approval gate placeholder
|
|
243
|
+
html += `<div class="approval-gate-banner" id="approval-gate-${issue}" style="display:none"></div>`;
|
|
244
|
+
|
|
245
|
+
html += renderArtifactViewer(issue!, detail);
|
|
246
|
+
body.innerHTML = html;
|
|
247
|
+
|
|
248
|
+
if (issue) {
|
|
249
|
+
renderGitHubStatus(issue);
|
|
250
|
+
setupArtifactTabs(issue);
|
|
251
|
+
checkApprovalGate(issue, detail.stage);
|
|
252
|
+
loadQualityGates(issue);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderArtifactViewer(issue: number, detail: PipelineDetail): string {
|
|
257
|
+
const tabs = [
|
|
258
|
+
{ key: "plan", label: "Plan", content: detail.plan },
|
|
259
|
+
{ key: "design", label: "Design", content: detail.design },
|
|
260
|
+
{ key: "dod", label: "DoD", content: detail.dod },
|
|
261
|
+
{ key: "reasoning", label: "Reasoning", content: null },
|
|
262
|
+
{ key: "failures", label: "Failures", content: null },
|
|
263
|
+
{ key: "tests", label: "Tests", content: null },
|
|
264
|
+
{ key: "review", label: "Review", content: null },
|
|
265
|
+
{ key: "logs", label: "Logs", content: null },
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
let html = '<div class="artifact-viewer"><div class="artifact-tabs">';
|
|
269
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
270
|
+
const activeClass = i === 0 ? " active" : "";
|
|
271
|
+
html += `<button class="artifact-tab-btn${activeClass}" data-artifact="${tabs[i].key}" data-issue="${issue}">${escapeHtml(tabs[i].label)}</button>`;
|
|
272
|
+
}
|
|
273
|
+
html += "</div>";
|
|
274
|
+
html += `<div class="artifact-content" id="artifact-content-${issue}">`;
|
|
275
|
+
if (detail.plan) {
|
|
276
|
+
html += `<div class="detail-plan-content">${formatMarkdown(detail.plan)}</div>`;
|
|
277
|
+
} else {
|
|
278
|
+
html += '<div class="empty-state"><p>No plan data</p></div>';
|
|
279
|
+
}
|
|
280
|
+
html += "</div></div>";
|
|
281
|
+
return html;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function setupArtifactTabs(issue: number): void {
|
|
285
|
+
const btns = document.querySelectorAll(
|
|
286
|
+
`.artifact-tab-btn[data-issue="${issue}"]`,
|
|
287
|
+
);
|
|
288
|
+
btns.forEach((btn) => {
|
|
289
|
+
btn.addEventListener("click", () => {
|
|
290
|
+
const artifact = btn.getAttribute("data-artifact");
|
|
291
|
+
const iss = btn.getAttribute("data-issue");
|
|
292
|
+
const siblings = document.querySelectorAll(
|
|
293
|
+
`.artifact-tab-btn[data-issue="${iss}"]`,
|
|
294
|
+
);
|
|
295
|
+
siblings.forEach((s) => s.classList.remove("active"));
|
|
296
|
+
btn.classList.add("active");
|
|
297
|
+
if (iss && artifact) fetchArtifact(Number(iss), artifact);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function fetchArtifact(issue: number, type: string): void {
|
|
303
|
+
const container = document.getElementById("artifact-content-" + issue);
|
|
304
|
+
if (!container) return;
|
|
305
|
+
container.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
|
|
306
|
+
|
|
307
|
+
if (localPipelineDetail) {
|
|
308
|
+
if (type === "plan" && localPipelineDetail.plan) {
|
|
309
|
+
container.innerHTML = `<div class="detail-plan-content">${formatMarkdown(localPipelineDetail.plan)}</div>`;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (type === "design" && localPipelineDetail.design) {
|
|
313
|
+
container.innerHTML = `<div class="detail-plan-content">${formatMarkdown(localPipelineDetail.design)}</div>`;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (type === "dod" && localPipelineDetail.dod) {
|
|
317
|
+
container.innerHTML = `<div class="detail-plan-content">${formatMarkdown(localPipelineDetail.dod)}</div>`;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Handle special artifact types
|
|
323
|
+
if (type === "reasoning") {
|
|
324
|
+
api
|
|
325
|
+
.fetchPipelineReasoning(issue)
|
|
326
|
+
.then((data) => {
|
|
327
|
+
const reasoning = data.reasoning || [];
|
|
328
|
+
if (reasoning.length === 0) {
|
|
329
|
+
container.innerHTML =
|
|
330
|
+
'<div class="empty-state"><p>No reasoning data available. Agent reasoning will appear here as stages complete.</p></div>';
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
let html = '<div class="reasoning-list">';
|
|
334
|
+
for (const r of reasoning) {
|
|
335
|
+
const stage = String(r.stage || "general");
|
|
336
|
+
const content = String(r.content || r.summary || r.description || "");
|
|
337
|
+
html +=
|
|
338
|
+
`<div class="reasoning-entry">` +
|
|
339
|
+
`<div class="reasoning-stage">${escapeHtml(stage)}</div>` +
|
|
340
|
+
`<div class="reasoning-content">${r.type === "markdown" ? formatMarkdown(content) : escapeHtml(content)}</div></div>`;
|
|
341
|
+
}
|
|
342
|
+
html += "</div>";
|
|
343
|
+
container.innerHTML = html;
|
|
344
|
+
})
|
|
345
|
+
.catch(() => {
|
|
346
|
+
container.innerHTML =
|
|
347
|
+
'<div class="empty-state"><p>Could not load reasoning data</p></div>';
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (type === "failures") {
|
|
353
|
+
api
|
|
354
|
+
.fetchPipelineFailures(issue)
|
|
355
|
+
.then((data) => {
|
|
356
|
+
const failures = data.failures || [];
|
|
357
|
+
if (failures.length === 0) {
|
|
358
|
+
container.innerHTML =
|
|
359
|
+
'<div class="empty-state"><p>No failures recorded for this pipeline.</p></div>';
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
let html = '<div class="failure-analysis-list">';
|
|
363
|
+
for (const f of failures) {
|
|
364
|
+
const fType = String(f.type || f.stage || "unknown");
|
|
365
|
+
const desc = String(f.description || f.error || f.message || "");
|
|
366
|
+
const rootCause = f.root_cause
|
|
367
|
+
? `<div class="failure-root-cause"><strong>Root cause:</strong> ${escapeHtml(String(f.root_cause))}</div>`
|
|
368
|
+
: "";
|
|
369
|
+
const fix = f.fix || f.suggested_fix;
|
|
370
|
+
const fixHtml = fix
|
|
371
|
+
? `<div class="failure-fix"><strong>Fix:</strong> ${escapeHtml(String(fix))}</div>`
|
|
372
|
+
: "";
|
|
373
|
+
html +=
|
|
374
|
+
`<div class="failure-entry">` +
|
|
375
|
+
`<div class="failure-type">${escapeHtml(fType)}</div>` +
|
|
376
|
+
`<div class="failure-desc">${escapeHtml(desc)}</div>` +
|
|
377
|
+
rootCause +
|
|
378
|
+
fixHtml +
|
|
379
|
+
"</div>";
|
|
380
|
+
}
|
|
381
|
+
html += "</div>";
|
|
382
|
+
container.innerHTML = html;
|
|
383
|
+
})
|
|
384
|
+
.catch(() => {
|
|
385
|
+
container.innerHTML =
|
|
386
|
+
'<div class="empty-state"><p>Could not load failure data</p></div>';
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
api
|
|
392
|
+
.fetchArtifact(issue, type)
|
|
393
|
+
.then((data) => {
|
|
394
|
+
if (type === "logs") {
|
|
395
|
+
container.innerHTML = renderLogViewer(data.content || "");
|
|
396
|
+
} else {
|
|
397
|
+
container.innerHTML = `<div class="detail-plan-content">${formatMarkdown(data.content || "")}</div>`;
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
.catch((err) => {
|
|
401
|
+
container.innerHTML = `<div class="empty-state"><p>Not available: ${escapeHtml(String(err))}</p></div>`;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function renderGitHubStatus(issue: number): void {
|
|
406
|
+
const container = document.getElementById("github-status-" + issue);
|
|
407
|
+
if (!container) return;
|
|
408
|
+
|
|
409
|
+
api
|
|
410
|
+
.fetchGitHubStatus(issue)
|
|
411
|
+
.then((data) => {
|
|
412
|
+
if (!(data as any).configured) {
|
|
413
|
+
container.innerHTML = "";
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
let html = '<div class="github-banner">';
|
|
417
|
+
if ((data as any).issue_state)
|
|
418
|
+
html += `<span class="github-badge ${escapeHtml((data as any).issue_state)}">${escapeHtml((data as any).issue_state)}</span>`;
|
|
419
|
+
if ((data as any).pr_number)
|
|
420
|
+
html += `<a class="github-link" href="${escapeHtml((data as any).pr_url || "")}" target="_blank">PR #${(data as any).pr_number}</a>`;
|
|
421
|
+
if ((data as any).checks?.length > 0) {
|
|
422
|
+
html += '<span class="github-checks">';
|
|
423
|
+
for (const check of (data as any).checks) {
|
|
424
|
+
const ci =
|
|
425
|
+
check.status === "success"
|
|
426
|
+
? "\u2713"
|
|
427
|
+
: check.status === "failure"
|
|
428
|
+
? "\u2717"
|
|
429
|
+
: "\u25CF";
|
|
430
|
+
const cls =
|
|
431
|
+
check.status === "success"
|
|
432
|
+
? "github-badge success"
|
|
433
|
+
: check.status === "failure"
|
|
434
|
+
? "github-badge failure"
|
|
435
|
+
: "github-badge pending";
|
|
436
|
+
html += `<span class="${cls}" title="${escapeHtml(check.name || "")}">${ci}</span>`;
|
|
437
|
+
}
|
|
438
|
+
html += "</span>";
|
|
439
|
+
}
|
|
440
|
+
html += "</div>";
|
|
441
|
+
container.innerHTML = html;
|
|
442
|
+
})
|
|
443
|
+
.catch(() => {
|
|
444
|
+
container.innerHTML = "";
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function renderErrorHighlight(issue: number): void {
|
|
449
|
+
const container = document.getElementById("error-highlight-" + issue);
|
|
450
|
+
if (!container) return;
|
|
451
|
+
|
|
452
|
+
api
|
|
453
|
+
.fetchLogs(issue)
|
|
454
|
+
.then((data) => {
|
|
455
|
+
const content = data.content || "";
|
|
456
|
+
const lines = content.split("\n");
|
|
457
|
+
const errorLines = lines.filter((l) => {
|
|
458
|
+
const lower = l.toLowerCase();
|
|
459
|
+
return lower.indexOf("error") !== -1 || lower.indexOf("fail") !== -1;
|
|
460
|
+
});
|
|
461
|
+
if (errorLines.length === 0) {
|
|
462
|
+
container.innerHTML = "";
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const lastError = errorLines[errorLines.length - 1];
|
|
466
|
+
container.innerHTML =
|
|
467
|
+
`<div class="error-highlight"><span class="error-highlight-title">LAST ERROR</span>` +
|
|
468
|
+
`<pre class="error-highlight-content">${escapeHtml(lastError)}</pre></div>`;
|
|
469
|
+
})
|
|
470
|
+
.catch(() => {
|
|
471
|
+
container.innerHTML = "";
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function loadQualityGates(issue: number): void {
|
|
476
|
+
const panel = document.getElementById("quality-gate-" + issue);
|
|
477
|
+
if (!panel) return;
|
|
478
|
+
|
|
479
|
+
api
|
|
480
|
+
.fetchPipelineQuality(issue)
|
|
481
|
+
.then((data) => {
|
|
482
|
+
const results = data.results || [];
|
|
483
|
+
if (results.length === 0) {
|
|
484
|
+
panel.style.display = "none";
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
panel.style.display = "";
|
|
488
|
+
const allPassed = results.every((r) => r.passed);
|
|
489
|
+
let html = `<div class="quality-gate-header"><span class="quality-icon">${allPassed ? "\u2705" : "\u26A0\uFE0F"}</span>`;
|
|
490
|
+
html += `<span class="quality-title">Quality Gates${allPassed ? " - All Passing" : ""}</span></div>`;
|
|
491
|
+
html += '<div class="quality-gate-rules">';
|
|
492
|
+
for (const r of results) {
|
|
493
|
+
const statusCls = r.passed ? "gate-pass" : "gate-fail";
|
|
494
|
+
const statusIcon = r.passed ? "\u2713" : "\u2717";
|
|
495
|
+
const valueStr =
|
|
496
|
+
r.value !== null && r.value !== undefined ? String(r.value) : "N/A";
|
|
497
|
+
html +=
|
|
498
|
+
`<div class="quality-gate-rule ${statusCls}">` +
|
|
499
|
+
`<span class="gate-status">${statusIcon}</span>` +
|
|
500
|
+
`<span class="gate-name">${escapeHtml(r.name.replace(/_/g, " "))}</span>` +
|
|
501
|
+
`<span class="gate-threshold">${r.operator} ${r.threshold}</span>` +
|
|
502
|
+
`<span class="gate-value">Current: ${escapeHtml(valueStr)}</span></div>`;
|
|
503
|
+
}
|
|
504
|
+
html += "</div>";
|
|
505
|
+
panel.innerHTML = html;
|
|
506
|
+
})
|
|
507
|
+
.catch(() => {
|
|
508
|
+
panel.style.display = "none";
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function checkApprovalGate(issue: number, currentStage: string): void {
|
|
513
|
+
const banner = document.getElementById("approval-gate-" + issue);
|
|
514
|
+
if (!banner) return;
|
|
515
|
+
|
|
516
|
+
api
|
|
517
|
+
.fetchApprovalGates()
|
|
518
|
+
.then((config) => {
|
|
519
|
+
if (!config.enabled) {
|
|
520
|
+
banner.style.display = "none";
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
// Check if current stage requires approval
|
|
524
|
+
const gatedStages = config.stages || [];
|
|
525
|
+
const pending = (config.pending || []).find((p) => p.issue === issue);
|
|
526
|
+
|
|
527
|
+
if (pending) {
|
|
528
|
+
banner.style.display = "";
|
|
529
|
+
banner.innerHTML =
|
|
530
|
+
`<div class="approval-gate-waiting">` +
|
|
531
|
+
`<span class="approval-icon">\u{1F6D1}</span>` +
|
|
532
|
+
`<span>Awaiting approval to proceed to <strong>${escapeHtml(pending.stage)}</strong></span>` +
|
|
533
|
+
`<div class="approval-actions">` +
|
|
534
|
+
`<button class="btn-primary btn-sm" id="approve-${issue}">Approve</button>` +
|
|
535
|
+
`<button class="btn-danger btn-sm" id="reject-${issue}">Reject</button></div></div>`;
|
|
536
|
+
document
|
|
537
|
+
.getElementById("approve-" + issue)
|
|
538
|
+
?.addEventListener("click", () => {
|
|
539
|
+
api.approveGate(issue, pending.stage).then(() => {
|
|
540
|
+
banner.innerHTML =
|
|
541
|
+
'<div class="approval-approved">Approved. Agent will proceed.</div>';
|
|
542
|
+
setTimeout(() => {
|
|
543
|
+
banner.style.display = "none";
|
|
544
|
+
}, 3000);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
document
|
|
548
|
+
.getElementById("reject-" + issue)
|
|
549
|
+
?.addEventListener("click", () => {
|
|
550
|
+
const reason = prompt("Reason for rejection (optional):");
|
|
551
|
+
api
|
|
552
|
+
.rejectGate(issue, pending.stage, reason || undefined)
|
|
553
|
+
.then(() => {
|
|
554
|
+
banner.innerHTML =
|
|
555
|
+
'<div class="approval-rejected">Rejected. Agent will stop.</div>';
|
|
556
|
+
setTimeout(() => {
|
|
557
|
+
banner.style.display = "none";
|
|
558
|
+
}, 3000);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
} else if (gatedStages.includes(currentStage)) {
|
|
562
|
+
banner.style.display = "";
|
|
563
|
+
banner.innerHTML =
|
|
564
|
+
`<div class="approval-gate-info">` +
|
|
565
|
+
`<span class="approval-icon">\u{1F512}</span>` +
|
|
566
|
+
`<span>Approval gates are enabled for: ${gatedStages.map((s) => escapeHtml(s)).join(", ")}</span></div>`;
|
|
567
|
+
} else {
|
|
568
|
+
banner.style.display = "none";
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
.catch(() => {
|
|
572
|
+
banner.style.display = "none";
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function closePipelineDetail(): void {
|
|
577
|
+
store.set("selectedPipelineIssue", null);
|
|
578
|
+
store.set("pipelineDetail", null);
|
|
579
|
+
localPipelineDetail = null;
|
|
580
|
+
const panel = document.getElementById("pipeline-detail-panel");
|
|
581
|
+
if (panel) panel.classList.remove("open");
|
|
582
|
+
document
|
|
583
|
+
.querySelectorAll("#pipeline-table-body .pipeline-row")
|
|
584
|
+
.forEach((tr) => tr.classList.remove("row-selected"));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export const pipelinesView: View = {
|
|
588
|
+
init() {
|
|
589
|
+
setupPipelineFilters();
|
|
590
|
+
},
|
|
591
|
+
render(data: FleetState) {
|
|
592
|
+
renderPipelinesTab(data);
|
|
593
|
+
},
|
|
594
|
+
destroy() {},
|
|
595
|
+
};
|