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.
@@ -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">&#9679; 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">&#9680;</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">&#9658;</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">&#9658;</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&#8230;</p>
901
+ </div>
902
+
903
+ </main>
904
+
905
+ <footer id="page-footer">
906
+ <p>served locally by <code>aid dashboard</code> &middot; read-only &middot;
907
+ refreshes every <span id="footer-interval">5</span>s &middot;
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 = '&#9680; Stale';
1125
+ } else {
1126
+ badge.className = 'badge badge-ok';
1127
+ badge.innerHTML = '<span class="pulse-dot">&#9679;</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>&#10074;&#10074; 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 += ' &mdash; 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>&#10005; 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, '&amp;')
2598
+ .replace(/</g, '&lt;')
2599
+ .replace(/>/g, '&gt;')
2600
+ .replace(/"/g, '&quot;');
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, '&amp;')
2645
+ .replace(/</g, '&lt;')
2646
+ .replace(/>/g, '&gt;')
2647
+ .replace(/\u2028/g, '&#8232;')
2648
+ .replace(/\u2029/g, '&#8233;');
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>