@julioborges/gantry 1.0.5 → 1.1.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/.agents/skills/gantry/SKILL.md +15 -1
- package/.agents/skills/gantry/capabilities/codex.json +4 -3
- package/.agents/skills/gantry/dashboard/static/app.js +847 -35
- package/.agents/skills/gantry/dashboard/static/history.html +180 -0
- package/.agents/skills/gantry/dashboard/static/index.html +152 -7
- package/.agents/skills/gantry/dashboard/static/style.css +1177 -36
- package/.agents/skills/gantry/hooks/antigravity.hooks.json +2 -2
- package/.agents/skills/gantry/reference/plan-workflow.md +13 -0
- package/.agents/skills/gantry/reference/round-workflow.md +90 -2
- package/.agents/skills/gantry/scripts/common.py +3 -0
- package/.agents/skills/gantry/scripts/dashboard.py +664 -11
- package/.agents/skills/gantry/scripts/discovery.py +117 -8
- package/.agents/skills/gantry/scripts/execution.py +131 -14
- package/.agents/skills/gantry/scripts/guard.py +6 -0
- package/.agents/skills/gantry/scripts/plan.py +658 -0
- package/.agents/skills/gantry/scripts/result.py +56 -1
- package/.agents/skills/gantry/scripts/runlog.py +2 -1
- package/.agents/skills/gantry/scripts/setup.py +102 -9
- package/.agents/skills/gantry/scripts/wait_gate.py +179 -0
- package/.agents/skills/gantry-dashboard/SKILL.md +21 -2
- package/.agents/skills/gantry-plan/SKILL.md +104 -0
- package/.agents/skills/gantry-setup/SKILL.md +9 -3
- package/README.md +125 -98
- package/package.json +2 -1
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"use strict";
|
|
5
5
|
|
|
6
6
|
const POLL_INTERVAL_MS = 1000;
|
|
7
|
+
let selectedProject = "ALL";
|
|
8
|
+
let latestState = null;
|
|
7
9
|
|
|
8
10
|
function badge(label, extraClass) {
|
|
9
11
|
const span = document.createElement("span");
|
|
@@ -12,79 +14,888 @@
|
|
|
12
14
|
return span;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
function formatDuration(seconds, prefix) {
|
|
18
|
+
if (typeof seconds !== "number" || isNaN(seconds) || seconds < 0) {
|
|
19
|
+
return (prefix !== undefined ? prefix : "") + "0s";
|
|
20
|
+
}
|
|
21
|
+
seconds = Math.floor(seconds);
|
|
22
|
+
const hours = Math.floor(seconds / 3600);
|
|
23
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
24
|
+
const secs = seconds % 60;
|
|
25
|
+
let formatted = "";
|
|
26
|
+
if (hours > 0) {
|
|
27
|
+
formatted = hours + "h " + minutes + "m " + secs + "s";
|
|
28
|
+
} else if (minutes > 0) {
|
|
29
|
+
formatted = minutes + "m " + secs + "s";
|
|
30
|
+
} else {
|
|
31
|
+
formatted = secs + "s";
|
|
32
|
+
}
|
|
33
|
+
return (prefix !== undefined ? prefix : "") + formatted;
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
function renderCard(issue) {
|
|
16
37
|
const card = document.createElement("div");
|
|
17
38
|
card.className = "card";
|
|
39
|
+
if (issue.unitId) card.dataset.unitId = issue.unitId;
|
|
40
|
+
if (issue.project) card.dataset.project = issue.project;
|
|
41
|
+
|
|
42
|
+
const title = document.createElement("div");
|
|
43
|
+
title.className = "card-title";
|
|
44
|
+
const titleText = document.createElement("span");
|
|
45
|
+
titleText.textContent = issue.issue;
|
|
46
|
+
title.appendChild(titleText);
|
|
18
47
|
|
|
19
|
-
|
|
20
|
-
|
|
48
|
+
if (issue.operatorWaiting) {
|
|
49
|
+
title.appendChild(badge("AWAITING OPERATOR", "waiting"));
|
|
50
|
+
} else if (issue.liveActivity) {
|
|
51
|
+
const isThinking = issue.liveActivity.toLowerCase().indexOf("thinking") !== -1;
|
|
52
|
+
title.appendChild(badge(issue.liveActivity, isThinking ? "waiting" : "live"));
|
|
53
|
+
}
|
|
21
54
|
card.appendChild(title);
|
|
22
|
-
card.appendChild(document.createElement("br"));
|
|
23
55
|
|
|
24
|
-
|
|
25
|
-
|
|
56
|
+
const badgesContainer = document.createElement("div");
|
|
57
|
+
badgesContainer.className = "card-badges";
|
|
58
|
+
|
|
59
|
+
if (issue.project) {
|
|
60
|
+
badgesContainer.appendChild(badge(issue.project, "project"));
|
|
61
|
+
}
|
|
62
|
+
if (issue.branch) {
|
|
63
|
+
badgesContainer.appendChild(badge("br: " + issue.branch, "branch"));
|
|
64
|
+
}
|
|
65
|
+
if (issue.worktree) {
|
|
66
|
+
badgesContainer.appendChild(badge("wt: " + issue.worktree, "branch worktree"));
|
|
67
|
+
}
|
|
26
68
|
Object.keys(issue.models || {}).forEach(function (role) {
|
|
27
|
-
|
|
69
|
+
badgesContainer.appendChild(badge(role + ": " + issue.models[role], "model"));
|
|
28
70
|
});
|
|
29
71
|
if (issue.correctionBudget) {
|
|
30
|
-
|
|
31
|
-
badge("corrections: " + issue.correctionBudget.used + "/" + issue.correctionBudget.ceiling)
|
|
72
|
+
badgesContainer.appendChild(
|
|
73
|
+
badge("corrections: " + issue.correctionBudget.used + "/" + issue.correctionBudget.ceiling, "budget")
|
|
32
74
|
);
|
|
33
75
|
}
|
|
34
|
-
if (
|
|
35
|
-
|
|
76
|
+
if (issue.column === "Done") {
|
|
77
|
+
const cycleSecs = typeof issue.totalCycleSeconds === "number" ? issue.totalCycleSeconds : (issue.elapsedPhaseSeconds || 0);
|
|
78
|
+
badgesContainer.appendChild(badge(formatDuration(cycleSecs, "Total: "), "elapsed total-cycle"));
|
|
79
|
+
} else if (typeof issue.elapsedPhaseSeconds === "number") {
|
|
80
|
+
badgesContainer.appendChild(badge(issue.elapsedPhaseSeconds + "s", "elapsed"));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (badgesContainer.children.length > 0) {
|
|
84
|
+
card.appendChild(badgesContainer);
|
|
36
85
|
}
|
|
86
|
+
|
|
37
87
|
if (issue.operatorWaiting) {
|
|
38
|
-
|
|
88
|
+
const actionBar = document.createElement("div");
|
|
89
|
+
actionBar.className = "card-action-bar";
|
|
90
|
+
const approveBtn = document.createElement("button");
|
|
91
|
+
approveBtn.type = "button";
|
|
92
|
+
approveBtn.className = "btn-approve-gate";
|
|
93
|
+
approveBtn.textContent = "Aprovar Gate";
|
|
94
|
+
approveBtn.addEventListener("click", function (e) {
|
|
95
|
+
e.stopPropagation();
|
|
96
|
+
approveGate(issue, approveBtn);
|
|
97
|
+
});
|
|
98
|
+
actionBar.appendChild(approveBtn);
|
|
99
|
+
card.appendChild(actionBar);
|
|
39
100
|
}
|
|
101
|
+
|
|
102
|
+
card.addEventListener("click", function () {
|
|
103
|
+
openExecutionModal(issue);
|
|
104
|
+
});
|
|
105
|
+
|
|
40
106
|
return card;
|
|
41
107
|
}
|
|
42
108
|
|
|
43
|
-
function
|
|
109
|
+
function updateProjectSelect(projects, runs) {
|
|
110
|
+
const select = document.getElementById("project-select");
|
|
111
|
+
if (!select) return;
|
|
112
|
+
|
|
113
|
+
const available = [];
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
|
|
116
|
+
(projects || []).forEach(function (p) {
|
|
117
|
+
if (!seen.has(p.unitId)) {
|
|
118
|
+
seen.add(p.unitId);
|
|
119
|
+
available.push({ id: p.unitId, name: p.name || p.unitId });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
(runs || []).forEach(function (r) {
|
|
124
|
+
if (!seen.has(r.unitId)) {
|
|
125
|
+
seen.add(r.unitId);
|
|
126
|
+
const repoName = r.repositoryRoot ? r.repositoryRoot.split("/").filter(Boolean).pop() : r.unitId;
|
|
127
|
+
available.push({ id: r.unitId, name: repoName || r.unitId });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const currentOptions = Array.from(select.options).map(function (o) {
|
|
132
|
+
return o.value;
|
|
133
|
+
});
|
|
134
|
+
const newOptions = ["ALL"].concat(
|
|
135
|
+
available.map(function (a) {
|
|
136
|
+
return a.id;
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
const changed =
|
|
140
|
+
currentOptions.length !== newOptions.length ||
|
|
141
|
+
!newOptions.every(function (val, i) {
|
|
142
|
+
return currentOptions[i] === val;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (changed) {
|
|
146
|
+
const prevVal = select.value || selectedProject;
|
|
147
|
+
select.innerHTML = "";
|
|
148
|
+
const allOpt = document.createElement("option");
|
|
149
|
+
allOpt.value = "ALL";
|
|
150
|
+
allOpt.textContent = "ALL";
|
|
151
|
+
select.appendChild(allOpt);
|
|
152
|
+
|
|
153
|
+
available.forEach(function (proj) {
|
|
154
|
+
const opt = document.createElement("option");
|
|
155
|
+
opt.value = proj.id;
|
|
156
|
+
opt.textContent = proj.name;
|
|
157
|
+
select.appendChild(opt);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (newOptions.indexOf(prevVal) !== -1) {
|
|
161
|
+
select.value = prevVal;
|
|
162
|
+
selectedProject = prevVal;
|
|
163
|
+
} else {
|
|
164
|
+
select.value = "ALL";
|
|
165
|
+
selectedProject = "ALL";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function render(state) {
|
|
171
|
+
latestState = state;
|
|
172
|
+
const root = document.getElementById("swimlanes");
|
|
173
|
+
root.textContent = "";
|
|
174
|
+
|
|
175
|
+
updateProjectSelect(state.projects, state.runs);
|
|
176
|
+
|
|
177
|
+
if (!state.runs || state.runs.length === 0) {
|
|
178
|
+
const empty = document.createElement("div");
|
|
179
|
+
empty.className = "empty-state";
|
|
180
|
+
const title = document.createElement("div");
|
|
181
|
+
title.className = "empty-state-title";
|
|
182
|
+
title.textContent = "NO ACTIVE RUNS DETECTED";
|
|
183
|
+
empty.appendChild(title);
|
|
184
|
+
const desc = document.createElement("p");
|
|
185
|
+
desc.textContent = "No execution units found in ~/.gantry/state.";
|
|
186
|
+
empty.appendChild(desc);
|
|
187
|
+
root.appendChild(empty);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const filteredRuns =
|
|
192
|
+
selectedProject === "ALL"
|
|
193
|
+
? state.runs
|
|
194
|
+
: state.runs.filter(function (run) {
|
|
195
|
+
return run.unitId === selectedProject;
|
|
196
|
+
});
|
|
197
|
+
|
|
44
198
|
const section = document.createElement("section");
|
|
45
|
-
|
|
199
|
+
const isStale = filteredRuns.some(function (r) {
|
|
200
|
+
return r.stale;
|
|
201
|
+
});
|
|
202
|
+
section.className = "swimlane" + (isStale ? " stale" : "");
|
|
203
|
+
|
|
204
|
+
const header = document.createElement("div");
|
|
205
|
+
header.className = "swimlane-header";
|
|
206
|
+
|
|
207
|
+
const titleGroup = document.createElement("div");
|
|
208
|
+
titleGroup.className = "swimlane-title-group";
|
|
46
209
|
|
|
47
210
|
const heading = document.createElement("h2");
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
211
|
+
if (selectedProject === "ALL") {
|
|
212
|
+
heading.textContent = "ALL PROJECTS — UNIFIED KANBAN";
|
|
213
|
+
} else {
|
|
214
|
+
const proj = (state.projects || []).find(function (p) {
|
|
215
|
+
return p.unitId === selectedProject;
|
|
216
|
+
});
|
|
217
|
+
const projName = proj ? proj.name : selectedProject;
|
|
218
|
+
heading.textContent = projName + " — KANBAN";
|
|
219
|
+
}
|
|
220
|
+
titleGroup.appendChild(heading);
|
|
51
221
|
|
|
52
|
-
|
|
53
|
-
|
|
222
|
+
const meta = document.createElement("div");
|
|
223
|
+
meta.className = "swimlane-meta";
|
|
224
|
+
|
|
225
|
+
if (selectedProject === "ALL") {
|
|
226
|
+
const uniqueProjects = new Set(
|
|
227
|
+
filteredRuns.map(function (r) {
|
|
228
|
+
return r.unitId;
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
meta.appendChild(badge("projects: " + uniqueProjects.size, "tier"));
|
|
232
|
+
meta.appendChild(badge("runs: " + filteredRuns.length, "tier"));
|
|
233
|
+
} else {
|
|
234
|
+
const runTiers = Array.from(
|
|
235
|
+
new Set(
|
|
236
|
+
filteredRuns.map(function (r) {
|
|
237
|
+
return r.tier;
|
|
238
|
+
})
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
if (runTiers.length > 0) {
|
|
242
|
+
meta.appendChild(badge("tier: " + runTiers.join(", "), "tier"));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isStale) {
|
|
247
|
+
meta.appendChild(badge("STALE", "stale"));
|
|
248
|
+
} else {
|
|
249
|
+
meta.appendChild(badge("LIVE", "live"));
|
|
54
250
|
}
|
|
55
251
|
|
|
252
|
+
header.appendChild(titleGroup);
|
|
253
|
+
header.appendChild(meta);
|
|
254
|
+
section.appendChild(header);
|
|
255
|
+
|
|
56
256
|
const columnsEl = document.createElement("div");
|
|
57
257
|
columnsEl.className = "columns";
|
|
58
258
|
|
|
59
|
-
|
|
259
|
+
const allIssues = [];
|
|
260
|
+
filteredRuns.forEach(function (run) {
|
|
261
|
+
(run.issues || []).forEach(function (issue) {
|
|
262
|
+
allIssues.push(issue);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
state.columns.forEach(function (columnName) {
|
|
60
267
|
const columnEl = document.createElement("div");
|
|
61
268
|
columnEl.className = "column";
|
|
62
|
-
|
|
269
|
+
|
|
270
|
+
const colHeader = document.createElement("div");
|
|
271
|
+
colHeader.className = "column-header";
|
|
272
|
+
|
|
273
|
+
const title = document.createElement("span");
|
|
274
|
+
title.className = "column-title";
|
|
63
275
|
title.textContent = columnName;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
276
|
+
colHeader.appendChild(title);
|
|
277
|
+
|
|
278
|
+
const matchingIssues = allIssues.filter(function (issue) {
|
|
279
|
+
return issue.column === columnName;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const count = document.createElement("span");
|
|
283
|
+
count.className = "column-count";
|
|
284
|
+
count.textContent = matchingIssues.length;
|
|
285
|
+
colHeader.appendChild(count);
|
|
286
|
+
|
|
287
|
+
columnEl.appendChild(colHeader);
|
|
288
|
+
|
|
289
|
+
const cardsContainer = document.createElement("div");
|
|
290
|
+
cardsContainer.className = "column-cards";
|
|
291
|
+
matchingIssues.forEach(function (issue) {
|
|
292
|
+
cardsContainer.appendChild(renderCard(issue));
|
|
293
|
+
});
|
|
294
|
+
columnEl.appendChild(cardsContainer);
|
|
295
|
+
|
|
72
296
|
columnsEl.appendChild(columnEl);
|
|
73
297
|
});
|
|
74
298
|
|
|
75
299
|
section.appendChild(columnsEl);
|
|
76
|
-
|
|
300
|
+
root.appendChild(section);
|
|
301
|
+
|
|
302
|
+
if (activeModalIssue) {
|
|
303
|
+
let found = null;
|
|
304
|
+
for (let r = 0; r < (state.runs || []).length; r++) {
|
|
305
|
+
const run = state.runs[r];
|
|
306
|
+
for (let i = 0; i < (run.issues || []).length; i++) {
|
|
307
|
+
const iss = run.issues[i];
|
|
308
|
+
if (iss.issue === activeModalIssue.issue && iss.unitId === activeModalIssue.unitId && iss.run === activeModalIssue.run) {
|
|
309
|
+
found = iss;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (found) break;
|
|
314
|
+
}
|
|
315
|
+
if (found) {
|
|
316
|
+
activeModalIssue = found;
|
|
317
|
+
updateModalDurations(found);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
77
320
|
}
|
|
78
321
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
322
|
+
let activeModalIssue = null;
|
|
323
|
+
let modalPollInterval = null;
|
|
324
|
+
|
|
325
|
+
function formatDiff(content) {
|
|
326
|
+
const container = document.createElement("div");
|
|
327
|
+
container.className = "diff-record";
|
|
328
|
+
const title = document.createElement("div");
|
|
329
|
+
title.className = "tool-name";
|
|
330
|
+
title.textContent = "CODE CHANGES / DIFF";
|
|
331
|
+
container.appendChild(title);
|
|
332
|
+
|
|
333
|
+
const diffEl = document.createElement("pre");
|
|
334
|
+
diffEl.className = "diff-content";
|
|
335
|
+
const lines = (content || "").split("\n");
|
|
336
|
+
lines.forEach(function (line) {
|
|
337
|
+
const lineSpan = document.createElement("span");
|
|
338
|
+
if (line.startsWith("+")) {
|
|
339
|
+
lineSpan.className = "diff-line-add";
|
|
340
|
+
} else if (line.startsWith("-")) {
|
|
341
|
+
lineSpan.className = "diff-line-del";
|
|
342
|
+
} else if (line.startsWith("@@") || line.startsWith("diff") || line.startsWith("index")) {
|
|
343
|
+
lineSpan.className = "diff-line-info";
|
|
344
|
+
}
|
|
345
|
+
lineSpan.textContent = line + "\n";
|
|
346
|
+
diffEl.appendChild(lineSpan);
|
|
347
|
+
});
|
|
348
|
+
container.appendChild(diffEl);
|
|
349
|
+
return container;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function renderTranscriptSteps(steps, container) {
|
|
353
|
+
container.innerHTML = "";
|
|
354
|
+
if (!steps || steps.length === 0) {
|
|
355
|
+
const empty = document.createElement("div");
|
|
356
|
+
empty.className = "transcript-loading";
|
|
357
|
+
empty.textContent = "No transcript steps recorded yet.";
|
|
358
|
+
container.appendChild(empty);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
steps.forEach(function (step) {
|
|
363
|
+
// 1. Collapsible thinking block
|
|
364
|
+
if (step.thinking && typeof step.thinking === "string") {
|
|
365
|
+
const block = document.createElement("div");
|
|
366
|
+
block.className = "thinking-block";
|
|
367
|
+
|
|
368
|
+
const header = document.createElement("div");
|
|
369
|
+
header.className = "thinking-header";
|
|
370
|
+
const headerText = document.createElement("span");
|
|
371
|
+
headerText.textContent = "MODEL REASONING (THINKING)";
|
|
372
|
+
const toggleText = document.createElement("span");
|
|
373
|
+
toggleText.className = "thinking-toggle";
|
|
374
|
+
toggleText.textContent = "[+] EXPAND";
|
|
375
|
+
header.appendChild(headerText);
|
|
376
|
+
header.appendChild(toggleText);
|
|
377
|
+
|
|
378
|
+
const content = document.createElement("div");
|
|
379
|
+
content.className = "thinking-content collapsed";
|
|
380
|
+
content.textContent = step.thinking;
|
|
381
|
+
|
|
382
|
+
header.addEventListener("click", function () {
|
|
383
|
+
const isCollapsed = content.classList.contains("collapsed");
|
|
384
|
+
if (isCollapsed) {
|
|
385
|
+
content.classList.remove("collapsed");
|
|
386
|
+
toggleText.textContent = "[-] COLLAPSE";
|
|
387
|
+
} else {
|
|
388
|
+
content.classList.add("collapsed");
|
|
389
|
+
toggleText.textContent = "[+] EXPAND";
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
block.appendChild(header);
|
|
394
|
+
block.appendChild(content);
|
|
395
|
+
container.appendChild(block);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 2. Tool calls
|
|
399
|
+
if (step.tool_calls && Array.isArray(step.tool_calls)) {
|
|
400
|
+
step.tool_calls.forEach(function (tool) {
|
|
401
|
+
const toolEl = document.createElement("div");
|
|
402
|
+
toolEl.className = "tool-record";
|
|
403
|
+
|
|
404
|
+
const nameEl = document.createElement("div");
|
|
405
|
+
nameEl.className = "tool-name";
|
|
406
|
+
nameEl.textContent = "TOOL: " + (tool.name || "unknown");
|
|
407
|
+
toolEl.appendChild(nameEl);
|
|
408
|
+
|
|
409
|
+
if (tool.args) {
|
|
410
|
+
const argsEl = document.createElement("pre");
|
|
411
|
+
argsEl.className = "tool-args";
|
|
412
|
+
argsEl.textContent = JSON.stringify(tool.args, null, 2);
|
|
413
|
+
toolEl.appendChild(argsEl);
|
|
414
|
+
}
|
|
415
|
+
container.appendChild(toolEl);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 3. Diff or output content
|
|
420
|
+
if (step.content && typeof step.content === "string") {
|
|
421
|
+
if (step.content.includes("diff --git") || step.content.includes("--- a/") || step.content.includes("+added line")) {
|
|
422
|
+
container.appendChild(formatDiff(step.content));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function approveGate(issue, btnEl, onComplete) {
|
|
429
|
+
if (!issue || !issue.unitId || !issue.run || !issue.issue) return;
|
|
430
|
+
if (btnEl) {
|
|
431
|
+
btnEl.disabled = true;
|
|
432
|
+
btnEl.textContent = "Aprovando...";
|
|
433
|
+
}
|
|
434
|
+
const encodedIssue = encodeURIComponent(issue.issue);
|
|
435
|
+
fetch("/api/runs/" + issue.unitId + "/" + issue.run + "/issues/" + encodedIssue + "/approve", {
|
|
436
|
+
method: "POST",
|
|
437
|
+
headers: { "Content-Type": "application/json" }
|
|
438
|
+
})
|
|
439
|
+
.then(function (res) {
|
|
440
|
+
return res.json();
|
|
441
|
+
})
|
|
442
|
+
.then(function (data) {
|
|
443
|
+
if (btnEl) {
|
|
444
|
+
btnEl.textContent = "Aprovado ✓";
|
|
445
|
+
btnEl.classList.add("approved");
|
|
446
|
+
}
|
|
447
|
+
issue.operatorWaiting = false;
|
|
448
|
+
issue.operatorApproved = true;
|
|
449
|
+
const gateAlert = document.getElementById("modal-gate-alert");
|
|
450
|
+
if (gateAlert) {
|
|
451
|
+
gateAlert.className = "gate-alert success";
|
|
452
|
+
const alertText = document.getElementById("modal-gate-alert-text");
|
|
453
|
+
if (alertText) alertText.textContent = "✓ Gate aprovado pelo operador. Iniciando integração...";
|
|
454
|
+
}
|
|
455
|
+
const activityBadge = document.getElementById("modal-activity-badge");
|
|
456
|
+
if (activityBadge) {
|
|
457
|
+
activityBadge.textContent = "GATE APROVADO";
|
|
458
|
+
activityBadge.className = "badge live";
|
|
459
|
+
}
|
|
460
|
+
if (typeof onComplete === "function") {
|
|
461
|
+
onComplete(data);
|
|
462
|
+
}
|
|
463
|
+
poll();
|
|
464
|
+
})
|
|
465
|
+
.catch(function () {
|
|
466
|
+
if (btnEl) {
|
|
467
|
+
btnEl.disabled = false;
|
|
468
|
+
btnEl.textContent = "Erro ao Aprovar";
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function renderGates(gates, container) {
|
|
474
|
+
container.innerHTML = "";
|
|
475
|
+
if (!gates || Object.keys(gates).length === 0) {
|
|
476
|
+
container.innerHTML = '<div class="transcript-loading">Nenhum parecer de gate registrado ainda.</div>';
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const phases = [
|
|
481
|
+
{ key: "plan", label: "Gate 1: Planejamento (Plan)", desc: "Critérios de aceitação e escopo planejado" },
|
|
482
|
+
{ key: "implement", label: "Gate 2: Implementação (Implement)", desc: "Provas de TDD e cobertura de testes" },
|
|
483
|
+
{ key: "critic", label: "Gate 3: Avaliação Adversarial (Critic)", desc: "Verificação rigorosa de critérios e evidências" },
|
|
484
|
+
{ key: "integrate", label: "Gate 4: Integração (Integrate)", desc: "Verificação pós-merge e entrega de código" },
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
phases.forEach(function (phase) {
|
|
488
|
+
const gdata = gates[phase.key];
|
|
489
|
+
const card = document.createElement("div");
|
|
490
|
+
card.className = "gate-card";
|
|
491
|
+
|
|
492
|
+
const header = document.createElement("div");
|
|
493
|
+
header.className = "gate-card-header";
|
|
494
|
+
const title = document.createElement("span");
|
|
495
|
+
title.className = "gate-card-title";
|
|
496
|
+
title.textContent = phase.label;
|
|
497
|
+
header.appendChild(title);
|
|
498
|
+
|
|
499
|
+
const statusBadge = document.createElement("span");
|
|
500
|
+
if (gdata) {
|
|
501
|
+
const isPassed = gdata.complete === true || gdata.verdict === "accepted" || gdata.verdict === "complete" || gdata.verdict === "tests_passed" || gdata.verdict === "merged";
|
|
502
|
+
statusBadge.className = "badge " + (isPassed ? "live" : "stale");
|
|
503
|
+
statusBadge.textContent = isPassed ? "APROVADO" : "PENDENTE / REFUTADO";
|
|
504
|
+
} else {
|
|
505
|
+
statusBadge.className = "badge";
|
|
506
|
+
statusBadge.textContent = "NÃO INICIADO";
|
|
507
|
+
}
|
|
508
|
+
header.appendChild(statusBadge);
|
|
509
|
+
card.appendChild(header);
|
|
510
|
+
|
|
511
|
+
const body = document.createElement("div");
|
|
512
|
+
body.className = "gate-card-body";
|
|
513
|
+
|
|
514
|
+
if (!gdata) {
|
|
515
|
+
body.innerHTML = '<div style="color: var(--slate-500); font-style: italic;">Aguardando execução desta fase.</div>';
|
|
516
|
+
} else {
|
|
517
|
+
if (phase.key === "plan") {
|
|
518
|
+
const scopeDiv = document.createElement("div");
|
|
519
|
+
scopeDiv.className = "gate-metric";
|
|
520
|
+
scopeDiv.innerHTML = '<span class="gate-metric-label">Escopo Planejado:</span><span>' + (Array.isArray(gdata.scope) ? gdata.scope.length + " itens" : (gdata.scope || "Definido")) + '</span>';
|
|
521
|
+
body.appendChild(scopeDiv);
|
|
522
|
+
|
|
523
|
+
if (gdata.criteria && Array.isArray(gdata.criteria)) {
|
|
524
|
+
const critDiv = document.createElement("div");
|
|
525
|
+
critDiv.innerHTML = '<div class="gate-metric-label">Critérios Avaliados:</div>';
|
|
526
|
+
const list = document.createElement("ul");
|
|
527
|
+
list.className = "gate-checklist";
|
|
528
|
+
gdata.criteria.forEach(function (c) {
|
|
529
|
+
const li = document.createElement("li");
|
|
530
|
+
li.className = "gate-checklist-item";
|
|
531
|
+
li.textContent = "✓ " + (typeof c === "string" ? c : c.text || JSON.stringify(c));
|
|
532
|
+
list.appendChild(li);
|
|
533
|
+
});
|
|
534
|
+
critDiv.appendChild(list);
|
|
535
|
+
body.appendChild(critDiv);
|
|
536
|
+
}
|
|
537
|
+
} else if (phase.key === "implement") {
|
|
538
|
+
const tddDiv = document.createElement("div");
|
|
539
|
+
tddDiv.className = "gate-metric";
|
|
540
|
+
tddDiv.innerHTML = '<span class="gate-metric-label">TDD Proofs:</span><span>' + (gdata.tddProofs ? "Passou (Red-Green Verificado)" : (gdata.verdict || "Concluído")) + '</span>';
|
|
541
|
+
body.appendChild(tddDiv);
|
|
542
|
+
|
|
543
|
+
if (gdata.attempt) {
|
|
544
|
+
const attDiv = document.createElement("div");
|
|
545
|
+
attDiv.className = "gate-metric";
|
|
546
|
+
attDiv.innerHTML = '<span class="gate-metric-label">Tentativas:</span><span>' + gdata.attempt + '</span>';
|
|
547
|
+
body.appendChild(attDiv);
|
|
548
|
+
}
|
|
549
|
+
} else if (phase.key === "critic") {
|
|
550
|
+
const compDiv = document.createElement("div");
|
|
551
|
+
compDiv.className = "gate-metric";
|
|
552
|
+
compDiv.innerHTML = '<span class="gate-metric-label">Veredito Adversarial:</span><span>' + (gdata.complete ? "Aprovado (Zero Refutações)" : "Refutado") + '</span>';
|
|
553
|
+
body.appendChild(compDiv);
|
|
554
|
+
|
|
555
|
+
if (gdata.evidence && Array.isArray(gdata.evidence) && gdata.evidence.length > 0) {
|
|
556
|
+
const evDiv = document.createElement("div");
|
|
557
|
+
evDiv.innerHTML = '<div class="gate-metric-label">Evidências de Verificação:</div>';
|
|
558
|
+
const list = document.createElement("ul");
|
|
559
|
+
list.className = "gate-checklist";
|
|
560
|
+
gdata.evidence.forEach(function (e) {
|
|
561
|
+
const li = document.createElement("li");
|
|
562
|
+
li.className = "gate-checklist-item";
|
|
563
|
+
li.textContent = "✓ " + (typeof e === "string" ? e : JSON.stringify(e));
|
|
564
|
+
list.appendChild(li);
|
|
565
|
+
});
|
|
566
|
+
evDiv.appendChild(list);
|
|
567
|
+
body.appendChild(evDiv);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (gdata.gateFailures && Array.isArray(gdata.gateFailures) && gdata.gateFailures.length > 0) {
|
|
571
|
+
const failDiv = document.createElement("div");
|
|
572
|
+
failDiv.innerHTML = '<div class="gate-metric-label" style="color: var(--red-alert)">Falhas Detectadas:</div>';
|
|
573
|
+
const list = document.createElement("ul");
|
|
574
|
+
list.className = "gate-checklist";
|
|
575
|
+
gdata.gateFailures.forEach(function (f) {
|
|
576
|
+
const li = document.createElement("li");
|
|
577
|
+
li.className = "gate-checklist-item";
|
|
578
|
+
li.style.color = "var(--red-alert)";
|
|
579
|
+
li.textContent = "✗ " + (typeof f === "string" ? f : JSON.stringify(f));
|
|
580
|
+
list.appendChild(li);
|
|
581
|
+
});
|
|
582
|
+
failDiv.appendChild(list);
|
|
583
|
+
body.appendChild(failDiv);
|
|
584
|
+
}
|
|
585
|
+
} else if (phase.key === "integrate") {
|
|
586
|
+
const intDiv = document.createElement("div");
|
|
587
|
+
intDiv.className = "gate-metric";
|
|
588
|
+
const strat = gdata.strategy === "pull-request" ? "Pull Request (Verificado)" : "Branch Merge (Verificado)";
|
|
589
|
+
intDiv.innerHTML = '<span class="gate-metric-label">Status de Integração:</span><span>' + (gdata.verdict === "merged" ? "Merge Concluído com Sucesso (" + strat + ")" : (gdata.verdict || "Concluído")) + '</span>';
|
|
590
|
+
body.appendChild(intDiv);
|
|
591
|
+
if (gdata.worktree) {
|
|
592
|
+
const wtDiv = document.createElement("div");
|
|
593
|
+
wtDiv.className = "gate-metric";
|
|
594
|
+
wtDiv.innerHTML = '<span class="gate-metric-label">Worktree:</span><span>' + gdata.worktree + '</span>';
|
|
595
|
+
body.appendChild(wtDiv);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
card.appendChild(body);
|
|
601
|
+
container.appendChild(card);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function fetchModalGates(issue) {
|
|
606
|
+
if (!issue || !issue.unitId || !issue.run || !issue.issue) return;
|
|
607
|
+
const container = document.getElementById("gates-container");
|
|
608
|
+
if (!container) return;
|
|
609
|
+
|
|
610
|
+
const encodedIssue = encodeURIComponent(issue.issue);
|
|
611
|
+
fetch("/api/runs/" + issue.unitId + "/" + issue.run + "/issues/" + encodedIssue + "/gates", { cache: "no-store" })
|
|
612
|
+
.then(function (res) {
|
|
613
|
+
return res.json();
|
|
614
|
+
})
|
|
615
|
+
.then(function (data) {
|
|
616
|
+
if (activeModalIssue && activeModalIssue.issue === issue.issue) {
|
|
617
|
+
renderGates(data.gates || {}, container);
|
|
618
|
+
}
|
|
619
|
+
})
|
|
620
|
+
.catch(function () {
|
|
621
|
+
// Keep existing on error
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function fetchModalTranscript(issue) {
|
|
626
|
+
if (!issue || !issue.unitId || !issue.run || !issue.issue) return;
|
|
627
|
+
const container = document.getElementById("modal-transcript-container");
|
|
628
|
+
if (!container) return;
|
|
629
|
+
|
|
630
|
+
const encodedIssue = encodeURIComponent(issue.issue);
|
|
631
|
+
fetch("/api/runs/" + issue.unitId + "/" + issue.run + "/issues/" + encodedIssue + "/transcript", { cache: "no-store" })
|
|
632
|
+
.then(function (res) {
|
|
633
|
+
return res.json();
|
|
634
|
+
})
|
|
635
|
+
.then(function (data) {
|
|
636
|
+
if (activeModalIssue && activeModalIssue.issue === issue.issue) {
|
|
637
|
+
renderTranscriptSteps(data.steps || [], container);
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
.catch(function () {
|
|
641
|
+
// Leave previous state on error
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function updateModalDurations(issue) {
|
|
646
|
+
if (!issue) return;
|
|
647
|
+
const durs = issue.phaseDurations || {};
|
|
648
|
+
const phases = ["Plan", "Implement", "Review", "Critic", "Integrate"];
|
|
649
|
+
phases.forEach(function (ph) {
|
|
650
|
+
const el = document.getElementById("dur-" + ph.toLowerCase());
|
|
651
|
+
if (el) {
|
|
652
|
+
if (typeof durs[ph] === "number") {
|
|
653
|
+
el.textContent = formatDuration(durs[ph]);
|
|
654
|
+
} else {
|
|
655
|
+
el.textContent = "-";
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
const totalEl = document.getElementById("dur-total");
|
|
660
|
+
if (totalEl) {
|
|
661
|
+
if (typeof issue.totalCycleSeconds === "number") {
|
|
662
|
+
totalEl.textContent = formatDuration(issue.totalCycleSeconds);
|
|
663
|
+
} else if (typeof issue.elapsedPhaseSeconds === "number") {
|
|
664
|
+
totalEl.textContent = formatDuration(issue.elapsedPhaseSeconds);
|
|
665
|
+
} else {
|
|
666
|
+
totalEl.textContent = "-";
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function openExecutionModal(issue) {
|
|
672
|
+
activeModalIssue = issue;
|
|
673
|
+
const modal = document.getElementById("execution-modal");
|
|
674
|
+
if (!modal) return;
|
|
675
|
+
|
|
676
|
+
updateModalDurations(issue);
|
|
677
|
+
|
|
678
|
+
document.getElementById("modal-issue-title").textContent = "ISSUE: " + issue.issue;
|
|
679
|
+
const phaseBadge = document.getElementById("modal-phase-badge");
|
|
680
|
+
phaseBadge.textContent = issue.column;
|
|
681
|
+
phaseBadge.className = "badge tier";
|
|
682
|
+
|
|
683
|
+
const activityBadge = document.getElementById("modal-activity-badge");
|
|
684
|
+
if (issue.operatorWaiting) {
|
|
685
|
+
activityBadge.textContent = "AWAITING OPERATOR";
|
|
686
|
+
activityBadge.className = "badge waiting";
|
|
687
|
+
activityBadge.style.display = "inline-flex";
|
|
688
|
+
} else if (issue.liveActivity) {
|
|
689
|
+
activityBadge.textContent = issue.liveActivity;
|
|
690
|
+
activityBadge.className = "badge live";
|
|
691
|
+
activityBadge.style.display = "inline-flex";
|
|
692
|
+
} else {
|
|
693
|
+
activityBadge.style.display = "none";
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
document.getElementById("modal-meta-project").textContent = "PROJECT: " + (issue.project || issue.unitId || "-");
|
|
697
|
+
document.getElementById("modal-meta-unit").textContent = "UNIT: " + (issue.unitId || "-");
|
|
698
|
+
document.getElementById("modal-meta-run").textContent = "RUN: " + (issue.run || "-");
|
|
699
|
+
document.getElementById("modal-meta-branch").textContent = "BRANCH: " + (issue.branch || "-");
|
|
700
|
+
|
|
701
|
+
// Gate alert strip
|
|
702
|
+
const gateAlert = document.getElementById("modal-gate-alert");
|
|
703
|
+
const modalApproveBtn = document.getElementById("modal-approve-btn");
|
|
704
|
+
if (gateAlert && modalApproveBtn) {
|
|
705
|
+
if (issue.operatorWaiting) {
|
|
706
|
+
gateAlert.className = "gate-alert";
|
|
707
|
+
gateAlert.classList.remove("hidden");
|
|
708
|
+
modalApproveBtn.disabled = false;
|
|
709
|
+
modalApproveBtn.textContent = "Aprovar Gate";
|
|
710
|
+
modalApproveBtn.onclick = function () {
|
|
711
|
+
approveGate(issue, modalApproveBtn);
|
|
712
|
+
};
|
|
713
|
+
} else {
|
|
714
|
+
gateAlert.classList.add("hidden");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Default to gates tab
|
|
719
|
+
const tabGates = document.getElementById("tab-btn-gates");
|
|
720
|
+
const tabHistory = document.getElementById("tab-btn-history");
|
|
721
|
+
const paneGates = document.getElementById("tab-content-gates");
|
|
722
|
+
const paneHistory = document.getElementById("tab-content-history");
|
|
723
|
+
if (tabGates && tabHistory && paneGates && paneHistory) {
|
|
724
|
+
tabGates.classList.add("active");
|
|
725
|
+
tabHistory.classList.remove("active");
|
|
726
|
+
paneGates.classList.remove("hidden");
|
|
727
|
+
paneHistory.classList.add("hidden");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Popout button
|
|
731
|
+
const popoutBtn = document.getElementById("btn-popout-history");
|
|
732
|
+
if (popoutBtn) {
|
|
733
|
+
popoutBtn.onclick = function () {
|
|
734
|
+
const url = "/history.html?unit=" + encodeURIComponent(issue.unitId) + "&run=" + encodeURIComponent(issue.run) + "&issue=" + encodeURIComponent(issue.issue);
|
|
735
|
+
window.open(url, "_blank");
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const gatesContainer = document.getElementById("gates-container");
|
|
740
|
+
if (gatesContainer) gatesContainer.innerHTML = '<div class="transcript-loading">Carregando pareceres dos gates...</div>';
|
|
741
|
+
|
|
742
|
+
const transcriptContainer = document.getElementById("modal-transcript-container");
|
|
743
|
+
if (transcriptContainer) transcriptContainer.innerHTML = '<div class="transcript-loading">Awaiting transcript events...</div>';
|
|
744
|
+
|
|
745
|
+
modal.classList.remove("hidden");
|
|
746
|
+
|
|
747
|
+
fetchModalGates(issue);
|
|
748
|
+
fetchModalTranscript(issue);
|
|
749
|
+
if (modalPollInterval) clearInterval(modalPollInterval);
|
|
750
|
+
modalPollInterval = setInterval(function () {
|
|
751
|
+
if (activeModalIssue) {
|
|
752
|
+
fetchModalGates(activeModalIssue);
|
|
753
|
+
fetchModalTranscript(activeModalIssue);
|
|
754
|
+
}
|
|
755
|
+
}, POLL_INTERVAL_MS);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function closeExecutionModal() {
|
|
759
|
+
activeModalIssue = null;
|
|
760
|
+
if (modalPollInterval) {
|
|
761
|
+
clearInterval(modalPollInterval);
|
|
762
|
+
modalPollInterval = null;
|
|
763
|
+
}
|
|
764
|
+
const modal = document.getElementById("execution-modal");
|
|
765
|
+
if (modal) {
|
|
766
|
+
modal.classList.add("hidden");
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function setupEvents() {
|
|
771
|
+
const select = document.getElementById("project-select");
|
|
772
|
+
if (select) {
|
|
773
|
+
select.addEventListener("change", function () {
|
|
774
|
+
selectedProject = this.value;
|
|
775
|
+
if (latestState) {
|
|
776
|
+
render(latestState);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const closeBtn = document.getElementById("modal-close-btn");
|
|
782
|
+
if (closeBtn) {
|
|
783
|
+
closeBtn.addEventListener("click", closeExecutionModal);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const modal = document.getElementById("execution-modal");
|
|
787
|
+
if (modal) {
|
|
788
|
+
modal.addEventListener("click", function (e) {
|
|
789
|
+
if (e.target === modal) {
|
|
790
|
+
closeExecutionModal();
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function openShutdownModal() {
|
|
796
|
+
const modal = document.getElementById("shutdown-modal");
|
|
797
|
+
if (modal) {
|
|
798
|
+
modal.classList.remove("hidden");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function closeShutdownModal() {
|
|
803
|
+
const modal = document.getElementById("shutdown-modal");
|
|
804
|
+
if (modal) {
|
|
805
|
+
modal.classList.add("hidden");
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function handleConfirmShutdown() {
|
|
810
|
+
isServerStopped = true;
|
|
811
|
+
if (pollIntervalId) {
|
|
812
|
+
clearInterval(pollIntervalId);
|
|
813
|
+
pollIntervalId = null;
|
|
814
|
+
}
|
|
815
|
+
fetch("/api/shutdown", { method: "POST" })
|
|
816
|
+
.catch(function () {
|
|
817
|
+
// Server process may terminate immediately
|
|
818
|
+
});
|
|
819
|
+
closeShutdownModal();
|
|
820
|
+
const overlay = document.getElementById("server-stopped-overlay");
|
|
821
|
+
if (overlay) {
|
|
822
|
+
overlay.classList.remove("hidden");
|
|
823
|
+
}
|
|
824
|
+
const statusText = document.querySelector(".status-text");
|
|
825
|
+
if (statusText) {
|
|
826
|
+
statusText.textContent = "SERVER STOPPED // OFFLINE";
|
|
827
|
+
}
|
|
828
|
+
const statusIndicator = document.querySelector(".status-indicator");
|
|
829
|
+
if (statusIndicator) {
|
|
830
|
+
statusIndicator.classList.add("stopped");
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const btnShutdown = document.getElementById("btn-shutdown");
|
|
835
|
+
if (btnShutdown) {
|
|
836
|
+
btnShutdown.addEventListener("click", openShutdownModal);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const shutdownCloseBtn = document.getElementById("shutdown-close-btn");
|
|
840
|
+
if (shutdownCloseBtn) {
|
|
841
|
+
shutdownCloseBtn.addEventListener("click", closeShutdownModal);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const shutdownCancelBtn = document.getElementById("shutdown-cancel-btn");
|
|
845
|
+
if (shutdownCancelBtn) {
|
|
846
|
+
shutdownCancelBtn.addEventListener("click", closeShutdownModal);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const shutdownConfirmBtn = document.getElementById("shutdown-confirm-btn");
|
|
850
|
+
if (shutdownConfirmBtn) {
|
|
851
|
+
shutdownConfirmBtn.addEventListener("click", handleConfirmShutdown);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const shutdownModal = document.getElementById("shutdown-modal");
|
|
855
|
+
if (shutdownModal) {
|
|
856
|
+
shutdownModal.addEventListener("click", function (e) {
|
|
857
|
+
if (e.target === shutdownModal) {
|
|
858
|
+
closeShutdownModal();
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
document.addEventListener("keydown", function (e) {
|
|
864
|
+
if (e.key === "Escape") {
|
|
865
|
+
if (activeModalIssue) {
|
|
866
|
+
closeExecutionModal();
|
|
867
|
+
}
|
|
868
|
+
closeShutdownModal();
|
|
869
|
+
}
|
|
84
870
|
});
|
|
871
|
+
|
|
872
|
+
const tabGates = document.getElementById("tab-btn-gates");
|
|
873
|
+
const tabHistory = document.getElementById("tab-btn-history");
|
|
874
|
+
const paneGates = document.getElementById("tab-content-gates");
|
|
875
|
+
const paneHistory = document.getElementById("tab-content-history");
|
|
876
|
+
|
|
877
|
+
if (tabGates && tabHistory && paneGates && paneHistory) {
|
|
878
|
+
tabGates.addEventListener("click", function () {
|
|
879
|
+
tabGates.classList.add("active");
|
|
880
|
+
tabHistory.classList.remove("active");
|
|
881
|
+
paneGates.classList.remove("hidden");
|
|
882
|
+
paneHistory.classList.add("hidden");
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
tabHistory.addEventListener("click", function () {
|
|
886
|
+
tabHistory.classList.add("active");
|
|
887
|
+
tabGates.classList.remove("active");
|
|
888
|
+
paneHistory.classList.remove("hidden");
|
|
889
|
+
paneGates.classList.add("hidden");
|
|
890
|
+
});
|
|
891
|
+
}
|
|
85
892
|
}
|
|
86
893
|
|
|
894
|
+
let pollIntervalId = null;
|
|
895
|
+
let isServerStopped = false;
|
|
896
|
+
|
|
87
897
|
function poll() {
|
|
898
|
+
if (isServerStopped) return;
|
|
88
899
|
fetch("/api/state", { cache: "no-store" })
|
|
89
900
|
.then(function (response) {
|
|
90
901
|
return response.json();
|
|
@@ -95,6 +906,7 @@
|
|
|
95
906
|
});
|
|
96
907
|
}
|
|
97
908
|
|
|
909
|
+
setupEvents();
|
|
98
910
|
poll();
|
|
99
|
-
setInterval(poll, POLL_INTERVAL_MS);
|
|
911
|
+
pollIntervalId = setInterval(poll, POLL_INTERVAL_MS);
|
|
100
912
|
})();
|