aid-installer 1.0.0 → 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,987 @@
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 CLI</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
+ /* Projects grid: fixed-width cards, as many per row as fit (4 on the 1200px desktop). */
210
+ .grid.projects { grid-template-columns: repeat(auto-fill, 270px); justify-content: start; }
211
+ .grid.g-lane { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
212
+ /* Main-page pipelines grid: auto-fit reflow (does NOT mutate shared .g3 fixed-3-col) */
213
+ .pipelines-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
214
+
215
+ .card {
216
+ background: var(--bg-elev);
217
+ border: 1px solid var(--border);
218
+ border-radius: var(--radius);
219
+ padding: 1.2rem 1.3rem;
220
+ box-shadow: var(--shadow-sm);
221
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
222
+ }
223
+ .card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
224
+ .card .kicker { font-size: 0.74rem; font-weight: 700; letter-spacing: 0.08em; color: var(--text-dim); text-transform: uppercase; margin-bottom: 0.3em; }
225
+ .card h3 { margin-bottom: 0.4em; font-size: 1.05rem; }
226
+ .card .stat { font-size: 2rem; font-weight: 700; color: var(--accent); line-height: 1; margin-bottom: 0.2em; }
227
+ .card .stat-sub { color: var(--text-muted); font-size: 0.9rem; }
228
+ .card .meta { color: var(--text-dim); font-size: 0.85rem; }
229
+ .card.card-primary { background: var(--primary); color: var(--primary-fg); border-color: transparent; }
230
+ .card.card-primary .kicker,
231
+ .card.card-primary .stat-sub,
232
+ .card.card-primary .meta { color: color-mix(in srgb, var(--primary-fg) 70%, transparent); }
233
+ .card.card-primary .stat { color: var(--accent); }
234
+
235
+ /* ---------- Callouts ---------- */
236
+ .callout {
237
+ padding: 0.6rem 1.1rem;
238
+ border-radius: var(--radius);
239
+ border-left: 4px solid var(--info);
240
+ background: var(--info-bg);
241
+ color: var(--text);
242
+ margin: 0.75rem 0;
243
+ font-size: 0.92rem;
244
+ }
245
+ .callout.warn { border-left-color: var(--warn); background: var(--warn-bg); color: var(--text); }
246
+ .callout.err { border-left-color: var(--err); background: var(--err-bg); color: var(--text); }
247
+ .callout.ok { border-left-color: var(--ok); background: var(--ok-bg); color: var(--text); }
248
+ .callout h4 { margin: 0 0 0.2em 0; font-size: 0.78rem; color: inherit; opacity: 0.85; }
249
+ .callout p:last-child { margin-bottom: 0; }
250
+
251
+ /* ---------- Footer ---------- */
252
+ footer {
253
+ max-width: 1200px;
254
+ margin: 0 auto;
255
+ padding: 2rem 1.5rem;
256
+ border-top: 1px solid var(--border);
257
+ color: var(--text-dim);
258
+ font-size: 0.85rem;
259
+ text-align: center;
260
+ }
261
+
262
+ /* ---------- Responsive ---------- */
263
+
264
+ /* Tablet: 2-col chip grid between 768px and 1024px */
265
+ @media (min-width: 769px) and (max-width: 1024px) {
266
+ .grid.g3, .grid.g4, .pipelines-grid { grid-template-columns: repeat(2, 1fr); }
267
+ }
268
+
269
+ @media (max-width: 768px) {
270
+ .top-bar { padding: 0.5rem 1rem; gap: 0.4rem 0.75rem; }
271
+ .top-bar .brand { font-size: 0.85rem; }
272
+ .breadcrumb { font-size: 0.78rem; min-width: 0; }
273
+ main { padding: 1rem 1rem 3rem; }
274
+ .grid, .grid.g2, .grid.g3, .grid.g4, .grid.projects, .grid.g-lane, .pipelines-grid { grid-template-columns: 1fr; }
275
+ .stage-rail { overflow-x: auto; flex-wrap: nowrap; }
276
+ .content-col { max-width: 100%; }
277
+ }
278
+
279
+ /* Extra-narrow: ensure top-bar wraps at 390px */
280
+ @media (max-width: 420px) {
281
+ .breadcrumb { display: none; }
282
+ .controls { width: 100%; justify-content: flex-end; }
283
+ }
284
+
285
+ /* ---------- Print ---------- */
286
+ @media print {
287
+ .top-bar, .controls { display: none !important; }
288
+ body { background: white; color: black; }
289
+ }
290
+
291
+ /* ---------- Accessibility ---------- */
292
+ .skip-link {
293
+ position: absolute;
294
+ top: -40px;
295
+ left: 8px;
296
+ z-index: 1000;
297
+ padding: 0.5rem 1rem;
298
+ background: var(--accent);
299
+ color: var(--accent-fg);
300
+ border-radius: var(--radius-sm);
301
+ font-weight: 600;
302
+ text-decoration: none;
303
+ }
304
+ .skip-link:focus { top: 8px; outline: 2px solid var(--text); outline-offset: 2px; }
305
+ :focus { outline: none; }
306
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
307
+ .btn-ghost:focus-visible { outline-offset: 3px; }
308
+ @media (prefers-reduced-motion: reduce) {
309
+ *, *::before, *::after {
310
+ animation-duration: 0.01ms !important;
311
+ animation-iteration-count: 1 !important;
312
+ transition-duration: 0.01ms !important;
313
+ scroll-behavior: auto !important;
314
+ }
315
+ html { scroll-behavior: auto; }
316
+ .card:hover { transform: none; }
317
+ }
318
+ .sr-only {
319
+ position: absolute !important;
320
+ width: 1px; height: 1px;
321
+ padding: 0; margin: -1px;
322
+ overflow: hidden;
323
+ clip: rect(0,0,0,0);
324
+ white-space: nowrap;
325
+ border: 0;
326
+ }
327
+ @media (forced-colors: active) {
328
+ .card, .callout { border: 1px solid CanvasText; }
329
+ .badge { border: 1px solid CanvasText; forced-color-adjust: none; }
330
+ }
331
+ .noscript-fallback {
332
+ max-width: 720px;
333
+ margin: 4rem auto;
334
+ padding: 2rem;
335
+ background: var(--bg-elev);
336
+ border: 2px solid var(--warn);
337
+ border-radius: var(--radius);
338
+ color: var(--text);
339
+ }
340
+
341
+ /* ---------- CLI-home specific ---------- */
342
+
343
+ /* Content-column wrapper */
344
+ .content-col {
345
+ max-width: 860px;
346
+ margin-left: auto;
347
+ margin-right: auto;
348
+ }
349
+
350
+ /* Machine panel (.card.plugin): dl two-column grid */
351
+ .card.plugin {
352
+ margin-bottom: 1.5rem;
353
+ }
354
+ .card.plugin dl {
355
+ display: grid;
356
+ grid-template-columns: max-content 1fr;
357
+ gap: 0.35rem 1rem;
358
+ margin: 0;
359
+ }
360
+ .card.plugin dt {
361
+ color: var(--text-dim);
362
+ font-size: 0.82rem;
363
+ font-weight: 600;
364
+ white-space: nowrap;
365
+ align-self: start;
366
+ padding-top: 0.1em;
367
+ }
368
+ .card.plugin dd {
369
+ margin: 0;
370
+ font-size: 0.88rem;
371
+ word-break: break-all;
372
+ }
373
+ .plugin-name {
374
+ font-family: "SF Mono", Monaco, Consolas, "Roboto Mono", monospace;
375
+ font-size: 0.85em;
376
+ }
377
+ .plugin-meta {
378
+ font-size: 0.8rem;
379
+ color: var(--text-dim);
380
+ margin-top: 0.6rem;
381
+ }
382
+
383
+ /* Card-link: neutralize global anchor styling for whole-card clickable cards */
384
+ .card-link { display: block; color: inherit; text-decoration: none; }
385
+ .card-link:hover { text-decoration: none; }
386
+
387
+ /* Main-page section headings */
388
+ .main-section-head { margin: 1.5rem 0 0.75rem; }
389
+
390
+ /* Stage rail (for CSS class integrity) */
391
+ .stage-rail {
392
+ display: flex;
393
+ gap: 0.4rem;
394
+ flex-wrap: wrap;
395
+ margin: 1rem 0;
396
+ align-items: center;
397
+ }
398
+
399
+ /* Wave section (kept for CSS class integrity) */
400
+ .wave-section { margin-bottom: 1.5rem; }
401
+ .wave-summary {
402
+ display: inline-flex;
403
+ align-items: center;
404
+ gap: 0.5em;
405
+ font-size: 0.78rem;
406
+ font-weight: 600;
407
+ letter-spacing: 0.05em;
408
+ text-transform: uppercase;
409
+ color: var(--text-dim);
410
+ background: var(--bg-sunken);
411
+ border: 1px solid var(--border);
412
+ border-radius: var(--radius);
413
+ padding: 0.35rem 0.85rem;
414
+ }
415
+
416
+ /* Interval input in controls */
417
+ .interval-control {
418
+ display: flex;
419
+ align-items: center;
420
+ gap: 0.3rem;
421
+ font-size: 0.85rem;
422
+ color: var(--text-muted);
423
+ }
424
+ .interval-input {
425
+ width: 3.5em;
426
+ padding: 0.35rem 0.4rem;
427
+ border: 1px solid var(--border);
428
+ border-radius: var(--radius-sm);
429
+ background: transparent;
430
+ color: var(--text);
431
+ font-size: 0.85rem;
432
+ font-family: inherit;
433
+ text-align: center;
434
+ }
435
+ .interval-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
436
+
437
+ /* Running pulse animation -- disabled with prefers-reduced-motion */
438
+ @keyframes pulse-dot {
439
+ 0%, 100% { opacity: 1; }
440
+ 50% { opacity: 0.4; }
441
+ }
442
+ .pulse-dot {
443
+ display: inline-block;
444
+ animation: pulse-dot 1.8s ease-in-out infinite;
445
+ }
446
+ @media (prefers-reduced-motion: reduce) {
447
+ .pulse-dot { animation: none; }
448
+ }
449
+
450
+ /* Empty / loading state */
451
+ .empty-state {
452
+ text-align: center;
453
+ color: var(--text-dim);
454
+ padding: 3rem 1rem;
455
+ font-size: 0.95rem;
456
+ }
457
+
458
+ /* Chip row */
459
+ .chip-row {
460
+ display: flex;
461
+ flex-wrap: wrap;
462
+ gap: 0.3rem;
463
+ margin-top: 0.5rem;
464
+ }
465
+
466
+ /* Unavailable card muted body */
467
+ .card-unavailable { color: var(--text-dim); }
468
+ .card-unavailable h3 { color: var(--text-dim); }
469
+
470
+ /* Work-card attention borders (kept for CSS class integrity) */
471
+ .card.border-warn { border-left: 4px solid var(--warn); }
472
+ .card.border-err { border-left: 4px solid var(--err); }
473
+
474
+ /* Progress bar classes (kept for CSS class integrity) */
475
+ .progress-row { margin: 0.55rem 0 0; }
476
+ .progress-track { height: 6px; background: var(--bg-sunken); border-radius: 4px; overflow: hidden; }
477
+ .progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; }
478
+ .progress-execution .progress-fill { background: var(--accent); }
479
+ .progress-readiness .progress-fill { background: var(--info); }
480
+ .progress-label { display: block; margin-bottom: 0.3rem; font-size: 0.8rem; color: var(--text-dim); }
481
+
482
+ /* kt-head / kt-section (kept for CSS class integrity) */
483
+ .kt-head { color: var(--info); }
484
+ .kt-section .card { border-left: 3px solid var(--info); }
485
+ </style>
486
+ </head>
487
+ <body>
488
+
489
+ <!-- Skip-to-content for keyboard / screen reader users -->
490
+ <a class="skip-link" href="#top">Skip to content</a>
491
+
492
+ <!-- Sticky top bar (UI-1) -->
493
+ <header class="top-bar" role="banner">
494
+ <span class="app-title">AID Dashboard <span class="beta-pill">beta</span></span>
495
+ <div class="brand">
496
+ <strong>Home</strong>
497
+ </div>
498
+ <div style="flex:1"></div>
499
+ <div class="controls">
500
+ <!-- Load status (static: the CLI home loads once, it does not auto-poll) -->
501
+ <span class="badge badge-dim" id="freshness-badge" aria-live="polite">&#9679; Loading</span>
502
+ <!-- Manual refresh: the CLI home does not auto-poll; click to reload the project list -->
503
+ <button type="button" class="btn-ghost" id="refresh-btn" title="Reload the project list">Refresh</button>
504
+ <!-- Theme toggle -->
505
+ <button type="button" class="btn-ghost" id="theme-toggle"
506
+ aria-label="Switch theme" title="Toggle light/dark theme">
507
+ <span id="theme-icon" aria-hidden="true">&#9680;</span>
508
+ <span id="theme-label">Dark</span>
509
+ </button>
510
+ </div>
511
+ </header>
512
+
513
+ <!-- Main page content -->
514
+ <main id="top">
515
+
516
+ <!-- Schema-mismatch banner (hidden by default; shown on schema_version mismatch) -->
517
+ <div class="callout warn" id="schema-mismatch-banner" style="display:none" role="alert">
518
+ <h4>Assets out of date</h4>
519
+ <p>Dashboard assets are out of date &#8212; restart <code>aid dashboard</code> to get the latest version.</p>
520
+ </div>
521
+
522
+ <!-- Machine panel (UI-H1) -->
523
+ <div id="machine-panel"></div>
524
+
525
+ <!-- Projects grid section (UI-H2) -->
526
+ <div id="repo-section" style="display:none">
527
+ <h2 class="main-section-head">Projects</h2>
528
+ <div class="grid projects" id="repo-grid"></div>
529
+ </div>
530
+
531
+ <!-- Empty registry state (UI-H3 empty) -->
532
+ <div class="empty-state" id="empty-registry" style="display:none">
533
+ <p>No repos registered yet &#8212; run <code>aid add &lt;tool&gt;</code> in a repo to see it here.</p>
534
+ </div>
535
+
536
+ <!-- Initial loading state -->
537
+ <div class="empty-state" id="loading-state">
538
+ <p>Connecting to AID dashboard&#8230;</p>
539
+ </div>
540
+
541
+ </main>
542
+
543
+ <footer id="page-footer">
544
+ <p>served locally by <code>aid dashboard</code> &middot; read-only &middot;
545
+ refresh to update &middot;
546
+ <span id="footer-generated-by"></span></p>
547
+ </footer>
548
+
549
+ <noscript>
550
+ <div class="noscript-fallback">
551
+ <h2>JavaScript required</h2>
552
+ <p>The AID dashboard uses JavaScript to poll <code>/api/home</code> and render machine state.
553
+ Please enable JavaScript to use this dashboard.</p>
554
+ </div>
555
+ </noscript>
556
+
557
+ <script>
558
+ (function () {
559
+ 'use strict';
560
+
561
+ // ---------------------------------------------------------------------------
562
+ // Constants
563
+ // ---------------------------------------------------------------------------
564
+ var EXPECTED_HOME_SCHEMA = 1;
565
+
566
+ // ---------------------------------------------------------------------------
567
+ // State
568
+ // ---------------------------------------------------------------------------
569
+ var fetchPending = false;
570
+ var lastGoodEnvelope = null;
571
+ var lastSuccessTime = null;
572
+
573
+ // ---------------------------------------------------------------------------
574
+ // Boot: the CLI home loads ONCE and does NOT auto-poll. Click Refresh (in-page
575
+ // reload of /api/home) or reload the browser tab to pick up add/remove changes.
576
+ // ---------------------------------------------------------------------------
577
+ (function boot() {
578
+ var refreshBtn = document.getElementById('refresh-btn');
579
+ if (refreshBtn) {
580
+ refreshBtn.addEventListener('click', function () { doFetch(); });
581
+ }
582
+ initTheme();
583
+ doFetch();
584
+ })();
585
+
586
+ // ---------------------------------------------------------------------------
587
+ // Fetch (single-shot; no recurring poll -- the CLI home updates only on Refresh
588
+ // or a browser reload)
589
+ // ---------------------------------------------------------------------------
590
+ function doFetch() {
591
+ if (fetchPending) return; // single in-flight
592
+ fetchPending = true;
593
+ updateFreshnessBadge('loading');
594
+ fetch('/api/home')
595
+ .then(function (resp) {
596
+ if (!resp.ok) {
597
+ throw new Error('HTTP ' + resp.status);
598
+ }
599
+ return resp.json();
600
+ })
601
+ .then(function (envelope) {
602
+ fetchPending = false;
603
+ onSuccess(envelope);
604
+ })
605
+ .catch(function (err) {
606
+ fetchPending = false;
607
+ onError(err);
608
+ });
609
+ }
610
+
611
+ function onSuccess(envelope) {
612
+ // Schema gate (FF-3 / DM-2)
613
+ if (envelope.schema_version !== EXPECTED_HOME_SCHEMA) {
614
+ showSchemaMismatch();
615
+ // Keep last good view; do not update
616
+ return;
617
+ }
618
+ hideSchemaMismatch();
619
+ lastSuccessTime = Date.now();
620
+ lastGoodEnvelope = envelope;
621
+
622
+ // Hide loading state
623
+ var loadEl = document.getElementById('loading-state');
624
+ if (loadEl) loadEl.style.display = 'none';
625
+
626
+ // Update footer generated_by
627
+ var genByEl = document.getElementById('footer-generated-by');
628
+ if (genByEl && envelope.generated_by) {
629
+ genByEl.textContent = 'via ' + envelope.generated_by;
630
+ }
631
+
632
+ renderMachinePanel(envelope.machine);
633
+ renderRepoGrid(envelope.repos);
634
+ updateFreshnessBadge('loaded');
635
+ }
636
+
637
+ function onError(err) {
638
+ updateFreshnessBadge('error');
639
+ // Keep last good view if we have one; never blank the page
640
+ if (!lastGoodEnvelope) {
641
+ var loadEl = document.getElementById('loading-state');
642
+ if (loadEl) loadEl.innerHTML = '<p>Could not load the project list. Use Refresh or reload the page.</p>';
643
+ }
644
+ }
645
+
646
+ // ---------------------------------------------------------------------------
647
+ // Freshness badge
648
+ // ---------------------------------------------------------------------------
649
+ // Static status badge -- reflects the last load, not a live poll.
650
+ function _fmtClock(ts) {
651
+ try {
652
+ var d = new Date(ts);
653
+ function p(n) { return (n < 10 ? '0' : '') + n; }
654
+ return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
655
+ } catch (e) { return ''; }
656
+ }
657
+
658
+ function updateFreshnessBadge(state) {
659
+ var badge = document.getElementById('freshness-badge');
660
+ if (!badge) return;
661
+ if (state === 'loading') {
662
+ badge.className = 'badge badge-dim';
663
+ badge.innerHTML = '&#9679; Loading';
664
+ } else if (state === 'error') {
665
+ badge.className = 'badge badge-err';
666
+ badge.textContent = 'Could not load';
667
+ } else { // loaded
668
+ badge.className = 'badge badge-ok';
669
+ badge.textContent = 'as of ' + _fmtClock(lastSuccessTime || Date.now());
670
+ }
671
+ }
672
+
673
+ // ---------------------------------------------------------------------------
674
+ // Schema mismatch banner
675
+ // ---------------------------------------------------------------------------
676
+ function showSchemaMismatch() {
677
+ var el = document.getElementById('schema-mismatch-banner');
678
+ if (el) el.style.display = '';
679
+ }
680
+
681
+ function hideSchemaMismatch() {
682
+ var el = document.getElementById('schema-mismatch-banner');
683
+ if (el) el.style.display = 'none';
684
+ }
685
+
686
+ // ---------------------------------------------------------------------------
687
+ // Escape HTML (safety for dynamic text insertion)
688
+ // ---------------------------------------------------------------------------
689
+ function escHtml(s) {
690
+ return String(s)
691
+ .replace(/&/g, '&amp;')
692
+ .replace(/</g, '&lt;')
693
+ .replace(/>/g, '&gt;')
694
+ .replace(/"/g, '&quot;');
695
+ }
696
+
697
+ // ---------------------------------------------------------------------------
698
+ // UI-H1b -- Machine panel
699
+ // ---------------------------------------------------------------------------
700
+ function renderMachinePanel(machine) {
701
+ var container = document.getElementById('machine-panel');
702
+ if (!container) return;
703
+ container.innerHTML = '';
704
+
705
+ var card = document.createElement('div');
706
+ card.className = 'card plugin';
707
+
708
+ // Kicker: "AID CLI" + the installed-version pill (e.g. v1.0.0). No "(this machine)".
709
+ var kicker = document.createElement('div');
710
+ kicker.className = 'kicker';
711
+ kicker.appendChild(document.createTextNode('AID CLI'));
712
+ if (machine && machine.aid_version) {
713
+ var vBadge = document.createElement('span');
714
+ vBadge.className = 'badge badge-info';
715
+ vBadge.style.marginLeft = '0.4rem';
716
+ vBadge.textContent = 'v' + machine.aid_version;
717
+ kicker.appendChild(vBadge);
718
+ } else {
719
+ var vNa = document.createElement('span');
720
+ vNa.style.marginLeft = '0.4rem';
721
+ vNa.textContent = '(version unavailable)';
722
+ kicker.appendChild(vNa);
723
+ }
724
+ card.appendChild(kicker);
725
+
726
+ // dl/dt/dd rows
727
+ var dl = document.createElement('dl');
728
+
729
+ // Row: install location
730
+ var dt2 = document.createElement('dt');
731
+ dt2.textContent = 'install location';
732
+ var dd2 = document.createElement('dd');
733
+ var loc = (machine && machine.aid_home) ? machine.aid_home : '—';
734
+ var locSpan = document.createElement('span');
735
+ locSpan.className = 'plugin-name';
736
+ locSpan.textContent = loc;
737
+ dd2.appendChild(locSpan);
738
+ dl.appendChild(dt2);
739
+ dl.appendChild(dd2);
740
+
741
+ // Row: available tools
742
+ var dt3 = document.createElement('dt');
743
+ dt3.textContent = 'available tools';
744
+ var dd3 = document.createElement('dd');
745
+ var catalog = (machine && Array.isArray(machine.tools_catalog)) ? machine.tools_catalog : [];
746
+ if (catalog.length === 0) {
747
+ dd3.textContent = '—';
748
+ } else {
749
+ var chipRow = document.createElement('span');
750
+ for (var i = 0; i < catalog.length; i++) {
751
+ var chip = document.createElement('span');
752
+ chip.className = 'badge badge-dim';
753
+ chip.style.marginRight = '0.25rem';
754
+ chip.textContent = catalog[i];
755
+ chipRow.appendChild(chip);
756
+ }
757
+ dd3.appendChild(chipRow);
758
+ }
759
+ dl.appendChild(dt3);
760
+ dl.appendChild(dd3);
761
+
762
+ card.appendChild(dl);
763
+
764
+ container.appendChild(card);
765
+ }
766
+
767
+ // ---------------------------------------------------------------------------
768
+ // UI-H2 + UI-H3 -- Repo card grid
769
+ // ---------------------------------------------------------------------------
770
+ function renderRepoGrid(repos) {
771
+ var repoSection = document.getElementById('repo-section');
772
+ var emptyRegistry = document.getElementById('empty-registry');
773
+ var grid = document.getElementById('repo-grid');
774
+
775
+ if (!repos || repos.length === 0) {
776
+ if (repoSection) repoSection.style.display = 'none';
777
+ if (emptyRegistry) emptyRegistry.style.display = '';
778
+ return;
779
+ }
780
+
781
+ if (emptyRegistry) emptyRegistry.style.display = 'none';
782
+ if (repoSection) repoSection.style.display = '';
783
+
784
+ if (!grid) return;
785
+ grid.innerHTML = '';
786
+
787
+ // Client-side sort by display name (case-insensitive, stable for UX)
788
+ var sorted = repos.slice().sort(function (a, b) {
789
+ var na = _displayName(a).toLowerCase();
790
+ var nb = _displayName(b).toLowerCase();
791
+ if (na < nb) return -1;
792
+ if (na > nb) return 1;
793
+ return 0;
794
+ });
795
+
796
+ for (var i = 0; i < sorted.length; i++) {
797
+ grid.appendChild(_renderRepoCard(sorted[i]));
798
+ }
799
+ }
800
+
801
+ // Derive the display name for a repo (never raw path/id as title).
802
+ // Returns the name, or the folder basename of path, or em-dash.
803
+ function _displayName(repo) {
804
+ if (repo.name) return repo.name;
805
+ // Folder basename fallback
806
+ if (repo.path) {
807
+ var parts = repo.path.replace(/\\/g, '/').split('/');
808
+ for (var k = parts.length - 1; k >= 0; k--) {
809
+ if (parts[k]) return parts[k];
810
+ }
811
+ }
812
+ return '—';
813
+ }
814
+
815
+ function _renderRepoCard(repo) {
816
+ var available = repo.available === true;
817
+ var hasHome = repo.has_home === true;
818
+
819
+ if (!available) {
820
+ return _renderUnavailableCard(repo);
821
+ }
822
+
823
+ // Available card: whole-card click if has_home, non-clickable div otherwise
824
+ var card;
825
+ if (hasHome) {
826
+ card = document.createElement('a');
827
+ card.className = 'card card-link';
828
+ card.href = '/r/' + repo.id + '/home.html';
829
+ } else {
830
+ card = document.createElement('div');
831
+ card.className = 'card';
832
+ }
833
+
834
+ // Title (h3)
835
+ var titleEl = document.createElement('h3');
836
+ titleEl.textContent = _displayName(repo);
837
+ card.appendChild(titleEl);
838
+
839
+ // Description
840
+ var descEl = document.createElement('p');
841
+ descEl.className = 'meta';
842
+ descEl.textContent = repo.description ? repo.description : '—';
843
+ card.appendChild(descEl);
844
+
845
+ // Chip row: each installed tool with the AID version after its name + KB.
846
+ var chipWrap = document.createElement('div');
847
+ chipWrap.className = 'chip-row';
848
+
849
+ // tools_installed chips -- "<tool> v<version>" (version after the tool name).
850
+ var tools = Array.isArray(repo.tools_installed) ? repo.tools_installed : [];
851
+ var verSuffix = repo.aid_version ? (' v' + repo.aid_version) : '';
852
+ for (var t = 0; t < tools.length; t++) {
853
+ var tChip = document.createElement('span');
854
+ tChip.className = 'badge badge-dim';
855
+ tChip.textContent = tools[t] + verSuffix;
856
+ chipWrap.appendChild(tChip);
857
+ }
858
+ // If no tools are listed but a version is known, still show it.
859
+ if (tools.length === 0 && repo.aid_version) {
860
+ var vOnly = document.createElement('span');
861
+ vOnly.className = 'badge badge-info';
862
+ vOnly.textContent = 'v' + repo.aid_version;
863
+ chipWrap.appendChild(vOnly);
864
+ }
865
+
866
+ // has_kb affordance (advisory non-interactive chip)
867
+ if (repo.has_kb) {
868
+ var kbChip = document.createElement('span');
869
+ kbChip.className = 'badge badge-purple';
870
+ kbChip.textContent = 'KB';
871
+ chipWrap.appendChild(kbChip);
872
+ }
873
+
874
+ if (chipWrap.hasChildNodes()) {
875
+ card.appendChild(chipWrap);
876
+ }
877
+
878
+ // Pipeline summary line (FR27): total pipelines + how many are in progress.
879
+ if (typeof repo.pipeline_count === 'number') {
880
+ var pc = repo.pipeline_count;
881
+ var ip = (typeof repo.pipelines_in_progress === 'number') ? repo.pipelines_in_progress : 0;
882
+ var pipeLine = document.createElement('p');
883
+ pipeLine.className = 'meta';
884
+ pipeLine.style.marginTop = '0.5rem';
885
+ pipeLine.textContent = pc + ' pipeline' + (pc === 1 ? '' : 's') + ' · ' + ip + ' in progress';
886
+ card.appendChild(pipeLine);
887
+ }
888
+
889
+ // has_home=false note
890
+ if (!hasHome) {
891
+ var noteEl = document.createElement('p');
892
+ noteEl.className = 'meta';
893
+ noteEl.style.marginTop = '0.5rem';
894
+ noteEl.textContent = 'dashboard not generated yet';
895
+ card.appendChild(noteEl);
896
+ }
897
+
898
+ return card;
899
+ }
900
+
901
+ // UI-H3 unavailable card: muted, non-clickable, FR18 prune guidance
902
+ function _renderUnavailableCard(repo) {
903
+ var card = document.createElement('div');
904
+ card.className = 'card card-unavailable';
905
+
906
+ // Status glyph
907
+ var statusChip = document.createElement('span');
908
+ statusChip.className = 'badge badge-dim';
909
+ statusChip.textContent = '⊘ unavailable';
910
+ card.appendChild(statusChip);
911
+
912
+ // Registered path
913
+ var pathEl = document.createElement('p');
914
+ pathEl.className = 'meta';
915
+ pathEl.style.marginTop = '0.5rem';
916
+ var pathCode = document.createElement('code');
917
+ pathCode.textContent = repo.path || '—';
918
+ pathEl.appendChild(pathCode);
919
+ card.appendChild(pathEl);
920
+
921
+ // FR18 prune guidance (step-by-step, no write button, NFR2)
922
+ var introEl = document.createElement('p');
923
+ introEl.className = 'meta';
924
+ introEl.style.marginTop = '0.75rem';
925
+ introEl.textContent = "This repo's folder is gone. To remove it from the CLI home:";
926
+ card.appendChild(introEl);
927
+
928
+ var ol = document.createElement('ol');
929
+ ol.style.color = 'var(--text-dim)';
930
+ ol.style.fontSize = '0.85rem';
931
+ ol.style.margin = '0.25rem 0 0 0';
932
+
933
+ var li1 = document.createElement('li');
934
+ li1.appendChild(document.createTextNode('Run '));
935
+ var cmdCode = document.createElement('code');
936
+ cmdCode.textContent = 'aid remove --target ' + (repo.path || '');
937
+ li1.appendChild(cmdCode);
938
+ li1.appendChild(document.createTextNode(' (unregisters the now-unavailable repo).'));
939
+ ol.appendChild(li1);
940
+
941
+ var li2 = document.createElement('li');
942
+ li2.textContent = 'Verify: this card disappears the next time you refresh the page.';
943
+ ol.appendChild(li2);
944
+
945
+ card.appendChild(ol);
946
+
947
+ return card;
948
+ }
949
+
950
+ // ---------------------------------------------------------------------------
951
+ // Theme (reused from donor)
952
+ // ---------------------------------------------------------------------------
953
+ function initTheme() {
954
+ var saved = localStorage.getItem('aid-dashboard-theme');
955
+ if (saved === 'dark' || saved === 'light') {
956
+ document.documentElement.setAttribute('data-theme', saved);
957
+ }
958
+ _syncThemeBtn();
959
+ var btn = document.getElementById('theme-toggle');
960
+ if (btn) btn.addEventListener('click', toggleTheme);
961
+ }
962
+
963
+ function toggleTheme() {
964
+ var cur = document.documentElement.getAttribute('data-theme') || 'light';
965
+ var next = cur === 'dark' ? 'light' : 'dark';
966
+ document.documentElement.setAttribute('data-theme', next);
967
+ localStorage.setItem('aid-dashboard-theme', next);
968
+ _syncThemeBtn();
969
+ }
970
+
971
+ function _syncThemeBtn() {
972
+ var cur = document.documentElement.getAttribute('data-theme') || 'light';
973
+ var icon = document.getElementById('theme-icon');
974
+ var label = document.getElementById('theme-label');
975
+ if (cur === 'dark') {
976
+ if (icon) icon.innerHTML = '&#9788;';
977
+ if (label) label.textContent = 'Light';
978
+ } else {
979
+ if (icon) icon.innerHTML = '&#9680;';
980
+ if (label) label.textContent = 'Dark';
981
+ }
982
+ }
983
+
984
+ })();
985
+ </script>
986
+ </body>
987
+ </html>