@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.
@@ -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
- const title = document.createElement("strong");
20
- title.textContent = issue.issue;
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
- if (issue.branch) card.appendChild(badge("branch: " + issue.branch));
25
- if (issue.worktree) card.appendChild(badge("worktree: " + issue.worktree));
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
- card.appendChild(badge(role + ": " + issue.models[role]));
69
+ badgesContainer.appendChild(badge(role + ": " + issue.models[role], "model"));
28
70
  });
29
71
  if (issue.correctionBudget) {
30
- card.appendChild(
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 (typeof issue.elapsedPhaseSeconds === "number") {
35
- card.appendChild(badge("elapsed: " + issue.elapsedPhaseSeconds + "s"));
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
- card.appendChild(badge("awaiting operator", "waiting"));
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 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
+
44
198
  const section = document.createElement("section");
45
- section.className = "swimlane" + (run.stale ? " stale" : "");
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
- heading.textContent =
49
- run.repositoryRoot + " — " + run.run + (run.stale ? " (stale)" : "") + " [tier: " + run.tier + "]";
50
- section.appendChild(heading);
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
- if (run.compactionAt) {
53
- section.appendChild(badge("compacted at " + run.compactionAt, "compaction"));
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
- 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) {
60
267
  const columnEl = document.createElement("div");
61
268
  columnEl.className = "column";
62
- const title = document.createElement("h3");
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
- columnEl.appendChild(title);
65
- run.issues
66
- .filter(function (issue) {
67
- return issue.column === columnName;
68
- })
69
- .forEach(function (issue) {
70
- columnEl.appendChild(renderCard(issue));
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
- 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
+ }
77
320
  }
78
321
 
79
- function render(state) {
80
- const root = document.getElementById("swimlanes");
81
- root.textContent = "";
82
- state.runs.forEach(function (run) {
83
- root.appendChild(renderRun(run, state.columns));
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
  })();