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,413 @@
|
|
|
1
|
+
// Modal system for intervention, machine management, and join links
|
|
2
|
+
|
|
3
|
+
import * as api from "../core/api";
|
|
4
|
+
import { store } from "../core/state";
|
|
5
|
+
import { escapeHtml } from "../core/helpers";
|
|
6
|
+
|
|
7
|
+
let interventionTarget: number | null = null;
|
|
8
|
+
let removeMachineTarget: string | null = null;
|
|
9
|
+
let workerUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
|
10
|
+
|
|
11
|
+
// Intervention Modal
|
|
12
|
+
export function setupInterventionModal(): void {
|
|
13
|
+
const modal = document.getElementById("intervention-modal");
|
|
14
|
+
const closeBtn = document.getElementById("modal-close");
|
|
15
|
+
const cancelBtn = document.getElementById("modal-cancel");
|
|
16
|
+
const sendBtn = document.getElementById("modal-send");
|
|
17
|
+
const msgEl = document.getElementById(
|
|
18
|
+
"modal-message",
|
|
19
|
+
) as HTMLTextAreaElement | null;
|
|
20
|
+
|
|
21
|
+
function closeModal() {
|
|
22
|
+
if (modal) modal.style.display = "none";
|
|
23
|
+
interventionTarget = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (closeBtn) closeBtn.addEventListener("click", closeModal);
|
|
27
|
+
if (cancelBtn) cancelBtn.addEventListener("click", closeModal);
|
|
28
|
+
if (modal)
|
|
29
|
+
modal.addEventListener("click", (e) => {
|
|
30
|
+
if (e.target === modal) closeModal();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (sendBtn) {
|
|
34
|
+
sendBtn.addEventListener("click", () => {
|
|
35
|
+
if (interventionTarget && msgEl?.value.trim()) {
|
|
36
|
+
api.sendIntervention(interventionTarget, "message", {
|
|
37
|
+
message: msgEl.value.trim(),
|
|
38
|
+
});
|
|
39
|
+
closeModal();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function openInterventionModal(issue: number): void {
|
|
46
|
+
interventionTarget = issue;
|
|
47
|
+
const modal = document.getElementById("intervention-modal");
|
|
48
|
+
const title = document.getElementById("modal-title");
|
|
49
|
+
const msg = document.getElementById(
|
|
50
|
+
"modal-message",
|
|
51
|
+
) as HTMLTextAreaElement | null;
|
|
52
|
+
if (modal) modal.style.display = "";
|
|
53
|
+
if (title) title.textContent = "Send Message to #" + issue;
|
|
54
|
+
if (msg) msg.value = "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function confirmAbort(issue: number): void {
|
|
58
|
+
if (
|
|
59
|
+
confirm("Abort pipeline for issue #" + issue + "? This cannot be undone.")
|
|
60
|
+
) {
|
|
61
|
+
api.sendIntervention(issue, "abort");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Bulk Actions
|
|
66
|
+
export function setupBulkActions(): void {
|
|
67
|
+
const pauseBtn = document.getElementById("bulk-pause");
|
|
68
|
+
const resumeBtn = document.getElementById("bulk-resume");
|
|
69
|
+
const abortBtn = document.getElementById("bulk-abort");
|
|
70
|
+
|
|
71
|
+
if (pauseBtn) {
|
|
72
|
+
pauseBtn.addEventListener("click", () => {
|
|
73
|
+
const issues = Object.keys(store.get("selectedIssues"));
|
|
74
|
+
issues.forEach((i) => api.sendIntervention(i, "pause"));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (resumeBtn) {
|
|
79
|
+
resumeBtn.addEventListener("click", () => {
|
|
80
|
+
const issues = Object.keys(store.get("selectedIssues"));
|
|
81
|
+
issues.forEach((i) => api.sendIntervention(i, "resume"));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (abortBtn) {
|
|
86
|
+
abortBtn.addEventListener("click", () => {
|
|
87
|
+
const issues = Object.keys(store.get("selectedIssues"));
|
|
88
|
+
if (issues.length === 0) return;
|
|
89
|
+
if (
|
|
90
|
+
confirm(
|
|
91
|
+
"Abort " + issues.length + " pipeline(s)? This cannot be undone.",
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
issues.forEach((i) => api.sendIntervention(i, "abort"));
|
|
95
|
+
store.set("selectedIssues", {});
|
|
96
|
+
updateBulkToolbar();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function updateBulkToolbar(): void {
|
|
103
|
+
const toolbar = document.getElementById("bulk-actions");
|
|
104
|
+
if (!toolbar) return;
|
|
105
|
+
const count = Object.keys(store.get("selectedIssues")).length;
|
|
106
|
+
if (count === 0) {
|
|
107
|
+
toolbar.style.display = "none";
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
toolbar.style.display = "";
|
|
111
|
+
const countEl = document.getElementById("bulk-count");
|
|
112
|
+
if (countEl) countEl.textContent = count + " selected";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Machine Modals
|
|
116
|
+
export function setupMachinesModals(): void {
|
|
117
|
+
const addBtn = document.getElementById("btn-add-machine");
|
|
118
|
+
if (addBtn) addBtn.addEventListener("click", openAddMachineModal);
|
|
119
|
+
|
|
120
|
+
const joinBtn = document.getElementById("btn-join-link");
|
|
121
|
+
if (joinBtn) joinBtn.addEventListener("click", openJoinLinkModal);
|
|
122
|
+
|
|
123
|
+
// Add machine modal
|
|
124
|
+
bindClick("machine-modal-close", closeAddMachineModal);
|
|
125
|
+
bindClick("machine-modal-cancel", closeAddMachineModal);
|
|
126
|
+
bindClick("machine-modal-submit", submitAddMachine);
|
|
127
|
+
|
|
128
|
+
// Join link modal
|
|
129
|
+
bindClick("join-modal-close", closeJoinLinkModal);
|
|
130
|
+
bindClick("join-modal-cancel", closeJoinLinkModal);
|
|
131
|
+
bindClick("join-modal-generate", generateJoinLink);
|
|
132
|
+
bindClick("join-copy-btn", copyJoinCommand);
|
|
133
|
+
|
|
134
|
+
// Remove machine modal
|
|
135
|
+
bindClick("remove-modal-close", () => {
|
|
136
|
+
const el = document.getElementById("remove-machine-modal");
|
|
137
|
+
if (el) el.style.display = "none";
|
|
138
|
+
});
|
|
139
|
+
bindClick("remove-modal-cancel", () => {
|
|
140
|
+
const el = document.getElementById("remove-machine-modal");
|
|
141
|
+
if (el) el.style.display = "none";
|
|
142
|
+
});
|
|
143
|
+
bindClick("remove-modal-confirm", executeRemoveMachine);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function bindClick(id: string, handler: () => void): void {
|
|
147
|
+
const el = document.getElementById(id);
|
|
148
|
+
if (el) el.addEventListener("click", handler);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function openAddMachineModal(): void {
|
|
152
|
+
const modal = document.getElementById("add-machine-modal");
|
|
153
|
+
if (modal) modal.style.display = "flex";
|
|
154
|
+
setVal("machine-name", "");
|
|
155
|
+
setVal("machine-host", "");
|
|
156
|
+
setVal("machine-ssh-user", "");
|
|
157
|
+
setVal("machine-path", "");
|
|
158
|
+
setVal("machine-workers", "4");
|
|
159
|
+
setVal("machine-role", "worker");
|
|
160
|
+
const err = document.getElementById("machine-modal-error");
|
|
161
|
+
if (err) err.style.display = "none";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function closeAddMachineModal(): void {
|
|
165
|
+
const modal = document.getElementById("add-machine-modal");
|
|
166
|
+
if (modal) modal.style.display = "none";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function submitAddMachine(): void {
|
|
170
|
+
const name = getVal("machine-name").trim();
|
|
171
|
+
const host = getVal("machine-host").trim();
|
|
172
|
+
const sshUser = getVal("machine-ssh-user").trim();
|
|
173
|
+
const swPath = getVal("machine-path").trim();
|
|
174
|
+
const maxWorkers = parseInt(getVal("machine-workers"), 10) || 4;
|
|
175
|
+
const role = getVal("machine-role");
|
|
176
|
+
const errEl = document.getElementById("machine-modal-error");
|
|
177
|
+
|
|
178
|
+
if (!name || !host) {
|
|
179
|
+
if (errEl) {
|
|
180
|
+
errEl.textContent = "Name and host are required";
|
|
181
|
+
errEl.style.display = "";
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const body: Record<string, unknown> = {
|
|
187
|
+
name,
|
|
188
|
+
host,
|
|
189
|
+
role,
|
|
190
|
+
max_workers: maxWorkers,
|
|
191
|
+
};
|
|
192
|
+
if (sshUser) body.ssh_user = sshUser;
|
|
193
|
+
if (swPath) body.shipwright_path = swPath;
|
|
194
|
+
|
|
195
|
+
api
|
|
196
|
+
.addMachine(body)
|
|
197
|
+
.then(() => {
|
|
198
|
+
closeAddMachineModal();
|
|
199
|
+
refreshMachines();
|
|
200
|
+
})
|
|
201
|
+
.catch((err) => {
|
|
202
|
+
if (errEl) {
|
|
203
|
+
errEl.textContent = err.message || "Failed to register machine";
|
|
204
|
+
errEl.style.display = "";
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function updateWorkerCount(name: string, value: string): void {
|
|
210
|
+
if (workerUpdateTimer) clearTimeout(workerUpdateTimer);
|
|
211
|
+
workerUpdateTimer = setTimeout(() => {
|
|
212
|
+
api
|
|
213
|
+
.updateMachine(name, { max_workers: parseInt(value, 10) })
|
|
214
|
+
.then((updated) => {
|
|
215
|
+
const card = document.getElementById("machine-card-" + name);
|
|
216
|
+
if (card) {
|
|
217
|
+
const countEl = card.querySelector(".workers-count");
|
|
218
|
+
if (countEl)
|
|
219
|
+
countEl.textContent =
|
|
220
|
+
(updated.active_workers || 0) +
|
|
221
|
+
" / " +
|
|
222
|
+
(updated.max_workers || value);
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
.catch((err) => console.error("Worker update failed:", err));
|
|
226
|
+
}, 500);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function machineHealthCheckAction(name: string): void {
|
|
230
|
+
const card = document.getElementById("machine-card-" + name);
|
|
231
|
+
if (card) {
|
|
232
|
+
const checkBtn = card.querySelector(
|
|
233
|
+
".machine-action-btn",
|
|
234
|
+
) as HTMLButtonElement;
|
|
235
|
+
if (checkBtn) {
|
|
236
|
+
checkBtn.textContent = "Checking\u2026";
|
|
237
|
+
checkBtn.disabled = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
api
|
|
242
|
+
.machineHealthCheck(name)
|
|
243
|
+
.then((result) => {
|
|
244
|
+
if (result.machine && card) {
|
|
245
|
+
const m = result.machine;
|
|
246
|
+
const health = m.health || {};
|
|
247
|
+
const healthRows = card.querySelectorAll(".machine-health-row");
|
|
248
|
+
if (healthRows.length >= 3) {
|
|
249
|
+
const statusEl = healthRows[0].querySelector(".health-status");
|
|
250
|
+
if (statusEl) {
|
|
251
|
+
statusEl.className =
|
|
252
|
+
"health-status " +
|
|
253
|
+
(health.daemon_running ? "running" : "stopped");
|
|
254
|
+
statusEl.textContent = health.daemon_running
|
|
255
|
+
? "Running"
|
|
256
|
+
: "Stopped";
|
|
257
|
+
}
|
|
258
|
+
const hbEl = healthRows[1].querySelector(".health-value");
|
|
259
|
+
if (hbEl) hbEl.textContent = String(health.heartbeat_count || 0);
|
|
260
|
+
const lastEl = healthRows[2].querySelector(".health-value");
|
|
261
|
+
if (lastEl)
|
|
262
|
+
lastEl.textContent = formatHbAge(health.last_heartbeat_s_ago);
|
|
263
|
+
}
|
|
264
|
+
const dot = card.querySelector(".presence-dot");
|
|
265
|
+
if (dot) dot.className = "presence-dot " + (m.status || "offline");
|
|
266
|
+
}
|
|
267
|
+
resetCheckBtn(card);
|
|
268
|
+
})
|
|
269
|
+
.catch((err) => {
|
|
270
|
+
console.error("Health check failed:", err);
|
|
271
|
+
resetCheckBtn(card);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function resetCheckBtn(card: HTMLElement | null): void {
|
|
276
|
+
if (card) {
|
|
277
|
+
const btn = card.querySelector(".machine-action-btn") as HTMLButtonElement;
|
|
278
|
+
if (btn) {
|
|
279
|
+
btn.textContent = "Check";
|
|
280
|
+
btn.disabled = false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatHbAge(age: number | undefined): string {
|
|
286
|
+
if (typeof age !== "number" || age >= 9999) return "\u2014";
|
|
287
|
+
if (age < 60) return age + "s ago";
|
|
288
|
+
if (age < 3600) return Math.floor(age / 60) + "m ago";
|
|
289
|
+
return Math.floor(age / 3600) + "h ago";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function confirmMachineRemove(name: string): void {
|
|
293
|
+
removeMachineTarget = name;
|
|
294
|
+
const el = document.getElementById("remove-machine-name");
|
|
295
|
+
if (el) el.textContent = name;
|
|
296
|
+
const cb = document.getElementById("remove-stop-daemon") as HTMLInputElement;
|
|
297
|
+
if (cb) cb.checked = false;
|
|
298
|
+
const modal = document.getElementById("remove-machine-modal");
|
|
299
|
+
if (modal) modal.style.display = "flex";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function executeRemoveMachine(): void {
|
|
303
|
+
if (!removeMachineTarget) return;
|
|
304
|
+
api
|
|
305
|
+
.removeMachine(removeMachineTarget)
|
|
306
|
+
.then(() => {
|
|
307
|
+
const modal = document.getElementById("remove-machine-modal");
|
|
308
|
+
if (modal) modal.style.display = "none";
|
|
309
|
+
removeMachineTarget = null;
|
|
310
|
+
refreshMachines();
|
|
311
|
+
})
|
|
312
|
+
.catch((err) => {
|
|
313
|
+
console.error("Remove machine failed:", err);
|
|
314
|
+
const modal = document.getElementById("remove-machine-modal");
|
|
315
|
+
if (modal) modal.style.display = "none";
|
|
316
|
+
removeMachineTarget = null;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function openJoinLinkModal(): void {
|
|
321
|
+
const modal = document.getElementById("join-link-modal");
|
|
322
|
+
if (modal) modal.style.display = "flex";
|
|
323
|
+
setVal("join-label", "");
|
|
324
|
+
setVal("join-workers", "4");
|
|
325
|
+
const cmdDisplay = document.getElementById("join-command-display");
|
|
326
|
+
if (cmdDisplay) cmdDisplay.style.display = "none";
|
|
327
|
+
const cmdText = document.getElementById("join-command-text");
|
|
328
|
+
if (cmdText) cmdText.textContent = "";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function closeJoinLinkModal(): void {
|
|
332
|
+
const modal = document.getElementById("join-link-modal");
|
|
333
|
+
if (modal) modal.style.display = "none";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function generateJoinLink(): void {
|
|
337
|
+
const label = getVal("join-label").trim();
|
|
338
|
+
const maxWorkers = parseInt(getVal("join-workers"), 10) || 4;
|
|
339
|
+
const btn = document.getElementById(
|
|
340
|
+
"join-modal-generate",
|
|
341
|
+
) as HTMLButtonElement;
|
|
342
|
+
if (btn) {
|
|
343
|
+
btn.textContent = "Generating\u2026";
|
|
344
|
+
btn.disabled = true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
api
|
|
348
|
+
.generateJoinToken({ label, max_workers: maxWorkers })
|
|
349
|
+
.then((data) => {
|
|
350
|
+
const cmdText = document.getElementById("join-command-text");
|
|
351
|
+
if (cmdText) cmdText.textContent = data.join_cmd || "";
|
|
352
|
+
const cmdDisplay = document.getElementById("join-command-display");
|
|
353
|
+
if (cmdDisplay) cmdDisplay.style.display = "";
|
|
354
|
+
if (btn) {
|
|
355
|
+
btn.textContent = "Generate";
|
|
356
|
+
btn.disabled = false;
|
|
357
|
+
}
|
|
358
|
+
refreshJoinTokens();
|
|
359
|
+
})
|
|
360
|
+
.catch((err) => {
|
|
361
|
+
console.error("Generate join link failed:", err);
|
|
362
|
+
if (btn) {
|
|
363
|
+
btn.textContent = "Generate";
|
|
364
|
+
btn.disabled = false;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function copyJoinCommand(): void {
|
|
370
|
+
const text = document.getElementById("join-command-text")?.textContent;
|
|
371
|
+
if (text && navigator.clipboard) {
|
|
372
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
373
|
+
const btn = document.getElementById("join-copy-btn");
|
|
374
|
+
if (btn) {
|
|
375
|
+
btn.textContent = "Copied!";
|
|
376
|
+
setTimeout(() => {
|
|
377
|
+
btn.textContent = "Copy";
|
|
378
|
+
}, 2000);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function getVal(id: string): string {
|
|
385
|
+
return (document.getElementById(id) as HTMLInputElement)?.value || "";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function setVal(id: string, value: string): void {
|
|
389
|
+
const el = document.getElementById(id) as HTMLInputElement;
|
|
390
|
+
if (el) el.value = value;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function refreshMachines(): void {
|
|
394
|
+
const tab = store.get("activeTab");
|
|
395
|
+
if (tab === "machines") {
|
|
396
|
+
api.fetchMachines().then((data) => {
|
|
397
|
+
const machines = Array.isArray(data)
|
|
398
|
+
? data
|
|
399
|
+
: ((data as any).machines ?? []);
|
|
400
|
+
store.set("machinesCache", machines);
|
|
401
|
+
});
|
|
402
|
+
api
|
|
403
|
+
.fetchJoinTokens()
|
|
404
|
+
.then(({ tokens }) => store.set("joinTokensCache", tokens));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function refreshJoinTokens(): void {
|
|
409
|
+
api
|
|
410
|
+
.fetchJoinTokens()
|
|
411
|
+
.then(({ tokens }) => store.set("joinTokensCache", tokens))
|
|
412
|
+
.catch(() => {});
|
|
413
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// ANSI terminal renderer for log streaming and live output
|
|
2
|
+
|
|
3
|
+
import { escapeHtml } from "../core/helpers";
|
|
4
|
+
|
|
5
|
+
const ANSI_COLORS: Record<string, string> = {
|
|
6
|
+
"30": "#060a14",
|
|
7
|
+
"31": "#f43f5e",
|
|
8
|
+
"32": "#4ade80",
|
|
9
|
+
"33": "#fbbf24",
|
|
10
|
+
"34": "#0066ff",
|
|
11
|
+
"35": "#7c3aed",
|
|
12
|
+
"36": "#00d4ff",
|
|
13
|
+
"37": "#e8ecf4",
|
|
14
|
+
"90": "#5a6d8a",
|
|
15
|
+
"91": "#f43f5e",
|
|
16
|
+
"92": "#4ade80",
|
|
17
|
+
"93": "#fbbf24",
|
|
18
|
+
"94": "#0066ff",
|
|
19
|
+
"95": "#7c3aed",
|
|
20
|
+
"96": "#00d4ff",
|
|
21
|
+
"97": "#e8ecf4",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function renderAnsiToHtml(text: string): string {
|
|
25
|
+
let result = "";
|
|
26
|
+
let openSpans = 0;
|
|
27
|
+
let i = 0;
|
|
28
|
+
|
|
29
|
+
while (i < text.length) {
|
|
30
|
+
if (text[i] === "\x1b" && text[i + 1] === "[") {
|
|
31
|
+
const end = text.indexOf("m", i + 2);
|
|
32
|
+
if (end !== -1) {
|
|
33
|
+
const codes = text.substring(i + 2, end).split(";");
|
|
34
|
+
for (const code of codes) {
|
|
35
|
+
if (code === "0" || code === "") {
|
|
36
|
+
while (openSpans > 0) {
|
|
37
|
+
result += "</span>";
|
|
38
|
+
openSpans--;
|
|
39
|
+
}
|
|
40
|
+
} else if (code === "1") {
|
|
41
|
+
result += '<span style="font-weight:bold">';
|
|
42
|
+
openSpans++;
|
|
43
|
+
} else if (code === "2") {
|
|
44
|
+
result += '<span style="opacity:0.7">';
|
|
45
|
+
openSpans++;
|
|
46
|
+
} else if (code === "3") {
|
|
47
|
+
result += '<span style="font-style:italic">';
|
|
48
|
+
openSpans++;
|
|
49
|
+
} else if (code === "4") {
|
|
50
|
+
result += '<span style="text-decoration:underline">';
|
|
51
|
+
openSpans++;
|
|
52
|
+
} else if (ANSI_COLORS[code]) {
|
|
53
|
+
result += `<span style="color:${ANSI_COLORS[code]}">`;
|
|
54
|
+
openSpans++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
i = end + 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
result += escapeHtml(text[i]);
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
while (openSpans > 0) {
|
|
65
|
+
result += "</span>";
|
|
66
|
+
openSpans--;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function stripAnsi(text: string): string {
|
|
72
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renderLogViewer(content: string): string {
|
|
76
|
+
if (!content)
|
|
77
|
+
return '<div class="empty-state"><p>No logs available</p></div>';
|
|
78
|
+
const clean = stripAnsi(content);
|
|
79
|
+
const lines = clean.split("\n");
|
|
80
|
+
let html = '<div class="log-viewer">';
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const lineNum = i + 1;
|
|
83
|
+
const lower = lines[i].toLowerCase();
|
|
84
|
+
const lineClass =
|
|
85
|
+
lower.indexOf("error") !== -1 || lower.indexOf("fail") !== -1
|
|
86
|
+
? " log-line-error"
|
|
87
|
+
: "";
|
|
88
|
+
html +=
|
|
89
|
+
`<div class="log-line${lineClass}">` +
|
|
90
|
+
`<span class="log-line-num">${lineNum}</span>` +
|
|
91
|
+
`<span class="log-line-text">${escapeHtml(lines[i])}</span></div>`;
|
|
92
|
+
}
|
|
93
|
+
html += "</div>";
|
|
94
|
+
return html;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class LiveTerminal {
|
|
98
|
+
private container: HTMLElement;
|
|
99
|
+
private lines: string[] = [];
|
|
100
|
+
private autoScroll = true;
|
|
101
|
+
private maxLines = 5000;
|
|
102
|
+
|
|
103
|
+
constructor(container: HTMLElement) {
|
|
104
|
+
this.container = container;
|
|
105
|
+
this.container.classList.add("live-terminal");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
append(text: string): void {
|
|
109
|
+
const newLines = text.split("\n");
|
|
110
|
+
this.lines.push(...newLines);
|
|
111
|
+
if (this.lines.length > this.maxLines) {
|
|
112
|
+
this.lines = this.lines.slice(-this.maxLines);
|
|
113
|
+
}
|
|
114
|
+
this.render();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
clear(): void {
|
|
118
|
+
this.lines = [];
|
|
119
|
+
this.render();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private render(): void {
|
|
123
|
+
const html = this.lines
|
|
124
|
+
.map((line, i) => {
|
|
125
|
+
const rendered = renderAnsiToHtml(line);
|
|
126
|
+
return `<div class="terminal-line"><span class="terminal-line-num">${i + 1}</span><span class="terminal-line-text">${rendered}</span></div>`;
|
|
127
|
+
})
|
|
128
|
+
.join("");
|
|
129
|
+
this.container.innerHTML = html;
|
|
130
|
+
|
|
131
|
+
if (this.autoScroll) {
|
|
132
|
+
this.container.scrollTop = this.container.scrollHeight;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setAutoScroll(enabled: boolean): void {
|
|
137
|
+
this.autoScroll = enabled;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
destroy(): void {
|
|
141
|
+
this.container.innerHTML = "";
|
|
142
|
+
this.lines = [];
|
|
143
|
+
}
|
|
144
|
+
}
|