aid-installer 0.7.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/VERSION +1 -1
- package/bin/aid +2445 -194
- package/bin/aid.ps1 +2361 -106
- package/dashboard/home.html +3321 -0
- package/dashboard/index.html +987 -0
- package/dashboard/reader/__init__.py +56 -0
- package/dashboard/reader/derivation.py +892 -0
- package/dashboard/reader/locator.py +228 -0
- package/dashboard/reader/models.py +408 -0
- package/dashboard/reader/parsers.py +2105 -0
- package/dashboard/reader/reader.py +1196 -0
- package/dashboard/server/__init__.py +3 -0
- package/dashboard/server/reader.mjs +3699 -0
- package/dashboard/server/server.mjs +780 -0
- package/dashboard/server/server.py +1004 -0
- package/lib/AidInstallCore.psm1 +446 -43
- package/lib/aid-install-core.sh +405 -48
- package/package.json +6 -3
- package/scripts/postinstall.js +106 -0
- package/scripts/vendor.js +98 -0
|
@@ -0,0 +1,3321 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<!-- Resolve the theme synchronously, before first paint, to avoid a light->dark flash (FOUC).
|
|
6
|
+
Shared key 'aid-dashboard-theme' (one theme across the whole dashboard, not per page);
|
|
7
|
+
saved, else prefers-color-scheme, else light. -->
|
|
8
|
+
<script>(function(){try{var t=localStorage.getItem('aid-dashboard-theme');if(t!=='dark'&&t!=='light'){t=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';}document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10
|
+
<meta name="color-scheme" content="light dark">
|
|
11
|
+
<meta name="robots" content="noindex">
|
|
12
|
+
<title id="page-title">AID Dashboard</title>
|
|
13
|
+
<style>
|
|
14
|
+
/* ---------- Color scheme (informs native UI: scrollbars, form controls) ---------- */
|
|
15
|
+
:root { color-scheme: light dark; }
|
|
16
|
+
html[data-theme="light"] { color-scheme: light; }
|
|
17
|
+
html[data-theme="dark"] { color-scheme: dark; }
|
|
18
|
+
|
|
19
|
+
/* ---------- Theme variables (copied verbatim from component-css.css :root) ---------- */
|
|
20
|
+
:root, html[data-theme="light"] {
|
|
21
|
+
--bg: #F7F9FC;
|
|
22
|
+
--bg-elev: #FFFFFF;
|
|
23
|
+
--bg-sunken: #EEF2F7;
|
|
24
|
+
--text: #101828;
|
|
25
|
+
--text-muted: #4B5565;
|
|
26
|
+
--text-dim: #667085;
|
|
27
|
+
--border: #E3E8EF;
|
|
28
|
+
--border-strong: #CDD5DF;
|
|
29
|
+
--primary: #0B1F3A;
|
|
30
|
+
--primary-fg: #FFFFFF;
|
|
31
|
+
--accent: #007F7D;
|
|
32
|
+
--accent-fg: #FFFFFF;
|
|
33
|
+
--ok: #2E7D32;
|
|
34
|
+
--ok-bg: #E8F5E9;
|
|
35
|
+
--warn: #B45309;
|
|
36
|
+
--warn-bg: #FEF3C7;
|
|
37
|
+
--err: #B42318;
|
|
38
|
+
--err-bg: #FEE4E2;
|
|
39
|
+
--info: #1D4ED8;
|
|
40
|
+
--info-bg: #DBEAFE;
|
|
41
|
+
--purple: #6941C6;
|
|
42
|
+
--purple-bg: #F4EBFF;
|
|
43
|
+
--shadow-sm: 0 1px 3px rgba(16,24,40,0.06), 0 1px 2px rgba(16,24,40,0.04);
|
|
44
|
+
--shadow-md: 0 4px 8px -2px rgba(16,24,40,0.08), 0 2px 4px -2px rgba(16,24,40,0.04);
|
|
45
|
+
--shadow-lg: 0 12px 24px -4px rgba(16,24,40,0.12), 0 4px 8px -4px rgba(16,24,40,0.06);
|
|
46
|
+
--radius-sm: 6px;
|
|
47
|
+
--radius: 10px;
|
|
48
|
+
--radius-lg: 14px;
|
|
49
|
+
}
|
|
50
|
+
html[data-theme="dark"] {
|
|
51
|
+
--bg: #0B1220;
|
|
52
|
+
--bg-elev: #111A2E;
|
|
53
|
+
--bg-sunken: #081021;
|
|
54
|
+
--text: #E5EAF2;
|
|
55
|
+
--text-muted: #9AA5B8;
|
|
56
|
+
--text-dim: #8A99B8;
|
|
57
|
+
--border: #1E293B;
|
|
58
|
+
--border-strong: #2B3A52;
|
|
59
|
+
--primary: #0D2A52;
|
|
60
|
+
--primary-fg: #E8F2FF;
|
|
61
|
+
--accent: #2DD4D2;
|
|
62
|
+
--accent-fg: #051514;
|
|
63
|
+
--ok: #4ADE80;
|
|
64
|
+
--ok-bg: rgba(34,197,94,0.15);
|
|
65
|
+
--warn: #FBBF24;
|
|
66
|
+
--warn-bg: rgba(251,191,36,0.15);
|
|
67
|
+
--err: #F87171;
|
|
68
|
+
--err-bg: rgba(220,38,38,0.20);
|
|
69
|
+
--info: #60A5FA;
|
|
70
|
+
--info-bg: rgba(37,99,235,0.18);
|
|
71
|
+
--purple: #C084FC;
|
|
72
|
+
--purple-bg: rgba(147,51,234,0.18);
|
|
73
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,0.35);
|
|
74
|
+
--shadow-md: 0 4px 10px rgba(0,0,0,0.40);
|
|
75
|
+
--shadow-lg: 0 14px 30px rgba(0,0,0,0.55);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ---------- Base ---------- */
|
|
79
|
+
* { box-sizing: border-box; }
|
|
80
|
+
html { scroll-behavior: smooth; scroll-padding-top: 80px; }
|
|
81
|
+
body {
|
|
82
|
+
margin: 0;
|
|
83
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
84
|
+
background: var(--bg);
|
|
85
|
+
color: var(--text);
|
|
86
|
+
line-height: 1.55;
|
|
87
|
+
-webkit-font-smoothing: antialiased;
|
|
88
|
+
-moz-osx-font-smoothing: grayscale;
|
|
89
|
+
}
|
|
90
|
+
a { color: var(--accent); text-decoration: none; }
|
|
91
|
+
a:hover { text-decoration: underline; }
|
|
92
|
+
h1, h2, h3, h4 { color: var(--text); font-weight: 600; line-height: 1.25; margin: 0 0 0.4em 0; }
|
|
93
|
+
h2 { font-size: 1.5rem; margin-top: 0; }
|
|
94
|
+
h3 { font-size: 1.15rem; }
|
|
95
|
+
h4 { font-size: 1rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
96
|
+
p { margin: 0 0 1em 0; color: var(--text-muted); }
|
|
97
|
+
code { font-family: "SF Mono", Monaco, Consolas, "Roboto Mono", monospace; font-size: 0.88em; background: var(--bg-sunken); padding: 0.12em 0.4em; border-radius: 4px; color: var(--text); }
|
|
98
|
+
ul, ol { margin: 0 0 1em 0; padding-left: 1.5em; color: var(--text-muted); }
|
|
99
|
+
ul li, ol li { margin-bottom: 0.3em; }
|
|
100
|
+
hr { border: 0; border-top: 1px solid var(--border); margin: 2em 0; }
|
|
101
|
+
|
|
102
|
+
/* ---------- Top bar ---------- */
|
|
103
|
+
.top-bar {
|
|
104
|
+
position: sticky;
|
|
105
|
+
top: 0;
|
|
106
|
+
z-index: 100;
|
|
107
|
+
background: var(--bg-elev);
|
|
108
|
+
border-bottom: 1px solid var(--border);
|
|
109
|
+
box-shadow: var(--shadow-sm);
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
flex-wrap: wrap;
|
|
113
|
+
gap: 0.5rem 1rem;
|
|
114
|
+
padding: 0.6rem 1.5rem;
|
|
115
|
+
backdrop-filter: saturate(140%) blur(8px);
|
|
116
|
+
}
|
|
117
|
+
.top-bar .brand {
|
|
118
|
+
font-weight: 700;
|
|
119
|
+
color: var(--text);
|
|
120
|
+
font-size: 0.95rem;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
}
|
|
123
|
+
.top-bar .brand .dot { color: var(--accent); margin: 0 0.4em; }
|
|
124
|
+
.app-title {
|
|
125
|
+
display: inline-flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
gap: 0.5em;
|
|
128
|
+
font-weight: 700;
|
|
129
|
+
font-size: 0.95rem;
|
|
130
|
+
color: var(--text);
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
}
|
|
133
|
+
.app-title .beta-pill {
|
|
134
|
+
font-size: 0.6rem;
|
|
135
|
+
font-weight: 700;
|
|
136
|
+
text-transform: uppercase;
|
|
137
|
+
letter-spacing: 0.06em;
|
|
138
|
+
color: var(--accent);
|
|
139
|
+
border: 1px solid var(--accent);
|
|
140
|
+
border-radius: 999px;
|
|
141
|
+
padding: 0.1em 0.5em;
|
|
142
|
+
line-height: 1.4;
|
|
143
|
+
}
|
|
144
|
+
.breadcrumb {
|
|
145
|
+
flex: 1;
|
|
146
|
+
color: var(--text-dim);
|
|
147
|
+
font-size: 0.87rem;
|
|
148
|
+
white-space: nowrap;
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
text-overflow: ellipsis;
|
|
151
|
+
}
|
|
152
|
+
.breadcrumb .sep { margin: 0 0.45em; opacity: 0.6; }
|
|
153
|
+
.breadcrumb .current { color: var(--text); font-weight: 500; }
|
|
154
|
+
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
|
|
155
|
+
.btn-ghost {
|
|
156
|
+
background: transparent;
|
|
157
|
+
border: 1px solid var(--border);
|
|
158
|
+
color: var(--text);
|
|
159
|
+
padding: 0.4rem 0.7rem;
|
|
160
|
+
border-radius: var(--radius-sm);
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
font-size: 0.85rem;
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
gap: 0.35rem;
|
|
166
|
+
font-family: inherit;
|
|
167
|
+
}
|
|
168
|
+
.btn-ghost:hover { background: var(--bg-sunken); }
|
|
169
|
+
|
|
170
|
+
/* ---------- Layout ---------- */
|
|
171
|
+
main {
|
|
172
|
+
max-width: 1200px;
|
|
173
|
+
margin: 0 auto;
|
|
174
|
+
padding: 2rem 1.5rem 4rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ---------- Badges ---------- */
|
|
178
|
+
.badge {
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 0.35em;
|
|
182
|
+
padding: 0.25em 0.75em;
|
|
183
|
+
border-radius: 999px;
|
|
184
|
+
font-size: 0.78rem;
|
|
185
|
+
font-weight: 500;
|
|
186
|
+
background: var(--bg-sunken);
|
|
187
|
+
color: var(--text);
|
|
188
|
+
border: 1px solid var(--border);
|
|
189
|
+
white-space: nowrap;
|
|
190
|
+
}
|
|
191
|
+
.badge-primary { background: var(--primary); color: var(--primary-fg); border-color: transparent; }
|
|
192
|
+
.badge-accent { background: var(--accent); color: var(--accent-fg); border-color: transparent; }
|
|
193
|
+
.badge-ok { background: var(--ok-bg); color: var(--ok); border-color: transparent; }
|
|
194
|
+
.badge-warn { background: var(--warn-bg); color: var(--warn); border-color: transparent; }
|
|
195
|
+
.badge-err { background: var(--err-bg); color: var(--err); border-color: transparent; }
|
|
196
|
+
.badge-info { background: var(--info-bg); color: var(--info); border-color: transparent; }
|
|
197
|
+
.badge-purple { background: var(--purple-bg); color: var(--purple); border-color: transparent; }
|
|
198
|
+
.badge-dim { color: var(--text-dim); }
|
|
199
|
+
|
|
200
|
+
/* ---------- Card grids ---------- */
|
|
201
|
+
.grid {
|
|
202
|
+
display: grid;
|
|
203
|
+
gap: 1rem;
|
|
204
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
205
|
+
}
|
|
206
|
+
.grid.g2 { grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); }
|
|
207
|
+
.grid.g3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
208
|
+
.grid.g4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
209
|
+
.grid.g-lane { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
|
|
210
|
+
/* Main-page pipelines grid: auto-fit reflow (does NOT mutate shared .g3 fixed-3-col) */
|
|
211
|
+
.pipelines-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
212
|
+
|
|
213
|
+
.card {
|
|
214
|
+
background: var(--bg-elev);
|
|
215
|
+
border: 1px solid var(--border);
|
|
216
|
+
border-radius: var(--radius);
|
|
217
|
+
padding: 1.2rem 1.3rem;
|
|
218
|
+
box-shadow: var(--shadow-sm);
|
|
219
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
220
|
+
}
|
|
221
|
+
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
|
|
222
|
+
.card .kicker { font-size: 0.74rem; font-weight: 700; letter-spacing: 0.08em; color: var(--text-dim); text-transform: uppercase; margin-bottom: 0.3em; }
|
|
223
|
+
.card h3 { margin-bottom: 0.4em; font-size: 1.05rem; }
|
|
224
|
+
.card .stat { font-size: 2rem; font-weight: 700; color: var(--accent); line-height: 1; margin-bottom: 0.2em; }
|
|
225
|
+
.card .stat-sub { color: var(--text-muted); font-size: 0.9rem; }
|
|
226
|
+
.card .meta { color: var(--text-dim); font-size: 0.85rem; }
|
|
227
|
+
.card.card-primary { background: var(--primary); color: var(--primary-fg); border-color: transparent; }
|
|
228
|
+
.card.card-primary .kicker,
|
|
229
|
+
.card.card-primary .stat-sub,
|
|
230
|
+
.card.card-primary .meta { color: color-mix(in srgb, var(--primary-fg) 70%, transparent); }
|
|
231
|
+
.card.card-primary .stat { color: var(--accent); }
|
|
232
|
+
|
|
233
|
+
/* ---------- Callouts ---------- */
|
|
234
|
+
.callout {
|
|
235
|
+
padding: 0.6rem 1.1rem;
|
|
236
|
+
border-radius: var(--radius);
|
|
237
|
+
border-left: 4px solid var(--info);
|
|
238
|
+
background: var(--info-bg);
|
|
239
|
+
color: var(--text);
|
|
240
|
+
margin: 0.75rem 0;
|
|
241
|
+
font-size: 0.92rem;
|
|
242
|
+
}
|
|
243
|
+
.callout.warn { border-left-color: var(--warn); background: var(--warn-bg); color: var(--text); }
|
|
244
|
+
.callout.err { border-left-color: var(--err); background: var(--err-bg); color: var(--text); }
|
|
245
|
+
.callout.ok { border-left-color: var(--ok); background: var(--ok-bg); color: var(--text); }
|
|
246
|
+
.callout h4 { margin: 0 0 0.2em 0; font-size: 0.78rem; color: inherit; opacity: 0.85; }
|
|
247
|
+
.callout p:last-child { margin-bottom: 0; }
|
|
248
|
+
|
|
249
|
+
/* ---------- Footer ---------- */
|
|
250
|
+
footer {
|
|
251
|
+
max-width: 1200px;
|
|
252
|
+
margin: 0 auto;
|
|
253
|
+
padding: 2rem 1.5rem;
|
|
254
|
+
border-top: 1px solid var(--border);
|
|
255
|
+
color: var(--text-dim);
|
|
256
|
+
font-size: 0.85rem;
|
|
257
|
+
text-align: center;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* ---------- Responsive ---------- */
|
|
261
|
+
|
|
262
|
+
/* Tablet: 2-col chip grid between 768px and 1024px */
|
|
263
|
+
@media (min-width: 769px) and (max-width: 1024px) {
|
|
264
|
+
.grid.g3, .grid.g4, .pipelines-grid { grid-template-columns: repeat(2, 1fr); }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@media (max-width: 768px) {
|
|
268
|
+
.top-bar { padding: 0.5rem 1rem; gap: 0.4rem 0.75rem; }
|
|
269
|
+
.top-bar .brand { font-size: 0.85rem; }
|
|
270
|
+
.breadcrumb { font-size: 0.78rem; min-width: 0; }
|
|
271
|
+
main { padding: 1rem 1rem 3rem; }
|
|
272
|
+
.grid, .grid.g2, .grid.g3, .grid.g4, .grid.g-lane, .pipelines-grid { grid-template-columns: 1fr; }
|
|
273
|
+
.stage-rail { overflow-x: auto; flex-wrap: nowrap; }
|
|
274
|
+
.content-col { max-width: 100%; }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* Extra-narrow: ensure top-bar wraps at 390px */
|
|
278
|
+
@media (max-width: 420px) {
|
|
279
|
+
.breadcrumb { display: none; }
|
|
280
|
+
.controls { width: 100%; justify-content: flex-end; }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* ---------- Print ---------- */
|
|
284
|
+
@media print {
|
|
285
|
+
.top-bar, .controls { display: none !important; }
|
|
286
|
+
body { background: white; color: black; }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ---------- Accessibility ---------- */
|
|
290
|
+
.skip-link {
|
|
291
|
+
position: absolute;
|
|
292
|
+
top: -40px;
|
|
293
|
+
left: 8px;
|
|
294
|
+
z-index: 1000;
|
|
295
|
+
padding: 0.5rem 1rem;
|
|
296
|
+
background: var(--accent);
|
|
297
|
+
color: var(--accent-fg);
|
|
298
|
+
border-radius: var(--radius-sm);
|
|
299
|
+
font-weight: 600;
|
|
300
|
+
text-decoration: none;
|
|
301
|
+
}
|
|
302
|
+
.skip-link:focus { top: 8px; outline: 2px solid var(--text); outline-offset: 2px; }
|
|
303
|
+
:focus { outline: none; }
|
|
304
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
|
|
305
|
+
.btn-ghost:focus-visible { outline-offset: 3px; }
|
|
306
|
+
@media (prefers-reduced-motion: reduce) {
|
|
307
|
+
*, *::before, *::after {
|
|
308
|
+
animation-duration: 0.01ms !important;
|
|
309
|
+
animation-iteration-count: 1 !important;
|
|
310
|
+
transition-duration: 0.01ms !important;
|
|
311
|
+
scroll-behavior: auto !important;
|
|
312
|
+
}
|
|
313
|
+
html { scroll-behavior: auto; }
|
|
314
|
+
.card:hover { transform: none; }
|
|
315
|
+
}
|
|
316
|
+
.sr-only {
|
|
317
|
+
position: absolute !important;
|
|
318
|
+
width: 1px; height: 1px;
|
|
319
|
+
padding: 0; margin: -1px;
|
|
320
|
+
overflow: hidden;
|
|
321
|
+
clip: rect(0,0,0,0);
|
|
322
|
+
white-space: nowrap;
|
|
323
|
+
border: 0;
|
|
324
|
+
}
|
|
325
|
+
@media (forced-colors: active) {
|
|
326
|
+
.card, .callout { border: 1px solid CanvasText; }
|
|
327
|
+
.badge { border: 1px solid CanvasText; forced-color-adjust: none; }
|
|
328
|
+
}
|
|
329
|
+
.noscript-fallback {
|
|
330
|
+
max-width: 720px;
|
|
331
|
+
margin: 4rem auto;
|
|
332
|
+
padding: 2rem;
|
|
333
|
+
background: var(--bg-elev);
|
|
334
|
+
border: 2px solid var(--warn);
|
|
335
|
+
border-radius: var(--radius);
|
|
336
|
+
color: var(--text);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ---------- Dashboard-specific ---------- */
|
|
340
|
+
|
|
341
|
+
/* Content-column wrapper: header card + stage rail centered at comfortable width */
|
|
342
|
+
.content-col {
|
|
343
|
+
max-width: 860px;
|
|
344
|
+
margin-left: auto;
|
|
345
|
+
margin-right: auto;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* Data-note chip in top bar */
|
|
349
|
+
#data-note-chip {
|
|
350
|
+
cursor: default;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* Work overview header (prototype: delivery-002) */
|
|
354
|
+
.work-overview {
|
|
355
|
+
background: var(--bg-elev);
|
|
356
|
+
border: 1px solid var(--border);
|
|
357
|
+
border-radius: var(--radius);
|
|
358
|
+
box-shadow: var(--shadow-sm);
|
|
359
|
+
padding: 1.2rem 1.4rem 1rem;
|
|
360
|
+
margin-bottom: 1rem;
|
|
361
|
+
}
|
|
362
|
+
.work-overview.border-warn { border-left: 4px solid var(--warn); }
|
|
363
|
+
.work-overview.border-err { border-left: 4px solid var(--err); }
|
|
364
|
+
|
|
365
|
+
/* Collapsible panel header row */
|
|
366
|
+
.work-overview-header {
|
|
367
|
+
display: flex;
|
|
368
|
+
align-items: center;
|
|
369
|
+
gap: 0.5rem;
|
|
370
|
+
cursor: pointer;
|
|
371
|
+
user-select: none;
|
|
372
|
+
padding: 0;
|
|
373
|
+
background: none;
|
|
374
|
+
border: none;
|
|
375
|
+
width: 100%;
|
|
376
|
+
text-align: left;
|
|
377
|
+
font-family: inherit;
|
|
378
|
+
margin-bottom: 0.25rem;
|
|
379
|
+
}
|
|
380
|
+
.work-overview-header:hover { opacity: 0.85; }
|
|
381
|
+
.work-overview-chevron {
|
|
382
|
+
font-size: 0.72rem;
|
|
383
|
+
display: inline-block;
|
|
384
|
+
transition: transform 0.18s ease;
|
|
385
|
+
color: var(--text-dim);
|
|
386
|
+
flex-shrink: 0;
|
|
387
|
+
}
|
|
388
|
+
.work-overview-header.open .work-overview-chevron { transform: rotate(90deg); }
|
|
389
|
+
.work-overview-body { display: none; }
|
|
390
|
+
.work-overview-body.open { display: block; }
|
|
391
|
+
|
|
392
|
+
.work-overview-identity {
|
|
393
|
+
display: flex;
|
|
394
|
+
align-items: baseline;
|
|
395
|
+
gap: 0.5rem;
|
|
396
|
+
flex-wrap: wrap;
|
|
397
|
+
margin-bottom: 0;
|
|
398
|
+
}
|
|
399
|
+
.work-overview-number {
|
|
400
|
+
font-size: 1.3rem;
|
|
401
|
+
font-weight: 700;
|
|
402
|
+
color: var(--text-dim);
|
|
403
|
+
font-family: "SF Mono", Monaco, Consolas, "Roboto Mono", monospace;
|
|
404
|
+
letter-spacing: 0.02em;
|
|
405
|
+
flex-shrink: 0;
|
|
406
|
+
}
|
|
407
|
+
.work-overview-title {
|
|
408
|
+
font-size: 1.25rem;
|
|
409
|
+
font-weight: 700;
|
|
410
|
+
color: var(--text);
|
|
411
|
+
line-height: 1.2;
|
|
412
|
+
}
|
|
413
|
+
.work-overview-desc {
|
|
414
|
+
font-size: 0.9rem;
|
|
415
|
+
color: var(--text-muted);
|
|
416
|
+
margin: 0 0 0.75rem 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* Objective collapsible */
|
|
420
|
+
.work-overview-obj-wrap {
|
|
421
|
+
margin-bottom: 0.75rem;
|
|
422
|
+
}
|
|
423
|
+
.work-overview-obj-body {
|
|
424
|
+
font-size: 0.88rem;
|
|
425
|
+
color: var(--text-muted);
|
|
426
|
+
line-height: 1.65;
|
|
427
|
+
white-space: pre-wrap;
|
|
428
|
+
word-break: break-word;
|
|
429
|
+
overflow: hidden;
|
|
430
|
+
max-height: 6.5em;
|
|
431
|
+
transition: max-height 0.25s ease;
|
|
432
|
+
position: relative;
|
|
433
|
+
}
|
|
434
|
+
.work-overview-obj-body.expanded {
|
|
435
|
+
max-height: none;
|
|
436
|
+
}
|
|
437
|
+
.work-overview-obj-fade {
|
|
438
|
+
position: absolute;
|
|
439
|
+
bottom: 0;
|
|
440
|
+
left: 0;
|
|
441
|
+
right: 0;
|
|
442
|
+
height: 2em;
|
|
443
|
+
background: linear-gradient(transparent, var(--bg-elev));
|
|
444
|
+
pointer-events: none;
|
|
445
|
+
}
|
|
446
|
+
.work-overview-obj-body.expanded .work-overview-obj-fade {
|
|
447
|
+
display: none;
|
|
448
|
+
}
|
|
449
|
+
.btn-text-toggle {
|
|
450
|
+
background: none;
|
|
451
|
+
border: none;
|
|
452
|
+
color: var(--accent);
|
|
453
|
+
cursor: pointer;
|
|
454
|
+
font-size: 0.82rem;
|
|
455
|
+
padding: 0.15rem 0;
|
|
456
|
+
font-family: inherit;
|
|
457
|
+
display: inline-block;
|
|
458
|
+
margin-top: 0.2rem;
|
|
459
|
+
}
|
|
460
|
+
.btn-text-toggle:hover { text-decoration: underline; }
|
|
461
|
+
|
|
462
|
+
/* Stat strip */
|
|
463
|
+
.work-stat-strip {
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-wrap: wrap;
|
|
466
|
+
gap: 0.4rem;
|
|
467
|
+
margin-bottom: 0.75rem;
|
|
468
|
+
align-items: center;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* Disclosure sections (features / deliverables) */
|
|
472
|
+
.work-disclosure {
|
|
473
|
+
border-top: 1px solid var(--border);
|
|
474
|
+
padding-top: 0.6rem;
|
|
475
|
+
margin-top: 0.5rem;
|
|
476
|
+
}
|
|
477
|
+
.work-disclosure + .work-disclosure {
|
|
478
|
+
margin-top: 0.35rem;
|
|
479
|
+
}
|
|
480
|
+
.work-disclosure-toggle {
|
|
481
|
+
background: none;
|
|
482
|
+
border: none;
|
|
483
|
+
color: var(--text-muted);
|
|
484
|
+
cursor: pointer;
|
|
485
|
+
font-size: 0.82rem;
|
|
486
|
+
font-weight: 600;
|
|
487
|
+
padding: 0.25rem 0;
|
|
488
|
+
font-family: inherit;
|
|
489
|
+
display: flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
gap: 0.4rem;
|
|
492
|
+
letter-spacing: 0.03em;
|
|
493
|
+
text-transform: uppercase;
|
|
494
|
+
}
|
|
495
|
+
.work-disclosure-toggle:hover { color: var(--text); }
|
|
496
|
+
.work-disclosure-toggle .disc-arrow {
|
|
497
|
+
font-size: 0.7rem;
|
|
498
|
+
display: inline-block;
|
|
499
|
+
transition: transform 0.18s ease;
|
|
500
|
+
}
|
|
501
|
+
.work-disclosure-toggle.open .disc-arrow { transform: rotate(90deg); }
|
|
502
|
+
.work-disclosure-body {
|
|
503
|
+
display: none;
|
|
504
|
+
padding: 0.5rem 0 0.25rem 0.5rem;
|
|
505
|
+
list-style: none;
|
|
506
|
+
margin: 0;
|
|
507
|
+
}
|
|
508
|
+
.work-disclosure-body.open { display: block; }
|
|
509
|
+
.work-disclosure-body li {
|
|
510
|
+
font-size: 0.84rem;
|
|
511
|
+
color: var(--text-muted);
|
|
512
|
+
padding: 0.2rem 0;
|
|
513
|
+
border-bottom: 1px solid var(--border);
|
|
514
|
+
}
|
|
515
|
+
.work-disclosure-body li:last-child { border-bottom: none; }
|
|
516
|
+
.work-disclosure-body .disc-num {
|
|
517
|
+
font-family: "SF Mono", Monaco, Consolas, "Roboto Mono", monospace;
|
|
518
|
+
font-size: 0.78rem;
|
|
519
|
+
color: var(--text-dim);
|
|
520
|
+
margin-right: 0.35rem;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* Stage rail (UI-2) */
|
|
524
|
+
.stage-rail {
|
|
525
|
+
display: flex;
|
|
526
|
+
gap: 0.4rem;
|
|
527
|
+
flex-wrap: wrap;
|
|
528
|
+
margin: 1rem 0;
|
|
529
|
+
align-items: center;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* Work header card attention borders (UI-4) */
|
|
533
|
+
.card.border-warn { border-left: 4px solid var(--warn); }
|
|
534
|
+
.card.border-err { border-left: 4px solid var(--err); }
|
|
535
|
+
|
|
536
|
+
/* Card-link: neutralize global anchor styling for whole-card clickable cards */
|
|
537
|
+
.card-link { display: block; color: inherit; text-decoration: none; }
|
|
538
|
+
.card-link:hover { text-decoration: none; }
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
/* Main-page section headings */
|
|
542
|
+
.main-section-head { margin: 1.5rem 0 0.75rem; }
|
|
543
|
+
/* Knowledge & Tool is the fixed-size header band: a cool (info) accent sets it apart
|
|
544
|
+
from the teal pipelines below. */
|
|
545
|
+
.kt-head { color: var(--info); }
|
|
546
|
+
.kt-section .card { border-left: 3px solid var(--info); }
|
|
547
|
+
/* Work-card progress bar: Readiness (pre-execution) vs Execution (task completion) */
|
|
548
|
+
.progress-row { margin: 0.55rem 0 0; }
|
|
549
|
+
.progress-track { height: 6px; background: var(--bg-sunken); border-radius: 4px; overflow: hidden; }
|
|
550
|
+
.progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; }
|
|
551
|
+
.progress-execution .progress-fill { background: var(--accent); }
|
|
552
|
+
.progress-readiness .progress-fill { background: var(--info); }
|
|
553
|
+
.progress-label { display: block; margin-bottom: 0.3rem; font-size: 0.8rem; color: var(--text-dim); }
|
|
554
|
+
|
|
555
|
+
/* Delivery panel (collapsible) */
|
|
556
|
+
.delivery-panel {
|
|
557
|
+
margin-bottom: 1.25rem;
|
|
558
|
+
border: 1px solid var(--border);
|
|
559
|
+
border-radius: var(--radius);
|
|
560
|
+
background: var(--bg-elev);
|
|
561
|
+
box-shadow: var(--shadow-sm);
|
|
562
|
+
overflow: hidden;
|
|
563
|
+
}
|
|
564
|
+
.delivery-panel-summary {
|
|
565
|
+
display: flex;
|
|
566
|
+
align-items: center;
|
|
567
|
+
gap: 0.6rem;
|
|
568
|
+
padding: 0.7rem 1rem;
|
|
569
|
+
cursor: pointer;
|
|
570
|
+
user-select: none;
|
|
571
|
+
background: none;
|
|
572
|
+
border: none;
|
|
573
|
+
width: 100%;
|
|
574
|
+
text-align: left;
|
|
575
|
+
font-family: inherit;
|
|
576
|
+
}
|
|
577
|
+
.delivery-panel-summary:hover { background: var(--bg-sunken); }
|
|
578
|
+
.delivery-panel-chevron {
|
|
579
|
+
font-size: 0.7rem;
|
|
580
|
+
display: inline-block;
|
|
581
|
+
transition: transform 0.18s ease;
|
|
582
|
+
color: var(--text-dim);
|
|
583
|
+
flex-shrink: 0;
|
|
584
|
+
}
|
|
585
|
+
.delivery-panel-summary.open .delivery-panel-chevron { transform: rotate(90deg); }
|
|
586
|
+
.delivery-panel-label {
|
|
587
|
+
font-size: 0.9rem;
|
|
588
|
+
font-weight: 700;
|
|
589
|
+
color: var(--text);
|
|
590
|
+
letter-spacing: 0.01em;
|
|
591
|
+
}
|
|
592
|
+
.delivery-panel-body {
|
|
593
|
+
display: none;
|
|
594
|
+
padding: 0.5rem 1rem 0.75rem;
|
|
595
|
+
border-top: 1px solid var(--border);
|
|
596
|
+
}
|
|
597
|
+
.delivery-panel-body.open { display: block; }
|
|
598
|
+
|
|
599
|
+
/* Lane panel (collapsible, inside delivery) */
|
|
600
|
+
.lane-panel {
|
|
601
|
+
margin-bottom: 0.75rem;
|
|
602
|
+
}
|
|
603
|
+
.lane-panel:last-child { margin-bottom: 0; }
|
|
604
|
+
.lane-panel-summary {
|
|
605
|
+
display: flex;
|
|
606
|
+
align-items: center;
|
|
607
|
+
gap: 0.5rem;
|
|
608
|
+
padding: 0.35rem 0.5rem;
|
|
609
|
+
cursor: pointer;
|
|
610
|
+
user-select: none;
|
|
611
|
+
background: none;
|
|
612
|
+
border: none;
|
|
613
|
+
width: 100%;
|
|
614
|
+
text-align: left;
|
|
615
|
+
font-family: inherit;
|
|
616
|
+
border-radius: var(--radius-sm);
|
|
617
|
+
}
|
|
618
|
+
.lane-panel-summary:hover { background: var(--bg-sunken); }
|
|
619
|
+
.lane-panel-chevron {
|
|
620
|
+
font-size: 0.68rem;
|
|
621
|
+
display: inline-block;
|
|
622
|
+
transition: transform 0.18s ease;
|
|
623
|
+
color: var(--text-dim);
|
|
624
|
+
flex-shrink: 0;
|
|
625
|
+
}
|
|
626
|
+
.lane-panel-summary.open .lane-panel-chevron { transform: rotate(90deg); }
|
|
627
|
+
.lane-panel-label {
|
|
628
|
+
font-size: 0.82rem;
|
|
629
|
+
font-weight: 600;
|
|
630
|
+
color: var(--text-muted);
|
|
631
|
+
letter-spacing: 0.01em;
|
|
632
|
+
}
|
|
633
|
+
.lane-panel-body {
|
|
634
|
+
display: none;
|
|
635
|
+
padding: 0.5rem 0 0.25rem 0;
|
|
636
|
+
}
|
|
637
|
+
.lane-panel-body.open { display: block; }
|
|
638
|
+
|
|
639
|
+
/* Task card (two-line chip inside a lane) */
|
|
640
|
+
.task-chip .chip-short-name {
|
|
641
|
+
font-size: 0.80rem;
|
|
642
|
+
color: var(--text-muted);
|
|
643
|
+
line-height: 1.35;
|
|
644
|
+
word-break: break-word;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/* Wave section (kept for CSS class integrity test) */
|
|
648
|
+
.wave-section {
|
|
649
|
+
margin-bottom: 1.5rem;
|
|
650
|
+
}
|
|
651
|
+
/* Done wave: muted pill (kept for CSS test) */
|
|
652
|
+
.wave-summary {
|
|
653
|
+
display: inline-flex;
|
|
654
|
+
align-items: center;
|
|
655
|
+
gap: 0.5em;
|
|
656
|
+
font-size: 0.78rem;
|
|
657
|
+
font-weight: 600;
|
|
658
|
+
letter-spacing: 0.05em;
|
|
659
|
+
text-transform: uppercase;
|
|
660
|
+
color: var(--text-dim);
|
|
661
|
+
background: var(--bg-sunken);
|
|
662
|
+
border: 1px solid var(--border);
|
|
663
|
+
border-radius: var(--radius);
|
|
664
|
+
padding: 0.35rem 0.85rem;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* Compact task chip — auto-height, content-sized */
|
|
668
|
+
.task-chip {
|
|
669
|
+
background: var(--bg-elev);
|
|
670
|
+
border: 1px solid var(--border);
|
|
671
|
+
border-radius: var(--radius);
|
|
672
|
+
padding: 0.65rem 0.85rem;
|
|
673
|
+
box-shadow: var(--shadow-sm);
|
|
674
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
675
|
+
display: flex;
|
|
676
|
+
flex-direction: column;
|
|
677
|
+
gap: 0.3rem;
|
|
678
|
+
align-items: flex-start;
|
|
679
|
+
}
|
|
680
|
+
.task-chip:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
|
|
681
|
+
.task-chip.chip-active { border-left: 3px solid var(--accent); }
|
|
682
|
+
.task-chip.chip-pending { opacity: 0.7; }
|
|
683
|
+
.task-chip.chip-done { opacity: 0.55; }
|
|
684
|
+
.task-chip .chip-top {
|
|
685
|
+
display: flex;
|
|
686
|
+
align-items: baseline;
|
|
687
|
+
gap: 0.5em;
|
|
688
|
+
}
|
|
689
|
+
.task-chip .chip-task-id {
|
|
690
|
+
font-size: 0.82rem;
|
|
691
|
+
font-weight: 700;
|
|
692
|
+
color: var(--text);
|
|
693
|
+
font-family: "SF Mono", Monaco, Consolas, "Roboto Mono", monospace;
|
|
694
|
+
}
|
|
695
|
+
.task-chip .chip-type {
|
|
696
|
+
font-size: 0.72rem;
|
|
697
|
+
font-weight: 700;
|
|
698
|
+
letter-spacing: 0.07em;
|
|
699
|
+
text-transform: uppercase;
|
|
700
|
+
color: var(--text-dim);
|
|
701
|
+
}
|
|
702
|
+
.task-chip .chip-meta {
|
|
703
|
+
font-size: 0.78rem;
|
|
704
|
+
color: var(--text-dim);
|
|
705
|
+
display: flex;
|
|
706
|
+
flex-wrap: wrap;
|
|
707
|
+
gap: 0.4em;
|
|
708
|
+
align-items: center;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* Interval input in controls */
|
|
712
|
+
.interval-control {
|
|
713
|
+
display: flex;
|
|
714
|
+
align-items: center;
|
|
715
|
+
gap: 0.3rem;
|
|
716
|
+
font-size: 0.85rem;
|
|
717
|
+
color: var(--text-muted);
|
|
718
|
+
}
|
|
719
|
+
.interval-input {
|
|
720
|
+
width: 3.5em;
|
|
721
|
+
padding: 0.35rem 0.4rem;
|
|
722
|
+
border: 1px solid var(--border);
|
|
723
|
+
border-radius: var(--radius-sm);
|
|
724
|
+
background: transparent;
|
|
725
|
+
color: var(--text);
|
|
726
|
+
font-size: 0.85rem;
|
|
727
|
+
font-family: inherit;
|
|
728
|
+
text-align: center;
|
|
729
|
+
}
|
|
730
|
+
.interval-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
731
|
+
|
|
732
|
+
/* Running pulse animation — disabled with prefers-reduced-motion */
|
|
733
|
+
@keyframes pulse-dot {
|
|
734
|
+
0%, 100% { opacity: 1; }
|
|
735
|
+
50% { opacity: 0.4; }
|
|
736
|
+
}
|
|
737
|
+
.pulse-dot {
|
|
738
|
+
display: inline-block;
|
|
739
|
+
animation: pulse-dot 1.8s ease-in-out infinite;
|
|
740
|
+
}
|
|
741
|
+
@media (prefers-reduced-motion: reduce) {
|
|
742
|
+
.pulse-dot { animation: none; }
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/* Empty / loading state */
|
|
746
|
+
.empty-state {
|
|
747
|
+
text-align: center;
|
|
748
|
+
color: var(--text-dim);
|
|
749
|
+
padding: 3rem 1rem;
|
|
750
|
+
font-size: 0.95rem;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* Pending inputs list */
|
|
754
|
+
.pending-input-item {
|
|
755
|
+
margin-top: 0.5rem;
|
|
756
|
+
padding: 0.5rem 0.75rem;
|
|
757
|
+
background: var(--bg-sunken);
|
|
758
|
+
border-radius: var(--radius-sm);
|
|
759
|
+
font-size: 0.85rem;
|
|
760
|
+
}
|
|
761
|
+
.pending-input-item .meta { color: var(--text-dim); font-size: 0.8rem; margin-top: 0.2rem; }
|
|
762
|
+
</style>
|
|
763
|
+
</head>
|
|
764
|
+
<body>
|
|
765
|
+
|
|
766
|
+
<!-- Skip-to-content for keyboard / screen reader users -->
|
|
767
|
+
<a class="skip-link" href="#top">Skip to content</a>
|
|
768
|
+
|
|
769
|
+
<!-- Sticky top bar (UI-1) -->
|
|
770
|
+
<header class="top-bar" role="banner">
|
|
771
|
+
<span class="app-title">AID Dashboard <span class="beta-pill">beta</span></span>
|
|
772
|
+
<div class="brand">
|
|
773
|
+
<span id="breadcrumb-trail" class="breadcrumb"></span>
|
|
774
|
+
</div>
|
|
775
|
+
<div style="flex:1"></div>
|
|
776
|
+
<div class="controls">
|
|
777
|
+
<!-- Data-note chip (inline warn chip; hidden when no warnings/fallbacks) -->
|
|
778
|
+
<span class="badge badge-warn" id="data-note-chip" style="display:none" title=""></span>
|
|
779
|
+
<!-- Freshness badge (Telemetry) -->
|
|
780
|
+
<span class="badge badge-dim" id="freshness-badge" aria-live="polite">● Loading</span>
|
|
781
|
+
<!-- Interval control (UI-5) -->
|
|
782
|
+
<label class="interval-control" title="Poll interval in seconds">
|
|
783
|
+
Refresh
|
|
784
|
+
<input type="number" class="interval-input" id="interval-input"
|
|
785
|
+
min="1" max="600" value="5" aria-label="Refresh interval in seconds">
|
|
786
|
+
s
|
|
787
|
+
</label>
|
|
788
|
+
<!-- Theme toggle (from html-skeleton.html) -->
|
|
789
|
+
<button type="button" class="btn-ghost" id="theme-toggle"
|
|
790
|
+
aria-label="Switch theme" title="Toggle light/dark theme">
|
|
791
|
+
<span id="theme-icon" aria-hidden="true">◐</span>
|
|
792
|
+
<span id="theme-label">Dark</span>
|
|
793
|
+
</button>
|
|
794
|
+
</div>
|
|
795
|
+
</header>
|
|
796
|
+
|
|
797
|
+
<!-- Main page content -->
|
|
798
|
+
<main id="top">
|
|
799
|
+
|
|
800
|
+
<!-- Schema-mismatch banner (hidden by default; shown on schema_version mismatch) -->
|
|
801
|
+
<div class="callout warn" id="schema-mismatch-banner" style="display:none" role="alert">
|
|
802
|
+
<h4>Assets out of date</h4>
|
|
803
|
+
<p>Dashboard assets are out of date — restart <code>aid dashboard</code> to get the latest version.</p>
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
<!-- Attention strip for Paused or Blocked (UI-4) — shown above stage rail -->
|
|
807
|
+
<div id="attention-strip" style="display:none" role="alert"></div>
|
|
808
|
+
|
|
809
|
+
<!-- Work header (UI-1) + overview panel -->
|
|
810
|
+
<div id="work-header" style="display:none">
|
|
811
|
+
<div class="content-col">
|
|
812
|
+
|
|
813
|
+
<!-- Work overview header (prototype: delivery-002) -->
|
|
814
|
+
<div class="work-overview" id="work-overview-panel">
|
|
815
|
+
|
|
816
|
+
<!-- Collapsible header row: always visible summary -->
|
|
817
|
+
<button type="button" class="work-overview-header" id="work-overview-header-btn"
|
|
818
|
+
aria-expanded="false" aria-controls="work-overview-body">
|
|
819
|
+
<!-- Chevron -->
|
|
820
|
+
<span class="work-overview-chevron" aria-hidden="true">►</span>
|
|
821
|
+
<!-- Identity line: #N · Title -->
|
|
822
|
+
<div class="work-overview-identity">
|
|
823
|
+
<span class="work-overview-number" id="overview-number"></span>
|
|
824
|
+
<span class="work-overview-title" id="overview-title"></span>
|
|
825
|
+
</div>
|
|
826
|
+
<!-- Lifecycle + skill badges (inline in summary) -->
|
|
827
|
+
<span id="overview-badges" style="display:inline-flex;gap:0.4rem;flex-wrap:wrap;align-items:center;margin-left:0.25rem"></span>
|
|
828
|
+
</button>
|
|
829
|
+
|
|
830
|
+
<!-- Collapsible body: all detail below the summary line -->
|
|
831
|
+
<div class="work-overview-body" id="work-overview-body">
|
|
832
|
+
|
|
833
|
+
<!-- Description -->
|
|
834
|
+
<p class="work-overview-desc" id="overview-desc" style="display:none"></p>
|
|
835
|
+
|
|
836
|
+
<!-- Objective collapsible -->
|
|
837
|
+
<div class="work-overview-obj-wrap" id="overview-obj-wrap" style="display:none">
|
|
838
|
+
<div class="work-overview-obj-body" id="overview-obj-body">
|
|
839
|
+
<div class="work-overview-obj-fade" id="overview-obj-fade"></div>
|
|
840
|
+
</div>
|
|
841
|
+
<button type="button" class="btn-text-toggle" id="overview-obj-toggle">Show more</button>
|
|
842
|
+
</div>
|
|
843
|
+
|
|
844
|
+
<!-- Updated / kicker line -->
|
|
845
|
+
<div class="meta" id="work-meta" style="font-size:0.8rem;color:var(--text-dim);margin-bottom:0.65rem"></div>
|
|
846
|
+
|
|
847
|
+
<!-- Stat strip -->
|
|
848
|
+
<div class="work-stat-strip" id="overview-stat-strip" style="display:none"></div>
|
|
849
|
+
|
|
850
|
+
<!-- Features disclosure (full path only) -->
|
|
851
|
+
<div class="work-disclosure" id="overview-features-disclosure" style="display:none">
|
|
852
|
+
<button type="button" class="work-disclosure-toggle" id="overview-features-toggle" aria-expanded="false">
|
|
853
|
+
<span class="disc-arrow">►</span>
|
|
854
|
+
<span id="overview-features-label">Features</span>
|
|
855
|
+
</button>
|
|
856
|
+
<ul class="work-disclosure-body" id="overview-features-body"></ul>
|
|
857
|
+
</div>
|
|
858
|
+
|
|
859
|
+
</div><!-- /#work-overview-body -->
|
|
860
|
+
|
|
861
|
+
</div><!-- /.work-overview -->
|
|
862
|
+
|
|
863
|
+
<!-- Stage rail (UI-2) -->
|
|
864
|
+
<div id="stage-rail-wrap">
|
|
865
|
+
<div class="stage-rail" id="stage-rail" role="list" aria-label="Pipeline stages"></div>
|
|
866
|
+
</div>
|
|
867
|
+
</div>
|
|
868
|
+
|
|
869
|
+
<!-- Tasks section (UI-3) — full width for grid -->
|
|
870
|
+
<div id="tasks-section" style="margin-top:1.5rem"></div>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
873
|
+
<!-- Main page (feature-006): card grid + KB card -->
|
|
874
|
+
<div id="main-page" style="display:none">
|
|
875
|
+
<!-- Knowledge Base section — fixed-size header band, rendered first -->
|
|
876
|
+
<h2 class="main-section-head kt-head">Knowledge Base</h2>
|
|
877
|
+
<div class="grid g2 kt-section" id="knowledge-tool-section"></div>
|
|
878
|
+
|
|
879
|
+
<!-- Pipelines section -->
|
|
880
|
+
<h2 class="main-section-head" style="margin-top:2rem">Pipelines</h2>
|
|
881
|
+
<div id="pipelines-section"></div>
|
|
882
|
+
</div>
|
|
883
|
+
|
|
884
|
+
<!-- KB view placeholder (SEAM-1 for feature-007) -->
|
|
885
|
+
<div id="kb-view" style="display:none"></div>
|
|
886
|
+
|
|
887
|
+
<!-- Stale work notice (FC-3) -->
|
|
888
|
+
<div id="stale-work-notice" style="display:none"></div>
|
|
889
|
+
|
|
890
|
+
<!-- Task drill view (SEAM-2, task-071) -->
|
|
891
|
+
<div id="task-view" style="display:none"></div>
|
|
892
|
+
|
|
893
|
+
<!-- Empty / no works state (legacy; hidden by feature-006 render path) -->
|
|
894
|
+
<div class="empty-state" id="empty-state" style="display:none">
|
|
895
|
+
<p>No works found in <code>.aid/</code>. Start a pipeline to see progress here.</p>
|
|
896
|
+
</div>
|
|
897
|
+
|
|
898
|
+
<!-- Initial loading state -->
|
|
899
|
+
<div class="empty-state" id="loading-state">
|
|
900
|
+
<p>Connecting to dashboard server…</p>
|
|
901
|
+
</div>
|
|
902
|
+
|
|
903
|
+
</main>
|
|
904
|
+
|
|
905
|
+
<footer id="page-footer">
|
|
906
|
+
<p>served locally by <code>aid dashboard</code> · read-only ·
|
|
907
|
+
refreshes every <span id="footer-interval">5</span>s ·
|
|
908
|
+
<span id="footer-generated-by"></span></p>
|
|
909
|
+
</footer>
|
|
910
|
+
|
|
911
|
+
<noscript>
|
|
912
|
+
<div class="noscript-fallback">
|
|
913
|
+
<h2>JavaScript required</h2>
|
|
914
|
+
<p>The AID dashboard uses JavaScript to poll <code>./api/model</code> and render pipeline state.
|
|
915
|
+
Please enable JavaScript to use this dashboard.</p>
|
|
916
|
+
</div>
|
|
917
|
+
</noscript>
|
|
918
|
+
|
|
919
|
+
<script>
|
|
920
|
+
(function () {
|
|
921
|
+
'use strict';
|
|
922
|
+
|
|
923
|
+
// ---------------------------------------------------------------------------
|
|
924
|
+
// Constants
|
|
925
|
+
// ---------------------------------------------------------------------------
|
|
926
|
+
var EXPECTED_SCHEMA_VERSION = 3;
|
|
927
|
+
var LS_KEY_POLL_MS = 'aid-dashboard-poll-ms';
|
|
928
|
+
var PHASE_ORDER = ['Interview', 'Specify', 'Plan', 'Detail', 'Execute', 'Deploy', 'Monitor'];
|
|
929
|
+
|
|
930
|
+
// ---------------------------------------------------------------------------
|
|
931
|
+
// State
|
|
932
|
+
// ---------------------------------------------------------------------------
|
|
933
|
+
var pollMs = 5000;
|
|
934
|
+
var pollTimer = null;
|
|
935
|
+
var fetchPending = false;
|
|
936
|
+
var lastGoodModel = null;
|
|
937
|
+
var lastSuccessTime = null;
|
|
938
|
+
var consecutiveErrors = 0;
|
|
939
|
+
|
|
940
|
+
// UI state: preserved across poll re-renders (Change 3)
|
|
941
|
+
var uiState = {
|
|
942
|
+
workOpen: false, // work panel expanded (default: collapsed)
|
|
943
|
+
objectiveOpen: false, // objective "Show more" expanded
|
|
944
|
+
featuresOpen: false, // features disclosure open
|
|
945
|
+
deliveries: {}, // per-delivery open state keyed by "d"+N (e.g. "d1")
|
|
946
|
+
lanes: {} // per-lane open state keyed by delivery-scoped key: "d<delivery>-lane<lane>" (e.g. "d1-lane2") or "d<delivery>-unseq" for unsequenced; never collides across deliveries
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// Open-drill set: composite keys "<work_id>/<task_id>" that are currently active.
|
|
950
|
+
// Derived from the live route; not persisted. Managed by enterDrill/leaveDrill.
|
|
951
|
+
var openDrillKeys = {}; // key -> true
|
|
952
|
+
|
|
953
|
+
// ---------------------------------------------------------------------------
|
|
954
|
+
// Boot: read interval from localStorage
|
|
955
|
+
// ---------------------------------------------------------------------------
|
|
956
|
+
(function boot() {
|
|
957
|
+
var stored = localStorage.getItem(LS_KEY_POLL_MS);
|
|
958
|
+
if (stored !== null) {
|
|
959
|
+
var n = parseInt(stored, 10);
|
|
960
|
+
if (!isNaN(n)) {
|
|
961
|
+
pollMs = clampInterval(n);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
var input = document.getElementById('interval-input');
|
|
965
|
+
if (input) {
|
|
966
|
+
input.value = Math.round(pollMs / 1000);
|
|
967
|
+
input.addEventListener('change', onIntervalChange);
|
|
968
|
+
}
|
|
969
|
+
updateFooterInterval();
|
|
970
|
+
initTheme();
|
|
971
|
+
// Wire hash-change handler (DD-1: back/forward re-renders without new fetch)
|
|
972
|
+
window.addEventListener('hashchange', onHashChange);
|
|
973
|
+
// Immediate first fetch
|
|
974
|
+
doFetch();
|
|
975
|
+
})();
|
|
976
|
+
|
|
977
|
+
// ---------------------------------------------------------------------------
|
|
978
|
+
// Interval helpers
|
|
979
|
+
// ---------------------------------------------------------------------------
|
|
980
|
+
function clampInterval(ms) {
|
|
981
|
+
// Clamp to [1000ms, 600000ms] = [1s, 600s]
|
|
982
|
+
if (ms < 1000) return 1000;
|
|
983
|
+
if (ms > 600000) return 600000;
|
|
984
|
+
return ms;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function onIntervalChange() {
|
|
988
|
+
var input = document.getElementById('interval-input');
|
|
989
|
+
if (!input) return;
|
|
990
|
+
var raw = parseFloat(input.value);
|
|
991
|
+
if (isNaN(raw)) raw = 5;
|
|
992
|
+
var clamped = clampInterval(Math.round(raw * 1000));
|
|
993
|
+
input.value = Math.round(clamped / 1000);
|
|
994
|
+
pollMs = clamped;
|
|
995
|
+
localStorage.setItem(LS_KEY_POLL_MS, String(pollMs));
|
|
996
|
+
updateFooterInterval();
|
|
997
|
+
// Reschedule: clear any pending timer; next tick fires after new interval
|
|
998
|
+
if (pollTimer !== null) {
|
|
999
|
+
clearTimeout(pollTimer);
|
|
1000
|
+
pollTimer = null;
|
|
1001
|
+
}
|
|
1002
|
+
scheduleNextPoll();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function updateFooterInterval() {
|
|
1006
|
+
var el = document.getElementById('footer-interval');
|
|
1007
|
+
if (el) el.textContent = Math.round(pollMs / 1000);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// ---------------------------------------------------------------------------
|
|
1011
|
+
// Poll loop
|
|
1012
|
+
// ---------------------------------------------------------------------------
|
|
1013
|
+
function scheduleNextPoll() {
|
|
1014
|
+
if (pollTimer !== null) return; // already scheduled
|
|
1015
|
+
// Backoff on consecutive errors: double interval up to 4x
|
|
1016
|
+
var delay = pollMs;
|
|
1017
|
+
if (consecutiveErrors > 0) {
|
|
1018
|
+
delay = Math.min(pollMs * Math.pow(2, Math.min(consecutiveErrors - 1, 2)), pollMs * 4);
|
|
1019
|
+
}
|
|
1020
|
+
pollTimer = setTimeout(function () {
|
|
1021
|
+
pollTimer = null;
|
|
1022
|
+
doFetch();
|
|
1023
|
+
}, delay);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function doFetch() {
|
|
1027
|
+
if (fetchPending) {
|
|
1028
|
+
// Single in-flight guard: skip this tick, reschedule
|
|
1029
|
+
scheduleNextPoll();
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
fetchPending = true;
|
|
1033
|
+
// Build ?detail= query from open-drill set (RC-1, NFR4)
|
|
1034
|
+
var detailKeys = Object.keys(openDrillKeys).filter(function(k) { return openDrillKeys[k]; });
|
|
1035
|
+
var fetchUrl = './api/model';
|
|
1036
|
+
if (detailKeys.length > 0) {
|
|
1037
|
+
fetchUrl += '?detail=' + detailKeys.map(function(k) { return encodeURIComponent(k); }).join(',');
|
|
1038
|
+
}
|
|
1039
|
+
fetch(fetchUrl)
|
|
1040
|
+
.then(function (resp) {
|
|
1041
|
+
if (!resp.ok) {
|
|
1042
|
+
throw new Error('HTTP ' + resp.status);
|
|
1043
|
+
}
|
|
1044
|
+
return resp.json();
|
|
1045
|
+
})
|
|
1046
|
+
.then(function (envelope) {
|
|
1047
|
+
fetchPending = false;
|
|
1048
|
+
consecutiveErrors = 0;
|
|
1049
|
+
onSuccess(envelope);
|
|
1050
|
+
scheduleNextPoll();
|
|
1051
|
+
})
|
|
1052
|
+
.catch(function (err) {
|
|
1053
|
+
fetchPending = false;
|
|
1054
|
+
consecutiveErrors += 1;
|
|
1055
|
+
onError(err);
|
|
1056
|
+
scheduleNextPoll();
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function onSuccess(envelope) {
|
|
1061
|
+
// Check schema_version
|
|
1062
|
+
if (envelope.schema_version !== EXPECTED_SCHEMA_VERSION) {
|
|
1063
|
+
showSchemaMismatch();
|
|
1064
|
+
// Keep last good view; do not update
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
hideSchemaMismatch();
|
|
1068
|
+
lastSuccessTime = Date.now();
|
|
1069
|
+
lastGoodModel = envelope.model;
|
|
1070
|
+
|
|
1071
|
+
// Graft the envelope-level `details` map (task-070 attaches it as a sibling of
|
|
1072
|
+
// `model`, present ONLY on ?detail= polls) onto the model so the drill view can
|
|
1073
|
+
// read it via model.details. Absent on bare polls -> {} (first-tick loading state).
|
|
1074
|
+
if (lastGoodModel) { lastGoodModel.details = envelope.details || {}; }
|
|
1075
|
+
|
|
1076
|
+
// Update footer generated_by (envelope-level field, NOT the model subtree)
|
|
1077
|
+
var genByEl = document.getElementById('footer-generated-by');
|
|
1078
|
+
if (genByEl && envelope.generated_by) {
|
|
1079
|
+
genByEl.textContent = 'via ' + envelope.generated_by;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
renderModel(lastGoodModel);
|
|
1083
|
+
updateFreshnessBadge('live');
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function onError(err) {
|
|
1087
|
+
updateFreshnessBadge('disconnected');
|
|
1088
|
+
// Keep last good view if we have one; never blank the page
|
|
1089
|
+
if (lastGoodModel) {
|
|
1090
|
+
// Model remains rendered from last successful render
|
|
1091
|
+
} else {
|
|
1092
|
+
// No model yet — show loading state with reconnecting message
|
|
1093
|
+
var loadEl = document.getElementById('loading-state');
|
|
1094
|
+
if (loadEl) loadEl.innerHTML = '<p>Reconnecting to dashboard server…</p>';
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// ---------------------------------------------------------------------------
|
|
1099
|
+
// Freshness badge (Telemetry)
|
|
1100
|
+
// ---------------------------------------------------------------------------
|
|
1101
|
+
function updateFreshnessBadge(state, readAt) {
|
|
1102
|
+
var badge = document.getElementById('freshness-badge');
|
|
1103
|
+
if (!badge) return;
|
|
1104
|
+
|
|
1105
|
+
if (state === 'disconnected') {
|
|
1106
|
+
badge.className = 'badge badge-err';
|
|
1107
|
+
badge.textContent = '○ Reconnecting';
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Compute freshness from read_at vs now (if provided) and lastSuccessTime
|
|
1112
|
+
if (state === 'live' && readAt && lastSuccessTime) {
|
|
1113
|
+
var readTime = new Date(readAt).getTime();
|
|
1114
|
+
var now = Date.now();
|
|
1115
|
+
var ageMs = now - readTime;
|
|
1116
|
+
// Stale if read_at older than ~2× interval
|
|
1117
|
+
if (ageMs > pollMs * 2) {
|
|
1118
|
+
state = 'stale';
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (state === 'stale') {
|
|
1123
|
+
badge.className = 'badge badge-warn';
|
|
1124
|
+
badge.innerHTML = '◐ Stale';
|
|
1125
|
+
} else {
|
|
1126
|
+
badge.className = 'badge badge-ok';
|
|
1127
|
+
badge.innerHTML = '<span class="pulse-dot">●</span> Live';
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// ---------------------------------------------------------------------------
|
|
1132
|
+
// Schema mismatch banner
|
|
1133
|
+
// ---------------------------------------------------------------------------
|
|
1134
|
+
function showSchemaMismatch() {
|
|
1135
|
+
var el = document.getElementById('schema-mismatch-banner');
|
|
1136
|
+
if (el) el.style.display = '';
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function hideSchemaMismatch() {
|
|
1140
|
+
var el = document.getElementById('schema-mismatch-banner');
|
|
1141
|
+
if (el) el.style.display = 'none';
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// ---------------------------------------------------------------------------
|
|
1145
|
+
// Hash router (DD-1, feature-006)
|
|
1146
|
+
// ---------------------------------------------------------------------------
|
|
1147
|
+
|
|
1148
|
+
// Parse location.hash into a route descriptor
|
|
1149
|
+
// Returns { view: "main" } | { view: "work", workId: string } |
|
|
1150
|
+
// { view: "task", workId: string, taskId: string } | { view: "kb" }
|
|
1151
|
+
function parseRoute(hash) {
|
|
1152
|
+
var h = hash || '';
|
|
1153
|
+
// Strip leading '#'
|
|
1154
|
+
if (h.charAt(0) === '#') h = h.slice(1);
|
|
1155
|
+
// Match /work/<work_id>/task/<task_id> (SEAM-2 — matched BEFORE /work/<id> arm)
|
|
1156
|
+
var taskMatch = h.match(/^\/work\/([^/]+)\/task\/(.+)$/);
|
|
1157
|
+
if (taskMatch) {
|
|
1158
|
+
var wid = taskMatch[1];
|
|
1159
|
+
var tid = taskMatch[2];
|
|
1160
|
+
try { wid = decodeURIComponent(wid); } catch (e) { /* malformed %xx: keep raw */ }
|
|
1161
|
+
try { tid = decodeURIComponent(tid); } catch (e) { /* malformed %xx: keep raw */ }
|
|
1162
|
+
return { view: 'task', workId: wid, taskId: tid };
|
|
1163
|
+
}
|
|
1164
|
+
// Match /work/<work_id>
|
|
1165
|
+
var workMatch = h.match(/^\/work\/(.+)$/);
|
|
1166
|
+
if (workMatch) {
|
|
1167
|
+
// location.hash is URL-encoded; decode to compare against the raw work_id.
|
|
1168
|
+
// (no-op for today's slug-shaped ids; robust if the slug charset ever widens)
|
|
1169
|
+
var wid2 = workMatch[1];
|
|
1170
|
+
try { wid2 = decodeURIComponent(wid2); } catch (e) { /* malformed %xx: keep raw */ }
|
|
1171
|
+
return { view: 'work', workId: wid2 };
|
|
1172
|
+
}
|
|
1173
|
+
// Match /kb
|
|
1174
|
+
if (h === '/kb') {
|
|
1175
|
+
return { view: 'kb' };
|
|
1176
|
+
}
|
|
1177
|
+
// Default: main (covers "", "/", and any unrecognized hash)
|
|
1178
|
+
return { view: 'main' };
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Find a work by work_id (find-by-key, never index-by-position)
|
|
1182
|
+
// Returns the matching work object or null
|
|
1183
|
+
function findWorkById(works, workId) {
|
|
1184
|
+
if (!works || !workId) return null;
|
|
1185
|
+
for (var i = 0; i < works.length; i++) {
|
|
1186
|
+
if (works[i].work_id === workId) {
|
|
1187
|
+
return works[i];
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// hashchange handler: re-render current lastGoodModel against new route (no fetch)
|
|
1194
|
+
function onHashChange() {
|
|
1195
|
+
if (lastGoodModel) {
|
|
1196
|
+
render(lastGoodModel, parseRoute(location.hash));
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// ---------------------------------------------------------------------------
|
|
1201
|
+
// Main render (router-driven, feature-006)
|
|
1202
|
+
// ---------------------------------------------------------------------------
|
|
1203
|
+
|
|
1204
|
+
// Thin wrapper: keeps existing poll-loop call site (renderModel(envelope.model)) intact
|
|
1205
|
+
function renderModel(model) {
|
|
1206
|
+
render(model, parseRoute(location.hash));
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Primary render entry: dispatches to the correct view based on route
|
|
1210
|
+
function render(model, route) {
|
|
1211
|
+
if (!route) route = parseRoute(location.hash);
|
|
1212
|
+
|
|
1213
|
+
// Hide loading state
|
|
1214
|
+
var loadEl = document.getElementById('loading-state');
|
|
1215
|
+
if (loadEl) loadEl.style.display = 'none';
|
|
1216
|
+
|
|
1217
|
+
// -- Route-independent shell head (runs for every route) --
|
|
1218
|
+
// Update page title from repo.project_name
|
|
1219
|
+
var projectName = (model.repo && model.repo.project_name) ? model.repo.project_name : 'AID Dashboard';
|
|
1220
|
+
document.title = projectName + ' — AID Dashboard';
|
|
1221
|
+
|
|
1222
|
+
// NAV-1: router-driven breadcrumb (recomputed every render from route + model).
|
|
1223
|
+
// The breadcrumb is the sole top-bar identity (no separate brand wordmark) — it
|
|
1224
|
+
// always roots at "Home" (the CLI home / all projects) so the project level is
|
|
1225
|
+
// distinct from the root even when the project itself is named "AID".
|
|
1226
|
+
renderBreadcrumb(model, route);
|
|
1227
|
+
|
|
1228
|
+
// Surface parse_warnings data note (Telemetry)
|
|
1229
|
+
renderParseWarnings(model.read);
|
|
1230
|
+
|
|
1231
|
+
// -- View branch --
|
|
1232
|
+
if (route.view === 'task') {
|
|
1233
|
+
// SEAM-2: task drill view — enter drill, render forensic panel
|
|
1234
|
+
var drillKey = route.workId + '/' + route.taskId;
|
|
1235
|
+
openDrillKeys[drillKey] = true;
|
|
1236
|
+
_showView('task');
|
|
1237
|
+
renderTaskView(model, route);
|
|
1238
|
+
} else if (route.view === 'work') {
|
|
1239
|
+
// Leaving task view: clear all drill keys (payload shrinks back — §3.2)
|
|
1240
|
+
openDrillKeys = {};
|
|
1241
|
+
var work = findWorkById(model.works || [], route.workId);
|
|
1242
|
+
if (!work) {
|
|
1243
|
+
_showView('stale');
|
|
1244
|
+
renderStaleWorkNotice(route.workId);
|
|
1245
|
+
} else {
|
|
1246
|
+
_showView('work');
|
|
1247
|
+
renderWorkView(work, model);
|
|
1248
|
+
}
|
|
1249
|
+
} else if (route.view === 'kb') {
|
|
1250
|
+
openDrillKeys = {};
|
|
1251
|
+
_showView('kb');
|
|
1252
|
+
renderKbView(model);
|
|
1253
|
+
} else {
|
|
1254
|
+
// Default: main page — clear all open drill keys when returning to main
|
|
1255
|
+
openDrillKeys = {};
|
|
1256
|
+
_showView('main');
|
|
1257
|
+
renderMainPage(model);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Show one of the top-level view panels; hide others
|
|
1262
|
+
// view: 'main' | 'work' | 'kb' | 'stale'
|
|
1263
|
+
function _showView(view) {
|
|
1264
|
+
var mainPage = document.getElementById('main-page');
|
|
1265
|
+
var workHeader = document.getElementById('work-header');
|
|
1266
|
+
var attnStrip = document.getElementById('attention-strip');
|
|
1267
|
+
var emptyState = document.getElementById('empty-state');
|
|
1268
|
+
var kbView = document.getElementById('kb-view');
|
|
1269
|
+
var staleNotice = document.getElementById('stale-work-notice');
|
|
1270
|
+
var taskView = document.getElementById('task-view');
|
|
1271
|
+
|
|
1272
|
+
// Always reset
|
|
1273
|
+
if (mainPage) mainPage.style.display = 'none';
|
|
1274
|
+
if (workHeader) workHeader.style.display = 'none';
|
|
1275
|
+
if (attnStrip) attnStrip.style.display = 'none';
|
|
1276
|
+
if (emptyState) emptyState.style.display = 'none';
|
|
1277
|
+
if (kbView) kbView.style.display = 'none';
|
|
1278
|
+
if (staleNotice) staleNotice.style.display = 'none';
|
|
1279
|
+
if (taskView) taskView.style.display = 'none';
|
|
1280
|
+
|
|
1281
|
+
if (view === 'main') { if (mainPage) mainPage.style.display = ''; }
|
|
1282
|
+
if (view === 'work') { if (workHeader) workHeader.style.display = ''; }
|
|
1283
|
+
if (view === 'kb') { if (kbView) kbView.style.display = ''; }
|
|
1284
|
+
if (view === 'stale') { if (staleNotice) staleNotice.style.display = ''; }
|
|
1285
|
+
if (view === 'task') { if (taskView) taskView.style.display = ''; }
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// ---------------------------------------------------------------------------
|
|
1289
|
+
// renderWorkView: the existing per-work pipeline view (moved from renderModel body)
|
|
1290
|
+
// The four per-work calls are VERBATIM; only their caller changes.
|
|
1291
|
+
// ---------------------------------------------------------------------------
|
|
1292
|
+
function renderWorkView(work, model) {
|
|
1293
|
+
renderWorkHeader(work);
|
|
1294
|
+
renderAttentionStrip(work);
|
|
1295
|
+
renderStageRail(work);
|
|
1296
|
+
renderTasks(work);
|
|
1297
|
+
|
|
1298
|
+
// Update freshness badge
|
|
1299
|
+
var readAt = model.read && model.read.read_at;
|
|
1300
|
+
var hasWarnings = model.read && model.read.parse_warnings && model.read.parse_warnings.length > 0;
|
|
1301
|
+
if (hasWarnings) {
|
|
1302
|
+
updateFreshnessBadge('stale', readAt);
|
|
1303
|
+
} else {
|
|
1304
|
+
updateFreshnessBadge('live', readAt);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// ---------------------------------------------------------------------------
|
|
1309
|
+
// renderMainPage: the feature-006 main page (card grid + KB card)
|
|
1310
|
+
// ---------------------------------------------------------------------------
|
|
1311
|
+
function renderMainPage(model) {
|
|
1312
|
+
var works = model.works || [];
|
|
1313
|
+
|
|
1314
|
+
// Render pipelines section
|
|
1315
|
+
var pipelinesEl = document.getElementById('pipelines-section');
|
|
1316
|
+
if (pipelinesEl) {
|
|
1317
|
+
pipelinesEl.innerHTML = '';
|
|
1318
|
+
if (works.length === 0) {
|
|
1319
|
+
pipelinesEl.appendChild(_renderEmptyState());
|
|
1320
|
+
} else {
|
|
1321
|
+
var grid = document.createElement('div');
|
|
1322
|
+
grid.className = 'grid pipelines-grid';
|
|
1323
|
+
// Two-pass render: attention cards first (Paused-Awaiting-Input, Blocked), then normal
|
|
1324
|
+
var ATTENTION_STATES = ['Paused-Awaiting-Input', 'Blocked'];
|
|
1325
|
+
for (var i = 0; i < works.length; i++) {
|
|
1326
|
+
if (ATTENTION_STATES.indexOf(works[i].lifecycle) !== -1) {
|
|
1327
|
+
grid.appendChild(_renderWorkCard(works[i]));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
for (var j = 0; j < works.length; j++) {
|
|
1331
|
+
if (ATTENTION_STATES.indexOf(works[j].lifecycle) === -1) {
|
|
1332
|
+
grid.appendChild(_renderWorkCard(works[j]));
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
pipelinesEl.appendChild(grid);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Render Knowledge & Tool section
|
|
1340
|
+
var ktEl = document.getElementById('knowledge-tool-section');
|
|
1341
|
+
if (ktEl) {
|
|
1342
|
+
ktEl.innerHTML = '';
|
|
1343
|
+
ktEl.appendChild(_renderKbCard(model.repo && model.repo.kb_state));
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Update freshness badge
|
|
1347
|
+
var readAt = model.read && model.read.read_at;
|
|
1348
|
+
var hasWarnings = model.read && model.read.parse_warnings && model.read.parse_warnings.length > 0;
|
|
1349
|
+
if (hasWarnings) {
|
|
1350
|
+
updateFreshnessBadge('stale', readAt);
|
|
1351
|
+
} else {
|
|
1352
|
+
updateFreshnessBadge('live', readAt);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Format an ISO timestamp in the browser's local format + timezone, without seconds.
|
|
1357
|
+
// Date-only values (YYYY-MM-DD) render as a date with no time (and no TZ shift).
|
|
1358
|
+
function _fmtLocalDateTime(iso) {
|
|
1359
|
+
if (!iso) return null;
|
|
1360
|
+
var dOnly = String(iso).trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
1361
|
+
if (dOnly) {
|
|
1362
|
+
var ld = new Date(parseInt(dOnly[1], 10), parseInt(dOnly[2], 10) - 1, parseInt(dOnly[3], 10));
|
|
1363
|
+
return ld.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
1364
|
+
}
|
|
1365
|
+
var d = new Date(iso);
|
|
1366
|
+
if (isNaN(d.getTime())) return String(iso); // unparseable -> show raw
|
|
1367
|
+
return d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Pretty-print a lite recipe id: "LITE-BUG-FIX" -> "Bug fix"
|
|
1371
|
+
function _formatRecipe(r) {
|
|
1372
|
+
if (!r) return '';
|
|
1373
|
+
var s = String(r).replace(/^lite-/i, '').replace(/[-_]+/g, ' ').trim().toLowerCase();
|
|
1374
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Progressive triage-path summary for a work card.
|
|
1378
|
+
// Full: "Full path: 9 features -> 7 deliveries -> 45 Tasks" (numbers fill in as produced)
|
|
1379
|
+
// Lite: "Lite path: Bug fix -> 5 Tasks"
|
|
1380
|
+
// A bracketed [stage] marks the step in progress; "[Identifying path]" before triage.
|
|
1381
|
+
function _pathSummary(work) {
|
|
1382
|
+
var ARROW = ' → ';
|
|
1383
|
+
var wp = (work.work_path || '').toLowerCase();
|
|
1384
|
+
var nf = (work.features || []).length;
|
|
1385
|
+
var nd = (work.deliverables || []).length;
|
|
1386
|
+
var nt = (work.tasks || []).length;
|
|
1387
|
+
function tasks(n) { return n + ' Task' + (n === 1 ? '' : 's'); }
|
|
1388
|
+
if (wp === 'full') {
|
|
1389
|
+
if (nf === 0) return 'Full path: [defining features]';
|
|
1390
|
+
var s = 'Full path: ' + nf + ' feature' + (nf === 1 ? '' : 's');
|
|
1391
|
+
if (nd === 0) return s + ARROW + '[planning deliveries]';
|
|
1392
|
+
s += ARROW + nd + ' deliver' + (nd === 1 ? 'y' : 'ies');
|
|
1393
|
+
if (nt === 0) return s + ARROW + '[writing tasks]';
|
|
1394
|
+
return s + ARROW + tasks(nt);
|
|
1395
|
+
}
|
|
1396
|
+
if (wp === 'lite') {
|
|
1397
|
+
var rec = _formatRecipe(work.recipe) || 'Lite';
|
|
1398
|
+
var base = 'Lite path: ' + rec;
|
|
1399
|
+
return nt === 0 ? base + ARROW + '[writing tasks]' : base + ARROW + tasks(nt);
|
|
1400
|
+
}
|
|
1401
|
+
return '[Identifying path]';
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Two-part progress model:
|
|
1405
|
+
// - Readiness: progress through the pre-execution pipeline (Interview->Specify->Plan->Detail),
|
|
1406
|
+
// i.e. how close to having an approved task list. 4 steps; the in-progress phase counts half.
|
|
1407
|
+
// - Execution: completed tasks / total (Canceled excluded), once the task list exists.
|
|
1408
|
+
var PRE_EXEC_STEPS = 4; // Interview, Specify, Plan, Detail (index of Execute in PHASE_ORDER)
|
|
1409
|
+
|
|
1410
|
+
function _readinessPct(work) {
|
|
1411
|
+
var idx = PHASE_ORDER.indexOf(work.phase);
|
|
1412
|
+
if (idx === -1) return null; // unknown phase
|
|
1413
|
+
if (idx >= PRE_EXEC_STEPS) return 100; // Execute or later -> task list ready
|
|
1414
|
+
return Math.round(((idx + 0.5) / PRE_EXEC_STEPS) * 100);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function _executionStats(work) {
|
|
1418
|
+
var tasks = work.tasks || [];
|
|
1419
|
+
var total = 0, done = 0;
|
|
1420
|
+
for (var i = 0; i < tasks.length; i++) {
|
|
1421
|
+
var s = (tasks[i].status || '').toLowerCase();
|
|
1422
|
+
if (s === 'canceled' || s === 'cancelled') continue; // out of scope
|
|
1423
|
+
total++;
|
|
1424
|
+
if (s === 'done') done++;
|
|
1425
|
+
}
|
|
1426
|
+
if (total === 0) return null;
|
|
1427
|
+
return { pct: Math.round((done / total) * 100), done: done, total: total };
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Build the contextual progress row: Readiness (pre-execution) or Execution (task list exists).
|
|
1431
|
+
function _renderProgress(work) {
|
|
1432
|
+
var idx = PHASE_ORDER.indexOf(work.phase);
|
|
1433
|
+
var label, pct, detail;
|
|
1434
|
+
if (idx !== -1 && idx < PRE_EXEC_STEPS) {
|
|
1435
|
+
pct = _readinessPct(work);
|
|
1436
|
+
if (pct === null) return null;
|
|
1437
|
+
label = 'Readiness';
|
|
1438
|
+
detail = work.phase;
|
|
1439
|
+
} else {
|
|
1440
|
+
var ex = _executionStats(work);
|
|
1441
|
+
if (!ex) return null; // no task list yet -> no bar
|
|
1442
|
+
label = 'Execution';
|
|
1443
|
+
pct = ex.pct;
|
|
1444
|
+
detail = ex.done + '/' + ex.total + ' tasks';
|
|
1445
|
+
}
|
|
1446
|
+
var row = document.createElement('div');
|
|
1447
|
+
row.className = 'progress-row progress-' + label.toLowerCase();
|
|
1448
|
+
var track = document.createElement('div');
|
|
1449
|
+
track.className = 'progress-track';
|
|
1450
|
+
var fill = document.createElement('div');
|
|
1451
|
+
fill.className = 'progress-fill';
|
|
1452
|
+
fill.style.width = pct + '%';
|
|
1453
|
+
track.appendChild(fill);
|
|
1454
|
+
var lbl = document.createElement('span');
|
|
1455
|
+
lbl.className = 'progress-label';
|
|
1456
|
+
lbl.textContent = label + ' ' + pct + '% · ' + detail;
|
|
1457
|
+
row.appendChild(lbl); // percentage first
|
|
1458
|
+
row.appendChild(track); // bar below
|
|
1459
|
+
return row;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Build a work-card DOM element for the main page grid (UI-3)
|
|
1463
|
+
function _renderWorkCard(work) {
|
|
1464
|
+
var isAttention = work.lifecycle === 'Paused-Awaiting-Input' || work.lifecycle === 'Blocked';
|
|
1465
|
+
var card = document.createElement('a');
|
|
1466
|
+
card.className = 'card card-link work-card';
|
|
1467
|
+
card.href = '#/work/' + encodeURIComponent(work.work_id);
|
|
1468
|
+
if (work.lifecycle === 'Paused-Awaiting-Input') card.classList.add('border-warn');
|
|
1469
|
+
if (work.lifecycle === 'Blocked') card.classList.add('border-err');
|
|
1470
|
+
|
|
1471
|
+
// Kicker: work_id
|
|
1472
|
+
var kicker = document.createElement('div');
|
|
1473
|
+
kicker.className = 'kicker';
|
|
1474
|
+
kicker.textContent = work.work_id || '';
|
|
1475
|
+
card.appendChild(kicker);
|
|
1476
|
+
|
|
1477
|
+
// Title: prefer title; fallback to de-slug of work_id (matches renderWorkHeader precedence)
|
|
1478
|
+
var titleEl = document.createElement('h3');
|
|
1479
|
+
if (work.title) {
|
|
1480
|
+
titleEl.textContent = work.title;
|
|
1481
|
+
} else {
|
|
1482
|
+
var deSlug = (work.work_id || '')
|
|
1483
|
+
.replace(/^work-\d+-/, '')
|
|
1484
|
+
.replace(/[-_]+/g, ' ')
|
|
1485
|
+
.replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
|
1486
|
+
titleEl.textContent = deSlug || work.work_id || '—';
|
|
1487
|
+
titleEl.style.opacity = '0.6';
|
|
1488
|
+
titleEl.title = 'Name not yet recorded (' + (work.work_id || '') + ')';
|
|
1489
|
+
}
|
|
1490
|
+
card.appendChild(titleEl);
|
|
1491
|
+
|
|
1492
|
+
// STATE (lifecycle badge) -- shown first; reuses makeLifecycleBadge verbatim
|
|
1493
|
+
var badgeWrap = document.createElement('div');
|
|
1494
|
+
badgeWrap.style.margin = '0.1rem 0 0.4rem';
|
|
1495
|
+
badgeWrap.appendChild(makeLifecycleBadge(work.lifecycle));
|
|
1496
|
+
card.appendChild(badgeWrap);
|
|
1497
|
+
|
|
1498
|
+
// Current phase -- labelled, no progress counter (the 7 phases are fixed and 2 are
|
|
1499
|
+
// optional, so an index/total adds little here)
|
|
1500
|
+
var phase = work.phase;
|
|
1501
|
+
if (phase && phase !== 'Unknown' && PHASE_ORDER.indexOf(phase) !== -1) {
|
|
1502
|
+
var phaseLine = document.createElement('div');
|
|
1503
|
+
phaseLine.style.cssText = 'display:flex;align-items:center;gap:0.45rem;flex-wrap:wrap;margin:0.4rem 0;';
|
|
1504
|
+
var phaseLabel = document.createElement('span');
|
|
1505
|
+
phaseLabel.className = 'meta';
|
|
1506
|
+
phaseLabel.textContent = 'Phase';
|
|
1507
|
+
phaseLine.appendChild(phaseLabel);
|
|
1508
|
+
var phasePill = document.createElement('span');
|
|
1509
|
+
phasePill.className = 'badge badge-primary';
|
|
1510
|
+
phasePill.textContent = '▸ ' + phase;
|
|
1511
|
+
phaseLine.appendChild(phasePill);
|
|
1512
|
+
card.appendChild(phaseLine);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Triage-path progress pipeline -- progressively disclosed by what's been produced so far.
|
|
1516
|
+
// Full: features -> deliveries -> tasks; Lite: recipe -> tasks. A bracketed [stage] marks
|
|
1517
|
+
// the step currently in progress; "[Identifying path]" before triage has run.
|
|
1518
|
+
var pathText = _pathSummary(work);
|
|
1519
|
+
if (pathText) {
|
|
1520
|
+
var pathLineEl = document.createElement('p');
|
|
1521
|
+
pathLineEl.className = 'meta';
|
|
1522
|
+
pathLineEl.style.margin = '0.35rem 0 0';
|
|
1523
|
+
pathLineEl.textContent = pathText;
|
|
1524
|
+
card.appendChild(pathLineEl);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Progress bar: Readiness (pre-execution) or Execution (task completion)
|
|
1528
|
+
var progressRow = _renderProgress(work);
|
|
1529
|
+
if (progressRow) card.appendChild(progressRow);
|
|
1530
|
+
|
|
1531
|
+
// Attention detail (pause_reason / block_reason + block_artifact)
|
|
1532
|
+
if (work.lifecycle === 'Paused-Awaiting-Input' && work.pause_reason) {
|
|
1533
|
+
var pauseDetail = document.createElement('p');
|
|
1534
|
+
pauseDetail.className = 'meta';
|
|
1535
|
+
pauseDetail.style.margin = '0.25rem 0';
|
|
1536
|
+
pauseDetail.textContent = work.pause_reason;
|
|
1537
|
+
card.appendChild(pauseDetail);
|
|
1538
|
+
}
|
|
1539
|
+
if (work.lifecycle === 'Blocked') {
|
|
1540
|
+
if (work.block_reason) {
|
|
1541
|
+
var blockDetail = document.createElement('p');
|
|
1542
|
+
blockDetail.className = 'meta';
|
|
1543
|
+
blockDetail.style.margin = '0.25rem 0';
|
|
1544
|
+
blockDetail.textContent = work.block_reason;
|
|
1545
|
+
card.appendChild(blockDetail);
|
|
1546
|
+
}
|
|
1547
|
+
if (work.block_artifact) {
|
|
1548
|
+
var artifactLine = document.createElement('p');
|
|
1549
|
+
artifactLine.className = 'meta';
|
|
1550
|
+
artifactLine.style.margin = '0.15rem 0';
|
|
1551
|
+
var artifactCode = document.createElement('code');
|
|
1552
|
+
artifactCode.textContent = work.block_artifact;
|
|
1553
|
+
artifactLine.appendChild(artifactCode);
|
|
1554
|
+
card.appendChild(artifactLine);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Meta footer: created + last-update (local format), source_mode chip.
|
|
1559
|
+
// (task count now lives in the path-summary line above)
|
|
1560
|
+
var metaEl = document.createElement('div');
|
|
1561
|
+
metaEl.className = 'meta';
|
|
1562
|
+
metaEl.style.marginTop = '0.5rem';
|
|
1563
|
+
var metaParts = [];
|
|
1564
|
+
var createdStr = _fmtLocalDateTime(work.created);
|
|
1565
|
+
if (createdStr) metaParts.push('Created: ' + createdStr);
|
|
1566
|
+
var updatedStr = _fmtLocalDateTime(work.updated);
|
|
1567
|
+
if (updatedStr) metaParts.push('Last Update: ' + updatedStr);
|
|
1568
|
+
metaEl.textContent = metaParts.join('; ');
|
|
1569
|
+
// source_mode chip (only when != "normalized")
|
|
1570
|
+
if (work.source_mode && work.source_mode !== 'normalized') {
|
|
1571
|
+
var modeChip = document.createElement('span');
|
|
1572
|
+
modeChip.className = 'badge badge-dim';
|
|
1573
|
+
modeChip.style.marginLeft = '0.4rem';
|
|
1574
|
+
modeChip.textContent = 'approx';
|
|
1575
|
+
modeChip.title = 'source_mode: ' + work.source_mode;
|
|
1576
|
+
metaEl.appendChild(modeChip);
|
|
1577
|
+
}
|
|
1578
|
+
card.appendChild(metaEl);
|
|
1579
|
+
|
|
1580
|
+
return card;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// Build the KB summary card — 5-state (UI-A, feature-007, task-065)
|
|
1584
|
+
// Reads kb_state.status LITERALLY (never re-derives client-side).
|
|
1585
|
+
// States: pending | generating | preparing | approved | outdated
|
|
1586
|
+
// An unknown/missing status degrades to the pending (dim) treatment (DM-A2).
|
|
1587
|
+
// Only approved/outdated are clickable (FR32); others render a dead (non-link) card.
|
|
1588
|
+
// Clickable href is location-relative ./kb.html (LC-A3).
|
|
1589
|
+
function _renderKbCard(kbState) {
|
|
1590
|
+
// Resolve status: read literally; null kbState or missing/unknown status -> 'pending'
|
|
1591
|
+
var KB_STATUSES = ['pending', 'generating', 'preparing', 'approved', 'outdated'];
|
|
1592
|
+
var status = (kbState && kbState.status && KB_STATUSES.indexOf(kbState.status) !== -1)
|
|
1593
|
+
? kbState.status
|
|
1594
|
+
: 'pending';
|
|
1595
|
+
|
|
1596
|
+
// Only approved and outdated are clickable (FR32)
|
|
1597
|
+
var isClickable = (status === 'approved' || status === 'outdated');
|
|
1598
|
+
|
|
1599
|
+
// Build the card element: anchor for clickable, div for dead states
|
|
1600
|
+
var card = isClickable ? document.createElement('a') : document.createElement('div');
|
|
1601
|
+
card.className = isClickable ? 'card card-link' : 'card';
|
|
1602
|
+
if (isClickable) {
|
|
1603
|
+
card.href = './kb.html'; // location-relative (LC-A3): resolves to /r/<id>/kb.html
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// Kicker
|
|
1607
|
+
var kicker = document.createElement('div');
|
|
1608
|
+
kicker.className = 'kicker';
|
|
1609
|
+
kicker.textContent = 'KNOWLEDGE BASE';
|
|
1610
|
+
card.appendChild(kicker);
|
|
1611
|
+
|
|
1612
|
+
// Badge + label per state (UI-A)
|
|
1613
|
+
var badge = document.createElement('span');
|
|
1614
|
+
badge.style.display = 'inline-block';
|
|
1615
|
+
badge.style.marginBottom = '0.4rem';
|
|
1616
|
+
|
|
1617
|
+
if (status === 'pending') {
|
|
1618
|
+
badge.className = 'badge badge-dim';
|
|
1619
|
+
badge.textContent = '⊘ No KB'; // ⊘
|
|
1620
|
+
var h3 = document.createElement('h3');
|
|
1621
|
+
h3.textContent = 'No Knowledge Base yet';
|
|
1622
|
+
card.appendChild(badge);
|
|
1623
|
+
card.appendChild(h3);
|
|
1624
|
+
var meta = document.createElement('p');
|
|
1625
|
+
meta.className = 'meta';
|
|
1626
|
+
meta.textContent = 'run /aid-discover to build the Knowledge Base';
|
|
1627
|
+
card.appendChild(meta);
|
|
1628
|
+
|
|
1629
|
+
} else if (status === 'generating') {
|
|
1630
|
+
badge.className = 'badge badge-info';
|
|
1631
|
+
badge.textContent = '◴ Building'; // ◴
|
|
1632
|
+
var h3 = document.createElement('h3');
|
|
1633
|
+
h3.textContent = 'Building';
|
|
1634
|
+
card.appendChild(badge);
|
|
1635
|
+
card.appendChild(h3);
|
|
1636
|
+
if (kbState && kbState.doc_count != null) {
|
|
1637
|
+
var stat = document.createElement('div');
|
|
1638
|
+
stat.className = 'stat';
|
|
1639
|
+
stat.textContent = String(kbState.doc_count);
|
|
1640
|
+
card.appendChild(stat);
|
|
1641
|
+
var statSub = document.createElement('div');
|
|
1642
|
+
statSub.className = 'stat-sub';
|
|
1643
|
+
statSub.textContent = 'docs';
|
|
1644
|
+
card.appendChild(statSub);
|
|
1645
|
+
}
|
|
1646
|
+
var meta = document.createElement('p');
|
|
1647
|
+
meta.className = 'meta';
|
|
1648
|
+
meta.textContent = 'discovery is building the KB…';
|
|
1649
|
+
card.appendChild(meta);
|
|
1650
|
+
|
|
1651
|
+
} else if (status === 'preparing') {
|
|
1652
|
+
badge.className = 'badge badge-info';
|
|
1653
|
+
badge.textContent = '◴ Preparing'; // ◴
|
|
1654
|
+
var h3 = document.createElement('h3');
|
|
1655
|
+
h3.textContent = 'Preparing';
|
|
1656
|
+
card.appendChild(badge);
|
|
1657
|
+
card.appendChild(h3);
|
|
1658
|
+
if (kbState && kbState.doc_count != null) {
|
|
1659
|
+
var stat = document.createElement('div');
|
|
1660
|
+
stat.className = 'stat';
|
|
1661
|
+
stat.textContent = String(kbState.doc_count);
|
|
1662
|
+
card.appendChild(stat);
|
|
1663
|
+
var statSub = document.createElement('div');
|
|
1664
|
+
statSub.className = 'stat-sub';
|
|
1665
|
+
statSub.textContent = 'docs';
|
|
1666
|
+
card.appendChild(statSub);
|
|
1667
|
+
}
|
|
1668
|
+
var meta = document.createElement('p');
|
|
1669
|
+
meta.className = 'meta';
|
|
1670
|
+
meta.textContent = 'summary generating — KB approved';
|
|
1671
|
+
card.appendChild(meta);
|
|
1672
|
+
|
|
1673
|
+
} else if (status === 'approved') {
|
|
1674
|
+
badge.className = 'badge badge-ok';
|
|
1675
|
+
badge.textContent = '✓ Ready'; // ✓
|
|
1676
|
+
var h3 = document.createElement('h3');
|
|
1677
|
+
h3.textContent = 'Ready';
|
|
1678
|
+
card.appendChild(badge);
|
|
1679
|
+
card.appendChild(h3);
|
|
1680
|
+
if (kbState && kbState.doc_count != null) {
|
|
1681
|
+
var stat = document.createElement('div');
|
|
1682
|
+
stat.className = 'stat';
|
|
1683
|
+
stat.textContent = String(kbState.doc_count);
|
|
1684
|
+
card.appendChild(stat);
|
|
1685
|
+
var statSub = document.createElement('div');
|
|
1686
|
+
statSub.className = 'stat-sub';
|
|
1687
|
+
statSub.textContent = 'docs';
|
|
1688
|
+
card.appendChild(statSub);
|
|
1689
|
+
}
|
|
1690
|
+
if (kbState && kbState.last_summary_date) {
|
|
1691
|
+
var meta = document.createElement('p');
|
|
1692
|
+
meta.className = 'meta';
|
|
1693
|
+
meta.style.marginTop = '0.5rem';
|
|
1694
|
+
meta.textContent = 'summary updated ' + (_fmtLocalDateTime(kbState.last_summary_date) || kbState.last_summary_date);
|
|
1695
|
+
card.appendChild(meta);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
} else {
|
|
1699
|
+
// status === 'outdated'
|
|
1700
|
+
badge.className = 'badge badge-warn';
|
|
1701
|
+
badge.textContent = '⚠ Outdated'; // ⚠
|
|
1702
|
+
var h3 = document.createElement('h3');
|
|
1703
|
+
h3.textContent = 'Outdated';
|
|
1704
|
+
card.appendChild(badge);
|
|
1705
|
+
card.appendChild(h3);
|
|
1706
|
+
if (kbState && kbState.doc_count != null) {
|
|
1707
|
+
var stat = document.createElement('div');
|
|
1708
|
+
stat.className = 'stat';
|
|
1709
|
+
stat.textContent = String(kbState.doc_count);
|
|
1710
|
+
card.appendChild(stat);
|
|
1711
|
+
var statSub = document.createElement('div');
|
|
1712
|
+
statSub.className = 'stat-sub';
|
|
1713
|
+
statSub.textContent = 'docs';
|
|
1714
|
+
card.appendChild(statSub);
|
|
1715
|
+
}
|
|
1716
|
+
var tipDate = (kbState && kbState.kb_baseline && kbState.kb_baseline.tip_date)
|
|
1717
|
+
? kbState.kb_baseline.tip_date : null;
|
|
1718
|
+
var metaText = tipDate
|
|
1719
|
+
? ('KB reflects ' + tipDate + '; branch has advanced')
|
|
1720
|
+
: 'KB baseline has been exceeded; branch has advanced';
|
|
1721
|
+
var meta = document.createElement('p');
|
|
1722
|
+
meta.className = 'meta';
|
|
1723
|
+
meta.style.marginTop = '0.5rem';
|
|
1724
|
+
meta.textContent = metaText;
|
|
1725
|
+
card.appendChild(meta);
|
|
1726
|
+
// Outdated refresh prompt (FR18-style, FR32)
|
|
1727
|
+
var prompt = document.createElement('p');
|
|
1728
|
+
prompt.className = 'meta';
|
|
1729
|
+
prompt.style.marginTop = '0.5rem';
|
|
1730
|
+
prompt.textContent = 'The branch has advanced past the KB baseline. 1. Run /aid-housekeep to reconcile the KB and refresh the summary. 2. Verify: this card returns to Ready on the next refresh.';
|
|
1731
|
+
card.appendChild(prompt);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return card;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// Build the FR18 step-by-step empty-state panel
|
|
1738
|
+
function _renderEmptyState() {
|
|
1739
|
+
var card = document.createElement('div');
|
|
1740
|
+
card.className = 'card empty-state';
|
|
1741
|
+
card.style.textAlign = 'left';
|
|
1742
|
+
|
|
1743
|
+
var kicker = document.createElement('div');
|
|
1744
|
+
kicker.className = 'kicker';
|
|
1745
|
+
kicker.textContent = 'NO PIPELINES YET';
|
|
1746
|
+
card.appendChild(kicker);
|
|
1747
|
+
|
|
1748
|
+
var h3 = document.createElement('h3');
|
|
1749
|
+
h3.textContent = 'This repo has no AID works in .aid/ yet.';
|
|
1750
|
+
card.appendChild(h3);
|
|
1751
|
+
|
|
1752
|
+
var intro = document.createElement('p');
|
|
1753
|
+
intro.textContent = 'To start your first pipeline:';
|
|
1754
|
+
card.appendChild(intro);
|
|
1755
|
+
|
|
1756
|
+
var ol = document.createElement('ol');
|
|
1757
|
+
var steps = [
|
|
1758
|
+
null, // step 1 is special (has <code>)
|
|
1759
|
+
'Follow the interview prompts to capture requirements.',
|
|
1760
|
+
'Verify: a work-NNN-* folder now appears under .aid/, and this page shows a card for it on the next refresh (within the poll interval).'
|
|
1761
|
+
];
|
|
1762
|
+
|
|
1763
|
+
// Step 1: special — contains code element
|
|
1764
|
+
var li1 = document.createElement('li');
|
|
1765
|
+
li1.appendChild(document.createTextNode('In this repo, run: '));
|
|
1766
|
+
var codeEl = document.createElement('code');
|
|
1767
|
+
codeEl.textContent = '/aid-interview';
|
|
1768
|
+
li1.appendChild(codeEl);
|
|
1769
|
+
li1.appendChild(document.createTextNode(' — begins a new work (creates .aid/work-NNN-<name>/ + its STATE.md).'));
|
|
1770
|
+
ol.appendChild(li1);
|
|
1771
|
+
|
|
1772
|
+
// Steps 2, 3
|
|
1773
|
+
for (var s = 1; s < steps.length; s++) {
|
|
1774
|
+
var li = document.createElement('li');
|
|
1775
|
+
li.textContent = steps[s];
|
|
1776
|
+
ol.appendChild(li);
|
|
1777
|
+
}
|
|
1778
|
+
card.appendChild(ol);
|
|
1779
|
+
|
|
1780
|
+
var meta = document.createElement('p');
|
|
1781
|
+
meta.className = 'meta';
|
|
1782
|
+
meta.textContent = 'this page refreshes every ' + Math.round(pollMs / 1000) + 's — the card appears automatically.';
|
|
1783
|
+
card.appendChild(meta);
|
|
1784
|
+
|
|
1785
|
+
return card;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// ---------------------------------------------------------------------------
|
|
1789
|
+
// renderKbView: SEAM-1 placeholder for feature-007
|
|
1790
|
+
// ---------------------------------------------------------------------------
|
|
1791
|
+
function renderKbView(model) {
|
|
1792
|
+
var kbView = document.getElementById('kb-view');
|
|
1793
|
+
if (!kbView) return;
|
|
1794
|
+
kbView.innerHTML = '';
|
|
1795
|
+
var callout = document.createElement('div');
|
|
1796
|
+
callout.className = 'callout';
|
|
1797
|
+
callout.innerHTML = '<h4>Knowledge Base</h4><p>KB dashboard — coming in feature-007.</p>';
|
|
1798
|
+
kbView.appendChild(callout);
|
|
1799
|
+
var backLink = document.createElement('p');
|
|
1800
|
+
backLink.style.marginTop = '1rem';
|
|
1801
|
+
var a = document.createElement('a');
|
|
1802
|
+
a.href = '#/';
|
|
1803
|
+
a.textContent = '← back to main';
|
|
1804
|
+
backLink.appendChild(a);
|
|
1805
|
+
kbView.appendChild(backLink);
|
|
1806
|
+
|
|
1807
|
+
var readAt = model.read && model.read.read_at;
|
|
1808
|
+
updateFreshnessBadge('live', readAt);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// ---------------------------------------------------------------------------
|
|
1812
|
+
// renderStaleWorkNotice: FC-3 -- unknown/stale work_id, never blank
|
|
1813
|
+
// ---------------------------------------------------------------------------
|
|
1814
|
+
function renderStaleWorkNotice(workId) {
|
|
1815
|
+
var el = document.getElementById('stale-work-notice');
|
|
1816
|
+
if (!el) return;
|
|
1817
|
+
el.innerHTML = '';
|
|
1818
|
+
var callout = document.createElement('div');
|
|
1819
|
+
callout.className = 'callout warn';
|
|
1820
|
+
callout.innerHTML = '<h4>Pipeline not found</h4>' +
|
|
1821
|
+
'<p>That pipeline (<code>' + escHtml(workId) + '</code>) is no longer in this repo.</p>';
|
|
1822
|
+
el.appendChild(callout);
|
|
1823
|
+
var backLink = document.createElement('p');
|
|
1824
|
+
backLink.style.marginTop = '0.75rem';
|
|
1825
|
+
var a = document.createElement('a');
|
|
1826
|
+
a.href = '#/';
|
|
1827
|
+
a.textContent = '← back to main';
|
|
1828
|
+
backLink.appendChild(a);
|
|
1829
|
+
el.appendChild(backLink);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// ---------------------------------------------------------------------------
|
|
1833
|
+
// Work selection (FR9 -- kept; no longer on the default render path under feature-006)
|
|
1834
|
+
// Active = first non-terminal lifecycle work, else highest work_id
|
|
1835
|
+
// ---------------------------------------------------------------------------
|
|
1836
|
+
function selectActiveWork(works) {
|
|
1837
|
+
if (!works || works.length === 0) return null;
|
|
1838
|
+
var NON_TERMINAL = ['Running', 'Paused-Awaiting-Input', 'Blocked'];
|
|
1839
|
+
for (var i = 0; i < works.length; i++) {
|
|
1840
|
+
if (NON_TERMINAL.indexOf(works[i].lifecycle) !== -1) {
|
|
1841
|
+
return works[i];
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
// All terminal: return highest work_id (works arrive sorted ascending per DM-2)
|
|
1845
|
+
return works[works.length - 1];
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// ---------------------------------------------------------------------------
|
|
1849
|
+
// Work overview header (prototype: delivery-002 work-overview header)
|
|
1850
|
+
// ---------------------------------------------------------------------------
|
|
1851
|
+
function renderWorkHeader(work) {
|
|
1852
|
+
var panel = document.getElementById('work-overview-panel');
|
|
1853
|
+
if (!panel) return;
|
|
1854
|
+
|
|
1855
|
+
// Attention border on the overview panel
|
|
1856
|
+
panel.classList.remove('border-warn', 'border-err');
|
|
1857
|
+
if (work.lifecycle === 'Paused-Awaiting-Input') panel.classList.add('border-warn');
|
|
1858
|
+
if (work.lifecycle === 'Blocked') panel.classList.add('border-err');
|
|
1859
|
+
|
|
1860
|
+
// Identity line: #N
|
|
1861
|
+
var numEl = document.getElementById('overview-number');
|
|
1862
|
+
if (numEl) {
|
|
1863
|
+
if (work.number != null) {
|
|
1864
|
+
numEl.textContent = '#' + work.number;
|
|
1865
|
+
numEl.style.display = '';
|
|
1866
|
+
} else {
|
|
1867
|
+
numEl.textContent = '';
|
|
1868
|
+
numEl.style.display = 'none';
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// Title: use the real authored Name; when absent show a labelled de-slug fallback (never raw work_id as if it were the name)
|
|
1873
|
+
var titleEl = document.getElementById('overview-title');
|
|
1874
|
+
if (titleEl) {
|
|
1875
|
+
if (work.title) {
|
|
1876
|
+
titleEl.textContent = work.title;
|
|
1877
|
+
titleEl.style.opacity = '';
|
|
1878
|
+
titleEl.title = '';
|
|
1879
|
+
} else {
|
|
1880
|
+
// PF-7: de-slug and title-case the work_id as a labelled fallback
|
|
1881
|
+
var deSlug = (work.work_id || '')
|
|
1882
|
+
.replace(/^work-\d+-/, '') // strip "work-NNN-" prefix
|
|
1883
|
+
.replace(/[-_]+/g, ' ') // hyphens/underscores → spaces
|
|
1884
|
+
.replace(/\b\w/g, function(c) { return c.toUpperCase(); }); // Title Case
|
|
1885
|
+
titleEl.textContent = deSlug || work.work_id || '—';
|
|
1886
|
+
titleEl.style.opacity = '0.6';
|
|
1887
|
+
titleEl.title = 'Name not yet recorded (' + (work.work_id || '') + ')';
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// Lifecycle + skill badges inline with identity
|
|
1892
|
+
var badgesEl = document.getElementById('overview-badges');
|
|
1893
|
+
if (badgesEl) {
|
|
1894
|
+
badgesEl.innerHTML = '';
|
|
1895
|
+
badgesEl.appendChild(makeLifecycleBadge(work.lifecycle));
|
|
1896
|
+
if (work.active_skill) {
|
|
1897
|
+
var skillBadge = document.createElement('span');
|
|
1898
|
+
skillBadge.className = 'badge badge-dim';
|
|
1899
|
+
skillBadge.textContent = work.active_skill;
|
|
1900
|
+
badgesEl.appendChild(skillBadge);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// Wire up work panel collapse/expand (Change 2 + 3)
|
|
1905
|
+
_applyWorkPanelState();
|
|
1906
|
+
|
|
1907
|
+
// Description
|
|
1908
|
+
var descEl = document.getElementById('overview-desc');
|
|
1909
|
+
if (descEl) {
|
|
1910
|
+
if (work.description) {
|
|
1911
|
+
descEl.textContent = work.description;
|
|
1912
|
+
descEl.style.display = '';
|
|
1913
|
+
} else {
|
|
1914
|
+
descEl.style.display = 'none';
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Objective collapsible
|
|
1919
|
+
_renderObjective(work.objective);
|
|
1920
|
+
|
|
1921
|
+
// Meta: updated time + work_id kicker
|
|
1922
|
+
var metaEl = document.getElementById('work-meta');
|
|
1923
|
+
if (metaEl) {
|
|
1924
|
+
var metaParts = [];
|
|
1925
|
+
metaParts.push(work.work_id);
|
|
1926
|
+
if (work.updated) metaParts.push('Updated: ' + work.updated);
|
|
1927
|
+
metaEl.textContent = metaParts.join(' · ');
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// Stat strip
|
|
1931
|
+
_renderStatStrip(work);
|
|
1932
|
+
|
|
1933
|
+
// Features disclosure (full path only); Deliverables removed from work card
|
|
1934
|
+
var isFullPath = work.work_path === 'full';
|
|
1935
|
+
_renderFeaturesDisclosure(work.features, isFullPath);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// Apply uiState.workOpen to the panel DOM (idempotent; called each render)
|
|
1939
|
+
function _applyWorkPanelState() {
|
|
1940
|
+
var btn = document.getElementById('work-overview-header-btn');
|
|
1941
|
+
var body = document.getElementById('work-overview-body');
|
|
1942
|
+
if (!btn || !body) return;
|
|
1943
|
+
|
|
1944
|
+
if (uiState.workOpen) {
|
|
1945
|
+
btn.classList.add('open');
|
|
1946
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
1947
|
+
body.classList.add('open');
|
|
1948
|
+
} else {
|
|
1949
|
+
btn.classList.remove('open');
|
|
1950
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
1951
|
+
body.classList.remove('open');
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// Re-attach click handler (idempotent — replaces any existing onclick)
|
|
1955
|
+
btn.onclick = function() {
|
|
1956
|
+
uiState.workOpen = !uiState.workOpen;
|
|
1957
|
+
_applyWorkPanelState();
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function _renderObjective(objective) {
|
|
1962
|
+
var wrap = document.getElementById('overview-obj-wrap');
|
|
1963
|
+
var body = document.getElementById('overview-obj-body');
|
|
1964
|
+
var fade = document.getElementById('overview-obj-fade');
|
|
1965
|
+
var toggle = document.getElementById('overview-obj-toggle');
|
|
1966
|
+
if (!wrap || !body || !toggle) return;
|
|
1967
|
+
|
|
1968
|
+
if (!objective) {
|
|
1969
|
+
wrap.style.display = 'none';
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
wrap.style.display = '';
|
|
1974
|
+
// Set text safely (escHtml then set innerHTML preserving paragraphs)
|
|
1975
|
+
body.innerHTML = '<div style="position:relative">' +
|
|
1976
|
+
escHtml(objective).replace(/\n\n+/g, '</div><div style="margin-top:0.6em">') +
|
|
1977
|
+
(fade ? '<div class="work-overview-obj-fade" id="overview-obj-fade-inner"></div>' : '') +
|
|
1978
|
+
'</div>';
|
|
1979
|
+
|
|
1980
|
+
// Restore uiState.objectiveOpen across re-renders (Change 3)
|
|
1981
|
+
if (uiState.objectiveOpen) {
|
|
1982
|
+
body.classList.add('expanded');
|
|
1983
|
+
toggle.textContent = 'Show less';
|
|
1984
|
+
} else {
|
|
1985
|
+
body.classList.remove('expanded');
|
|
1986
|
+
toggle.textContent = 'Show more';
|
|
1987
|
+
}
|
|
1988
|
+
toggle.onclick = function() {
|
|
1989
|
+
uiState.objectiveOpen = !uiState.objectiveOpen;
|
|
1990
|
+
if (uiState.objectiveOpen) {
|
|
1991
|
+
body.classList.add('expanded');
|
|
1992
|
+
toggle.textContent = 'Show less';
|
|
1993
|
+
} else {
|
|
1994
|
+
body.classList.remove('expanded');
|
|
1995
|
+
toggle.textContent = 'Show more';
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function _renderStatStrip(work) {
|
|
2001
|
+
var strip = document.getElementById('overview-stat-strip');
|
|
2002
|
+
if (!strip) return;
|
|
2003
|
+
|
|
2004
|
+
var path = work.work_path || null;
|
|
2005
|
+
var features = work.features || [];
|
|
2006
|
+
var deliverables = work.deliverables || [];
|
|
2007
|
+
var tasks = work.tasks || [];
|
|
2008
|
+
|
|
2009
|
+
// Total task count = actual tasks array length (DM-2 authoritative source)
|
|
2010
|
+
var totalTasks = tasks.length;
|
|
2011
|
+
|
|
2012
|
+
strip.innerHTML = '';
|
|
2013
|
+
strip.style.display = '';
|
|
2014
|
+
|
|
2015
|
+
function addBadge(text, cls) {
|
|
2016
|
+
var b = document.createElement('span');
|
|
2017
|
+
b.className = 'badge ' + (cls || '');
|
|
2018
|
+
b.textContent = text;
|
|
2019
|
+
strip.appendChild(b);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
if (path === 'full') {
|
|
2023
|
+
addBadge('Full', 'badge-info');
|
|
2024
|
+
} else if (path === 'lite') {
|
|
2025
|
+
addBadge('Lite', 'badge-purple');
|
|
2026
|
+
if (work.recipe) addBadge('Recipe: ' + work.recipe, '');
|
|
2027
|
+
} else if (path) {
|
|
2028
|
+
addBadge(path, '');
|
|
2029
|
+
}
|
|
2030
|
+
// Always show counts derivable regardless of path (features/deliverables/tasks)
|
|
2031
|
+
if (features.length > 0) addBadge(features.length + ' Feature' + (features.length !== 1 ? 's' : ''), '');
|
|
2032
|
+
if (deliverables.length > 0) addBadge(deliverables.length + ' Deliverable' + (deliverables.length !== 1 ? 's' : ''), '');
|
|
2033
|
+
if (totalTasks > 0) addBadge(totalTasks + ' Task' + (totalTasks !== 1 ? 's' : ''), 'badge-dim');
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function _renderFeaturesDisclosure(features, show) {
|
|
2037
|
+
var disc = document.getElementById('overview-features-disclosure');
|
|
2038
|
+
var toggle = document.getElementById('overview-features-toggle');
|
|
2039
|
+
var body = document.getElementById('overview-features-body');
|
|
2040
|
+
var label = document.getElementById('overview-features-label');
|
|
2041
|
+
if (!disc) return;
|
|
2042
|
+
|
|
2043
|
+
if (!show || !features || features.length === 0) {
|
|
2044
|
+
disc.style.display = 'none';
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
disc.style.display = '';
|
|
2049
|
+
if (label) label.textContent = 'Features (' + features.length + ')';
|
|
2050
|
+
|
|
2051
|
+
body.innerHTML = '';
|
|
2052
|
+
for (var i = 0; i < features.length; i++) {
|
|
2053
|
+
var f = features[i];
|
|
2054
|
+
var li = document.createElement('li');
|
|
2055
|
+
li.innerHTML = '<span class="disc-num">#' + f.number + '</span>' +
|
|
2056
|
+
escHtml(f.name || '');
|
|
2057
|
+
body.appendChild(li);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
_attachDisclosureToggle(toggle, body, 'featuresOpen');
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// Attach disclosure toggle, restoring uiState[stateKey] across re-renders (Change 3)
|
|
2064
|
+
function _attachDisclosureToggle(toggle, body, stateKey) {
|
|
2065
|
+
if (!toggle || !body) return;
|
|
2066
|
+
|
|
2067
|
+
// Restore from uiState
|
|
2068
|
+
if (uiState[stateKey]) {
|
|
2069
|
+
body.classList.add('open');
|
|
2070
|
+
toggle.classList.add('open');
|
|
2071
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
2072
|
+
} else {
|
|
2073
|
+
toggle.classList.remove('open');
|
|
2074
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
2075
|
+
body.classList.remove('open');
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
toggle.onclick = function() {
|
|
2079
|
+
var isOpen = body.classList.contains('open');
|
|
2080
|
+
if (isOpen) {
|
|
2081
|
+
body.classList.remove('open');
|
|
2082
|
+
toggle.classList.remove('open');
|
|
2083
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
2084
|
+
if (stateKey) uiState[stateKey] = false;
|
|
2085
|
+
} else {
|
|
2086
|
+
body.classList.add('open');
|
|
2087
|
+
toggle.classList.add('open');
|
|
2088
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
2089
|
+
if (stateKey) uiState[stateKey] = true;
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// ---------------------------------------------------------------------------
|
|
2095
|
+
// Attention strip (UI-4 — top-of-page for Paused / Blocked)
|
|
2096
|
+
// ---------------------------------------------------------------------------
|
|
2097
|
+
function renderAttentionStrip(work) {
|
|
2098
|
+
var strip = document.getElementById('attention-strip');
|
|
2099
|
+
if (!strip) return;
|
|
2100
|
+
|
|
2101
|
+
if (work.lifecycle === 'Paused-Awaiting-Input') {
|
|
2102
|
+
strip.style.display = '';
|
|
2103
|
+
var html = '<div class="callout warn"><h4>❚❚ Awaiting Input</h4>';
|
|
2104
|
+
if (work.pause_reason) {
|
|
2105
|
+
html += '<p>' + escHtml(work.pause_reason) + '</p>';
|
|
2106
|
+
}
|
|
2107
|
+
// pending_inputs list
|
|
2108
|
+
if (work.pending_inputs && work.pending_inputs.length > 0) {
|
|
2109
|
+
html += '<ul style="margin-top:0.5rem">';
|
|
2110
|
+
for (var i = 0; i < work.pending_inputs.length; i++) {
|
|
2111
|
+
var pi = work.pending_inputs[i];
|
|
2112
|
+
html += '<li><strong>' + escHtml(pi.question_id) + '</strong>';
|
|
2113
|
+
if (pi.category) html += ' <span class="meta">' + escHtml(pi.category) + '</span>';
|
|
2114
|
+
if (pi.impact) html += ' — impact: ' + escHtml(pi.impact);
|
|
2115
|
+
html += '</li>';
|
|
2116
|
+
}
|
|
2117
|
+
html += '</ul>';
|
|
2118
|
+
}
|
|
2119
|
+
html += '</div>';
|
|
2120
|
+
strip.innerHTML = html;
|
|
2121
|
+
} else if (work.lifecycle === 'Blocked') {
|
|
2122
|
+
strip.style.display = '';
|
|
2123
|
+
var html2 = '<div class="callout err"><h4>✕ Blocked</h4>';
|
|
2124
|
+
if (work.block_reason) {
|
|
2125
|
+
html2 += '<p>' + escHtml(work.block_reason) + '</p>';
|
|
2126
|
+
}
|
|
2127
|
+
if (work.block_artifact) {
|
|
2128
|
+
html2 += '<p>Artifact: <code>' + escHtml(work.block_artifact) + '</code></p>';
|
|
2129
|
+
}
|
|
2130
|
+
html2 += '</div>';
|
|
2131
|
+
strip.innerHTML = html2;
|
|
2132
|
+
} else {
|
|
2133
|
+
strip.style.display = 'none';
|
|
2134
|
+
strip.innerHTML = '';
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// ---------------------------------------------------------------------------
|
|
2139
|
+
// Stage rail (UI-2)
|
|
2140
|
+
// ---------------------------------------------------------------------------
|
|
2141
|
+
function renderStageRail(work) {
|
|
2142
|
+
var rail = document.getElementById('stage-rail');
|
|
2143
|
+
if (!rail) return;
|
|
2144
|
+
rail.innerHTML = '';
|
|
2145
|
+
|
|
2146
|
+
var phase = work.phase; // may be null or "Unknown"
|
|
2147
|
+
if (!phase || phase === 'Unknown') {
|
|
2148
|
+
// PF-4/PF-7: neutral sentinel — not a garbage "phase unknown" error badge
|
|
2149
|
+
var notYetPill = document.createElement('span');
|
|
2150
|
+
notYetPill.className = 'badge badge-dim';
|
|
2151
|
+
notYetPill.setAttribute('role', 'listitem');
|
|
2152
|
+
notYetPill.textContent = '— phase not yet recorded';
|
|
2153
|
+
rail.appendChild(notYetPill);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
var currentIdx = PHASE_ORDER.indexOf(phase);
|
|
2158
|
+
// If phase not in canonical order (future/unrecognized), treat as not-yet-recorded (PF-7 forward-compat)
|
|
2159
|
+
if (currentIdx === -1) {
|
|
2160
|
+
var notYetPill2 = document.createElement('span');
|
|
2161
|
+
notYetPill2.className = 'badge badge-dim';
|
|
2162
|
+
notYetPill2.setAttribute('role', 'listitem');
|
|
2163
|
+
notYetPill2.textContent = '— phase not yet recorded';
|
|
2164
|
+
rail.appendChild(notYetPill2);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
for (var i = 0; i < PHASE_ORDER.length; i++) {
|
|
2169
|
+
var p = PHASE_ORDER[i];
|
|
2170
|
+
var pill = document.createElement('span');
|
|
2171
|
+
pill.setAttribute('role', 'listitem');
|
|
2172
|
+
pill.setAttribute('aria-current', i === currentIdx ? 'step' : 'false');
|
|
2173
|
+
|
|
2174
|
+
if (i < currentIdx) {
|
|
2175
|
+
// Prior — done
|
|
2176
|
+
pill.className = 'badge badge-ok';
|
|
2177
|
+
pill.textContent = '✓ ' + p;
|
|
2178
|
+
} else if (i === currentIdx) {
|
|
2179
|
+
// Current — emphasized
|
|
2180
|
+
pill.className = 'badge badge-primary';
|
|
2181
|
+
pill.textContent = '▸ ' + p;
|
|
2182
|
+
} else {
|
|
2183
|
+
// Later — upcoming
|
|
2184
|
+
pill.className = 'badge badge-dim';
|
|
2185
|
+
pill.textContent = '○ ' + p;
|
|
2186
|
+
}
|
|
2187
|
+
rail.appendChild(pill);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// ---------------------------------------------------------------------------
|
|
2192
|
+
// Task section: Delivery > Lane > Task hierarchy (UI-3)
|
|
2193
|
+
// ---------------------------------------------------------------------------
|
|
2194
|
+
|
|
2195
|
+
// Parse integer task number from task_id "task-008" -> 8
|
|
2196
|
+
function parseTaskNumber(taskId) {
|
|
2197
|
+
if (!taskId) return null;
|
|
2198
|
+
var m = String(taskId).match(/(\d+)$/);
|
|
2199
|
+
return m ? parseInt(m[1], 10) : null;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// Derive aggregate state from a list of tasks
|
|
2203
|
+
function getGroupState(tasks) {
|
|
2204
|
+
// Returns: 'done' | 'pending' | 'active' | 'mixed'
|
|
2205
|
+
var allDone = tasks.every(function(t) { return t.status === 'Done' || t.status === 'Canceled'; });
|
|
2206
|
+
if (allDone) return 'done';
|
|
2207
|
+
var allPending = tasks.every(function(t) { return t.status === 'Pending'; });
|
|
2208
|
+
if (allPending) return 'pending';
|
|
2209
|
+
var anyActive = tasks.some(function(t) { return t.status === 'In Progress' || t.status === 'In Review'; });
|
|
2210
|
+
if (anyActive) return 'active';
|
|
2211
|
+
return 'mixed';
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// Make a status badge for a delivery or lane aggregate state
|
|
2215
|
+
function makeGroupBadge(state, tasks) {
|
|
2216
|
+
var badge = document.createElement('span');
|
|
2217
|
+
if (state === 'done') {
|
|
2218
|
+
badge.className = 'badge badge-ok';
|
|
2219
|
+
badge.textContent = '✓ Done';
|
|
2220
|
+
} else if (state === 'pending') {
|
|
2221
|
+
badge.className = 'badge badge-dim';
|
|
2222
|
+
badge.textContent = '○ Pending';
|
|
2223
|
+
} else if (state === 'active') {
|
|
2224
|
+
var activeCount = tasks.filter(function(t) { return t.status === 'In Progress' || t.status === 'In Review'; }).length;
|
|
2225
|
+
badge.className = 'badge badge-accent';
|
|
2226
|
+
badge.textContent = '▶ In Progress (' + activeCount + ')';
|
|
2227
|
+
} else {
|
|
2228
|
+
// mixed: show done + pending counts
|
|
2229
|
+
var doneC = tasks.filter(function(t) { return t.status === 'Done' || t.status === 'Canceled'; }).length;
|
|
2230
|
+
var pendC = tasks.filter(function(t) { return t.status === 'Pending'; }).length;
|
|
2231
|
+
badge.className = 'badge badge-dim';
|
|
2232
|
+
badge.textContent = doneC + ' Done · ' + pendC + ' Pending';
|
|
2233
|
+
}
|
|
2234
|
+
return badge;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function renderTasks(work) {
|
|
2238
|
+
var section = document.getElementById('tasks-section');
|
|
2239
|
+
if (!section) return;
|
|
2240
|
+
section.innerHTML = '';
|
|
2241
|
+
|
|
2242
|
+
var tasks = work.tasks || [];
|
|
2243
|
+
if (tasks.length === 0) {
|
|
2244
|
+
section.innerHTML = '<p class="meta" style="color:var(--text-dim)">No tasks recorded.</p>';
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
var workPath = work.work_path || 'full';
|
|
2249
|
+
|
|
2250
|
+
if (workPath === 'full') {
|
|
2251
|
+
renderTasksFull(section, tasks, work.work_id);
|
|
2252
|
+
} else {
|
|
2253
|
+
// Lite: render lanes at top level (no delivery wrapper)
|
|
2254
|
+
renderLanes(section, tasks, null, work.work_id);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function renderTasksFull(container, tasks, workId) {
|
|
2259
|
+
// Group by task.delivery (real integer from reader; null -> "unsequenced" bucket)
|
|
2260
|
+
// Never uses parseWave() or the invented delivery-NNN-wave-M string.
|
|
2261
|
+
var deliveryOrder = []; // integer delivery numbers, ascending (null last)
|
|
2262
|
+
var deliveryMap = {}; // dKey -> task[] where dKey is integer or null
|
|
2263
|
+
|
|
2264
|
+
for (var i = 0; i < tasks.length; i++) {
|
|
2265
|
+
var t = tasks[i];
|
|
2266
|
+
// task.delivery is an integer (from STATE Wave delivery-NNN) or null
|
|
2267
|
+
var dNum = (t.delivery != null) ? t.delivery : null;
|
|
2268
|
+
var dMapKey = (dNum != null) ? dNum : '__unseq__';
|
|
2269
|
+
if (!deliveryMap[dMapKey]) {
|
|
2270
|
+
deliveryMap[dMapKey] = [];
|
|
2271
|
+
deliveryOrder.push(dMapKey);
|
|
2272
|
+
}
|
|
2273
|
+
deliveryMap[dMapKey].push(t);
|
|
2274
|
+
}
|
|
2275
|
+
// Sort: real delivery numbers ascending; null/unseq bucket last
|
|
2276
|
+
deliveryOrder.sort(function(a, b) {
|
|
2277
|
+
if (a === '__unseq__') return 1;
|
|
2278
|
+
if (b === '__unseq__') return -1;
|
|
2279
|
+
return a - b;
|
|
2280
|
+
});
|
|
2281
|
+
|
|
2282
|
+
for (var di = 0; di < deliveryOrder.length; di++) {
|
|
2283
|
+
var dMapKey2 = deliveryOrder[di];
|
|
2284
|
+
var dNum2 = (dMapKey2 !== '__unseq__') ? dMapKey2 : null;
|
|
2285
|
+
var dTasks = deliveryMap[dMapKey2];
|
|
2286
|
+
var dState = getGroupState(dTasks);
|
|
2287
|
+
// uiState key: "d<N>" for real deliveries, "d-unseq" for unsequenced
|
|
2288
|
+
var dStateKey = (dNum2 != null) ? ('d' + dNum2) : 'd-unseq';
|
|
2289
|
+
|
|
2290
|
+
// Default open: any active task -> expanded; otherwise collapsed
|
|
2291
|
+
var hasActive = dTasks.some(function(t) { return t.status === 'In Progress' || t.status === 'In Review'; });
|
|
2292
|
+
if (!(dStateKey in uiState.deliveries)) {
|
|
2293
|
+
uiState.deliveries[dStateKey] = hasActive;
|
|
2294
|
+
}
|
|
2295
|
+
var dOpen = uiState.deliveries[dStateKey];
|
|
2296
|
+
|
|
2297
|
+
// Build delivery panel
|
|
2298
|
+
var panel = document.createElement('div');
|
|
2299
|
+
panel.className = 'delivery-panel';
|
|
2300
|
+
|
|
2301
|
+
var summaryBtn = document.createElement('button');
|
|
2302
|
+
summaryBtn.type = 'button';
|
|
2303
|
+
summaryBtn.className = 'delivery-panel-summary' + (dOpen ? ' open' : '');
|
|
2304
|
+
summaryBtn.setAttribute('aria-expanded', dOpen ? 'true' : 'false');
|
|
2305
|
+
|
|
2306
|
+
var chevron = document.createElement('span');
|
|
2307
|
+
chevron.className = 'delivery-panel-chevron';
|
|
2308
|
+
chevron.setAttribute('aria-hidden', 'true');
|
|
2309
|
+
chevron.textContent = '►';
|
|
2310
|
+
summaryBtn.appendChild(chevron);
|
|
2311
|
+
|
|
2312
|
+
var labelEl = document.createElement('span');
|
|
2313
|
+
labelEl.className = 'delivery-panel-label';
|
|
2314
|
+
// PF-5/PF-7: real integer delivery number; null -> "Unsequenced" (never "Delivery #0")
|
|
2315
|
+
labelEl.textContent = (dNum2 != null) ? ('Delivery #' + dNum2) : 'Unsequenced';
|
|
2316
|
+
summaryBtn.appendChild(labelEl);
|
|
2317
|
+
|
|
2318
|
+
summaryBtn.appendChild(makeGroupBadge(dState, dTasks));
|
|
2319
|
+
|
|
2320
|
+
var body = document.createElement('div');
|
|
2321
|
+
body.className = 'delivery-panel-body' + (dOpen ? ' open' : '');
|
|
2322
|
+
|
|
2323
|
+
// Render lanes inside the body (pass dNum2 for delivery-scoped uiState keys)
|
|
2324
|
+
renderLanes(body, dTasks, dNum2, workId);
|
|
2325
|
+
|
|
2326
|
+
// Toggle handler (closure over dStateKey, summaryBtn, body)
|
|
2327
|
+
(function(sk, btn, bd) {
|
|
2328
|
+
btn.onclick = function() {
|
|
2329
|
+
var nowOpen = bd.classList.contains('open');
|
|
2330
|
+
if (nowOpen) {
|
|
2331
|
+
bd.classList.remove('open');
|
|
2332
|
+
btn.classList.remove('open');
|
|
2333
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
2334
|
+
uiState.deliveries[sk] = false;
|
|
2335
|
+
} else {
|
|
2336
|
+
bd.classList.add('open');
|
|
2337
|
+
btn.classList.add('open');
|
|
2338
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
2339
|
+
uiState.deliveries[sk] = true;
|
|
2340
|
+
}
|
|
2341
|
+
};
|
|
2342
|
+
})(dStateKey, summaryBtn, body);
|
|
2343
|
+
|
|
2344
|
+
panel.appendChild(summaryBtn);
|
|
2345
|
+
panel.appendChild(body);
|
|
2346
|
+
container.appendChild(panel);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function renderLanes(container, tasks, deliveryNum, workId) {
|
|
2351
|
+
// Group by task.lane (integer from PLAN execution graph; null -> "unsequenced" lane)
|
|
2352
|
+
// deliveryNum: the integer delivery number (or null for the unsequenced delivery bucket)
|
|
2353
|
+
// uiState key is delivery-scoped: "d<D>-lane<L>" or "d<D>-unseq" — never collides across deliveries
|
|
2354
|
+
var laneOrder = []; // lane integer keys (ascending), null last
|
|
2355
|
+
var laneMap = {}; // lMapKey -> { num: int|null, tasks: [] }
|
|
2356
|
+
|
|
2357
|
+
for (var i = 0; i < tasks.length; i++) {
|
|
2358
|
+
var t = tasks[i];
|
|
2359
|
+
// task.lane is an integer (from PLAN wave) or null
|
|
2360
|
+
var lNum = (t.lane != null) ? t.lane : null;
|
|
2361
|
+
var lMapKey = (lNum != null) ? lNum : '__unseq__';
|
|
2362
|
+
if (!laneMap[lMapKey]) {
|
|
2363
|
+
laneMap[lMapKey] = { num: lNum, tasks: [] };
|
|
2364
|
+
laneOrder.push(lMapKey);
|
|
2365
|
+
}
|
|
2366
|
+
laneMap[lMapKey].tasks.push(t);
|
|
2367
|
+
}
|
|
2368
|
+
// Sort: real lane numbers ascending; null/unseq bucket last
|
|
2369
|
+
laneOrder.sort(function(a, b) {
|
|
2370
|
+
if (a === '__unseq__') return 1;
|
|
2371
|
+
if (b === '__unseq__') return -1;
|
|
2372
|
+
return a - b;
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
for (var li = 0; li < laneOrder.length; li++) {
|
|
2376
|
+
var lMapKey2 = laneOrder[li];
|
|
2377
|
+
var lEntry = laneMap[lMapKey2];
|
|
2378
|
+
var lNum2 = lEntry.num; // integer or null
|
|
2379
|
+
var lTasks = lEntry.tasks;
|
|
2380
|
+
var lState = getGroupState(lTasks);
|
|
2381
|
+
|
|
2382
|
+
// Delivery-scoped uiState key (PF-5): "d<D>-lane<L>" or "d<D>-unseq"
|
|
2383
|
+
// For the unsequenced delivery bucket, deliveryNum is null -> "d-unseq-lane<L>" or "d-unseq-unseq"
|
|
2384
|
+
var dPart = (deliveryNum != null) ? ('d' + deliveryNum) : 'd-unseq';
|
|
2385
|
+
var lUiKey = (lNum2 != null) ? (dPart + '-lane' + lNum2) : (dPart + '-unseq');
|
|
2386
|
+
|
|
2387
|
+
// Default open: any active task -> expanded; otherwise collapsed
|
|
2388
|
+
var lHasActive = lTasks.some(function(t) { return t.status === 'In Progress' || t.status === 'In Review'; });
|
|
2389
|
+
if (!(lUiKey in uiState.lanes)) {
|
|
2390
|
+
uiState.lanes[lUiKey] = lHasActive;
|
|
2391
|
+
}
|
|
2392
|
+
var lOpen = uiState.lanes[lUiKey];
|
|
2393
|
+
|
|
2394
|
+
var lPanel = document.createElement('div');
|
|
2395
|
+
lPanel.className = 'lane-panel';
|
|
2396
|
+
|
|
2397
|
+
var lSummary = document.createElement('button');
|
|
2398
|
+
lSummary.type = 'button';
|
|
2399
|
+
lSummary.className = 'lane-panel-summary' + (lOpen ? ' open' : '');
|
|
2400
|
+
lSummary.setAttribute('aria-expanded', lOpen ? 'true' : 'false');
|
|
2401
|
+
|
|
2402
|
+
var lChevron = document.createElement('span');
|
|
2403
|
+
lChevron.className = 'lane-panel-chevron';
|
|
2404
|
+
lChevron.setAttribute('aria-hidden', 'true');
|
|
2405
|
+
lChevron.textContent = '►';
|
|
2406
|
+
lSummary.appendChild(lChevron);
|
|
2407
|
+
|
|
2408
|
+
// Lane label: "Lane N" or "Unsequenced" (never "#0" lane — PF-7)
|
|
2409
|
+
var lLabelEl = document.createElement('span');
|
|
2410
|
+
lLabelEl.className = 'lane-panel-label';
|
|
2411
|
+
lLabelEl.textContent = (lNum2 != null) ? ('Lane ' + lNum2) : 'Unsequenced';
|
|
2412
|
+
lSummary.appendChild(lLabelEl);
|
|
2413
|
+
|
|
2414
|
+
lSummary.appendChild(makeGroupBadge(lState, lTasks));
|
|
2415
|
+
|
|
2416
|
+
var lBody = document.createElement('div');
|
|
2417
|
+
lBody.className = 'lane-panel-body' + (lOpen ? ' open' : '');
|
|
2418
|
+
|
|
2419
|
+
// Task cards laid out horizontally (parallel) using adaptive chip grid
|
|
2420
|
+
var grid = document.createElement('div');
|
|
2421
|
+
grid.className = 'grid g-lane';
|
|
2422
|
+
for (var ti = 0; ti < lTasks.length; ti++) {
|
|
2423
|
+
grid.appendChild(makeTaskChip(lTasks[ti], workId));
|
|
2424
|
+
}
|
|
2425
|
+
lBody.appendChild(grid);
|
|
2426
|
+
|
|
2427
|
+
// Toggle handler (closure over lUiKey)
|
|
2428
|
+
(function(lk, btn, bd) {
|
|
2429
|
+
btn.onclick = function() {
|
|
2430
|
+
var nowOpen = bd.classList.contains('open');
|
|
2431
|
+
if (nowOpen) {
|
|
2432
|
+
bd.classList.remove('open');
|
|
2433
|
+
btn.classList.remove('open');
|
|
2434
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
2435
|
+
uiState.lanes[lk] = false;
|
|
2436
|
+
} else {
|
|
2437
|
+
bd.classList.add('open');
|
|
2438
|
+
btn.classList.add('open');
|
|
2439
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
2440
|
+
uiState.lanes[lk] = true;
|
|
2441
|
+
}
|
|
2442
|
+
};
|
|
2443
|
+
})(lUiKey, lSummary, lBody);
|
|
2444
|
+
|
|
2445
|
+
lPanel.appendChild(lSummary);
|
|
2446
|
+
lPanel.appendChild(lBody);
|
|
2447
|
+
container.appendChild(lPanel);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function makeTaskChip(task, workId) {
|
|
2452
|
+
var card = document.createElement('div');
|
|
2453
|
+
card.className = 'task-chip';
|
|
2454
|
+
|
|
2455
|
+
// SEAM-2: make chip clickable — navigate to task drill view
|
|
2456
|
+
if (workId && task.task_id) {
|
|
2457
|
+
card.style.cursor = 'pointer';
|
|
2458
|
+
card.title = 'Drill into ' + task.task_id;
|
|
2459
|
+
(function(wid, tid) {
|
|
2460
|
+
card.onclick = function() {
|
|
2461
|
+
location.hash = '#/work/' + encodeURIComponent(wid) + '/task/' + encodeURIComponent(tid);
|
|
2462
|
+
};
|
|
2463
|
+
})(workId, task.task_id);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
// Visual state class for active/pending/done
|
|
2467
|
+
var s = task.status;
|
|
2468
|
+
if (s === 'In Progress' || s === 'In Review') {
|
|
2469
|
+
card.classList.add('chip-active');
|
|
2470
|
+
} else if (s === 'Pending') {
|
|
2471
|
+
card.classList.add('chip-pending');
|
|
2472
|
+
} else if (s === 'Done' || s === 'Canceled') {
|
|
2473
|
+
card.classList.add('chip-done');
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// Line 1: #N + TYPE + status badge
|
|
2477
|
+
var topRow = document.createElement('div');
|
|
2478
|
+
topRow.className = 'chip-top';
|
|
2479
|
+
|
|
2480
|
+
var idEl = document.createElement('span');
|
|
2481
|
+
idEl.className = 'chip-task-id';
|
|
2482
|
+
var taskNum = parseTaskNumber(task.task_id);
|
|
2483
|
+
idEl.textContent = taskNum !== null ? '#' + taskNum : (task.task_id || '—');
|
|
2484
|
+
topRow.appendChild(idEl);
|
|
2485
|
+
|
|
2486
|
+
var typeEl = document.createElement('span');
|
|
2487
|
+
typeEl.className = 'chip-type';
|
|
2488
|
+
typeEl.textContent = task.type || '—';
|
|
2489
|
+
topRow.appendChild(typeEl);
|
|
2490
|
+
|
|
2491
|
+
card.appendChild(topRow);
|
|
2492
|
+
|
|
2493
|
+
// Status badge (line 1 continues)
|
|
2494
|
+
var statusBadge = makeTaskStatusBadge(task.status);
|
|
2495
|
+
card.appendChild(statusBadge);
|
|
2496
|
+
|
|
2497
|
+
// Line 2: short_name from task file header (PF-3); fallback to task_id when null (PF-7)
|
|
2498
|
+
var shortName = (task.short_name != null && String(task.short_name).trim()) ? task.short_name : task.task_id;
|
|
2499
|
+
var nameEl = document.createElement('div');
|
|
2500
|
+
nameEl.className = 'chip-short-name';
|
|
2501
|
+
nameEl.textContent = shortName || '';
|
|
2502
|
+
card.appendChild(nameEl);
|
|
2503
|
+
|
|
2504
|
+
return card;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// ---------------------------------------------------------------------------
|
|
2508
|
+
// Badge factories
|
|
2509
|
+
// ---------------------------------------------------------------------------
|
|
2510
|
+
|
|
2511
|
+
// Lifecycle → badge (UI-4)
|
|
2512
|
+
function makeLifecycleBadge(lifecycle) {
|
|
2513
|
+
var LIFECYCLE_MAP = {
|
|
2514
|
+
'Running': { cls: 'badge-accent', glyph: '▶', word: 'Running' },
|
|
2515
|
+
'Paused-Awaiting-Input': { cls: 'badge-warn', glyph: '❚❚', word: 'Input' },
|
|
2516
|
+
'Blocked': { cls: 'badge-err', glyph: '✕', word: 'Blocked' },
|
|
2517
|
+
'Completed': { cls: 'badge-ok', glyph: '✓', word: 'Done' },
|
|
2518
|
+
'Canceled': { cls: 'badge-dim', glyph: '⊘', word: 'Canceled' }
|
|
2519
|
+
};
|
|
2520
|
+
var badge = document.createElement('span');
|
|
2521
|
+
var mapping = LIFECYCLE_MAP[lifecycle];
|
|
2522
|
+
if (mapping) {
|
|
2523
|
+
badge.className = 'badge ' + mapping.cls;
|
|
2524
|
+
badge.textContent = mapping.glyph + ' ' + mapping.word;
|
|
2525
|
+
} else {
|
|
2526
|
+
// Unknown or unrecognized: neutral badge (NFR7 forward-compat)
|
|
2527
|
+
badge.className = 'badge';
|
|
2528
|
+
badge.textContent = '? ' + (lifecycle || 'Unknown');
|
|
2529
|
+
}
|
|
2530
|
+
return badge;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// TaskStatus → badge (UI-3 / UI-4)
|
|
2534
|
+
function makeTaskStatusBadge(status) {
|
|
2535
|
+
var STATUS_MAP = {
|
|
2536
|
+
'Pending': { cls: 'badge-dim', glyph: '○', word: 'Pending' },
|
|
2537
|
+
'In Progress': { cls: 'badge-accent', glyph: '▶', word: 'In Progress' },
|
|
2538
|
+
'In Review': { cls: 'badge-info', glyph: '◑', word: 'In Review' },
|
|
2539
|
+
'Blocked': { cls: 'badge-err', glyph: '✕', word: 'Blocked' },
|
|
2540
|
+
'Done': { cls: 'badge-ok', glyph: '✓', word: 'Done' },
|
|
2541
|
+
'Failed': { cls: 'badge-err', glyph: '✕', word: 'Failed' },
|
|
2542
|
+
'Canceled': { cls: 'badge-dim', glyph: '⊘', word: 'Canceled' },
|
|
2543
|
+
'Unknown': { cls: '', glyph: '?', word: 'Unknown' }
|
|
2544
|
+
};
|
|
2545
|
+
var badge = document.createElement('span');
|
|
2546
|
+
badge.style.marginTop = '0.3rem';
|
|
2547
|
+
var mapping = STATUS_MAP[status];
|
|
2548
|
+
if (mapping) {
|
|
2549
|
+
badge.className = 'badge ' + mapping.cls;
|
|
2550
|
+
badge.textContent = mapping.glyph + ' ' + mapping.word;
|
|
2551
|
+
} else {
|
|
2552
|
+
// Unrecognized status: neutral badge (NFR7 forward-compat, never throws)
|
|
2553
|
+
badge.className = 'badge';
|
|
2554
|
+
badge.textContent = '? ' + (status || 'Unknown');
|
|
2555
|
+
}
|
|
2556
|
+
return badge;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// ---------------------------------------------------------------------------
|
|
2560
|
+
// Parse warnings / data note chip (Telemetry) — Change 1
|
|
2561
|
+
// ---------------------------------------------------------------------------
|
|
2562
|
+
function renderParseWarnings(readMeta) {
|
|
2563
|
+
var chipEl = document.getElementById('data-note-chip');
|
|
2564
|
+
if (!chipEl) return;
|
|
2565
|
+
|
|
2566
|
+
if (!readMeta) { chipEl.style.display = 'none'; return; }
|
|
2567
|
+
|
|
2568
|
+
var warnings = readMeta.parse_warnings || [];
|
|
2569
|
+
var fallbacks = readMeta.fallback_works || [];
|
|
2570
|
+
|
|
2571
|
+
if (warnings.length === 0 && fallbacks.length === 0) {
|
|
2572
|
+
chipEl.style.display = 'none';
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// Build compact label
|
|
2577
|
+
var labelParts = [];
|
|
2578
|
+
if (warnings.length > 0) labelParts.push(warnings.length + ' warning' + (warnings.length !== 1 ? 's' : ''));
|
|
2579
|
+
if (fallbacks.length > 0) labelParts.push(fallbacks.length + ' fallback' + (fallbacks.length !== 1 ? 's' : ''));
|
|
2580
|
+
chipEl.textContent = '! ' + labelParts.join(', ');
|
|
2581
|
+
|
|
2582
|
+
// Full tooltip text
|
|
2583
|
+
var titleParts = [];
|
|
2584
|
+
if (warnings.length > 0) titleParts.push(warnings.length + ' parse warning(s) this read');
|
|
2585
|
+
if (fallbacks.length > 0) titleParts.push(fallbacks.length + ' work(s) read via fallback derivation');
|
|
2586
|
+
chipEl.title = titleParts.join(' · ');
|
|
2587
|
+
|
|
2588
|
+
chipEl.style.display = '';
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
// ---------------------------------------------------------------------------
|
|
2592
|
+
// Utility: HTML-escape
|
|
2593
|
+
// ---------------------------------------------------------------------------
|
|
2594
|
+
function escHtml(str) {
|
|
2595
|
+
if (!str) return '';
|
|
2596
|
+
return String(str)
|
|
2597
|
+
.replace(/&/g, '&')
|
|
2598
|
+
.replace(/</g, '<')
|
|
2599
|
+
.replace(/>/g, '>')
|
|
2600
|
+
.replace(/"/g, '"');
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// ---------------------------------------------------------------------------
|
|
2604
|
+
// Theme toggle (from html-skeleton.html)
|
|
2605
|
+
// ---------------------------------------------------------------------------
|
|
2606
|
+
function initTheme() {
|
|
2607
|
+
var saved = localStorage.getItem('aid-dashboard-theme');
|
|
2608
|
+
if (saved === 'dark' || saved === 'light') {
|
|
2609
|
+
applyTheme(saved);
|
|
2610
|
+
}
|
|
2611
|
+
var btn = document.getElementById('theme-toggle');
|
|
2612
|
+
if (btn) {
|
|
2613
|
+
btn.addEventListener('click', function () {
|
|
2614
|
+
var current = document.documentElement.getAttribute('data-theme') || 'light';
|
|
2615
|
+
var next = current === 'light' ? 'dark' : 'light';
|
|
2616
|
+
applyTheme(next);
|
|
2617
|
+
localStorage.setItem('aid-dashboard-theme', next);
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
function applyTheme(theme) {
|
|
2623
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
2624
|
+
var icon = document.getElementById('theme-icon');
|
|
2625
|
+
var label = document.getElementById('theme-label');
|
|
2626
|
+
if (theme === 'dark') {
|
|
2627
|
+
if (icon) icon.textContent = '◐';
|
|
2628
|
+
if (label) label.textContent = 'Light';
|
|
2629
|
+
} else {
|
|
2630
|
+
if (icon) icon.textContent = '◑';
|
|
2631
|
+
if (label) label.textContent = 'Dark';
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// ---------------------------------------------------------------------------
|
|
2636
|
+
// escRawState: HTML-escape for raw STATE.md content (R15, AC4)
|
|
2637
|
+
// Escapes <, >, &, U+2028 (LS), U+2029 (PS) so arbitrary .aid/ content
|
|
2638
|
+
// cannot inject into the page. Separate from escHtml so injections via
|
|
2639
|
+
// line-separator codepoints are always neutralised.
|
|
2640
|
+
// ---------------------------------------------------------------------------
|
|
2641
|
+
function escRawState(str) {
|
|
2642
|
+
if (!str) return '';
|
|
2643
|
+
return String(str)
|
|
2644
|
+
.replace(/&/g, '&')
|
|
2645
|
+
.replace(/</g, '<')
|
|
2646
|
+
.replace(/>/g, '>')
|
|
2647
|
+
.replace(/\u2028/g, '
')
|
|
2648
|
+
.replace(/\u2029/g, '
');
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// ---------------------------------------------------------------------------
|
|
2652
|
+
// NAV-1: renderBreadcrumb — router-driven, called from route-independent shell head
|
|
2653
|
+
// ---------------------------------------------------------------------------
|
|
2654
|
+
function renderBreadcrumb(model, route) {
|
|
2655
|
+
var el = document.getElementById('breadcrumb-trail');
|
|
2656
|
+
if (!el) return;
|
|
2657
|
+
el.innerHTML = '';
|
|
2658
|
+
|
|
2659
|
+
// Helper: make a breadcrumb link. Both absolute ("/", location.pathname) and
|
|
2660
|
+
// hash ("#/work/<id>") hrefs are assigned the same way — the browser resolves
|
|
2661
|
+
// each relative to the current document, so no branch is needed.
|
|
2662
|
+
function _bcLink(label, href) {
|
|
2663
|
+
var a = document.createElement('a');
|
|
2664
|
+
a.textContent = label;
|
|
2665
|
+
a.href = href;
|
|
2666
|
+
a.style.color = 'inherit';
|
|
2667
|
+
return a;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// Helper: separator span
|
|
2671
|
+
function _sep() {
|
|
2672
|
+
var s = document.createElement('span');
|
|
2673
|
+
s.className = 'sep';
|
|
2674
|
+
s.textContent = '›';
|
|
2675
|
+
return s;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// Helper: leaf span
|
|
2679
|
+
function _leaf(label) {
|
|
2680
|
+
var s = document.createElement('span');
|
|
2681
|
+
s.className = 'current';
|
|
2682
|
+
s.textContent = label;
|
|
2683
|
+
return s;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
var projectName = (model.repo && model.repo.project_name) ? model.repo.project_name : 'AID Dashboard';
|
|
2687
|
+
|
|
2688
|
+
// Level 1 (Main) is ALWAYS the first crumb: the CLI home (absolute "/") listing
|
|
2689
|
+
// every project on this machine. Labeled "Home" (not the product name) so it never
|
|
2690
|
+
// collides with a project that happens to be named "AID".
|
|
2691
|
+
el.appendChild(_bcLink('Home', '/'));
|
|
2692
|
+
el.appendChild(_sep());
|
|
2693
|
+
|
|
2694
|
+
if (route.view === 'work' || route.view === 'task') {
|
|
2695
|
+
// Home › <project> (link) › <pipeline> (leaf or link) [› <task> (leaf)]
|
|
2696
|
+
el.appendChild(_bcLink(projectName, location.pathname));
|
|
2697
|
+
el.appendChild(_sep());
|
|
2698
|
+
|
|
2699
|
+
// Pipeline label: prefer work.title; de-slug fallback
|
|
2700
|
+
var workObj = findWorkById(model.works || [], route.workId);
|
|
2701
|
+
var pipelineLabel;
|
|
2702
|
+
if (workObj) {
|
|
2703
|
+
if (workObj.title) {
|
|
2704
|
+
pipelineLabel = workObj.title;
|
|
2705
|
+
} else {
|
|
2706
|
+
pipelineLabel = (workObj.work_id || '')
|
|
2707
|
+
.replace(/^work-\d+-/, '')
|
|
2708
|
+
.replace(/[-_]+/g, ' ')
|
|
2709
|
+
.replace(/\b\w/g, function(c) { return c.toUpperCase(); }) || workObj.work_id || route.workId;
|
|
2710
|
+
}
|
|
2711
|
+
} else {
|
|
2712
|
+
pipelineLabel = route.workId;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
if (route.view === 'work') {
|
|
2716
|
+
// Pipeline is leaf
|
|
2717
|
+
el.appendChild(_leaf(pipelineLabel));
|
|
2718
|
+
} else {
|
|
2719
|
+
// task view: Pipeline is a link → #/work/<work_id>; Task is leaf
|
|
2720
|
+
el.appendChild(_bcLink(pipelineLabel, '#/work/' + encodeURIComponent(route.workId)));
|
|
2721
|
+
el.appendChild(_sep());
|
|
2722
|
+
// Display the task as "Task #<n>" (drop the "task-" prefix + leading zeros);
|
|
2723
|
+
// fall back to the raw id if it has no trailing number.
|
|
2724
|
+
var _tnum = (String(route.taskId).match(/(\d+)\s*$/) || [])[1];
|
|
2725
|
+
el.appendChild(_leaf(_tnum ? ('Task #' + parseInt(_tnum, 10)) : route.taskId));
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
} else {
|
|
2729
|
+
// main (repo overview) / KB / unknown → Home › <project> (leaf, the level you're on)
|
|
2730
|
+
el.appendChild(_leaf(projectName));
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// ---------------------------------------------------------------------------
|
|
2735
|
+
// renderTaskView: SEAM-2 drill view (task-071)
|
|
2736
|
+
// ---------------------------------------------------------------------------
|
|
2737
|
+
function renderTaskView(model, route) {
|
|
2738
|
+
var container = document.getElementById('task-view');
|
|
2739
|
+
if (!container) return;
|
|
2740
|
+
container.innerHTML = '';
|
|
2741
|
+
|
|
2742
|
+
var workId = route.workId;
|
|
2743
|
+
var taskId = route.taskId;
|
|
2744
|
+
var detailKey = workId + '/' + taskId;
|
|
2745
|
+
|
|
2746
|
+
// Find the work
|
|
2747
|
+
var work = findWorkById(model.works || [], workId);
|
|
2748
|
+
if (!work) {
|
|
2749
|
+
// Work no longer in state (FC-3 parallel: task version)
|
|
2750
|
+
var callout = document.createElement('div');
|
|
2751
|
+
callout.className = 'callout warn';
|
|
2752
|
+
callout.innerHTML = '<h4>Work not found</h4>' +
|
|
2753
|
+
'<p>The work <code>' + escHtml(workId) + '</code> is no longer in this repo.</p>';
|
|
2754
|
+
container.appendChild(callout);
|
|
2755
|
+
var backP = document.createElement('p');
|
|
2756
|
+
backP.style.marginTop = '0.75rem';
|
|
2757
|
+
var backA = document.createElement('a');
|
|
2758
|
+
backA.href = '#/';
|
|
2759
|
+
backA.textContent = '← back to main';
|
|
2760
|
+
backP.appendChild(backA);
|
|
2761
|
+
container.appendChild(backP);
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// Find the task within the work
|
|
2766
|
+
var task = null;
|
|
2767
|
+
var tasks = work.tasks || [];
|
|
2768
|
+
for (var i = 0; i < tasks.length; i++) {
|
|
2769
|
+
if (tasks[i].task_id === taskId) { task = tasks[i]; break; }
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// Back link + at-a-glance header
|
|
2773
|
+
var headerDiv = document.createElement('div');
|
|
2774
|
+
headerDiv.style.cssText = 'display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:1rem;';
|
|
2775
|
+
var backBtn = document.createElement('a');
|
|
2776
|
+
backBtn.className = 'btn-ghost';
|
|
2777
|
+
backBtn.href = '#/work/' + encodeURIComponent(workId);
|
|
2778
|
+
backBtn.textContent = '◄ back to pipeline';
|
|
2779
|
+
// Leaving the task drill drops the detail key
|
|
2780
|
+
backBtn.addEventListener('click', function() {
|
|
2781
|
+
delete openDrillKeys[detailKey];
|
|
2782
|
+
});
|
|
2783
|
+
headerDiv.appendChild(backBtn);
|
|
2784
|
+
|
|
2785
|
+
if (task) {
|
|
2786
|
+
var taskNum = parseTaskNumber(task.task_id);
|
|
2787
|
+
var taskIdEl = document.createElement('span');
|
|
2788
|
+
taskIdEl.className = 'kicker';
|
|
2789
|
+
taskIdEl.style.fontSize = '1rem';
|
|
2790
|
+
taskIdEl.textContent = taskNum !== null ? '#' + taskNum : taskId;
|
|
2791
|
+
headerDiv.appendChild(taskIdEl);
|
|
2792
|
+
if (task.type) {
|
|
2793
|
+
var typeEl = document.createElement('span');
|
|
2794
|
+
typeEl.className = 'badge badge-dim';
|
|
2795
|
+
typeEl.textContent = task.type;
|
|
2796
|
+
headerDiv.appendChild(typeEl);
|
|
2797
|
+
}
|
|
2798
|
+
headerDiv.appendChild(makeTaskStatusBadge(task.status));
|
|
2799
|
+
if (task.short_name) {
|
|
2800
|
+
var snEl = document.createElement('span');
|
|
2801
|
+
snEl.style.cssText = 'font-weight:500;font-size:0.95rem;';
|
|
2802
|
+
snEl.textContent = task.short_name;
|
|
2803
|
+
headerDiv.appendChild(snEl);
|
|
2804
|
+
}
|
|
2805
|
+
} else {
|
|
2806
|
+
// Task not found — show notice, never blank (FC-3/UI-5)
|
|
2807
|
+
var noTaskCallout = document.createElement('div');
|
|
2808
|
+
noTaskCallout.className = 'callout warn';
|
|
2809
|
+
noTaskCallout.innerHTML = '<h4>Task not found</h4>' +
|
|
2810
|
+
'<p>Task <code>' + escHtml(taskId) + '</code> is no longer in this work\'s state.</p>' +
|
|
2811
|
+
'<p><a href="#/work/' + encodeURIComponent(workId) + '">← back to pipeline</a></p>';
|
|
2812
|
+
container.appendChild(headerDiv);
|
|
2813
|
+
container.appendChild(noTaskCallout);
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
container.appendChild(headerDiv);
|
|
2817
|
+
|
|
2818
|
+
// Get detail (may be absent on first tick — lazy load)
|
|
2819
|
+
var details = (model.details) ? model.details : {};
|
|
2820
|
+
var detail = details[detailKey] || null;
|
|
2821
|
+
|
|
2822
|
+
if (!detail) {
|
|
2823
|
+
// First tick: show at-a-glance + "loading detail…" affordance (never blank)
|
|
2824
|
+
var loadCard = document.createElement('div');
|
|
2825
|
+
loadCard.className = 'card';
|
|
2826
|
+
loadCard.style.marginBottom = '1rem';
|
|
2827
|
+
var loadKicker = document.createElement('div');
|
|
2828
|
+
loadKicker.className = 'kicker';
|
|
2829
|
+
loadKicker.textContent = 'FORENSIC DETAIL';
|
|
2830
|
+
loadCard.appendChild(loadKicker);
|
|
2831
|
+
var loadMsg = document.createElement('p');
|
|
2832
|
+
loadMsg.className = 'meta';
|
|
2833
|
+
loadMsg.style.cssText = 'color:var(--text-dim);margin-top:0.5rem;';
|
|
2834
|
+
loadMsg.textContent = 'Loading detail… (polling server)';
|
|
2835
|
+
loadCard.appendChild(loadMsg);
|
|
2836
|
+
if (task) {
|
|
2837
|
+
var atGlance = document.createElement('p');
|
|
2838
|
+
atGlance.className = 'meta';
|
|
2839
|
+
atGlance.style.marginTop = '0.4rem';
|
|
2840
|
+
atGlance.textContent = 'task_id: ' + task.task_id +
|
|
2841
|
+
(task.type ? ' · type: ' + task.type : '') +
|
|
2842
|
+
(task.status ? ' · status: ' + task.status : '');
|
|
2843
|
+
loadCard.appendChild(atGlance);
|
|
2844
|
+
}
|
|
2845
|
+
container.appendChild(loadCard);
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
// Forensic panels grid: findings ∥ ledger (UI-2, UI-6 — desktop side-by-side, mobile stacked)
|
|
2850
|
+
var forensicGrid = document.createElement('div');
|
|
2851
|
+
forensicGrid.className = 'grid g2';
|
|
2852
|
+
forensicGrid.style.marginBottom = '1.5rem';
|
|
2853
|
+
forensicGrid.appendChild(_renderFindingsPanel(detail));
|
|
2854
|
+
forensicGrid.appendChild(_renderLedgerPanel(detail));
|
|
2855
|
+
container.appendChild(forensicGrid);
|
|
2856
|
+
|
|
2857
|
+
// Raw STATE.md viewer (UI-3) — full width, collapsed by default
|
|
2858
|
+
container.appendChild(_renderRawStatePanel(detail, taskId, workId));
|
|
2859
|
+
|
|
2860
|
+
// Logs panel (UI-4) — full width
|
|
2861
|
+
container.appendChild(_renderLogsPanel(detail, task, work));
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// ---------------------------------------------------------------------------
|
|
2865
|
+
// UI-2: Findings panel
|
|
2866
|
+
// ---------------------------------------------------------------------------
|
|
2867
|
+
function _renderFindingsPanel(detail) {
|
|
2868
|
+
var card = document.createElement('div');
|
|
2869
|
+
card.className = 'card';
|
|
2870
|
+
|
|
2871
|
+
var kicker = document.createElement('div');
|
|
2872
|
+
kicker.className = 'kicker';
|
|
2873
|
+
kicker.textContent = 'QUICK-CHECK FINDINGS';
|
|
2874
|
+
card.appendChild(kicker);
|
|
2875
|
+
|
|
2876
|
+
var findings = (detail.findings && Array.isArray(detail.findings)) ? detail.findings : [];
|
|
2877
|
+
|
|
2878
|
+
if (findings.length === 0) {
|
|
2879
|
+
var empty = document.createElement('p');
|
|
2880
|
+
empty.className = 'meta';
|
|
2881
|
+
empty.style.cssText = 'color:var(--text-dim);margin-top:0.5rem;';
|
|
2882
|
+
empty.textContent = 'No quick-check findings recorded for this task.';
|
|
2883
|
+
card.appendChild(empty);
|
|
2884
|
+
return card;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
for (var i = 0; i < findings.length; i++) {
|
|
2888
|
+
var f = findings[i];
|
|
2889
|
+
var row = document.createElement('div');
|
|
2890
|
+
row.style.cssText = 'display:flex;flex-direction:column;gap:0.2rem;padding:0.5rem 0;border-bottom:1px solid var(--border);';
|
|
2891
|
+
if (i === findings.length - 1) row.style.borderBottom = 'none';
|
|
2892
|
+
|
|
2893
|
+
// Severity chip + description row
|
|
2894
|
+
var topRow = document.createElement('div');
|
|
2895
|
+
topRow.style.cssText = 'display:flex;align-items:flex-start;gap:0.5rem;flex-wrap:wrap;';
|
|
2896
|
+
|
|
2897
|
+
var sevBadge = document.createElement('span');
|
|
2898
|
+
var sev = (f.severity || '').toUpperCase();
|
|
2899
|
+
if (sev === '[CRITICAL]' || sev === 'CRITICAL' || sev === '[CRITICAL') {
|
|
2900
|
+
sevBadge.className = 'badge badge-err';
|
|
2901
|
+
sevBadge.textContent = '✕ CRITICAL';
|
|
2902
|
+
} else if (sev === '[HIGH]' || sev === 'HIGH' || sev === '[HIGH') {
|
|
2903
|
+
sevBadge.className = 'badge badge-warn';
|
|
2904
|
+
sevBadge.textContent = '⚠ HIGH';
|
|
2905
|
+
} else {
|
|
2906
|
+
sevBadge.className = 'badge badge-dim';
|
|
2907
|
+
sevBadge.textContent = f.severity || 'unknown';
|
|
2908
|
+
}
|
|
2909
|
+
topRow.appendChild(sevBadge);
|
|
2910
|
+
|
|
2911
|
+
var desc = document.createElement('span');
|
|
2912
|
+
desc.style.cssText = 'font-size:0.9rem;flex:1;min-width:0;';
|
|
2913
|
+
desc.textContent = f.description || '';
|
|
2914
|
+
topRow.appendChild(desc);
|
|
2915
|
+
row.appendChild(topRow);
|
|
2916
|
+
|
|
2917
|
+
// Location (meta, only when present)
|
|
2918
|
+
if (f.location) {
|
|
2919
|
+
var locEl = document.createElement('code');
|
|
2920
|
+
locEl.className = 'meta';
|
|
2921
|
+
locEl.style.cssText = 'font-size:0.8rem;color:var(--text-dim);margin-left:0.25rem;';
|
|
2922
|
+
locEl.textContent = f.location;
|
|
2923
|
+
row.appendChild(locEl);
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
// Disposition chip
|
|
2927
|
+
if (f.disposition) {
|
|
2928
|
+
var dispBadge = document.createElement('span');
|
|
2929
|
+
if (f.disposition === 'Fixed-on-spot') {
|
|
2930
|
+
dispBadge.className = 'badge badge-ok';
|
|
2931
|
+
dispBadge.textContent = '✓ Fixed-on-spot';
|
|
2932
|
+
} else if (f.disposition === 'Deferred-to-gate') {
|
|
2933
|
+
dispBadge.className = 'badge badge-info';
|
|
2934
|
+
dispBadge.textContent = '→ Deferred-to-gate';
|
|
2935
|
+
} else {
|
|
2936
|
+
dispBadge.className = 'badge badge-dim';
|
|
2937
|
+
dispBadge.textContent = f.disposition;
|
|
2938
|
+
}
|
|
2939
|
+
dispBadge.style.marginLeft = '0.25rem';
|
|
2940
|
+
row.appendChild(dispBadge);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
card.appendChild(row);
|
|
2944
|
+
}
|
|
2945
|
+
return card;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// ---------------------------------------------------------------------------
|
|
2949
|
+
// UI-2: Ledger / delivery grade panel
|
|
2950
|
+
// ---------------------------------------------------------------------------
|
|
2951
|
+
function _renderLedgerPanel(detail) {
|
|
2952
|
+
var card = document.createElement('div');
|
|
2953
|
+
card.className = 'card';
|
|
2954
|
+
|
|
2955
|
+
var kicker = document.createElement('div');
|
|
2956
|
+
kicker.className = 'kicker';
|
|
2957
|
+
kicker.textContent = 'REVIEW LEDGER';
|
|
2958
|
+
card.appendChild(kicker);
|
|
2959
|
+
|
|
2960
|
+
var ledger = detail.ledger || null;
|
|
2961
|
+
|
|
2962
|
+
if (!ledger || ledger.delivery_id === null || ledger.delivery_id === undefined) {
|
|
2963
|
+
var noGrade = document.createElement('p');
|
|
2964
|
+
noGrade.className = 'meta';
|
|
2965
|
+
noGrade.style.cssText = 'color:var(--text-dim);margin-top:0.5rem;';
|
|
2966
|
+
noGrade.textContent = 'Not yet graded (no delivery gate run)';
|
|
2967
|
+
card.appendChild(noGrade);
|
|
2968
|
+
} else {
|
|
2969
|
+
// Delivery grade chip — captioned "delivery grade (delivery-NNN)" never "task grade"
|
|
2970
|
+
var gradeRow = document.createElement('div');
|
|
2971
|
+
gradeRow.style.cssText = 'display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;margin-top:0.5rem;';
|
|
2972
|
+
|
|
2973
|
+
var gradeLabel = document.createElement('span');
|
|
2974
|
+
gradeLabel.className = 'meta';
|
|
2975
|
+
gradeLabel.textContent = 'delivery grade (' + ledger.delivery_id + ')';
|
|
2976
|
+
gradeRow.appendChild(gradeLabel);
|
|
2977
|
+
|
|
2978
|
+
var gradeBadge = document.createElement('span');
|
|
2979
|
+
var g = ledger.grade || '';
|
|
2980
|
+
if (g === 'A+' || g === 'A' || g === 'pass' || g === 'Pass') {
|
|
2981
|
+
gradeBadge.className = 'badge badge-ok';
|
|
2982
|
+
} else if (!g || g === 'Pending') {
|
|
2983
|
+
gradeBadge.className = 'badge badge-dim';
|
|
2984
|
+
} else {
|
|
2985
|
+
gradeBadge.className = 'badge badge-warn';
|
|
2986
|
+
}
|
|
2987
|
+
gradeBadge.textContent = g || 'Pending';
|
|
2988
|
+
gradeRow.appendChild(gradeBadge);
|
|
2989
|
+
card.appendChild(gradeRow);
|
|
2990
|
+
|
|
2991
|
+
// Reviewer tier + gate timestamp in .meta
|
|
2992
|
+
var metaLine = document.createElement('p');
|
|
2993
|
+
metaLine.className = 'meta';
|
|
2994
|
+
metaLine.style.cssText = 'font-size:0.8rem;color:var(--text-dim);margin-top:0.35rem;';
|
|
2995
|
+
var metaParts = [];
|
|
2996
|
+
if (ledger.reviewer_tier) metaParts.push('reviewer: ' + ledger.reviewer_tier);
|
|
2997
|
+
if (ledger.gate_timestamp) metaParts.push('gate: ' + _fmtLocalDateTime(ledger.gate_timestamp));
|
|
2998
|
+
metaLine.textContent = metaParts.join(' · ');
|
|
2999
|
+
card.appendChild(metaLine);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// Deferred [HIGH] issues table
|
|
3003
|
+
var h3Deferred = document.createElement('h3');
|
|
3004
|
+
h3Deferred.style.cssText = 'font-size:0.9rem;margin:1rem 0 0.4rem;';
|
|
3005
|
+
h3Deferred.textContent = 'Deferred Issues';
|
|
3006
|
+
card.appendChild(h3Deferred);
|
|
3007
|
+
|
|
3008
|
+
var deferredIssues = (ledger && ledger.deferred_issues && Array.isArray(ledger.deferred_issues))
|
|
3009
|
+
? ledger.deferred_issues : [];
|
|
3010
|
+
|
|
3011
|
+
if (deferredIssues.length === 0) {
|
|
3012
|
+
var noDeferred = document.createElement('p');
|
|
3013
|
+
noDeferred.className = 'meta';
|
|
3014
|
+
noDeferred.style.cssText = 'color:var(--text-dim);font-size:0.85rem;';
|
|
3015
|
+
noDeferred.textContent = 'No deferred issues for this task.';
|
|
3016
|
+
card.appendChild(noDeferred);
|
|
3017
|
+
} else {
|
|
3018
|
+
var tbl = document.createElement('table');
|
|
3019
|
+
tbl.style.cssText = 'width:100%;border-collapse:collapse;font-size:0.85rem;margin-top:0.25rem;';
|
|
3020
|
+
|
|
3021
|
+
var thead = document.createElement('thead');
|
|
3022
|
+
var headRow = document.createElement('tr');
|
|
3023
|
+
['Severity', 'Description', 'Status'].forEach(function(h) {
|
|
3024
|
+
var th = document.createElement('th');
|
|
3025
|
+
th.textContent = h;
|
|
3026
|
+
th.style.cssText = 'text-align:left;padding:0.25rem 0.4rem;border-bottom:1px solid var(--border);color:var(--text-dim);font-weight:500;';
|
|
3027
|
+
headRow.appendChild(th);
|
|
3028
|
+
});
|
|
3029
|
+
thead.appendChild(headRow);
|
|
3030
|
+
tbl.appendChild(thead);
|
|
3031
|
+
|
|
3032
|
+
var tbody = document.createElement('tbody');
|
|
3033
|
+
for (var j = 0; j < deferredIssues.length; j++) {
|
|
3034
|
+
var issue = deferredIssues[j];
|
|
3035
|
+
var tr = document.createElement('tr');
|
|
3036
|
+
|
|
3037
|
+
var tdSev = document.createElement('td');
|
|
3038
|
+
tdSev.style.cssText = 'padding:0.2rem 0.4rem;vertical-align:top;';
|
|
3039
|
+
tdSev.textContent = issue.severity || '';
|
|
3040
|
+
tr.appendChild(tdSev);
|
|
3041
|
+
|
|
3042
|
+
var tdDesc = document.createElement('td');
|
|
3043
|
+
tdDesc.style.cssText = 'padding:0.2rem 0.4rem;vertical-align:top;';
|
|
3044
|
+
tdDesc.textContent = issue.description || '';
|
|
3045
|
+
tr.appendChild(tdDesc);
|
|
3046
|
+
|
|
3047
|
+
var tdStatus = document.createElement('td');
|
|
3048
|
+
tdStatus.style.cssText = 'padding:0.2rem 0.4rem;vertical-align:top;white-space:nowrap;';
|
|
3049
|
+
var statusBadge = document.createElement('span');
|
|
3050
|
+
var st = issue.status || '';
|
|
3051
|
+
if (st === 'Open') {
|
|
3052
|
+
statusBadge.className = 'badge badge-warn';
|
|
3053
|
+
} else if (st === 'Resolved') {
|
|
3054
|
+
statusBadge.className = 'badge badge-ok';
|
|
3055
|
+
} else if (st === 'Accepted') {
|
|
3056
|
+
statusBadge.className = 'badge badge-info';
|
|
3057
|
+
} else {
|
|
3058
|
+
statusBadge.className = 'badge badge-dim';
|
|
3059
|
+
}
|
|
3060
|
+
statusBadge.textContent = st || 'Unknown';
|
|
3061
|
+
tdStatus.appendChild(statusBadge);
|
|
3062
|
+
tr.appendChild(tdStatus);
|
|
3063
|
+
|
|
3064
|
+
tbody.appendChild(tr);
|
|
3065
|
+
}
|
|
3066
|
+
tbl.appendChild(tbody);
|
|
3067
|
+
card.appendChild(tbl);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
return card;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// ---------------------------------------------------------------------------
|
|
3074
|
+
// UI-3: Raw STATE.md viewer — read-only, escaped, collapsed by default (R15)
|
|
3075
|
+
// ---------------------------------------------------------------------------
|
|
3076
|
+
function _renderRawStatePanel(detail, taskId, workId) {
|
|
3077
|
+
var card = document.createElement('div');
|
|
3078
|
+
card.className = 'card';
|
|
3079
|
+
card.style.marginBottom = '1rem';
|
|
3080
|
+
|
|
3081
|
+
var kicker = document.createElement('div');
|
|
3082
|
+
kicker.className = 'kicker';
|
|
3083
|
+
kicker.textContent = 'RAW STATE.md VIEWER';
|
|
3084
|
+
card.appendChild(kicker);
|
|
3085
|
+
|
|
3086
|
+
var rawState = detail.raw_state || null;
|
|
3087
|
+
if (!rawState || !rawState.text) {
|
|
3088
|
+
var noRaw = document.createElement('p');
|
|
3089
|
+
noRaw.className = 'meta';
|
|
3090
|
+
noRaw.style.cssText = 'color:var(--text-dim);margin-top:0.5rem;';
|
|
3091
|
+
noRaw.textContent = 'Raw STATE.md text not available.';
|
|
3092
|
+
card.appendChild(noRaw);
|
|
3093
|
+
return card;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
var captionEl = document.createElement('p');
|
|
3097
|
+
captionEl.className = 'meta';
|
|
3098
|
+
captionEl.style.cssText = 'font-size:0.8rem;color:var(--text-dim);margin:0.3rem 0 0.5rem;';
|
|
3099
|
+
var sourcePath = rawState.path ? rawState.path : ('.aid/' + workId + '/STATE.md');
|
|
3100
|
+
captionEl.textContent = 'source: ' + sourcePath + ' · read-only';
|
|
3101
|
+
card.appendChild(captionEl);
|
|
3102
|
+
|
|
3103
|
+
// Byte-length affordance (collapsed by default)
|
|
3104
|
+
var byteLen = rawState.byte_len || 0;
|
|
3105
|
+
var kbDisplay = byteLen > 0 ? (Math.ceil(byteLen / 1024)) + ' KB' : 'file';
|
|
3106
|
+
|
|
3107
|
+
var toggleBtn = document.createElement('button');
|
|
3108
|
+
toggleBtn.type = 'button';
|
|
3109
|
+
toggleBtn.className = 'btn-ghost';
|
|
3110
|
+
toggleBtn.style.marginBottom = '0.5rem';
|
|
3111
|
+
toggleBtn.textContent = 'show raw STATE.md (' + kbDisplay + ')';
|
|
3112
|
+
|
|
3113
|
+
// Pre element — hidden initially (collapsed by default)
|
|
3114
|
+
var preEl = document.createElement('pre');
|
|
3115
|
+
preEl.id = 'raw-state-pre-' + encodeURIComponent(taskId);
|
|
3116
|
+
preEl.style.cssText = [
|
|
3117
|
+
'display:none',
|
|
3118
|
+
'overflow-x:auto',
|
|
3119
|
+
'overflow-y:auto',
|
|
3120
|
+
'max-height:60vh',
|
|
3121
|
+
'background:var(--bg-sunken)',
|
|
3122
|
+
'border:1px solid var(--border)',
|
|
3123
|
+
'border-radius:var(--radius-sm)',
|
|
3124
|
+
'padding:0.75rem',
|
|
3125
|
+
'font-size:0.82rem',
|
|
3126
|
+
'white-space:pre',
|
|
3127
|
+
'word-wrap:normal',
|
|
3128
|
+
'margin-top:0.5rem'
|
|
3129
|
+
].join(';');
|
|
3130
|
+
// Explicitly NOT contenteditable, NOT textarea, NOT form input
|
|
3131
|
+
preEl.removeAttribute('contenteditable');
|
|
3132
|
+
|
|
3133
|
+
// Escape raw text (R15: no injection via <, >, &, U+2028, U+2029)
|
|
3134
|
+
var escaped = escRawState(rawState.text);
|
|
3135
|
+
preEl.innerHTML = escaped;
|
|
3136
|
+
|
|
3137
|
+
var isOpen = false;
|
|
3138
|
+
|
|
3139
|
+
toggleBtn.addEventListener('click', function() {
|
|
3140
|
+
isOpen = !isOpen;
|
|
3141
|
+
if (isOpen) {
|
|
3142
|
+
preEl.style.display = '';
|
|
3143
|
+
toggleBtn.textContent = 'hide raw STATE.md';
|
|
3144
|
+
// Deep-anchor (DD-3): scroll to task's section within the pre
|
|
3145
|
+
_anchorRawState(preEl, taskId);
|
|
3146
|
+
} else {
|
|
3147
|
+
preEl.style.display = 'none';
|
|
3148
|
+
toggleBtn.textContent = 'show raw STATE.md (' + kbDisplay + ')';
|
|
3149
|
+
}
|
|
3150
|
+
});
|
|
3151
|
+
|
|
3152
|
+
card.appendChild(toggleBtn);
|
|
3153
|
+
card.appendChild(preEl);
|
|
3154
|
+
return card;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// Deep-anchor within the raw-state pre: find the task's block and scroll to it (DD-3)
|
|
3158
|
+
function _anchorRawState(preEl, taskId) {
|
|
3159
|
+
// Look for "### <task_id>" or "## Tasks State/Status" row matching task_id
|
|
3160
|
+
// Tolerates BOTH "## Tasks Status" (legacy) and "## Tasks State" (new, work-004 rename).
|
|
3161
|
+
var text = preEl.textContent || '';
|
|
3162
|
+
var lines = text.split('\n');
|
|
3163
|
+
var targetLine = -1;
|
|
3164
|
+
// First try: "### task-NNN" heading
|
|
3165
|
+
for (var i = 0; i < lines.length; i++) {
|
|
3166
|
+
if (lines[i].match(new RegExp('^###\\s+' + taskId.replace(/-/g, '[-_]')))) {
|
|
3167
|
+
targetLine = i;
|
|
3168
|
+
break;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
// Second try: "## Tasks State" or "## Tasks Status" section containing task_id
|
|
3172
|
+
if (targetLine < 0) {
|
|
3173
|
+
var inTasksSection = false;
|
|
3174
|
+
for (var j = 0; j < lines.length; j++) {
|
|
3175
|
+
if (/^##\s+Tasks\s+(?:State|Status)\s*$/i.test(lines[j])) {
|
|
3176
|
+
inTasksSection = true;
|
|
3177
|
+
continue;
|
|
3178
|
+
}
|
|
3179
|
+
if (inTasksSection) {
|
|
3180
|
+
if (/^##\s+/.test(lines[j])) { inTasksSection = false; continue; }
|
|
3181
|
+
if (lines[j].indexOf(taskId) !== -1) { targetLine = j; break; }
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
// Third try: any line containing the task_id
|
|
3186
|
+
if (targetLine < 0) {
|
|
3187
|
+
for (var k = 0; k < lines.length; k++) {
|
|
3188
|
+
if (lines[k].indexOf(taskId) !== -1) {
|
|
3189
|
+
targetLine = k;
|
|
3190
|
+
break;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
if (targetLine < 0) return;
|
|
3195
|
+
// Estimate scroll offset: each line ~19px
|
|
3196
|
+
var approxOffset = targetLine * 19;
|
|
3197
|
+
preEl.scrollTop = Math.max(0, approxOffset - 40);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// ---------------------------------------------------------------------------
|
|
3201
|
+
// UI-4: Honest logs panel (KI-008)
|
|
3202
|
+
// ---------------------------------------------------------------------------
|
|
3203
|
+
function _renderLogsPanel(detail, task, work) {
|
|
3204
|
+
var card = document.createElement('div');
|
|
3205
|
+
card.className = 'card';
|
|
3206
|
+
card.style.marginBottom = '1rem';
|
|
3207
|
+
|
|
3208
|
+
var kicker = document.createElement('div');
|
|
3209
|
+
kicker.className = 'kicker';
|
|
3210
|
+
kicker.textContent = 'LOGS';
|
|
3211
|
+
card.appendChild(kicker);
|
|
3212
|
+
|
|
3213
|
+
var logs = detail.logs || {};
|
|
3214
|
+
var taskLogs = logs.task_logs || 'none';
|
|
3215
|
+
var serverLogPresent = logs.server_log_present || false;
|
|
3216
|
+
var heartbeatPresent = logs.heartbeat_present || false;
|
|
3217
|
+
|
|
3218
|
+
// task_logs == none (always today): show guidance card
|
|
3219
|
+
if (taskLogs === 'none' || !taskLogs) {
|
|
3220
|
+
var noLogsCard = document.createElement('div');
|
|
3221
|
+
noLogsCard.style.cssText = 'background:var(--bg-sunken);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.75rem;margin-top:0.5rem;';
|
|
3222
|
+
|
|
3223
|
+
var noLogsKicker = document.createElement('div');
|
|
3224
|
+
noLogsKicker.className = 'kicker';
|
|
3225
|
+
noLogsKicker.style.marginBottom = '0.4rem';
|
|
3226
|
+
noLogsKicker.textContent = 'NO TASK LOGS CAPTURED';
|
|
3227
|
+
noLogsCard.appendChild(noLogsKicker);
|
|
3228
|
+
|
|
3229
|
+
var guidance = document.createElement('p');
|
|
3230
|
+
guidance.className = 'meta';
|
|
3231
|
+
guidance.style.cssText = 'font-size:0.85rem;margin:0 0 0.4rem;';
|
|
3232
|
+
guidance.textContent = 'AID does not capture per-task log files. For task diagnostics:';
|
|
3233
|
+
noLogsCard.appendChild(guidance);
|
|
3234
|
+
|
|
3235
|
+
var ol = document.createElement('ol');
|
|
3236
|
+
ol.style.cssText = 'font-size:0.85rem;margin:0.25rem 0 0.25rem 1.25rem;line-height:1.6;';
|
|
3237
|
+
var steps = [
|
|
3238
|
+
'The dashboard server\'s own log is at .aid/.temp/dashboard.log (created by aid dashboard start; records server boot/errors, not task execution).',
|
|
3239
|
+
'For pipeline/task troubleshooting, re-run the relevant skill (e.g. /aid-execute) and watch its live terminal output.',
|
|
3240
|
+
'AID writes task forensics to this work\'s STATE.md (see Quick Check Findings, Delivery Gates / Delivery Gate above) — shown on this page.',
|
|
3241
|
+
'After a re-run, this page\'s Findings/Ledger sections update on the next refresh (within the poll interval).'
|
|
3242
|
+
];
|
|
3243
|
+
for (var s = 0; s < steps.length; s++) {
|
|
3244
|
+
var li = document.createElement('li');
|
|
3245
|
+
li.textContent = steps[s];
|
|
3246
|
+
ol.appendChild(li);
|
|
3247
|
+
}
|
|
3248
|
+
noLogsCard.appendChild(ol);
|
|
3249
|
+
|
|
3250
|
+
// Meta: poll interval advisory
|
|
3251
|
+
var pollAdvisory = document.createElement('p');
|
|
3252
|
+
pollAdvisory.className = 'meta';
|
|
3253
|
+
pollAdvisory.style.cssText = 'font-size:0.8rem;color:var(--text-dim);margin-top:0.4rem;';
|
|
3254
|
+
var intervalSec = Math.round(pollMs / 1000);
|
|
3255
|
+
pollAdvisory.textContent = 'This page refreshes every ' + intervalSec + 's — new findings appear automatically.';
|
|
3256
|
+
noLogsCard.appendChild(pollAdvisory);
|
|
3257
|
+
|
|
3258
|
+
card.appendChild(noLogsCard);
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// server_log_present: surfaced as tool diagnostic, not a task log
|
|
3262
|
+
if (serverLogPresent) {
|
|
3263
|
+
var srvDiv = document.createElement('div');
|
|
3264
|
+
srvDiv.style.cssText = 'margin-top:0.75rem;padding:0.5rem 0.75rem;background:var(--bg-sunken);border:1px solid var(--border);border-radius:var(--radius-sm);';
|
|
3265
|
+
var srvTitle = document.createElement('span');
|
|
3266
|
+
srvTitle.className = 'badge badge-dim';
|
|
3267
|
+
srvTitle.textContent = 'Dashboard server log (tool diagnostic — not a task log)';
|
|
3268
|
+
srvDiv.appendChild(srvTitle);
|
|
3269
|
+
var srvMeta = document.createElement('p');
|
|
3270
|
+
srvMeta.className = 'meta';
|
|
3271
|
+
srvMeta.style.cssText = 'font-size:0.8rem;color:var(--text-dim);margin-top:0.3rem;';
|
|
3272
|
+
srvMeta.textContent = '.aid/.temp/dashboard.log is present. Open it with your editor or terminal for server-level diagnostics.';
|
|
3273
|
+
srvDiv.appendChild(srvMeta);
|
|
3274
|
+
card.appendChild(srvDiv);
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// heartbeat_present: advisory liveness hint
|
|
3278
|
+
if (heartbeatPresent) {
|
|
3279
|
+
var hbDiv = document.createElement('div');
|
|
3280
|
+
hbDiv.style.cssText = 'margin-top:0.5rem;';
|
|
3281
|
+
var hbBadge = document.createElement('span');
|
|
3282
|
+
hbBadge.className = 'badge badge-info';
|
|
3283
|
+
hbBadge.textContent = 'heartbeat present';
|
|
3284
|
+
hbDiv.appendChild(hbBadge);
|
|
3285
|
+
var hbMeta = document.createElement('span');
|
|
3286
|
+
hbMeta.className = 'meta';
|
|
3287
|
+
hbMeta.style.cssText = 'font-size:0.8rem;color:var(--text-dim);margin-left:0.5rem;';
|
|
3288
|
+
hbMeta.textContent = 'last seen: a heartbeat file exists for this work (liveness hint — not a log).';
|
|
3289
|
+
hbDiv.appendChild(hbMeta);
|
|
3290
|
+
card.appendChild(hbDiv);
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// Blocked-work IMPEDIMENT pointer (FR18)
|
|
3294
|
+
if (work && work.lifecycle === 'Blocked' && work.block_artifact) {
|
|
3295
|
+
var impDiv = document.createElement('div');
|
|
3296
|
+
impDiv.style.cssText = 'margin-top:0.75rem;padding:0.5rem 0.75rem;border:1px solid var(--err);border-radius:var(--radius-sm);background:var(--bg);';
|
|
3297
|
+
var impTitle = document.createElement('div');
|
|
3298
|
+
impTitle.className = 'kicker';
|
|
3299
|
+
impTitle.style.color = 'var(--err)';
|
|
3300
|
+
impTitle.textContent = 'IMPEDIMENT';
|
|
3301
|
+
impDiv.appendChild(impTitle);
|
|
3302
|
+
var impMeta = document.createElement('p');
|
|
3303
|
+
impMeta.className = 'meta';
|
|
3304
|
+
impMeta.style.cssText = 'font-size:0.85rem;margin-top:0.3rem;';
|
|
3305
|
+
impMeta.textContent = 'This work is Blocked. Review the impediment file for operator action:';
|
|
3306
|
+
impDiv.appendChild(impMeta);
|
|
3307
|
+
var impPath = document.createElement('code');
|
|
3308
|
+
impPath.style.cssText = 'font-size:0.85rem;display:block;margin-top:0.25rem;word-break:break-all;';
|
|
3309
|
+
impPath.textContent = work.block_artifact;
|
|
3310
|
+
impDiv.appendChild(impPath);
|
|
3311
|
+
card.appendChild(impDiv);
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
return card;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
})();
|
|
3318
|
+
</script>
|
|
3319
|
+
|
|
3320
|
+
</body>
|
|
3321
|
+
</html>
|