issue-flow 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/{analyze-RBNKXWL5.js → analyze-VSWAEVN3.js} +6 -6
- package/dist/{chunk-BH7DYADV.js → chunk-57MH6RYJ.js} +97 -7
- package/dist/chunk-57MH6RYJ.js.map +1 -0
- package/dist/{chunk-J4SICTAN.js → chunk-66ZGYXTH.js} +53 -9
- package/dist/chunk-66ZGYXTH.js.map +1 -0
- package/dist/{chunk-TGP6WK65.js → chunk-DP43WJY4.js} +75 -11
- package/dist/chunk-DP43WJY4.js.map +1 -0
- package/dist/{chunk-QO2MWMAZ.js → chunk-DTNM3LTO.js} +33 -10
- package/dist/chunk-DTNM3LTO.js.map +1 -0
- package/dist/{chunk-JWZKV3CL.js → chunk-JGKPSWOA.js} +2 -2
- package/dist/{chunk-HSGTZ3CT.js → chunk-KXAG24DD.js} +219 -3
- package/dist/chunk-KXAG24DD.js.map +1 -0
- package/dist/{chunk-J3PJBZXJ.js → chunk-M37YZYDO.js} +3 -3
- package/dist/{chunk-VPDWTZ3F.js → chunk-M72ZEPWD.js} +2 -2
- package/dist/{chunk-BASRRXAM.js → chunk-MTJQOOMG.js} +105 -4
- package/dist/chunk-MTJQOOMG.js.map +1 -0
- package/dist/{chunk-RQW5W4XC.js → chunk-NB6WKTBL.js} +2 -2
- package/dist/chunk-TJMZWRS6.js +265 -0
- package/dist/chunk-TJMZWRS6.js.map +1 -0
- package/dist/{chunk-VSF2CR3G.js → chunk-TLSIK656.js} +6 -6
- package/dist/{chunk-F52S6ZGF.js → chunk-VNKSMWDN.js} +17 -3
- package/dist/chunk-VNKSMWDN.js.map +1 -0
- package/dist/{chunk-36ECROTC.js → chunk-WHEU55ZP.js} +7 -7
- package/dist/{chunk-A4M6AKIK.js → chunk-X7XBF5TS.js} +6 -6
- package/dist/cli.js +91 -29
- package/dist/cli.js.map +1 -1
- package/dist/{config-R7O5YC65.js → config-J5DW6HSY.js} +3 -3
- package/dist/execute-Z2DKGVLR.js +14 -0
- package/dist/{generate-TGJQLGMV.js → generate-ERDPOTKX.js} +6 -6
- package/dist/init-UY6ZCIFT.js +9 -0
- package/dist/plan-REMAMCX4.js +16 -0
- package/dist/pr-KERP52TI.js +20 -0
- package/dist/pr-review-IU4N3TEK.js +16 -0
- package/dist/prd-SHVQW6RM.js +16 -0
- package/dist/review-L5F77QAC.js +14 -0
- package/dist/run-BRBFHVUO.js +1860 -0
- package/dist/run-BRBFHVUO.js.map +1 -0
- package/dist/{web-JVPK2PIF.js → web-WQISLO66.js} +4 -4
- package/package.json +1 -1
- package/prompts/execute.md +2 -2
- package/prompts/plan.md +10 -1
- package/prompts/pr.md +9 -4
- package/web/CLAUDE.md +22 -8
- package/web/public/app.css +178 -0
- package/web/public/app.js +336 -17
- package/web/public/index.html +24 -0
- package/dist/chunk-BASRRXAM.js.map +0 -1
- package/dist/chunk-BH7DYADV.js.map +0 -1
- package/dist/chunk-F52S6ZGF.js.map +0 -1
- package/dist/chunk-HSGTZ3CT.js.map +0 -1
- package/dist/chunk-J4SICTAN.js.map +0 -1
- package/dist/chunk-MK43N4X3.js +0 -123
- package/dist/chunk-MK43N4X3.js.map +0 -1
- package/dist/chunk-QO2MWMAZ.js.map +0 -1
- package/dist/chunk-TGP6WK65.js.map +0 -1
- package/dist/execute-LU3HWA54.js +0 -14
- package/dist/init-WP7Z2LFQ.js +0 -9
- package/dist/plan-2MWMCKRY.js +0 -16
- package/dist/pr-XVQDEQ7B.js +0 -16
- package/dist/pr-review-VO6ZXAFM.js +0 -16
- package/dist/prd-FPZAHQEO.js +0 -16
- package/dist/review-AHALLMXO.js +0 -14
- package/dist/run-2CXVR3VZ.js +0 -733
- package/dist/run-2CXVR3VZ.js.map +0 -1
- /package/dist/{analyze-RBNKXWL5.js.map → analyze-VSWAEVN3.js.map} +0 -0
- /package/dist/{chunk-JWZKV3CL.js.map → chunk-JGKPSWOA.js.map} +0 -0
- /package/dist/{chunk-J3PJBZXJ.js.map → chunk-M37YZYDO.js.map} +0 -0
- /package/dist/{chunk-VPDWTZ3F.js.map → chunk-M72ZEPWD.js.map} +0 -0
- /package/dist/{chunk-RQW5W4XC.js.map → chunk-NB6WKTBL.js.map} +0 -0
- /package/dist/{chunk-VSF2CR3G.js.map → chunk-TLSIK656.js.map} +0 -0
- /package/dist/{chunk-36ECROTC.js.map → chunk-WHEU55ZP.js.map} +0 -0
- /package/dist/{chunk-A4M6AKIK.js.map → chunk-X7XBF5TS.js.map} +0 -0
- /package/dist/{config-R7O5YC65.js.map → config-J5DW6HSY.js.map} +0 -0
- /package/dist/{execute-LU3HWA54.js.map → execute-Z2DKGVLR.js.map} +0 -0
- /package/dist/{generate-TGJQLGMV.js.map → generate-ERDPOTKX.js.map} +0 -0
- /package/dist/{init-WP7Z2LFQ.js.map → init-UY6ZCIFT.js.map} +0 -0
- /package/dist/{plan-2MWMCKRY.js.map → plan-REMAMCX4.js.map} +0 -0
- /package/dist/{pr-XVQDEQ7B.js.map → pr-KERP52TI.js.map} +0 -0
- /package/dist/{pr-review-VO6ZXAFM.js.map → pr-review-IU4N3TEK.js.map} +0 -0
- /package/dist/{prd-FPZAHQEO.js.map → prd-SHVQW6RM.js.map} +0 -0
- /package/dist/{review-AHALLMXO.js.map → review-L5F77QAC.js.map} +0 -0
- /package/dist/{web-JVPK2PIF.js.map → web-WQISLO66.js.map} +0 -0
package/web/public/app.css
CHANGED
|
@@ -465,6 +465,16 @@ progress {
|
|
|
465
465
|
font-weight: 700;
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
/* Story cuja stage é 'executing' (issue 38) — destaque visual na lista de
|
|
469
|
+
"User stories". A borda esquerda usa box-shadow (não border) para não
|
|
470
|
+
deslocar o conteúdo do item ao ganhar/perder a classe. */
|
|
471
|
+
.story-executing {
|
|
472
|
+
background: var(--run-bg);
|
|
473
|
+
border-radius: 6px;
|
|
474
|
+
box-shadow: inset 3px 0 0 0 var(--run);
|
|
475
|
+
padding-left: 9px;
|
|
476
|
+
}
|
|
477
|
+
|
|
468
478
|
.icon-completed {
|
|
469
479
|
color: var(--ok);
|
|
470
480
|
}
|
|
@@ -836,4 +846,172 @@ progress {
|
|
|
836
846
|
.header-side {
|
|
837
847
|
align-items: flex-start;
|
|
838
848
|
}
|
|
849
|
+
|
|
850
|
+
.dashboard-grid {
|
|
851
|
+
grid-template-columns: 1fr;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/* ---- Views: dashboard multi-sessão vs detalhe (#35) ---- */
|
|
856
|
+
|
|
857
|
+
.view {
|
|
858
|
+
display: grid;
|
|
859
|
+
gap: 16px;
|
|
860
|
+
min-width: 0;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.view[hidden] {
|
|
864
|
+
display: none;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.view > * {
|
|
868
|
+
min-width: 0;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.back-link {
|
|
872
|
+
appearance: none;
|
|
873
|
+
background: none;
|
|
874
|
+
border: none;
|
|
875
|
+
padding: 0;
|
|
876
|
+
margin: 0 0 6px;
|
|
877
|
+
font: inherit;
|
|
878
|
+
font-size: 0.85rem;
|
|
879
|
+
font-weight: 600;
|
|
880
|
+
color: var(--accent);
|
|
881
|
+
cursor: pointer;
|
|
882
|
+
display: inline-block;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.back-link:hover {
|
|
886
|
+
text-decoration: underline;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.dashboard-grid {
|
|
890
|
+
display: grid;
|
|
891
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
892
|
+
gap: 16px;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.dashboard-card {
|
|
896
|
+
appearance: none;
|
|
897
|
+
text-align: left;
|
|
898
|
+
background: var(--card);
|
|
899
|
+
border: 1px solid var(--border);
|
|
900
|
+
border-radius: 10px;
|
|
901
|
+
padding: 16px;
|
|
902
|
+
box-shadow: var(--shadow);
|
|
903
|
+
color: inherit;
|
|
904
|
+
font: inherit;
|
|
905
|
+
cursor: pointer;
|
|
906
|
+
display: grid;
|
|
907
|
+
gap: 10px;
|
|
908
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.dashboard-card:hover,
|
|
912
|
+
.dashboard-card:focus-visible {
|
|
913
|
+
border-color: var(--accent);
|
|
914
|
+
outline: none;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.dashboard-card.is-live {
|
|
918
|
+
border-color: var(--run);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.dashboard-card-head {
|
|
922
|
+
display: flex;
|
|
923
|
+
justify-content: space-between;
|
|
924
|
+
align-items: center;
|
|
925
|
+
gap: 8px;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.dashboard-project {
|
|
929
|
+
font-size: 0.82rem;
|
|
930
|
+
font-weight: 600;
|
|
931
|
+
color: var(--muted);
|
|
932
|
+
overflow-wrap: anywhere;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.dashboard-title-row {
|
|
936
|
+
display: flex;
|
|
937
|
+
flex-wrap: wrap;
|
|
938
|
+
align-items: baseline;
|
|
939
|
+
gap: 8px;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.dashboard-issue {
|
|
943
|
+
font-weight: 700;
|
|
944
|
+
color: var(--accent);
|
|
945
|
+
font-variant-numeric: tabular-nums;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.dashboard-title {
|
|
949
|
+
font-weight: 600;
|
|
950
|
+
overflow-wrap: anywhere;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.dashboard-summary {
|
|
954
|
+
margin: 0;
|
|
955
|
+
font-size: 0.9rem;
|
|
956
|
+
overflow: hidden;
|
|
957
|
+
display: -webkit-box;
|
|
958
|
+
-webkit-box-orient: vertical;
|
|
959
|
+
-webkit-line-clamp: 3;
|
|
960
|
+
line-clamp: 3;
|
|
961
|
+
overflow-wrap: anywhere;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
.dashboard-meta-row {
|
|
965
|
+
display: flex;
|
|
966
|
+
flex-wrap: wrap;
|
|
967
|
+
gap: 10px;
|
|
968
|
+
font-size: 0.82rem;
|
|
969
|
+
color: var(--muted);
|
|
970
|
+
font-variant-numeric: tabular-nums;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.dashboard-progress {
|
|
974
|
+
display: block;
|
|
975
|
+
height: 6px;
|
|
976
|
+
background: var(--idle-bg);
|
|
977
|
+
border-radius: 999px;
|
|
978
|
+
overflow: hidden;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.dashboard-progress-bar {
|
|
982
|
+
display: block;
|
|
983
|
+
height: 100%;
|
|
984
|
+
background: var(--run);
|
|
985
|
+
border-radius: 999px;
|
|
986
|
+
transition: width 0.3s ease;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.dashboard-live-dot {
|
|
990
|
+
font-size: 0.75rem;
|
|
991
|
+
font-weight: 600;
|
|
992
|
+
color: var(--run);
|
|
993
|
+
text-transform: uppercase;
|
|
994
|
+
letter-spacing: 0.04em;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.dashboard-live-dot::before {
|
|
998
|
+
content: '';
|
|
999
|
+
display: inline-block;
|
|
1000
|
+
width: 7px;
|
|
1001
|
+
height: 7px;
|
|
1002
|
+
margin-right: 6px;
|
|
1003
|
+
border-radius: 50%;
|
|
1004
|
+
background: var(--run);
|
|
1005
|
+
vertical-align: middle;
|
|
1006
|
+
animation: dashboard-pulse 1.4s ease-in-out infinite;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
@keyframes dashboard-pulse {
|
|
1010
|
+
0%,
|
|
1011
|
+
100% {
|
|
1012
|
+
opacity: 1;
|
|
1013
|
+
}
|
|
1014
|
+
50% {
|
|
1015
|
+
opacity: 0.35;
|
|
1016
|
+
}
|
|
839
1017
|
}
|
package/web/public/app.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Interface do monitoramento web do issue-flow.
|
|
2
2
|
// JS puro, sem framework e sem recursos externos — funciona offline.
|
|
3
|
-
// Consome
|
|
4
|
-
// dinâmico entra via textContent (nunca innerHTML com dados
|
|
3
|
+
// Consome GET api/sessions, GET api/status (ETag/304) e GET api/health;
|
|
4
|
+
// todo texto dinâmico entra via textContent (nunca innerHTML com dados
|
|
5
|
+
// do snapshot).
|
|
5
6
|
(() => {
|
|
6
7
|
'use strict';
|
|
7
8
|
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
const STORAGE_KEY = 'issue-flow:refresh-seconds';
|
|
11
12
|
const MAX_BACKOFF_MS = 60000;
|
|
12
13
|
const ALERT_PREVIEW = 3;
|
|
14
|
+
const DESCRIPTION_PREVIEW = 140;
|
|
13
15
|
|
|
14
16
|
const STATUS_LABELS = {
|
|
15
17
|
idle: 'aguardando',
|
|
@@ -32,6 +34,20 @@
|
|
|
32
34
|
done: 'concluída',
|
|
33
35
|
};
|
|
34
36
|
|
|
37
|
+
// Rótulos do `stage` granular (issue 38) — mais fino que STORY_STATUS_LABELS,
|
|
38
|
+
// deriva de eventos reais do pipeline (iteration:start, stories:update,
|
|
39
|
+
// phase:start/phase:end da review, correction:cycle). Ver README, seção
|
|
40
|
+
// "Story stage".
|
|
41
|
+
const STORY_STAGE_LABELS = {
|
|
42
|
+
pending: 'aguardando',
|
|
43
|
+
executing: 'em execução',
|
|
44
|
+
awaiting_review: 'aguardando revisão',
|
|
45
|
+
in_review: 'em revisão',
|
|
46
|
+
in_correction: 'em correção',
|
|
47
|
+
done: 'concluída',
|
|
48
|
+
failed: 'falhou',
|
|
49
|
+
};
|
|
50
|
+
|
|
35
51
|
// Colunas do Kanban, na ordem em que a execução avança. Os títulos são os das
|
|
36
52
|
// colunas, não os rótulos dos badges (STORY_STATUS_LABELS), que seguem em minúsculas.
|
|
37
53
|
const KANBAN_COLUMNS = [
|
|
@@ -43,12 +59,18 @@
|
|
|
43
59
|
|
|
44
60
|
const els = {
|
|
45
61
|
banner: document.getElementById('banner-disconnected'),
|
|
62
|
+
viewDashboard: document.getElementById('view-dashboard'),
|
|
63
|
+
viewDetail: document.getElementById('view-detail'),
|
|
64
|
+
dashboard: document.getElementById('dashboard'),
|
|
65
|
+
dashboardMeta: document.getElementById('dashboard-meta'),
|
|
66
|
+
backToDashboard: document.getElementById('back-to-dashboard'),
|
|
46
67
|
issueLink: document.getElementById('issue-link'),
|
|
47
68
|
branchLine: document.getElementById('branch-line'),
|
|
48
69
|
statusBadge: document.getElementById('status-badge'),
|
|
49
70
|
elapsed: document.getElementById('elapsed'),
|
|
50
71
|
estimate: document.getElementById('estimate'),
|
|
51
72
|
refreshSelect: document.getElementById('refresh-select'),
|
|
73
|
+
refreshSelectDashboard: document.getElementById('refresh-select-dashboard'),
|
|
52
74
|
alerts: document.getElementById('alerts'),
|
|
53
75
|
alertsBody: document.getElementById('alerts-body'),
|
|
54
76
|
issueSummary: document.getElementById('issue-summary'),
|
|
@@ -86,6 +108,16 @@
|
|
|
86
108
|
// Só o id: o card que abriu o drawer é destruído no próximo render, então
|
|
87
109
|
// guardar o nó levaria a uma referência morta.
|
|
88
110
|
selectedStoryId: null,
|
|
111
|
+
// Multi-sessão (#35): lista de /api/sessions e seleção explícita do usuário.
|
|
112
|
+
// selectedSessionId null = modo automático (1 sessão → detalhe; 2+ → dashboard).
|
|
113
|
+
sessions: [],
|
|
114
|
+
selectedSessionId: null,
|
|
115
|
+
// sessionId cujo snapshot/detalhe está na tela (evita flash ao trocar).
|
|
116
|
+
detailSessionId: null,
|
|
117
|
+
viewMode: 'detail', // 'detail' | 'dashboard'
|
|
118
|
+
statusUrl: 'api/status',
|
|
119
|
+
// Se selectSession/clearSessionSelection chega durante um poll, reexecuta.
|
|
120
|
+
pollAgain: false,
|
|
89
121
|
};
|
|
90
122
|
|
|
91
123
|
// ---- Utilitários ----------------------------------------------------------
|
|
@@ -226,9 +258,14 @@
|
|
|
226
258
|
// que nenhum consumidor precise repetir a checagem de ausência.
|
|
227
259
|
function normalizeStory(story) {
|
|
228
260
|
const status = story.status !== null && story.status !== undefined ? story.status : 'backlog';
|
|
261
|
+
const stage = story.stage !== null && story.stage !== undefined ? story.stage : 'pending';
|
|
229
262
|
return {
|
|
230
263
|
...story,
|
|
231
264
|
status: STORY_STATUS_LABELS[status] !== undefined ? status : 'backlog',
|
|
265
|
+
stage: STORY_STAGE_LABELS[stage] !== undefined ? stage : 'pending',
|
|
266
|
+
stageSince: story.stageSince !== null && story.stageSince !== undefined ? story.stageSince : null,
|
|
267
|
+
stageDetail:
|
|
268
|
+
story.stageDetail !== null && story.stageDetail !== undefined ? story.stageDetail : null,
|
|
232
269
|
dependencies: list(story.dependencies),
|
|
233
270
|
description: text(story.description),
|
|
234
271
|
acceptanceCriteria: list(story.acceptanceCriteria),
|
|
@@ -286,22 +323,28 @@
|
|
|
286
323
|
}
|
|
287
324
|
}
|
|
288
325
|
|
|
289
|
-
function
|
|
326
|
+
function fillRefreshSelect(select) {
|
|
327
|
+
if (!select) return;
|
|
290
328
|
const values = REFRESH_OPTIONS.slice();
|
|
291
329
|
if (state.refreshSeconds !== PAUSED && values.indexOf(state.refreshSeconds) === -1) {
|
|
292
330
|
values.push(state.refreshSeconds);
|
|
293
331
|
values.sort((a, b) => a - b);
|
|
294
332
|
}
|
|
295
|
-
clear(
|
|
333
|
+
clear(select);
|
|
296
334
|
for (const value of values) {
|
|
297
335
|
const option = el('option', null, value + 's');
|
|
298
336
|
option.value = String(value);
|
|
299
|
-
|
|
337
|
+
select.appendChild(option);
|
|
300
338
|
}
|
|
301
339
|
const pause = el('option', null, 'pausar');
|
|
302
340
|
pause.value = String(PAUSED);
|
|
303
|
-
|
|
304
|
-
|
|
341
|
+
select.appendChild(pause);
|
|
342
|
+
select.value = String(state.refreshSeconds);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function buildRefreshSelect() {
|
|
346
|
+
fillRefreshSelect(els.refreshSelect);
|
|
347
|
+
fillRefreshSelect(els.refreshSelectDashboard);
|
|
305
348
|
}
|
|
306
349
|
|
|
307
350
|
function clearTimer() {
|
|
@@ -319,19 +362,161 @@
|
|
|
319
362
|
state.timer = window.setTimeout(poll, Math.min(backoff, MAX_BACKOFF_MS));
|
|
320
363
|
}
|
|
321
364
|
|
|
365
|
+
// Decide dashboard vs detalhe a partir de /api/sessions. selectedSessionId
|
|
366
|
+
// null = automático; string = escolha explícita do usuário (card).
|
|
367
|
+
function resolveView(sessions) {
|
|
368
|
+
state.sessions = sessions;
|
|
369
|
+
|
|
370
|
+
if (state.selectedSessionId !== null) {
|
|
371
|
+
const stillThere = sessions.some((s) => s.sessionId === state.selectedSessionId);
|
|
372
|
+
if (!stillThere) {
|
|
373
|
+
state.selectedSessionId = null;
|
|
374
|
+
state.etag = null;
|
|
375
|
+
} else if (sessions.length === 1) {
|
|
376
|
+
// Seleção explícita deixa de fazer sentido com uma única sessão.
|
|
377
|
+
state.selectedSessionId = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (state.selectedSessionId !== null) {
|
|
382
|
+
const selected = sessions.find((s) => s.sessionId === state.selectedSessionId);
|
|
383
|
+
return { mode: 'detail', session: selected || null };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (sessions.length === 0) {
|
|
387
|
+
return { mode: 'detail', session: null };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (sessions.length === 1) {
|
|
391
|
+
return { mode: 'detail', session: sessions[0] };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { mode: 'dashboard', session: null };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function setViewMode(mode) {
|
|
398
|
+
state.viewMode = mode;
|
|
399
|
+
els.viewDashboard.hidden = mode !== 'dashboard';
|
|
400
|
+
els.viewDetail.hidden = mode !== 'detail';
|
|
401
|
+
// Back só quando o usuário escolheu um card e ainda há dashboard para voltar.
|
|
402
|
+
const showBack =
|
|
403
|
+
mode === 'detail' && state.selectedSessionId !== null && state.sessions.length >= 2;
|
|
404
|
+
els.backToDashboard.hidden = !showBack;
|
|
405
|
+
if (mode === 'dashboard' && state.selectedStoryId !== null) {
|
|
406
|
+
closeDrawer();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function statusUrlFor(session) {
|
|
411
|
+
if (!session || !session.statusUrl) return 'api/status';
|
|
412
|
+
// statusUrl vem com barra inicial (/api/status?session=…); o fetch relativo
|
|
413
|
+
// ao path do painel precisa da forma sem a barra absoluta do host.
|
|
414
|
+
return session.statusUrl.replace(/^\//, '');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function clearDetailState() {
|
|
418
|
+
state.etag = null;
|
|
419
|
+
state.snapshot = null;
|
|
420
|
+
state.detailSessionId = null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function requestPoll() {
|
|
424
|
+
clearTimer();
|
|
425
|
+
if (state.polling) {
|
|
426
|
+
state.pollAgain = true;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
poll();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function selectSession(sessionId) {
|
|
433
|
+
if (state.selectedSessionId !== sessionId) {
|
|
434
|
+
clearDetailState();
|
|
435
|
+
}
|
|
436
|
+
state.selectedSessionId = sessionId;
|
|
437
|
+
requestPoll();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function clearSessionSelection() {
|
|
441
|
+
state.selectedSessionId = null;
|
|
442
|
+
clearDetailState();
|
|
443
|
+
requestPoll();
|
|
444
|
+
}
|
|
445
|
+
|
|
322
446
|
async function poll() {
|
|
323
|
-
if (state.polling)
|
|
447
|
+
if (state.polling) {
|
|
448
|
+
state.pollAgain = true;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
324
451
|
state.polling = true;
|
|
452
|
+
state.pollAgain = false;
|
|
325
453
|
try {
|
|
454
|
+
const sessionsRes = await fetch('api/sessions', { cache: 'no-store' });
|
|
455
|
+
if (!sessionsRes.ok) throw new Error('HTTP ' + sessionsRes.status);
|
|
456
|
+
const sessions = await sessionsRes.json();
|
|
457
|
+
if (!Array.isArray(sessions)) throw new Error('sessions payload invalid');
|
|
458
|
+
|
|
459
|
+
const resolved = resolveView(sessions);
|
|
460
|
+
|
|
461
|
+
if (resolved.mode === 'dashboard') {
|
|
462
|
+
clearDetailState();
|
|
463
|
+
setViewMode('dashboard');
|
|
464
|
+
renderDashboard(sessions);
|
|
465
|
+
state.failures = 0;
|
|
466
|
+
els.banner.hidden = true;
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const nextId =
|
|
471
|
+
resolved.session && resolved.session.sessionId != null
|
|
472
|
+
? resolved.session.sessionId
|
|
473
|
+
: null;
|
|
474
|
+
const sessionChanged = state.detailSessionId !== nextId;
|
|
475
|
+
if (sessionChanged) {
|
|
476
|
+
clearDetailState();
|
|
477
|
+
state.detailSessionId = nextId;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!resolved.session) {
|
|
481
|
+
setViewMode('detail');
|
|
482
|
+
// Zero sessões: mantém a última tela de detalhe se houver, sem explodir.
|
|
483
|
+
renderEmptyDetail();
|
|
484
|
+
state.failures = 0;
|
|
485
|
+
els.banner.hidden = true;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const nextUrl = statusUrlFor(resolved.session);
|
|
490
|
+
if (nextUrl !== state.statusUrl) {
|
|
491
|
+
state.statusUrl = nextUrl;
|
|
492
|
+
state.etag = null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Evita flash da sessão anterior enquanto o status novo carrega.
|
|
496
|
+
if (sessionChanged || !state.snapshot) {
|
|
497
|
+
els.viewDashboard.hidden = true;
|
|
498
|
+
els.viewDetail.hidden = true;
|
|
499
|
+
} else {
|
|
500
|
+
setViewMode('detail');
|
|
501
|
+
}
|
|
502
|
+
|
|
326
503
|
const headers = {};
|
|
327
504
|
if (state.etag) headers['If-None-Match'] = state.etag;
|
|
328
|
-
const res = await fetch(
|
|
505
|
+
const res = await fetch(state.statusUrl, { headers, cache: 'no-store' });
|
|
506
|
+
// Se selectSession/clear pediu outro poll no meio do fetch, descarta este
|
|
507
|
+
// status — o ciclo seguinte aplica a seleção atual.
|
|
508
|
+
if (state.pollAgain) {
|
|
509
|
+
state.failures = 0;
|
|
510
|
+
els.banner.hidden = true;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
329
513
|
if (res.status !== 304) {
|
|
330
514
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
331
515
|
state.etag = res.headers.get('ETag');
|
|
332
516
|
state.snapshot = await res.json();
|
|
333
517
|
render();
|
|
334
518
|
}
|
|
519
|
+
setViewMode('detail');
|
|
335
520
|
state.failures = 0;
|
|
336
521
|
els.banner.hidden = true;
|
|
337
522
|
} catch (err) {
|
|
@@ -339,7 +524,124 @@
|
|
|
339
524
|
els.banner.hidden = false;
|
|
340
525
|
} finally {
|
|
341
526
|
state.polling = false;
|
|
342
|
-
|
|
527
|
+
if (state.pollAgain) {
|
|
528
|
+
state.pollAgain = false;
|
|
529
|
+
poll();
|
|
530
|
+
} else {
|
|
531
|
+
schedule();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function renderEmptyDetail() {
|
|
537
|
+
if (state.snapshot) return;
|
|
538
|
+
document.title = 'issue-flow';
|
|
539
|
+
els.statusBadge.textContent = STATUS_LABELS.idle;
|
|
540
|
+
els.statusBadge.className = 'badge status-idle';
|
|
541
|
+
els.elapsed.textContent = '—';
|
|
542
|
+
els.estimate.hidden = true;
|
|
543
|
+
els.sessionMeta.textContent = 'nenhuma execução ativa';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function truncateText(text, max) {
|
|
547
|
+
if (!text) return '';
|
|
548
|
+
const normalized = String(text).replace(/\s+/g, ' ').trim();
|
|
549
|
+
if (normalized.length <= max) return normalized;
|
|
550
|
+
return normalized.slice(0, max - 1).trimEnd() + '…';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function renderDashboard(sessions) {
|
|
554
|
+
const active = document.activeElement;
|
|
555
|
+
const focusedId =
|
|
556
|
+
active &&
|
|
557
|
+
active.dataset &&
|
|
558
|
+
els.dashboard.contains(active) &&
|
|
559
|
+
active.dataset.sessionId
|
|
560
|
+
? active.dataset.sessionId
|
|
561
|
+
: null;
|
|
562
|
+
|
|
563
|
+
clear(els.dashboard);
|
|
564
|
+
|
|
565
|
+
if (sessions.length === 0) {
|
|
566
|
+
els.dashboard.appendChild(el('p', 'empty', 'Nenhuma execução ativa.'));
|
|
567
|
+
els.dashboardMeta.textContent = '0 execuções';
|
|
568
|
+
document.title = 'issue-flow';
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
for (const session of sessions) {
|
|
573
|
+
// <button> só aceita phrasing content — mesmos spans do Kanban.
|
|
574
|
+
const card = el('button', 'dashboard-card');
|
|
575
|
+
card.type = 'button';
|
|
576
|
+
if (session.sessionId) card.dataset.sessionId = session.sessionId;
|
|
577
|
+
if (session.status === 'running') card.classList.add('is-live');
|
|
578
|
+
|
|
579
|
+
const head = el('span', 'dashboard-card-head');
|
|
580
|
+
const project = el(
|
|
581
|
+
'span',
|
|
582
|
+
'dashboard-project',
|
|
583
|
+
session.repositoryName || 'Projeto desconhecido',
|
|
584
|
+
);
|
|
585
|
+
const badge = el('span', 'badge status-' + (session.status || 'idle'));
|
|
586
|
+
badge.textContent = STATUS_LABELS[session.status] || session.status || '—';
|
|
587
|
+
head.appendChild(project);
|
|
588
|
+
head.appendChild(badge);
|
|
589
|
+
card.appendChild(head);
|
|
590
|
+
|
|
591
|
+
const titleRow = el('span', 'dashboard-title-row');
|
|
592
|
+
if (session.issueNumber !== null && session.issueNumber !== undefined) {
|
|
593
|
+
titleRow.appendChild(el('span', 'dashboard-issue', '#' + session.issueNumber));
|
|
594
|
+
}
|
|
595
|
+
titleRow.appendChild(
|
|
596
|
+
el('span', 'dashboard-title', session.issueTitle || 'Sem título'),
|
|
597
|
+
);
|
|
598
|
+
card.appendChild(titleRow);
|
|
599
|
+
|
|
600
|
+
const summary = truncateText(session.issueDescription, DESCRIPTION_PREVIEW);
|
|
601
|
+
card.appendChild(el('span', 'dashboard-summary muted', summary || 'Sem descrição'));
|
|
602
|
+
|
|
603
|
+
const meta = el('span', 'dashboard-meta-row');
|
|
604
|
+
meta.appendChild(
|
|
605
|
+
el('span', null, 'Fase: ' + (session.currentPhase || '—')),
|
|
606
|
+
);
|
|
607
|
+
const percent =
|
|
608
|
+
typeof session.progressPercent === 'number' ? session.progressPercent : 0;
|
|
609
|
+
meta.appendChild(el('span', null, percent + '%'));
|
|
610
|
+
const elapsed =
|
|
611
|
+
typeof session.elapsedSeconds === 'number'
|
|
612
|
+
? formatDuration(session.elapsedSeconds)
|
|
613
|
+
: session.startedAt
|
|
614
|
+
? formatAgo(session.startedAt)
|
|
615
|
+
: '—';
|
|
616
|
+
meta.appendChild(el('span', null, elapsed));
|
|
617
|
+
card.appendChild(meta);
|
|
618
|
+
|
|
619
|
+
const progress = el('span', 'dashboard-progress');
|
|
620
|
+
const bar = el('span', 'dashboard-progress-bar');
|
|
621
|
+
bar.style.width = Math.max(0, Math.min(100, percent)) + '%';
|
|
622
|
+
progress.appendChild(bar);
|
|
623
|
+
card.appendChild(progress);
|
|
624
|
+
|
|
625
|
+
if (session.status === 'running') {
|
|
626
|
+
card.appendChild(el('span', 'dashboard-live-dot', 'ao vivo'));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
card.addEventListener('click', () => {
|
|
630
|
+
if (session.sessionId) selectSession(session.sessionId);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
els.dashboard.appendChild(card);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
els.dashboardMeta.textContent =
|
|
637
|
+
sessions.length + (sessions.length === 1 ? ' execução' : ' execuções');
|
|
638
|
+
document.title = sessions.length + ' execuções · issue-flow';
|
|
639
|
+
|
|
640
|
+
if (focusedId) {
|
|
641
|
+
const card = els.dashboard.querySelector(
|
|
642
|
+
'[data-session-id="' + focusedId.replace(/"/g, '') + '"]',
|
|
643
|
+
);
|
|
644
|
+
if (card) card.focus();
|
|
343
645
|
}
|
|
344
646
|
}
|
|
345
647
|
|
|
@@ -588,7 +890,10 @@
|
|
|
588
890
|
return;
|
|
589
891
|
}
|
|
590
892
|
for (const story of snapshot.stories) {
|
|
591
|
-
|
|
893
|
+
// stage vem com default no schema ('pending'), mas session.json gravado
|
|
894
|
+
// antes da issue 38 pode chegar sem ele.
|
|
895
|
+
const storyStage = story.stage !== null && story.stage !== undefined ? story.stage : 'pending';
|
|
896
|
+
const item = el('li', storyStage === 'executing' ? 'story-executing' : null);
|
|
592
897
|
const status = story.passes ? 'completed' : 'pending';
|
|
593
898
|
item.appendChild(el('span', 'item-icon icon-' + status, story.passes ? '✓' : '○'));
|
|
594
899
|
const main = el('div', 'item-main');
|
|
@@ -612,7 +917,12 @@
|
|
|
612
917
|
|
|
613
918
|
item.appendChild(main);
|
|
614
919
|
const duration = metric(story.durationSeconds);
|
|
920
|
+
const stageLabel = STORY_STAGE_LABELS[storyStage] || storyStage;
|
|
921
|
+
const stageText = story.stageSince
|
|
922
|
+
? stageLabel + ' ' + formatAgo(story.stageSince)
|
|
923
|
+
: stageLabel;
|
|
615
924
|
const side = itemSideText([
|
|
925
|
+
stageText,
|
|
616
926
|
story.completedAt ? 'concluída ' + formatClock(story.completedAt) : '',
|
|
617
927
|
duration !== null ? formatDuration(duration) : '',
|
|
618
928
|
formatUsage(story),
|
|
@@ -862,6 +1172,14 @@
|
|
|
862
1172
|
|
|
863
1173
|
// ---- Inicialização --------------------------------------------------------
|
|
864
1174
|
|
|
1175
|
+
function onRefreshChange(select) {
|
|
1176
|
+
state.refreshSeconds = Number(select.value);
|
|
1177
|
+
storeRefresh(state.refreshSeconds);
|
|
1178
|
+
buildRefreshSelect();
|
|
1179
|
+
clearTimer();
|
|
1180
|
+
if (state.refreshSeconds !== PAUSED) poll();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
865
1183
|
async function init() {
|
|
866
1184
|
for (const tab of els.tabs) {
|
|
867
1185
|
tab.addEventListener('click', () => setActiveTab(tab.id));
|
|
@@ -870,13 +1188,14 @@
|
|
|
870
1188
|
|
|
871
1189
|
els.drawerClose.addEventListener('click', closeDrawer);
|
|
872
1190
|
els.drawerOverlay.addEventListener('click', closeDrawer);
|
|
1191
|
+
els.backToDashboard.addEventListener('click', clearSessionSelection);
|
|
873
1192
|
|
|
874
|
-
els.refreshSelect.addEventListener('change', () =>
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
1193
|
+
els.refreshSelect.addEventListener('change', () => onRefreshChange(els.refreshSelect));
|
|
1194
|
+
if (els.refreshSelectDashboard) {
|
|
1195
|
+
els.refreshSelectDashboard.addEventListener('change', () =>
|
|
1196
|
+
onRefreshChange(els.refreshSelectDashboard),
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
880
1199
|
|
|
881
1200
|
els.logFilter.addEventListener('change', () => {
|
|
882
1201
|
state.logFilter = els.logFilter.value;
|
package/web/public/index.html
CHANGED
|
@@ -15,8 +15,31 @@
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
17
|
<main>
|
|
18
|
+
<div id="view-dashboard" class="view" hidden>
|
|
19
|
+
<header class="card header">
|
|
20
|
+
<div class="header-main">
|
|
21
|
+
<h1><span class="brand">issue-flow</span></h1>
|
|
22
|
+
<p class="muted">Execuções ativas</p>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="header-side">
|
|
25
|
+
<label class="refresh-control">
|
|
26
|
+
Atualizar
|
|
27
|
+
<select id="refresh-select-dashboard" aria-label="Intervalo de atualização"></select>
|
|
28
|
+
</label>
|
|
29
|
+
</div>
|
|
30
|
+
</header>
|
|
31
|
+
<div id="dashboard" class="dashboard-grid"></div>
|
|
32
|
+
<footer class="muted footer">
|
|
33
|
+
<span id="dashboard-meta" aria-live="polite"></span>
|
|
34
|
+
</footer>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div id="view-detail" class="view">
|
|
18
38
|
<header class="card header">
|
|
19
39
|
<div class="header-main">
|
|
40
|
+
<button type="button" id="back-to-dashboard" class="back-link" hidden>
|
|
41
|
+
← Todas as execuções
|
|
42
|
+
</button>
|
|
20
43
|
<h1>
|
|
21
44
|
<span class="brand">issue-flow</span>
|
|
22
45
|
<a id="issue-link" class="issue-link" target="_blank" rel="noopener" hidden>#—</a>
|
|
@@ -128,6 +151,7 @@
|
|
|
128
151
|
<footer class="muted footer">
|
|
129
152
|
<span id="session-meta"></span>
|
|
130
153
|
</footer>
|
|
154
|
+
</div>
|
|
131
155
|
</main>
|
|
132
156
|
|
|
133
157
|
<div id="drawer-overlay" class="drawer-overlay" hidden></div>
|