issue-flow 0.13.0 → 0.15.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/dist/agent-AT64RXTA.js +30 -0
- package/dist/{analyze-WS47UYGN.js → analyze-O2PYDIFK.js} +21 -18
- package/dist/{analyze-WS47UYGN.js.map → analyze-O2PYDIFK.js.map} +1 -1
- package/dist/{apply-OS62UWRN.js → apply-SSU6QCD4.js} +14 -11
- package/dist/{apply-OS62UWRN.js.map → apply-SSU6QCD4.js.map} +1 -1
- package/dist/{bench-SA3BOME5.js → bench-ODFPAGW6.js} +30 -26
- package/dist/{bench-SA3BOME5.js.map → bench-ODFPAGW6.js.map} +1 -1
- package/dist/{chunk-HJGGNLWI.js → chunk-2ZAM45PZ.js} +9 -9
- package/dist/{chunk-QCXPNSEL.js → chunk-3BWZO4YI.js} +2 -2
- package/dist/{chunk-J62QSVP6.js → chunk-4RXYFWP7.js} +8 -7
- package/dist/{chunk-J62QSVP6.js.map → chunk-4RXYFWP7.js.map} +1 -1
- package/dist/{chunk-ARFVHC2F.js → chunk-5ZKBIXET.js} +115 -23
- package/dist/chunk-5ZKBIXET.js.map +1 -0
- package/dist/{chunk-HEF2RMKN.js → chunk-6E2PEBFT.js} +18 -16
- package/dist/{chunk-HEF2RMKN.js.map → chunk-6E2PEBFT.js.map} +1 -1
- package/dist/{agent-BXRIEKVM.js → chunk-6FXOU2KR.js} +22 -22
- package/dist/chunk-6FXOU2KR.js.map +1 -0
- package/dist/{chunk-IYV6KNVF.js → chunk-6QYMSNVA.js} +66 -240
- package/dist/chunk-6QYMSNVA.js.map +1 -0
- package/dist/{chunk-UOCPC2Y2.js → chunk-6SRE4KD7.js} +4 -4
- package/dist/{chunk-MIVHK5YI.js → chunk-A35HE4V7.js} +3 -3
- package/dist/{chunk-TM67EHSV.js → chunk-ARWNYXFL.js} +19 -19
- package/dist/chunk-ARWNYXFL.js.map +1 -0
- package/dist/chunk-AYYMW33T.js +163 -0
- package/dist/chunk-AYYMW33T.js.map +1 -0
- package/dist/chunk-BMHGRYL6.js +890 -0
- package/dist/chunk-BMHGRYL6.js.map +1 -0
- package/dist/{chunk-PV4OX5TC.js → chunk-F4OK7KEP.js} +11 -11
- package/dist/{chunk-QXDNBYQF.js → chunk-FK4IDY6P.js} +21 -10
- package/dist/chunk-FK4IDY6P.js.map +1 -0
- package/dist/{chunk-KQHB7ZT3.js → chunk-GSZCQUCY.js} +82 -16
- package/dist/chunk-GSZCQUCY.js.map +1 -0
- package/dist/{chunk-TX5WIVYP.js → chunk-HAKDLR6N.js} +6 -1
- package/dist/chunk-HAKDLR6N.js.map +1 -0
- package/dist/{chunk-OPNGPUKJ.js → chunk-HGSYD57B.js} +143 -48
- package/dist/chunk-HGSYD57B.js.map +1 -0
- package/dist/{chunk-PIVGJM6K.js → chunk-KAHQ6F5C.js} +11 -11
- package/dist/{chunk-BHJCGWQX.js → chunk-MEFY4B7T.js} +9 -9
- package/dist/{chunk-BQDQTIM7.js → chunk-MGCOQU6N.js} +4 -4
- package/dist/{chunk-5TB6KLC2.js → chunk-MROS3SKL.js} +8 -8
- package/dist/chunk-NVZMBXTE.js +243 -0
- package/dist/chunk-NVZMBXTE.js.map +1 -0
- package/dist/{chunk-664Y3NDX.js → chunk-PCN7ZQND.js} +8 -8
- package/dist/{chunk-IV5SS4JN.js → chunk-QRCGW2ZO.js} +9 -7
- package/dist/{chunk-IV5SS4JN.js.map → chunk-QRCGW2ZO.js.map} +1 -1
- package/dist/{chunk-7KD66LIM.js → chunk-RPXDBX6R.js} +3 -3
- package/dist/{chunk-SNTTOQAD.js → chunk-RWEXK55D.js} +2 -2
- package/dist/{chunk-6FDFRNXZ.js → chunk-TBDP6QPJ.js} +25 -20
- package/dist/chunk-TBDP6QPJ.js.map +1 -0
- package/dist/chunk-UNQYMR3H.js +25 -0
- package/dist/chunk-UNQYMR3H.js.map +1 -0
- package/dist/{chunk-HQAHNIAR.js → chunk-VR34TXHF.js} +14 -14
- package/dist/{chunk-CNBYCMSU.js → chunk-WM3Y7Q4A.js} +27 -18
- package/dist/{chunk-CNBYCMSU.js.map → chunk-WM3Y7Q4A.js.map} +1 -1
- package/dist/{chunk-NEZZ6KDR.js → chunk-XDKGA6UX.js} +52 -3
- package/dist/chunk-XDKGA6UX.js.map +1 -0
- package/dist/{chunk-FZDR7CGH.js → chunk-XG2NYDRA.js} +3 -3
- package/dist/{chunk-3XLCNVMD.js → chunk-XLVGPXNL.js} +2 -2
- package/dist/{chunk-DIG5XKQQ.js → chunk-XQHFC62U.js} +2 -2
- package/dist/{chunk-UJ7XWK7J.js → chunk-XRLRKTQA.js} +16 -14
- package/dist/{chunk-UJ7XWK7J.js.map → chunk-XRLRKTQA.js.map} +1 -1
- package/dist/{chunk-LZ6GFQAG.js → chunk-ZFIXEYUJ.js} +44 -31
- package/dist/chunk-ZFIXEYUJ.js.map +1 -0
- package/dist/{chunk-37N2RJW2.js → chunk-ZPIVCM7Q.js} +3 -3
- package/dist/{chunk-TVN2JVOF.js → chunk-ZXPASFHG.js} +3 -3
- package/dist/cli.js +58 -64
- package/dist/cli.js.map +1 -1
- package/dist/{config-MEM2IWWN.js → config-UMP6DBJB.js} +12 -9
- package/dist/{conventions-WV4NDXGH.js → conventions-DKH7AE4T.js} +14 -11
- package/dist/{conventions-WV4NDXGH.js.map → conventions-DKH7AE4T.js.map} +1 -1
- package/dist/diagnostics-GOGCLTRS.js +23 -0
- package/dist/execute-E3BRMDAQ.js +35 -0
- package/dist/{generate-MVQI7A45.js → generate-AGBT7EHX.js} +23 -20
- package/dist/{generate-MVQI7A45.js.map → generate-AGBT7EHX.js.map} +1 -1
- package/dist/{git-YRVRUN3C.js → git-AC542GU2.js} +4 -2
- package/dist/init-LLBVJNPE.js +24 -0
- package/dist/{operations-LU6JA3BX.js → operations-JQYK2EMT.js} +17 -14
- package/dist/{operations-LU6JA3BX.js.map → operations-JQYK2EMT.js.map} +1 -1
- package/dist/{permissions-7NMDFU27.js → permissions-6HL52K54.js} +4 -4
- package/dist/plan-HSLWDJEZ.js +34 -0
- package/dist/{policy-JSYEK5B2.js → policy-IMRXGG7X.js} +13 -10
- package/dist/{policy-JSYEK5B2.js.map → policy-IMRXGG7X.js.map} +1 -1
- package/dist/pr-6TCZXG6K.js +39 -0
- package/dist/pr-review-6MWJO2NP.js +32 -0
- package/dist/prd-D6YDYAFX.js +33 -0
- package/dist/{ps-TNE44R5V.js → ps-KUUZ6TUL.js} +11 -8
- package/dist/{ps-TNE44R5V.js.map → ps-KUUZ6TUL.js.map} +1 -1
- package/dist/{recorder-XGF55WXI.js → recorder-BJ2KIAWT.js} +5 -4
- package/dist/registry-SB7XTMGE.js +15 -0
- package/dist/{resume-QMEPMSZW.js → resume-QNLQSCR5.js} +44 -40
- package/dist/{resume-QMEPMSZW.js.map → resume-QNLQSCR5.js.map} +1 -1
- package/dist/review-3KJ4BH2V.js +35 -0
- package/dist/{routing-2ZUMZPJB.js → routing-TN7GLMGU.js} +13 -10
- package/dist/{routing-2ZUMZPJB.js.map → routing-TN7GLMGU.js.map} +1 -1
- package/dist/run-5EHIFF2V.js +54 -0
- package/dist/run-5EHIFF2V.js.map +1 -0
- package/dist/session-publisher-T7EI3CIP.js +11 -0
- package/dist/session-publisher-T7EI3CIP.js.map +1 -0
- package/dist/{state-manager-X3PT3FGT.js → state-manager-ZEPWTCA6.js} +3 -3
- package/dist/state-manager-ZEPWTCA6.js.map +1 -0
- package/dist/{usage-VCFLDA5Y.js → usage-QL563I7F.js} +13 -10
- package/dist/{usage-VCFLDA5Y.js.map → usage-QL563I7F.js.map} +1 -1
- package/dist/{web-XVHLX62B.js → web-PPSMNTQ6.js} +22 -57
- package/dist/{web-XVHLX62B.js.map → web-PPSMNTQ6.js.map} +1 -1
- package/package.json +1 -1
- package/web/AGENTS.md +154 -11
- package/web/public/app.css +343 -110
- package/web/public/app.js +436 -27
- package/web/public/index.html +44 -1
- package/dist/agent-BXRIEKVM.js.map +0 -1
- package/dist/chunk-6FDFRNXZ.js.map +0 -1
- package/dist/chunk-ARFVHC2F.js.map +0 -1
- package/dist/chunk-IHIEXUTC.js +0 -483
- package/dist/chunk-IHIEXUTC.js.map +0 -1
- package/dist/chunk-IYV6KNVF.js.map +0 -1
- package/dist/chunk-KQHB7ZT3.js.map +0 -1
- package/dist/chunk-LZ6GFQAG.js.map +0 -1
- package/dist/chunk-NEZZ6KDR.js.map +0 -1
- package/dist/chunk-OPNGPUKJ.js.map +0 -1
- package/dist/chunk-QXDNBYQF.js.map +0 -1
- package/dist/chunk-TM67EHSV.js.map +0 -1
- package/dist/chunk-TX5WIVYP.js.map +0 -1
- package/dist/execute-NITIHHCK.js +0 -31
- package/dist/init-OKWEC3EV.js +0 -21
- package/dist/plan-LEJJF3DE.js +0 -31
- package/dist/pr-HPVUZ7S3.js +0 -36
- package/dist/pr-review-R2QZBT3F.js +0 -29
- package/dist/prd-5O6GYCC6.js +0 -30
- package/dist/registry-LGPE53QD.js +0 -15
- package/dist/review-YKWD6SNN.js +0 -32
- package/dist/run-YU4TR62R.js +0 -50
- /package/dist/{config-MEM2IWWN.js.map → agent-AT64RXTA.js.map} +0 -0
- /package/dist/{chunk-HJGGNLWI.js.map → chunk-2ZAM45PZ.js.map} +0 -0
- /package/dist/{chunk-QCXPNSEL.js.map → chunk-3BWZO4YI.js.map} +0 -0
- /package/dist/{chunk-UOCPC2Y2.js.map → chunk-6SRE4KD7.js.map} +0 -0
- /package/dist/{chunk-MIVHK5YI.js.map → chunk-A35HE4V7.js.map} +0 -0
- /package/dist/{chunk-PV4OX5TC.js.map → chunk-F4OK7KEP.js.map} +0 -0
- /package/dist/{chunk-PIVGJM6K.js.map → chunk-KAHQ6F5C.js.map} +0 -0
- /package/dist/{chunk-BHJCGWQX.js.map → chunk-MEFY4B7T.js.map} +0 -0
- /package/dist/{chunk-BQDQTIM7.js.map → chunk-MGCOQU6N.js.map} +0 -0
- /package/dist/{chunk-5TB6KLC2.js.map → chunk-MROS3SKL.js.map} +0 -0
- /package/dist/{chunk-664Y3NDX.js.map → chunk-PCN7ZQND.js.map} +0 -0
- /package/dist/{chunk-7KD66LIM.js.map → chunk-RPXDBX6R.js.map} +0 -0
- /package/dist/{chunk-SNTTOQAD.js.map → chunk-RWEXK55D.js.map} +0 -0
- /package/dist/{chunk-HQAHNIAR.js.map → chunk-VR34TXHF.js.map} +0 -0
- /package/dist/{chunk-FZDR7CGH.js.map → chunk-XG2NYDRA.js.map} +0 -0
- /package/dist/{chunk-3XLCNVMD.js.map → chunk-XLVGPXNL.js.map} +0 -0
- /package/dist/{chunk-DIG5XKQQ.js.map → chunk-XQHFC62U.js.map} +0 -0
- /package/dist/{chunk-37N2RJW2.js.map → chunk-ZPIVCM7Q.js.map} +0 -0
- /package/dist/{chunk-TVN2JVOF.js.map → chunk-ZXPASFHG.js.map} +0 -0
- /package/dist/{execute-NITIHHCK.js.map → config-UMP6DBJB.js.map} +0 -0
- /package/dist/{git-YRVRUN3C.js.map → diagnostics-GOGCLTRS.js.map} +0 -0
- /package/dist/{init-OKWEC3EV.js.map → execute-E3BRMDAQ.js.map} +0 -0
- /package/dist/{permissions-7NMDFU27.js.map → git-AC542GU2.js.map} +0 -0
- /package/dist/{plan-LEJJF3DE.js.map → init-LLBVJNPE.js.map} +0 -0
- /package/dist/{pr-HPVUZ7S3.js.map → permissions-6HL52K54.js.map} +0 -0
- /package/dist/{pr-review-R2QZBT3F.js.map → plan-HSLWDJEZ.js.map} +0 -0
- /package/dist/{prd-5O6GYCC6.js.map → pr-6TCZXG6K.js.map} +0 -0
- /package/dist/{recorder-XGF55WXI.js.map → pr-review-6MWJO2NP.js.map} +0 -0
- /package/dist/{registry-LGPE53QD.js.map → prd-D6YDYAFX.js.map} +0 -0
- /package/dist/{review-YKWD6SNN.js.map → recorder-BJ2KIAWT.js.map} +0 -0
- /package/dist/{run-YU4TR62R.js.map → registry-SB7XTMGE.js.map} +0 -0
- /package/dist/{state-manager-X3PT3FGT.js.map → review-3KJ4BH2V.js.map} +0 -0
package/web/public/app.js
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
const REFRESH_OPTIONS = [3, 5, 10, 30];
|
|
10
10
|
const PAUSED = 0;
|
|
11
11
|
const STORAGE_KEY = 'issue-flow:refresh-seconds';
|
|
12
|
+
const THEME_STORAGE_KEY = 'issue-flow:theme';
|
|
13
|
+
// Esta chave também é lida pelo <script> inline do <head>
|
|
14
|
+
// do index.html, que aplica o tema antes do primeiro paint. A duplicação
|
|
15
|
+
// é deliberada: aquele script roda antes deste arquivo existir na página e
|
|
16
|
+
// não pode depender de nenhum símbolo daqui. Mudou o formato do valor,
|
|
17
|
+
// mude nos dois lugares.
|
|
12
18
|
const MAX_BACKOFF_MS = 60000;
|
|
13
19
|
const ALERT_PREVIEW = 3;
|
|
14
20
|
const DESCRIPTION_PREVIEW = 140;
|
|
@@ -71,6 +77,8 @@
|
|
|
71
77
|
estimate: document.getElementById('estimate'),
|
|
72
78
|
refreshSelect: document.getElementById('refresh-select'),
|
|
73
79
|
refreshSelectDashboard: document.getElementById('refresh-select-dashboard'),
|
|
80
|
+
themeSelect: document.getElementById('theme-select'),
|
|
81
|
+
themeSelectDashboard: document.getElementById('theme-select-dashboard'),
|
|
74
82
|
alerts: document.getElementById('alerts'),
|
|
75
83
|
alertsBody: document.getElementById('alerts-body'),
|
|
76
84
|
issueSummary: document.getElementById('issue-summary'),
|
|
@@ -80,6 +88,7 @@
|
|
|
80
88
|
progressCounters: document.getElementById('progress-counters'),
|
|
81
89
|
now: document.getElementById('now'),
|
|
82
90
|
resilience: document.getElementById('resilience'),
|
|
91
|
+
configuration: document.getElementById('configuration'),
|
|
83
92
|
phases: document.getElementById('phases'),
|
|
84
93
|
nextSteps: document.getElementById('next-steps'),
|
|
85
94
|
stories: document.getElementById('stories'),
|
|
@@ -108,11 +117,19 @@
|
|
|
108
117
|
polling: false,
|
|
109
118
|
logFilter: 'all',
|
|
110
119
|
activeTab: 'tab-execution',
|
|
120
|
+
// 'system' | 'light' | 'dark'. Valor inicial em initTheme(), lido da
|
|
121
|
+
// preferência guardada em localStorage por readStoredTheme().
|
|
122
|
+
theme: 'system',
|
|
111
123
|
historyFilter: 'all',
|
|
112
124
|
events: [],
|
|
125
|
+
diagnostics: [],
|
|
113
126
|
// Só o id: o card que abriu o drawer é destruído no próximo render, então
|
|
114
127
|
// guardar o nó levaria a uma referência morta.
|
|
115
|
-
|
|
128
|
+
selectedDetail: null,
|
|
129
|
+
configWritable: false,
|
|
130
|
+
// Identifica o processo que serviu os assets atuais. Uma troca significa
|
|
131
|
+
// que --restart-web colocou código novo no mesmo origin e exige reload.
|
|
132
|
+
serverInstanceId: null,
|
|
116
133
|
// Multi-sessão (#35): lista de /api/sessions e seleção explícita do usuário.
|
|
117
134
|
// selectedSessionId null = modo automático (1 sessão → detalhe; 2+ → dashboard).
|
|
118
135
|
sessions: [],
|
|
@@ -192,8 +209,9 @@
|
|
|
192
209
|
}
|
|
193
210
|
|
|
194
211
|
// Custos abaixo de um centavo perderiam todo o significado com 2 casas.
|
|
195
|
-
function formatCost(value) {
|
|
196
|
-
|
|
212
|
+
function formatCost(value, approximate) {
|
|
213
|
+
const prefix = approximate === false ? '$' : '~$';
|
|
214
|
+
return prefix + (Math.abs(value) < 0.01 ? value.toFixed(4) : value.toFixed(2));
|
|
197
215
|
}
|
|
198
216
|
|
|
199
217
|
// Ex.: '12.4k in / 3.1k out · 88.0k cache · ~$0.42'. Segmentos sem dado são
|
|
@@ -307,6 +325,88 @@
|
|
|
307
325
|
}
|
|
308
326
|
}
|
|
309
327
|
|
|
328
|
+
// ---- Tema ------------------------------------------------------------------
|
|
329
|
+
// Três estados: 'system' segue o SO (o @media decide), 'light'/'dark' forçam.
|
|
330
|
+
// Quem pinta é o CSS; aqui só se define (ou remove) o data-theme da raiz.
|
|
331
|
+
|
|
332
|
+
// Par ler/gravar tolerante a exceção, na mesma forma de readStoredRefresh()
|
|
333
|
+
// e storeRefresh(): armazenamento bloqueado (janela privada, cookies de
|
|
334
|
+
// terceiros desligados) não pode derrubar o painel.
|
|
335
|
+
function readStoredTheme() {
|
|
336
|
+
let raw = null;
|
|
337
|
+
try {
|
|
338
|
+
raw = window.localStorage.getItem(THEME_STORAGE_KEY);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
// localStorage indisponível (ex.: bloqueado) — segue no modo sistema.
|
|
341
|
+
}
|
|
342
|
+
return raw === 'light' || raw === 'dark' ? raw : 'system';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function storeTheme(value) {
|
|
346
|
+
try {
|
|
347
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, value);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
// Persistência é conveniência; falha é ignorada. Sem gravar, a escolha
|
|
350
|
+
// continua valendo nesta aba — só não sobrevive ao reload.
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function applyTheme(theme) {
|
|
355
|
+
// 'system' **remove** o atributo em vez de gravar 'system': é a ausência
|
|
356
|
+
// do data-theme que devolve a decisão ao @media (prefers-color-scheme).
|
|
357
|
+
if (theme === 'light' || theme === 'dark') {
|
|
358
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
359
|
+
} else {
|
|
360
|
+
document.documentElement.removeAttribute('data-theme');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Os dois headers têm o seu seletor; mudar num precisa refletir no outro.
|
|
365
|
+
// As três opções são estáticas no HTML — aqui só se sincroniza o .value.
|
|
366
|
+
function syncThemeSelects() {
|
|
367
|
+
if (els.themeSelect) els.themeSelect.value = state.theme;
|
|
368
|
+
if (els.themeSelectDashboard) els.themeSelectDashboard.value = state.theme;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Preferência do SO. Só se observa no modo 'system': é ali que a troca de
|
|
372
|
+
// claro para escuro no SO precisa chegar ao painel com a página aberta.
|
|
373
|
+
const systemThemeQuery =
|
|
374
|
+
typeof window.matchMedia === 'function'
|
|
375
|
+
? window.matchMedia('(prefers-color-scheme: dark)')
|
|
376
|
+
: null;
|
|
377
|
+
|
|
378
|
+
function onSystemThemeChange() {
|
|
379
|
+
// Sincroniza o lado JS do tema com a preferência nova do SO: a raiz (que
|
|
380
|
+
// no modo sistema segue sem data-theme, para o @media decidir) e o .value
|
|
381
|
+
// dos dois seletores. O repaint das cores é do próprio @media, que o
|
|
382
|
+
// navegador reavalia sozinho — por isso não há nada a pintar aqui.
|
|
383
|
+
applyTheme(state.theme);
|
|
384
|
+
syncThemeSelects();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Anexa no modo 'system' e desanexa nos modos forçados: com tema forçado a
|
|
388
|
+
// preferência do SO não vale, e continuar ouvindo seria ruído.
|
|
389
|
+
function watchSystemTheme(enabled) {
|
|
390
|
+
if (!systemThemeQuery) return;
|
|
391
|
+
if (enabled) systemThemeQuery.addEventListener('change', onSystemThemeChange);
|
|
392
|
+
else systemThemeQuery.removeEventListener('change', onSystemThemeChange);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function setTheme(theme) {
|
|
396
|
+
state.theme = theme === 'light' || theme === 'dark' ? theme : 'system';
|
|
397
|
+
applyTheme(state.theme);
|
|
398
|
+
syncThemeSelects();
|
|
399
|
+
watchSystemTheme(state.theme === 'system');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function initTheme() {
|
|
403
|
+
// O <script> inline do <head> já leu a mesma chave e aplicou o tema antes
|
|
404
|
+
// do primeiro paint; esta é a segunda metade da duplicação deliberada
|
|
405
|
+
// documentada em THEME_STORAGE_KEY — traz a preferência para o state e,
|
|
406
|
+
// com ela, para os dois seletores.
|
|
407
|
+
setTheme(readStoredTheme());
|
|
408
|
+
}
|
|
409
|
+
|
|
310
410
|
// ---- Polling: intervalo configurável, aba oculta e backoff ----------------
|
|
311
411
|
|
|
312
412
|
function readStoredRefresh() {
|
|
@@ -390,6 +490,10 @@
|
|
|
390
490
|
return { mode: 'detail', session: null };
|
|
391
491
|
}
|
|
392
492
|
|
|
493
|
+
if (sessions.length === 1) {
|
|
494
|
+
return { mode: 'detail', session: sessions[0] };
|
|
495
|
+
}
|
|
496
|
+
|
|
393
497
|
return { mode: 'dashboard', session: null };
|
|
394
498
|
}
|
|
395
499
|
|
|
@@ -401,7 +505,7 @@
|
|
|
401
505
|
const showBack =
|
|
402
506
|
mode === 'detail' && state.selectedSessionId !== null && state.sessions.length >= 1;
|
|
403
507
|
els.backToDashboard.hidden = !showBack;
|
|
404
|
-
if (mode === 'dashboard' && state.
|
|
508
|
+
if (mode === 'dashboard' && state.selectedDetail !== null) {
|
|
405
509
|
closeDrawer();
|
|
406
510
|
}
|
|
407
511
|
}
|
|
@@ -423,9 +527,24 @@
|
|
|
423
527
|
state.snapshot = null;
|
|
424
528
|
state.detailSessionId = null;
|
|
425
529
|
state.events = [];
|
|
530
|
+
state.diagnostics = [];
|
|
426
531
|
state.eventsUrl = null;
|
|
427
532
|
}
|
|
428
533
|
|
|
534
|
+
function serverInstanceChanged(response) {
|
|
535
|
+
const instanceId = response.headers.get('X-Issue-Flow-Instance');
|
|
536
|
+
if (!instanceId) return false; // servidor anterior à identidade de instância
|
|
537
|
+
if (state.serverInstanceId === null) {
|
|
538
|
+
state.serverInstanceId = instanceId;
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
if (state.serverInstanceId !== instanceId) {
|
|
542
|
+
window.location.reload();
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
429
548
|
function requestPoll() {
|
|
430
549
|
clearTimer();
|
|
431
550
|
if (state.polling) {
|
|
@@ -458,6 +577,7 @@
|
|
|
458
577
|
state.pollAgain = false;
|
|
459
578
|
try {
|
|
460
579
|
const sessionsRes = await fetch('api/sessions', { cache: 'no-store' });
|
|
580
|
+
if (serverInstanceChanged(sessionsRes)) return;
|
|
461
581
|
if (!sessionsRes.ok) throw new Error('HTTP ' + sessionsRes.status);
|
|
462
582
|
const sessions = await sessionsRes.json();
|
|
463
583
|
if (!Array.isArray(sessions)) throw new Error('sessions payload invalid');
|
|
@@ -528,7 +648,15 @@
|
|
|
528
648
|
if (!eventsRes.ok) throw new Error('HTTP ' + eventsRes.status);
|
|
529
649
|
const entries = await eventsRes.json();
|
|
530
650
|
state.events = Array.isArray(entries) ? entries : [];
|
|
651
|
+
const diagnosticUrl =
|
|
652
|
+
'api/diagnostics?session=' + encodeURIComponent(state.detailSessionId || '');
|
|
653
|
+
const diagnosticsRes = await fetch(diagnosticUrl, { cache: 'no-store' });
|
|
654
|
+
if (diagnosticsRes.ok) {
|
|
655
|
+
const diagnostics = await diagnosticsRes.json();
|
|
656
|
+
state.diagnostics = Array.isArray(diagnostics) ? diagnostics : [];
|
|
657
|
+
}
|
|
531
658
|
renderHistory();
|
|
659
|
+
renderDrawer(state.snapshot);
|
|
532
660
|
}
|
|
533
661
|
setViewMode('detail');
|
|
534
662
|
state.failures = 0;
|
|
@@ -689,6 +817,7 @@
|
|
|
689
817
|
renderProgress(snapshot);
|
|
690
818
|
renderNow(snapshot);
|
|
691
819
|
renderResilience(snapshot);
|
|
820
|
+
renderConfiguration(snapshot);
|
|
692
821
|
renderPhases(snapshot);
|
|
693
822
|
renderNextSteps(snapshot);
|
|
694
823
|
renderStories(snapshot);
|
|
@@ -724,7 +853,15 @@
|
|
|
724
853
|
|
|
725
854
|
const branch = snapshot.git.branch;
|
|
726
855
|
const base = snapshot.git.baseBranch;
|
|
727
|
-
|
|
856
|
+
const branchMode =
|
|
857
|
+
snapshot.git.branchCreated === false
|
|
858
|
+
? 'branch atual · não criada pelo Issue Flow'
|
|
859
|
+
: snapshot.git.branchCreated === true
|
|
860
|
+
? 'criada pelo Issue Flow'
|
|
861
|
+
: 'origem da branch não informada';
|
|
862
|
+
els.branchLine.textContent = branch
|
|
863
|
+
? (base ? branch + ' ← ' + base : branch) + ' · ' + branchMode
|
|
864
|
+
: '';
|
|
728
865
|
|
|
729
866
|
els.statusBadge.textContent = STATUS_LABELS[snapshot.status] || snapshot.status;
|
|
730
867
|
els.statusBadge.className = 'badge status-' + snapshot.status;
|
|
@@ -899,6 +1036,105 @@
|
|
|
899
1036
|
els.resilience.appendChild(grid);
|
|
900
1037
|
}
|
|
901
1038
|
|
|
1039
|
+
function configSourceLabel(source) {
|
|
1040
|
+
const labels = {
|
|
1041
|
+
default: 'default do Issue Flow',
|
|
1042
|
+
global: 'configuração global',
|
|
1043
|
+
project: 'configuração do projeto',
|
|
1044
|
+
env: 'variável de ambiente',
|
|
1045
|
+
cli: 'override da execução',
|
|
1046
|
+
fallback: 'fallback',
|
|
1047
|
+
};
|
|
1048
|
+
return labels[source] || source || 'não informado';
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function renderConfiguration(snapshot) {
|
|
1052
|
+
clear(els.configuration);
|
|
1053
|
+
const config = snapshot.configuration;
|
|
1054
|
+
if (!config) {
|
|
1055
|
+
els.configuration.appendChild(el('p', 'empty', 'Configuração não capturada nesta execução.'));
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const summary = el('dl', 'now-grid');
|
|
1060
|
+
nowRow(
|
|
1061
|
+
summary,
|
|
1062
|
+
'Harness padrão',
|
|
1063
|
+
(config.defaultProvider.value || '—') + ' · ' + configSourceLabel(config.defaultProvider.source),
|
|
1064
|
+
);
|
|
1065
|
+
nowRow(
|
|
1066
|
+
summary,
|
|
1067
|
+
'Modelo padrão',
|
|
1068
|
+
(config.defaultModel.value || 'default do provider') +
|
|
1069
|
+
' · ' +
|
|
1070
|
+
configSourceLabel(config.defaultModel.source),
|
|
1071
|
+
);
|
|
1072
|
+
nowRow(summary, 'Fallbacks', list(config.fallbacks).join(' → ') || 'nenhum configurado');
|
|
1073
|
+
nowRow(summary, 'Precedência', list(config.precedence).join(' → '));
|
|
1074
|
+
els.configuration.appendChild(summary);
|
|
1075
|
+
|
|
1076
|
+
const phases = el('div', 'config-phase-grid');
|
|
1077
|
+
for (const phase of list(config.phases)) {
|
|
1078
|
+
const row = el('button', 'config-phase-row');
|
|
1079
|
+
row.type = 'button';
|
|
1080
|
+
row.appendChild(el('span', 'mono', phase.phase));
|
|
1081
|
+
row.appendChild(el('span', null, phase.provider.value || '—'));
|
|
1082
|
+
row.appendChild(el('span', null, phase.model.value || 'default do provider'));
|
|
1083
|
+
row.appendChild(
|
|
1084
|
+
el(
|
|
1085
|
+
'span',
|
|
1086
|
+
'muted',
|
|
1087
|
+
configSourceLabel(phase.provider.source) + ' / ' + configSourceLabel(phase.model.source),
|
|
1088
|
+
),
|
|
1089
|
+
);
|
|
1090
|
+
row.addEventListener('click', () => openDrawer('phase', phase.phase));
|
|
1091
|
+
phases.appendChild(row);
|
|
1092
|
+
}
|
|
1093
|
+
els.configuration.appendChild(phases);
|
|
1094
|
+
|
|
1095
|
+
const form = el('form', 'config-form');
|
|
1096
|
+
const provider = el('select');
|
|
1097
|
+
provider.setAttribute('aria-label', 'Harness padrão para execuções futuras');
|
|
1098
|
+
for (const id of ['claude', 'codex', 'cursor', 'antigravity']) {
|
|
1099
|
+
const option = el('option', null, id);
|
|
1100
|
+
option.value = id;
|
|
1101
|
+
provider.appendChild(option);
|
|
1102
|
+
}
|
|
1103
|
+
provider.value = config.defaultProvider.value || 'claude';
|
|
1104
|
+
const model = el('input');
|
|
1105
|
+
model.type = 'text';
|
|
1106
|
+
model.placeholder = 'modelo (vazio = default)';
|
|
1107
|
+
model.value = config.defaultModel.value || '';
|
|
1108
|
+
const save = el('button', 'config-save', 'Salvar preferência global');
|
|
1109
|
+
save.type = 'submit';
|
|
1110
|
+
const feedback = el('span', 'muted');
|
|
1111
|
+
form.appendChild(provider);
|
|
1112
|
+
form.appendChild(model);
|
|
1113
|
+
form.appendChild(save);
|
|
1114
|
+
form.appendChild(feedback);
|
|
1115
|
+
form.addEventListener('submit', async (event) => {
|
|
1116
|
+
event.preventDefault();
|
|
1117
|
+
save.disabled = true;
|
|
1118
|
+
feedback.textContent = 'salvando…';
|
|
1119
|
+
try {
|
|
1120
|
+
const response = await fetch('api/config/agent', {
|
|
1121
|
+
method: 'POST',
|
|
1122
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1123
|
+
body: JSON.stringify({ provider: provider.value, model: model.value }),
|
|
1124
|
+
});
|
|
1125
|
+
const result = await response.json();
|
|
1126
|
+
if (!response.ok) throw new Error(result.error || 'falha ao salvar');
|
|
1127
|
+
feedback.textContent = 'salvo para execuções futuras';
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
feedback.textContent = error.message || 'falha ao salvar';
|
|
1130
|
+
} finally {
|
|
1131
|
+
save.disabled = false;
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
form.hidden = !state.configWritable;
|
|
1135
|
+
els.configuration.appendChild(form);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
902
1138
|
function renderPhases(snapshot) {
|
|
903
1139
|
clear(els.phases);
|
|
904
1140
|
if (snapshot.phases.length === 0) {
|
|
@@ -907,6 +1143,7 @@
|
|
|
907
1143
|
}
|
|
908
1144
|
for (const phase of snapshot.phases) {
|
|
909
1145
|
const item = el('li');
|
|
1146
|
+
item.className = 'detail-row';
|
|
910
1147
|
item.appendChild(el('span', 'item-icon icon-' + phase.status, PHASE_ICONS[phase.status] || '○'));
|
|
911
1148
|
const main = el('div', 'item-main');
|
|
912
1149
|
main.appendChild(el('div', 'item-title', phase.name));
|
|
@@ -918,6 +1155,12 @@
|
|
|
918
1155
|
formatUsage(phase),
|
|
919
1156
|
]);
|
|
920
1157
|
if (side) item.appendChild(el('span', 'item-side', side));
|
|
1158
|
+
item.tabIndex = 0;
|
|
1159
|
+
item.setAttribute('role', 'button');
|
|
1160
|
+
item.addEventListener('click', () => openDrawer('phase', phase.name));
|
|
1161
|
+
item.addEventListener('keydown', (event) => {
|
|
1162
|
+
if (event.key === 'Enter' || event.key === ' ') openDrawer('phase', phase.name);
|
|
1163
|
+
});
|
|
921
1164
|
els.phases.appendChild(item);
|
|
922
1165
|
}
|
|
923
1166
|
}
|
|
@@ -948,6 +1191,7 @@
|
|
|
948
1191
|
// antes da issue 38 pode chegar sem ele.
|
|
949
1192
|
const storyStage = story.stage !== null && story.stage !== undefined ? story.stage : 'pending';
|
|
950
1193
|
const item = el('li', storyStage === 'executing' ? 'story-executing' : null);
|
|
1194
|
+
item.classList.add('detail-row');
|
|
951
1195
|
const status = story.passes ? 'completed' : 'pending';
|
|
952
1196
|
item.appendChild(el('span', 'item-icon icon-' + status, story.passes ? '✓' : '○'));
|
|
953
1197
|
const main = el('div', 'item-main');
|
|
@@ -982,6 +1226,12 @@
|
|
|
982
1226
|
formatUsage(story),
|
|
983
1227
|
]);
|
|
984
1228
|
if (side) item.appendChild(el('span', 'item-side', side));
|
|
1229
|
+
item.tabIndex = 0;
|
|
1230
|
+
item.setAttribute('role', 'button');
|
|
1231
|
+
item.addEventListener('click', () => openDrawer('story', story.id));
|
|
1232
|
+
item.addEventListener('keydown', (event) => {
|
|
1233
|
+
if (event.key === 'Enter' || event.key === ' ') openDrawer('story', story.id);
|
|
1234
|
+
});
|
|
985
1235
|
els.stories.appendChild(item);
|
|
986
1236
|
}
|
|
987
1237
|
}
|
|
@@ -1008,7 +1258,7 @@
|
|
|
1008
1258
|
card.appendChild(
|
|
1009
1259
|
el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
|
|
1010
1260
|
);
|
|
1011
|
-
card.addEventListener('click', () => openDrawer(story.id));
|
|
1261
|
+
card.addEventListener('click', () => openDrawer('story', story.id));
|
|
1012
1262
|
return card;
|
|
1013
1263
|
}
|
|
1014
1264
|
|
|
@@ -1039,18 +1289,145 @@
|
|
|
1039
1289
|
if (event.key === 'Escape') closeDrawer();
|
|
1040
1290
|
}
|
|
1041
1291
|
|
|
1042
|
-
|
|
1292
|
+
function executionsFor(snapshot, kind, id) {
|
|
1293
|
+
return list(snapshot.executions).filter((execution) => {
|
|
1294
|
+
if (kind === 'phase') return execution.purpose === id;
|
|
1295
|
+
return list(execution.storyIds).includes(id);
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function executionCost(execution) {
|
|
1300
|
+
const cost = execution.cost || {};
|
|
1301
|
+
if (cost.status === 'reported' || cost.status === 'estimated') {
|
|
1302
|
+
return formatCost(cost.amount, cost.status === 'estimated');
|
|
1303
|
+
}
|
|
1304
|
+
return cost.reason ? 'não informado (' + cost.reason + ')' : 'não informado';
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function renderExecutionHistory(snapshot, executions) {
|
|
1308
|
+
drawerSection('Tentativas, revisões e correções', (body) => {
|
|
1309
|
+
if (executions.length === 0) {
|
|
1310
|
+
body.appendChild(el('p', 'empty', 'Nenhuma invocação associada.'));
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const timeline = el('ol', 'execution-timeline');
|
|
1314
|
+
for (const execution of executions.slice().reverse()) {
|
|
1315
|
+
const item = el('li', 'execution-entry');
|
|
1316
|
+
const title = el('div', 'execution-entry-head');
|
|
1317
|
+
title.appendChild(
|
|
1318
|
+
el(
|
|
1319
|
+
'span',
|
|
1320
|
+
'badge status-' + (execution.status === 'completed' ? 'completed' : execution.status),
|
|
1321
|
+
execution.status,
|
|
1322
|
+
),
|
|
1323
|
+
);
|
|
1324
|
+
title.appendChild(
|
|
1325
|
+
el(
|
|
1326
|
+
'strong',
|
|
1327
|
+
null,
|
|
1328
|
+
execution.purpose + ' · tentativa ' + execution.attempt + ' · ' + execution.trigger,
|
|
1329
|
+
),
|
|
1330
|
+
);
|
|
1331
|
+
item.appendChild(title);
|
|
1332
|
+
const grid = el('dl', 'now-grid execution-grid');
|
|
1333
|
+
nowRow(grid, 'Harness', execution.agent?.harness || '—');
|
|
1334
|
+
nowRow(grid, 'Modelo', execution.agent?.model?.resolved || execution.agent?.model?.requested || '—');
|
|
1335
|
+
nowRow(grid, 'Início', formatClock(execution.startedAt));
|
|
1336
|
+
nowRow(grid, 'Fim', execution.finishedAt ? formatClock(execution.finishedAt) : 'em andamento');
|
|
1337
|
+
nowRow(grid, 'Duração', metric(execution.durationMs) !== null ? formatDuration(execution.durationMs / 1000) : '—');
|
|
1338
|
+
nowRow(grid, 'Tokens', formatUsage(execution.usage || {}) || '—');
|
|
1339
|
+
nowRow(grid, 'Custo', executionCost(execution));
|
|
1340
|
+
if (execution.correctionCycle) nowRow(grid, 'Correção', 'ciclo ' + execution.correctionCycle);
|
|
1341
|
+
if (execution.verdict?.status) nowRow(grid, 'Veredito', execution.verdict.status);
|
|
1342
|
+
item.appendChild(grid);
|
|
1343
|
+
if (execution.failure?.message) item.appendChild(el('p', 'item-error', execution.failure.message));
|
|
1344
|
+
timeline.appendChild(item);
|
|
1345
|
+
}
|
|
1346
|
+
body.appendChild(timeline);
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function renderProcessLogs(snapshot, executions, phase) {
|
|
1351
|
+
const ids = new Set(executions.map((execution) => execution.id));
|
|
1352
|
+
const logs = list(snapshot.processLogs).filter(
|
|
1353
|
+
(entry) => ids.has(entry.executionId) || (ids.size === 0 && entry.phase === phase),
|
|
1354
|
+
);
|
|
1355
|
+
drawerSection('Saída do processo', (body) => {
|
|
1356
|
+
const details = el('details', 'process-output');
|
|
1357
|
+
const summary = el('summary', null, logs.length + ' linha(s) sanitizada(s)');
|
|
1358
|
+
details.appendChild(summary);
|
|
1359
|
+
if (logs.length === 0) {
|
|
1360
|
+
details.appendChild(el('p', 'empty', 'Nenhuma saída capturada.'));
|
|
1361
|
+
} else {
|
|
1362
|
+
const output = el('pre', 'process-output-body');
|
|
1363
|
+
output.textContent = logs
|
|
1364
|
+
.slice(-200)
|
|
1365
|
+
.map((entry) => formatClock(entry.at) + ' ' + entry.message)
|
|
1366
|
+
.join('\n');
|
|
1367
|
+
details.appendChild(output);
|
|
1368
|
+
}
|
|
1369
|
+
body.appendChild(details);
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function renderGlobalDiagnostics(kind, id, executions) {
|
|
1374
|
+
const ids = new Set(executions.map((execution) => execution.id));
|
|
1375
|
+
const entries = state.diagnostics.filter((entry) => {
|
|
1376
|
+
if (ids.has(entry.executionId)) return true;
|
|
1377
|
+
if (kind === 'phase') return entry.phase === id;
|
|
1378
|
+
return typeof entry.story === 'string' && entry.story.split(',').includes(id);
|
|
1379
|
+
});
|
|
1380
|
+
if (entries.length === 0) return;
|
|
1381
|
+
drawerSection('Diagnóstico global persistente', (body) => {
|
|
1382
|
+
const details = el('details', 'process-output');
|
|
1383
|
+
details.appendChild(el('summary', null, entries.length + ' registro(s) em ~/.issue-flow/logs'));
|
|
1384
|
+
const output = el('pre', 'process-output-body');
|
|
1385
|
+
output.textContent = entries
|
|
1386
|
+
.slice(0, 200)
|
|
1387
|
+
.map((entry) => formatClock(entry.timestamp) + ' ' + entry.level + ' ' + entry.message)
|
|
1388
|
+
.join('\n');
|
|
1389
|
+
details.appendChild(output);
|
|
1390
|
+
body.appendChild(details);
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Reidrata a partir de kind/id a cada render: o drawer sobrevive ao poll.
|
|
1043
1395
|
function renderDrawer(snapshot) {
|
|
1044
|
-
if (state.
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1396
|
+
if (state.selectedDetail === null) return;
|
|
1397
|
+
const kind = state.selectedDetail.kind;
|
|
1398
|
+
const id = state.selectedDetail.id;
|
|
1399
|
+
const story = kind === 'story' ? getStoryById(snapshot, id) : null;
|
|
1400
|
+
const phase = kind === 'phase' ? list(snapshot.phases).find((entry) => entry.name === id) : null;
|
|
1401
|
+
if ((kind === 'story' && story === null) || (kind === 'phase' && !phase)) {
|
|
1048
1402
|
closeDrawer();
|
|
1049
1403
|
return;
|
|
1050
1404
|
}
|
|
1051
|
-
els.drawerTitle.textContent = story.id + ' · ' + story.title;
|
|
1052
1405
|
|
|
1053
1406
|
clear(els.drawerBody);
|
|
1407
|
+
if (kind === 'phase') {
|
|
1408
|
+
els.drawerTitle.textContent = 'Fase · ' + phase.name;
|
|
1409
|
+
els.drawerBody.appendChild(el('span', 'badge status-' + phase.status, phase.status));
|
|
1410
|
+
const grid = el('dl', 'now-grid drawer-summary-grid');
|
|
1411
|
+
nowRow(grid, 'Início', phase.startedAt ? formatClock(phase.startedAt) : '—');
|
|
1412
|
+
nowRow(grid, 'Fim', phase.endedAt ? formatClock(phase.endedAt) : '—');
|
|
1413
|
+
nowRow(grid, 'Duração', metric(phase.durationSeconds) !== null ? formatDuration(phase.durationSeconds) : '—');
|
|
1414
|
+
nowRow(grid, 'Uso total', formatUsage(phase) || '—');
|
|
1415
|
+
const configured = list(snapshot.configuration?.phases).find((entry) => entry.phase === phase.name);
|
|
1416
|
+
if (configured) {
|
|
1417
|
+
nowRow(grid, 'Harness efetivo', configured.provider.value || '—');
|
|
1418
|
+
nowRow(grid, 'Origem', configSourceLabel(configured.provider.source));
|
|
1419
|
+
nowRow(grid, 'Modelo efetivo', configured.model.value || 'default do provider');
|
|
1420
|
+
}
|
|
1421
|
+
els.drawerBody.appendChild(grid);
|
|
1422
|
+
if (phase.error) els.drawerBody.appendChild(el('p', 'item-error', phase.error));
|
|
1423
|
+
const executions = executionsFor(snapshot, kind, id);
|
|
1424
|
+
renderExecutionHistory(snapshot, executions);
|
|
1425
|
+
renderProcessLogs(snapshot, executions, phase.name);
|
|
1426
|
+
renderGlobalDiagnostics(kind, id, executions);
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
els.drawerTitle.textContent = story.id + ' · ' + story.title;
|
|
1054
1431
|
els.drawerBody.appendChild(
|
|
1055
1432
|
el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
|
|
1056
1433
|
);
|
|
@@ -1100,12 +1477,21 @@
|
|
|
1100
1477
|
for (const entry of history) {
|
|
1101
1478
|
const item = el('li');
|
|
1102
1479
|
if (entry.at) item.appendChild(el('span', 'log-time', formatClock(entry.at)));
|
|
1103
|
-
item.appendChild(
|
|
1480
|
+
item.appendChild(
|
|
1481
|
+
document.createTextNode(
|
|
1482
|
+
(STORY_STAGE_LABELS[entry.stage] || entry.stage || '') +
|
|
1483
|
+
(entry.detail ? ' · ' + entry.detail : ''),
|
|
1484
|
+
),
|
|
1485
|
+
);
|
|
1104
1486
|
entries.appendChild(item);
|
|
1105
1487
|
}
|
|
1106
1488
|
body.appendChild(entries);
|
|
1107
1489
|
});
|
|
1108
1490
|
}
|
|
1491
|
+
const executions = executionsFor(snapshot, kind, id);
|
|
1492
|
+
renderExecutionHistory(snapshot, executions);
|
|
1493
|
+
renderProcessLogs(snapshot, executions, 'execute');
|
|
1494
|
+
renderGlobalDiagnostics(kind, id, executions);
|
|
1109
1495
|
}
|
|
1110
1496
|
|
|
1111
1497
|
function drawerSection(title, fill) {
|
|
@@ -1115,8 +1501,8 @@
|
|
|
1115
1501
|
els.drawerBody.appendChild(section);
|
|
1116
1502
|
}
|
|
1117
1503
|
|
|
1118
|
-
function openDrawer(id) {
|
|
1119
|
-
state.
|
|
1504
|
+
function openDrawer(kind, id) {
|
|
1505
|
+
state.selectedDetail = { kind, id };
|
|
1120
1506
|
els.drawer.hidden = false;
|
|
1121
1507
|
els.drawerOverlay.hidden = false;
|
|
1122
1508
|
document.addEventListener('keydown', onDrawerKeydown);
|
|
@@ -1125,15 +1511,18 @@
|
|
|
1125
1511
|
}
|
|
1126
1512
|
|
|
1127
1513
|
function closeDrawer() {
|
|
1128
|
-
const
|
|
1129
|
-
state.
|
|
1514
|
+
const selected = state.selectedDetail;
|
|
1515
|
+
state.selectedDetail = null;
|
|
1130
1516
|
els.drawer.hidden = true;
|
|
1131
1517
|
els.drawerOverlay.hidden = true;
|
|
1132
1518
|
document.removeEventListener('keydown', onDrawerKeydown);
|
|
1133
1519
|
clear(els.drawerBody);
|
|
1134
1520
|
// O card é recriado a cada render, então o foco volta pelo id, não por uma
|
|
1135
1521
|
// referência guardada na abertura.
|
|
1136
|
-
const card =
|
|
1522
|
+
const card =
|
|
1523
|
+
selected && selected.kind === 'story'
|
|
1524
|
+
? els.kanban.querySelector('[data-story-id="' + selected.id + '"]')
|
|
1525
|
+
: null;
|
|
1137
1526
|
if (card) card.focus();
|
|
1138
1527
|
}
|
|
1139
1528
|
|
|
@@ -1151,6 +1540,11 @@
|
|
|
1151
1540
|
: el('span', 'mono commit-hash', commit.hash);
|
|
1152
1541
|
item.appendChild(hash);
|
|
1153
1542
|
const subject = el('div', 'item-main item-title', commit.subject);
|
|
1543
|
+
const meta = itemSideText([
|
|
1544
|
+
commit.storyId || '',
|
|
1545
|
+
commit.committedAt ? formatClock(commit.committedAt) : '',
|
|
1546
|
+
]);
|
|
1547
|
+
if (meta) subject.appendChild(el('span', 'muted commit-meta', meta));
|
|
1154
1548
|
item.appendChild(subject);
|
|
1155
1549
|
els.commits.appendChild(item);
|
|
1156
1550
|
}
|
|
@@ -1290,6 +1684,11 @@
|
|
|
1290
1684
|
|
|
1291
1685
|
// ---- Inicialização --------------------------------------------------------
|
|
1292
1686
|
|
|
1687
|
+
function onThemeChange(select) {
|
|
1688
|
+
setTheme(select.value);
|
|
1689
|
+
storeTheme(state.theme);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1293
1692
|
function onRefreshChange(select) {
|
|
1294
1693
|
state.refreshSeconds = Number(select.value);
|
|
1295
1694
|
storeRefresh(state.refreshSeconds);
|
|
@@ -1315,6 +1714,14 @@
|
|
|
1315
1714
|
);
|
|
1316
1715
|
}
|
|
1317
1716
|
|
|
1717
|
+
els.themeSelect.addEventListener('change', () => onThemeChange(els.themeSelect));
|
|
1718
|
+
if (els.themeSelectDashboard) {
|
|
1719
|
+
els.themeSelectDashboard.addEventListener('change', () =>
|
|
1720
|
+
onThemeChange(els.themeSelectDashboard),
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
initTheme();
|
|
1724
|
+
|
|
1318
1725
|
els.logFilter.addEventListener('change', () => {
|
|
1319
1726
|
state.logFilter = els.logFilter.value;
|
|
1320
1727
|
if (state.snapshot) renderLogs(state.snapshot);
|
|
@@ -1332,17 +1739,19 @@
|
|
|
1332
1739
|
// Default do seletor vem da configuração do servidor (/api/health);
|
|
1333
1740
|
// a escolha do usuário em localStorage tem precedência.
|
|
1334
1741
|
const stored = readStoredRefresh();
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
// Sem /api/health segue o default local (5s).
|
|
1742
|
+
try {
|
|
1743
|
+
const healthRes = await fetch('api/health', { cache: 'no-store' });
|
|
1744
|
+
serverInstanceChanged(healthRes);
|
|
1745
|
+
const health = await healthRes.json();
|
|
1746
|
+
state.configWritable = list(health.capabilities).includes('config:agent:write');
|
|
1747
|
+
const suggested = Number(health.refreshSeconds);
|
|
1748
|
+
if (stored === null && Number.isFinite(suggested) && suggested > 0) {
|
|
1749
|
+
state.refreshSeconds = suggested;
|
|
1344
1750
|
}
|
|
1751
|
+
} catch (err) {
|
|
1752
|
+
// Sem /api/health segue o default local (5s).
|
|
1345
1753
|
}
|
|
1754
|
+
if (stored !== null) state.refreshSeconds = stored;
|
|
1346
1755
|
buildRefreshSelect();
|
|
1347
1756
|
|
|
1348
1757
|
window.setInterval(renderTimers, 1000);
|