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,362 @@
|
|
|
1
|
+
// Team tab - connected developers grid + team activity
|
|
2
|
+
|
|
3
|
+
import { store } from "../core/state";
|
|
4
|
+
import { escapeHtml, timeAgo } from "../core/helpers";
|
|
5
|
+
import { icon } from "../design/icons";
|
|
6
|
+
import * as api from "../core/api";
|
|
7
|
+
import type {
|
|
8
|
+
FleetState,
|
|
9
|
+
View,
|
|
10
|
+
TeamData,
|
|
11
|
+
TeamDeveloper,
|
|
12
|
+
TeamActivityEvent,
|
|
13
|
+
} from "../types/api";
|
|
14
|
+
|
|
15
|
+
let teamRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
16
|
+
|
|
17
|
+
function fetchTeamData(): void {
|
|
18
|
+
api
|
|
19
|
+
.fetchTeam()
|
|
20
|
+
.then((data) => {
|
|
21
|
+
store.set("teamCache", data);
|
|
22
|
+
renderTeamGrid(data);
|
|
23
|
+
renderTeamStats(data);
|
|
24
|
+
})
|
|
25
|
+
.catch(() => {});
|
|
26
|
+
|
|
27
|
+
api
|
|
28
|
+
.fetchTeamActivity()
|
|
29
|
+
.then((events) => {
|
|
30
|
+
store.set("teamActivityCache", events);
|
|
31
|
+
renderTeamActivity(events);
|
|
32
|
+
})
|
|
33
|
+
.catch(() => {});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function renderTeamStats(data: TeamData): void {
|
|
37
|
+
const el1 = document.getElementById("team-stat-online");
|
|
38
|
+
if (el1) el1.textContent = String(data.total_online || 0);
|
|
39
|
+
const el2 = document.getElementById("team-stat-pipelines");
|
|
40
|
+
if (el2) el2.textContent = String(data.total_active_pipelines || 0);
|
|
41
|
+
const el3 = document.getElementById("team-stat-queued");
|
|
42
|
+
if (el3) el3.textContent = String(data.total_queued || 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderTeamGrid(data: TeamData): void {
|
|
46
|
+
const grid = document.getElementById("team-grid");
|
|
47
|
+
if (!grid) return;
|
|
48
|
+
const devs = data.developers || [];
|
|
49
|
+
if (devs.length === 0) {
|
|
50
|
+
grid.innerHTML = `<div class="empty-state">${icon("users-round", 32)}<p>No developers connected. Run <code>shipwright connect start</code> to join.</p></div>`;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
grid.innerHTML = devs
|
|
55
|
+
.map((dev) => {
|
|
56
|
+
const presence = dev._presence || "offline";
|
|
57
|
+
const initials = (dev.developer_id || "?").substring(0, 2).toUpperCase();
|
|
58
|
+
const pipelines = (dev.active_jobs || [])
|
|
59
|
+
.map(
|
|
60
|
+
(job) =>
|
|
61
|
+
`<div class="team-card-pipeline-item">` +
|
|
62
|
+
`<span class="team-card-pipeline-issue">#${escapeHtml(String(job.issue))}</span>` +
|
|
63
|
+
`<span class="team-card-pipeline-stage">${escapeHtml(job.stage || "\u2014")}</span></div>`,
|
|
64
|
+
)
|
|
65
|
+
.join("");
|
|
66
|
+
const pipelineSection = pipelines
|
|
67
|
+
? `<div class="team-card-pipelines">${pipelines}</div>`
|
|
68
|
+
: "";
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
`<div class="team-card"><div class="team-card-header">` +
|
|
72
|
+
`<div class="team-card-avatar">${escapeHtml(initials)}</div>` +
|
|
73
|
+
`<div class="team-card-info"><div class="team-card-name">${escapeHtml(dev.developer_id)}</div>` +
|
|
74
|
+
`<div class="team-card-machine">${escapeHtml(dev.machine_name)}</div></div>` +
|
|
75
|
+
`<div class="presence-dot ${presence}" title="${presence}"></div></div>` +
|
|
76
|
+
`<div class="team-card-body">` +
|
|
77
|
+
`<div class="team-card-row"><span class="team-card-row-label">Daemon</span>` +
|
|
78
|
+
`<span class="team-card-row-value">${dev.daemon_running ? "\u25cf Running" : "\u25cb Stopped"}</span></div>` +
|
|
79
|
+
`<div class="team-card-row"><span class="team-card-row-label">Active</span>` +
|
|
80
|
+
`<span class="team-card-row-value">${(dev.active_jobs || []).length} pipelines</span></div>` +
|
|
81
|
+
`<div class="team-card-row"><span class="team-card-row-label">Queued</span>` +
|
|
82
|
+
`<span class="team-card-row-value">${(dev.queued || []).length} issues</span></div>` +
|
|
83
|
+
pipelineSection +
|
|
84
|
+
"</div></div>"
|
|
85
|
+
);
|
|
86
|
+
})
|
|
87
|
+
.join("");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderTeamActivity(events: TeamActivityEvent[]): void {
|
|
91
|
+
const container = document.getElementById("team-activity");
|
|
92
|
+
if (!container) return;
|
|
93
|
+
const items = Array.isArray(events) ? events : [];
|
|
94
|
+
if (items.length === 0) {
|
|
95
|
+
container.innerHTML =
|
|
96
|
+
'<div class="empty-state">No team activity yet.</div>';
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
container.innerHTML = items
|
|
101
|
+
.slice(0, 50)
|
|
102
|
+
.map((evt) => {
|
|
103
|
+
const isCI = evt.from_developer === "github-actions";
|
|
104
|
+
const badgeClass = isCI ? "ci" : "local";
|
|
105
|
+
const badgeText = isCI ? "CI" : evt.from_developer || "local";
|
|
106
|
+
const text = formatTeamEvent(evt);
|
|
107
|
+
const time = evt.ts ? timeAgo(new Date(evt.ts)) : "";
|
|
108
|
+
return (
|
|
109
|
+
`<div class="team-activity-item"><span class="source-badge ${badgeClass}">${escapeHtml(badgeText)}</span>` +
|
|
110
|
+
`<div class="team-activity-content"><div class="team-activity-text">${text}</div>` +
|
|
111
|
+
`<div class="team-activity-time">${time}</div></div></div>`
|
|
112
|
+
);
|
|
113
|
+
})
|
|
114
|
+
.join("");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatTeamEvent(evt: TeamActivityEvent): string {
|
|
118
|
+
const type = evt.type || "";
|
|
119
|
+
const issue = evt.issue ? " #" + evt.issue : "";
|
|
120
|
+
if (type.indexOf("pipeline.started") !== -1)
|
|
121
|
+
return "Pipeline started" + issue;
|
|
122
|
+
if (
|
|
123
|
+
type.indexOf("pipeline.completed") !== -1 ||
|
|
124
|
+
type.indexOf("pipeline_completed") !== -1
|
|
125
|
+
) {
|
|
126
|
+
const result = evt.result === "success" ? "\u2713" : "\u2717";
|
|
127
|
+
return "Pipeline " + result + issue;
|
|
128
|
+
}
|
|
129
|
+
if (type.indexOf("stage.") !== -1) {
|
|
130
|
+
const stage = evt.stage || type.split(".").pop() || "";
|
|
131
|
+
return "Stage " + escapeHtml(stage) + issue;
|
|
132
|
+
}
|
|
133
|
+
if (type.indexOf("daemon.") !== -1)
|
|
134
|
+
return type.replace("daemon.", "Daemon: ");
|
|
135
|
+
if (type.indexOf("ci.") !== -1) return type.replace("ci.", "CI: ") + issue;
|
|
136
|
+
return escapeHtml(type) + issue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function setupInviteButton(): void {
|
|
140
|
+
const btn = document.getElementById("btn-create-invite");
|
|
141
|
+
if (!btn) return;
|
|
142
|
+
btn.addEventListener("click", () => {
|
|
143
|
+
(btn as HTMLButtonElement).disabled = true;
|
|
144
|
+
btn.textContent = "Creating...";
|
|
145
|
+
api
|
|
146
|
+
.createTeamInvite({ expires_hours: 72 })
|
|
147
|
+
.then((data) => {
|
|
148
|
+
const result = document.getElementById("team-invite-result");
|
|
149
|
+
if (result) {
|
|
150
|
+
result.style.display = "";
|
|
151
|
+
result.innerHTML =
|
|
152
|
+
`<div class="invite-link-box">` +
|
|
153
|
+
`<span class="invite-label">Invite link (expires in 72h):</span>` +
|
|
154
|
+
`<code class="invite-url" id="invite-url">${escapeHtml(data.url || data.token)}</code>` +
|
|
155
|
+
`<button class="btn-sm" id="copy-invite">Copy</button></div>`;
|
|
156
|
+
const copyBtn = document.getElementById("copy-invite");
|
|
157
|
+
if (copyBtn) {
|
|
158
|
+
copyBtn.addEventListener("click", () => {
|
|
159
|
+
const url = data.url || data.token;
|
|
160
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
161
|
+
copyBtn.textContent = "Copied!";
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
copyBtn.textContent = "Copy";
|
|
164
|
+
}, 2000);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
.catch(() => {
|
|
171
|
+
const result = document.getElementById("team-invite-result");
|
|
172
|
+
if (result) {
|
|
173
|
+
result.style.display = "";
|
|
174
|
+
result.innerHTML =
|
|
175
|
+
'<div class="invite-error">Failed to create invite. Check server logs.</div>';
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
.finally(() => {
|
|
179
|
+
(btn as HTMLButtonElement).disabled = false;
|
|
180
|
+
btn.textContent = "Create Invite Link";
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function loadIntegrationsStatus(): void {
|
|
186
|
+
const container = document.getElementById("integrations-status");
|
|
187
|
+
if (!container) return;
|
|
188
|
+
|
|
189
|
+
api
|
|
190
|
+
.fetchLinearStatus()
|
|
191
|
+
.then((data) => {
|
|
192
|
+
let html = '<div class="integrations-grid">';
|
|
193
|
+
const connected = data.connected || data.configured || false;
|
|
194
|
+
const statusCls = connected
|
|
195
|
+
? "integration-active"
|
|
196
|
+
: "integration-inactive";
|
|
197
|
+
const statusText = connected ? "Connected" : "Not configured";
|
|
198
|
+
html +=
|
|
199
|
+
`<div class="integration-card ${statusCls}">` +
|
|
200
|
+
`<div class="integration-name">${icon("git-branch", 18)} Linear</div>` +
|
|
201
|
+
`<div class="integration-status">${statusText}</div>`;
|
|
202
|
+
if (data.workspace)
|
|
203
|
+
html += `<div class="integration-detail">Workspace: ${escapeHtml(String(data.workspace))}</div>`;
|
|
204
|
+
if (data.team_id)
|
|
205
|
+
html += `<div class="integration-detail">Team: ${escapeHtml(String(data.team_id))}</div>`;
|
|
206
|
+
html += "</div>";
|
|
207
|
+
|
|
208
|
+
// GitHub (always available if dashboard is running)
|
|
209
|
+
html +=
|
|
210
|
+
`<div class="integration-card integration-active">` +
|
|
211
|
+
`<div class="integration-name">${icon("git-branch", 18)} GitHub</div>` +
|
|
212
|
+
`<div class="integration-status">Connected</div></div>`;
|
|
213
|
+
|
|
214
|
+
html += "</div>";
|
|
215
|
+
container.innerHTML = html;
|
|
216
|
+
})
|
|
217
|
+
.catch(() => {
|
|
218
|
+
container.innerHTML =
|
|
219
|
+
'<div class="empty-state"><p>Could not load status</p></div>';
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function setupAdminDebug(): void {
|
|
224
|
+
const output = document.getElementById("admin-debug-output");
|
|
225
|
+
if (!output) return;
|
|
226
|
+
|
|
227
|
+
const renderJson = (data: unknown) => {
|
|
228
|
+
output.innerHTML = `<pre class="admin-debug-pre">${escapeHtml(JSON.stringify(data, null, 2))}</pre>`;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
document.getElementById("btn-db-health")?.addEventListener("click", () => {
|
|
232
|
+
output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
|
|
233
|
+
api
|
|
234
|
+
.fetchDbHealth()
|
|
235
|
+
.then(renderJson)
|
|
236
|
+
.catch((e) => {
|
|
237
|
+
output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
document.getElementById("btn-db-events")?.addEventListener("click", () => {
|
|
241
|
+
output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
|
|
242
|
+
api
|
|
243
|
+
.fetchDbEvents(0, 50)
|
|
244
|
+
.then(renderJson)
|
|
245
|
+
.catch((e) => {
|
|
246
|
+
output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
document.getElementById("btn-db-jobs")?.addEventListener("click", () => {
|
|
250
|
+
output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
|
|
251
|
+
api
|
|
252
|
+
.fetchDbJobs()
|
|
253
|
+
.then(renderJson)
|
|
254
|
+
.catch((e) => {
|
|
255
|
+
output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
document
|
|
259
|
+
.getElementById("btn-db-heartbeats")
|
|
260
|
+
?.addEventListener("click", () => {
|
|
261
|
+
output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
|
|
262
|
+
api
|
|
263
|
+
.fetchDbHeartbeats()
|
|
264
|
+
.then(renderJson)
|
|
265
|
+
.catch((e) => {
|
|
266
|
+
output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
document.getElementById("btn-db-costs")?.addEventListener("click", () => {
|
|
270
|
+
output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
|
|
271
|
+
api
|
|
272
|
+
.fetchDbCostsToday()
|
|
273
|
+
.then(renderJson)
|
|
274
|
+
.catch((e) => {
|
|
275
|
+
output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function setupClaimPanel(): void {
|
|
281
|
+
const resultEl = document.getElementById("claim-result");
|
|
282
|
+
const issueInput = document.getElementById("claim-issue") as HTMLInputElement;
|
|
283
|
+
const machineInput = document.getElementById(
|
|
284
|
+
"claim-machine",
|
|
285
|
+
) as HTMLInputElement;
|
|
286
|
+
|
|
287
|
+
document.getElementById("btn-claim")?.addEventListener("click", () => {
|
|
288
|
+
const issue = parseInt(issueInput?.value || "0");
|
|
289
|
+
const machine = machineInput?.value.trim();
|
|
290
|
+
if (!issue || !machine) {
|
|
291
|
+
if (resultEl)
|
|
292
|
+
resultEl.innerHTML =
|
|
293
|
+
'<span style="color:var(--rose)">Issue # and machine name required</span>';
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
api
|
|
297
|
+
.claimIssue(issue, machine)
|
|
298
|
+
.then((data) => {
|
|
299
|
+
if (resultEl) {
|
|
300
|
+
if (data.approved) {
|
|
301
|
+
resultEl.innerHTML = `<span style="color:var(--green)">Claimed #${issue} for ${escapeHtml(machine)}</span>`;
|
|
302
|
+
} else {
|
|
303
|
+
resultEl.innerHTML = `<span style="color:var(--amber)">Already claimed by ${escapeHtml(data.claimed_by || "unknown")}</span>`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
.catch((e) => {
|
|
308
|
+
if (resultEl)
|
|
309
|
+
resultEl.innerHTML = `<span style="color:var(--rose)">Error: ${escapeHtml(String(e))}</span>`;
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
document.getElementById("btn-release")?.addEventListener("click", () => {
|
|
314
|
+
const issue = parseInt(issueInput?.value || "0");
|
|
315
|
+
const machine = machineInput?.value.trim();
|
|
316
|
+
if (!issue) {
|
|
317
|
+
if (resultEl)
|
|
318
|
+
resultEl.innerHTML =
|
|
319
|
+
'<span style="color:var(--rose)">Issue # required</span>';
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
api
|
|
323
|
+
.releaseIssue(issue, machine || undefined)
|
|
324
|
+
.then(() => {
|
|
325
|
+
if (resultEl)
|
|
326
|
+
resultEl.innerHTML = `<span style="color:var(--green)">Released claim on #${issue}</span>`;
|
|
327
|
+
})
|
|
328
|
+
.catch((e) => {
|
|
329
|
+
if (resultEl)
|
|
330
|
+
resultEl.innerHTML = `<span style="color:var(--rose)">Error: ${escapeHtml(String(e))}</span>`;
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export const teamView: View = {
|
|
336
|
+
init() {
|
|
337
|
+
fetchTeamData();
|
|
338
|
+
setupInviteButton();
|
|
339
|
+
loadIntegrationsStatus();
|
|
340
|
+
setupAdminDebug();
|
|
341
|
+
setupClaimPanel();
|
|
342
|
+
teamRefreshTimer = setInterval(fetchTeamData, 10000);
|
|
343
|
+
},
|
|
344
|
+
render(data: FleetState) {
|
|
345
|
+
if (data.team) {
|
|
346
|
+
renderTeamGrid(data.team);
|
|
347
|
+
renderTeamStats(data.team);
|
|
348
|
+
} else {
|
|
349
|
+
const cache = store.get("teamCache");
|
|
350
|
+
if (cache) {
|
|
351
|
+
renderTeamGrid(cache);
|
|
352
|
+
renderTeamStats(cache);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
destroy() {
|
|
357
|
+
if (teamRefreshTimer) {
|
|
358
|
+
clearInterval(teamRefreshTimer);
|
|
359
|
+
teamRefreshTimer = null;
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
};
|