@julioborges/gantry 1.0.6 → 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.
@@ -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,9 +14,30 @@
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;
18
41
 
19
42
  const title = document.createElement("div");
20
43
  title.className = "card-title";
@@ -24,17 +47,23 @@
24
47
 
25
48
  if (issue.operatorWaiting) {
26
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"));
27
53
  }
28
54
  card.appendChild(title);
29
55
 
30
56
  const badgesContainer = document.createElement("div");
31
57
  badgesContainer.className = "card-badges";
32
58
 
59
+ if (issue.project) {
60
+ badgesContainer.appendChild(badge(issue.project, "project"));
61
+ }
33
62
  if (issue.branch) {
34
63
  badgesContainer.appendChild(badge("br: " + issue.branch, "branch"));
35
64
  }
36
65
  if (issue.worktree) {
37
- badgesContainer.appendChild(badge("wt: " + issue.worktree, "branch"));
66
+ badgesContainer.appendChild(badge("wt: " + issue.worktree, "branch worktree"));
38
67
  }
39
68
  Object.keys(issue.models || {}).forEach(function (role) {
40
69
  badgesContainer.appendChild(badge(role + ": " + issue.models[role], "model"));
@@ -44,19 +73,133 @@
44
73
  badge("corrections: " + issue.correctionBudget.used + "/" + issue.correctionBudget.ceiling, "budget")
45
74
  );
46
75
  }
47
- if (typeof issue.elapsedPhaseSeconds === "number") {
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") {
48
80
  badgesContainer.appendChild(badge(issue.elapsedPhaseSeconds + "s", "elapsed"));
49
81
  }
50
82
 
51
83
  if (badgesContainer.children.length > 0) {
52
84
  card.appendChild(badgesContainer);
53
85
  }
86
+
87
+ if (issue.operatorWaiting) {
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);
100
+ }
101
+
102
+ card.addEventListener("click", function () {
103
+ openExecutionModal(issue);
104
+ });
105
+
54
106
  return card;
55
107
  }
56
108
 
57
- function renderRun(run, columns) {
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
+
58
198
  const section = document.createElement("section");
59
- section.className = "swimlane" + (run.stale ? " stale" : "");
199
+ const isStale = filteredRuns.some(function (r) {
200
+ return r.stale;
201
+ });
202
+ section.className = "swimlane" + (isStale ? " stale" : "");
60
203
 
61
204
  const header = document.createElement("div");
62
205
  header.className = "swimlane-header";
@@ -65,24 +208,47 @@
65
208
  titleGroup.className = "swimlane-title-group";
66
209
 
67
210
  const heading = document.createElement("h2");
68
- heading.textContent = run.repositoryRoot + "" + run.run;
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
+ }
69
220
  titleGroup.appendChild(heading);
70
221
 
71
222
  const meta = document.createElement("div");
72
223
  meta.className = "swimlane-meta";
73
224
 
74
- meta.appendChild(badge("tier: " + run.tier, "tier"));
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
+ }
75
245
 
76
- if (run.stale) {
246
+ if (isStale) {
77
247
  meta.appendChild(badge("STALE", "stale"));
78
248
  } else {
79
249
  meta.appendChild(badge("LIVE", "live"));
80
250
  }
81
251
 
82
- if (run.compactionAt) {
83
- meta.appendChild(badge("compacted: " + run.compactionAt, "compaction"));
84
- }
85
-
86
252
  header.appendChild(titleGroup);
87
253
  header.appendChild(meta);
88
254
  section.appendChild(header);
@@ -90,7 +256,14 @@
90
256
  const columnsEl = document.createElement("div");
91
257
  columnsEl.className = "columns";
92
258
 
93
- columns.forEach(function (columnName) {
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) {
94
267
  const columnEl = document.createElement("div");
95
268
  columnEl.className = "column";
96
269
 
@@ -102,7 +275,7 @@
102
275
  title.textContent = columnName;
103
276
  colHeader.appendChild(title);
104
277
 
105
- const matchingIssues = run.issues.filter(function (issue) {
278
+ const matchingIssues = allIssues.filter(function (issue) {
106
279
  return issue.column === columnName;
107
280
  });
108
281
 
@@ -124,33 +297,605 @@
124
297
  });
125
298
 
126
299
  section.appendChild(columnsEl);
127
- return section;
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
+ }
128
320
  }
129
321
 
130
- function render(state) {
131
- const root = document.getElementById("swimlanes");
132
- root.textContent = "";
322
+ let activeModalIssue = null;
323
+ let modalPollInterval = null;
133
324
 
134
- if (!state.runs || state.runs.length === 0) {
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) {
135
355
  const empty = document.createElement("div");
136
- empty.className = "empty-state";
137
- const title = document.createElement("div");
138
- title.className = "empty-state-title";
139
- title.textContent = "NO ACTIVE RUNS DETECTED";
140
- empty.appendChild(title);
141
- const desc = document.createElement("p");
142
- desc.textContent = "No execution units found in ~/.gantry/state.";
143
- empty.appendChild(desc);
144
- root.appendChild(empty);
356
+ empty.className = "transcript-loading";
357
+ empty.textContent = "No transcript steps recorded yet.";
358
+ container.appendChild(empty);
145
359
  return;
146
360
  }
147
361
 
148
- state.runs.forEach(function (run) {
149
- root.appendChild(renderRun(run, state.columns));
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
+ }
150
425
  });
151
426
  }
152
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
+ }
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
+ }
892
+ }
893
+
894
+ let pollIntervalId = null;
895
+ let isServerStopped = false;
896
+
153
897
  function poll() {
898
+ if (isServerStopped) return;
154
899
  fetch("/api/state", { cache: "no-store" })
155
900
  .then(function (response) {
156
901
  return response.json();
@@ -161,6 +906,7 @@
161
906
  });
162
907
  }
163
908
 
909
+ setupEvents();
164
910
  poll();
165
- setInterval(poll, POLL_INTERVAL_MS);
911
+ pollIntervalId = setInterval(poll, POLL_INTERVAL_MS);
166
912
  })();