@jakkrichm/create-nexus-devflow 2.0.21 → 2.0.23

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,771 @@
1
+ import { spawn } from "node:child_process";
2
+ import http from "node:http";
3
+ import { readHistory } from "./history.js";
4
+ import { readProjectStatus } from "./status.js";
5
+ const DASHBOARD_HOST = "127.0.0.1";
6
+ async function startDashboardServer(startPath = process.cwd(), options = {}) {
7
+ const initialStatus = await readProjectStatus(startPath);
8
+ const projectRoot = initialStatus.project.root;
9
+ const server = http.createServer((request, response) => {
10
+ void handleRequest(projectRoot, request, response);
11
+ });
12
+ await new Promise((resolve, reject) => {
13
+ const onError = (error) => {
14
+ server.off("listening", onListening);
15
+ reject(error);
16
+ };
17
+ const onListening = () => {
18
+ server.off("error", onError);
19
+ resolve();
20
+ };
21
+ server.once("error", onError);
22
+ server.once("listening", onListening);
23
+ server.listen(options.port ?? 0, DASHBOARD_HOST);
24
+ });
25
+ const address = server.address();
26
+ if (!address || typeof address === "string") {
27
+ await closeServer(server);
28
+ throw new Error("Nexus-DevFlow dashboard could not determine its local address.");
29
+ }
30
+ return {
31
+ url: `http://${DASHBOARD_HOST}:${address.port}`,
32
+ close: () => closeServer(server)
33
+ };
34
+ }
35
+ async function handleRequest(projectRoot, request, response) {
36
+ const method = request.method || "GET";
37
+ if (method !== "GET" && method !== "HEAD") {
38
+ response.setHeader("Allow", "GET, HEAD");
39
+ sendResponse(response, method, 405, "text/plain; charset=utf-8", "Method not allowed.\n");
40
+ return;
41
+ }
42
+ const pathname = new URL(request.url || "/", `http://${DASHBOARD_HOST}`).pathname;
43
+ if (pathname === "/") {
44
+ response.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src * 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'");
45
+ sendResponse(response, method, 200, "text/html; charset=utf-8", DASHBOARD_HTML);
46
+ return;
47
+ }
48
+ if (pathname === "/api/status") {
49
+ try {
50
+ const status = await readProjectStatus(projectRoot);
51
+ sendResponse(response, method, 200, "application/json; charset=utf-8", `${JSON.stringify(status)}\n`);
52
+ }
53
+ catch (error) {
54
+ sendResponse(response, method, 500, "application/json; charset=utf-8", `${JSON.stringify({
55
+ error: error instanceof Error ? error.message : "Unable to read Nexus-DevFlow status."
56
+ })}\n`);
57
+ }
58
+ return;
59
+ }
60
+ if (pathname === "/api/history") {
61
+ try {
62
+ const history = await readHistory(projectRoot);
63
+ sendResponse(response, method, 200, "application/json; charset=utf-8", `${JSON.stringify(history)}\n`);
64
+ }
65
+ catch (error) {
66
+ sendResponse(response, method, 500, "application/json; charset=utf-8", `${JSON.stringify({
67
+ error: error instanceof Error ? error.message : "Unable to read DevFlow history."
68
+ })}\n`);
69
+ }
70
+ return;
71
+ }
72
+ if (pathname === "/favicon.ico") {
73
+ sendResponse(response, method, 204, "text/plain; charset=utf-8", "");
74
+ return;
75
+ }
76
+ sendResponse(response, method, 404, "text/plain; charset=utf-8", "Not found.\n");
77
+ }
78
+ function sendResponse(response, method, statusCode, contentType, body) {
79
+ response.statusCode = statusCode;
80
+ response.setHeader("Cache-Control", "no-store");
81
+ response.setHeader("Content-Type", contentType);
82
+ response.setHeader("Access-Control-Allow-Origin", "*");
83
+ response.setHeader("X-Content-Type-Options", "nosniff");
84
+ response.end(method === "HEAD" ? undefined : body);
85
+ }
86
+ async function closeServer(server) {
87
+ if (!server.listening) {
88
+ return;
89
+ }
90
+ await new Promise((resolve, reject) => {
91
+ server.close((error) => {
92
+ if (error) {
93
+ reject(error);
94
+ return;
95
+ }
96
+ resolve();
97
+ });
98
+ });
99
+ }
100
+ async function openDashboard(url) {
101
+ const command = process.platform === "darwin"
102
+ ? "open"
103
+ : process.platform === "win32"
104
+ ? "cmd"
105
+ : "xdg-open";
106
+ const args = process.platform === "win32"
107
+ ? ["/c", "start", "", url]
108
+ : [url];
109
+ await new Promise((resolve, reject) => {
110
+ const child = spawn(command, args, {
111
+ detached: true,
112
+ stdio: "ignore"
113
+ });
114
+ child.once("error", reject);
115
+ child.once("spawn", () => {
116
+ child.unref();
117
+ resolve();
118
+ });
119
+ });
120
+ }
121
+ const DASHBOARD_HTML = `<!doctype html>
122
+ <html lang="en">
123
+ <head>
124
+ <meta charset="utf-8">
125
+ <meta name="viewport" content="width=device-width, initial-scale=1">
126
+ <title>Nexus-DevFlow Dashboard</title>
127
+ <style>
128
+ :root {
129
+ color-scheme: light;
130
+ --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
131
+ --font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
132
+ --paper: #f5f6f3;
133
+ --surface: rgba(255, 255, 255, .84);
134
+ --surface-solid: #ffffff;
135
+ --surface-muted: #eef1ed;
136
+ --ink: #121817;
137
+ --ink-soft: #45504d;
138
+ --ink-muted: #717b78;
139
+ --line: #d9ded9;
140
+ --line-strong: #bdc7c1;
141
+ --blue: #155eef;
142
+ --blue-dark: #0b43ba;
143
+ --green: #0b7a53;
144
+ --green-soft: #e7f5ee;
145
+ --amber: #9a5700;
146
+ --amber-soft: #fff2d9;
147
+ --red: #a5333f;
148
+ --red-soft: #fdebed;
149
+ --code: #111715;
150
+ --code-line: #2c3532;
151
+ --code-text: #d9dfdc;
152
+ --code-muted: #9ba7a2;
153
+ --code-blue: #76a8ff;
154
+ --code-green: #70d5a9;
155
+ font-family: var(--font-sans);
156
+ background: var(--paper);
157
+ color: var(--ink);
158
+ }
159
+
160
+ * { box-sizing: border-box; }
161
+
162
+ body {
163
+ margin: 0;
164
+ min-width: 320px;
165
+ min-height: 100vh;
166
+ background:
167
+ linear-gradient(rgba(21, 94, 239, .09) 1px, transparent 1px),
168
+ linear-gradient(90deg, rgba(21, 94, 239, .09) 1px, transparent 1px),
169
+ radial-gradient(circle at 12% 0%, rgba(21, 94, 239, .08), transparent 34rem),
170
+ var(--paper);
171
+ background-size: 40px 40px, 40px 40px, auto, auto;
172
+ -webkit-font-smoothing: antialiased;
173
+ }
174
+
175
+ .shell { width: min(1180px, calc(100% - 40px)); margin: 0 auto; padding: 38px 0 64px; }
176
+
177
+ header {
178
+ display: flex;
179
+ align-items: flex-start;
180
+ justify-content: space-between;
181
+ gap: 24px;
182
+ margin-bottom: 30px;
183
+ }
184
+
185
+ .brand {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 11px;
189
+ margin-bottom: 28px;
190
+ color: var(--ink);
191
+ font-size: 16px;
192
+ font-weight: 700;
193
+ letter-spacing: -.02em;
194
+ cursor: pointer;
195
+ }
196
+
197
+ .brand-mark {
198
+ width: 28px;
199
+ height: 28px;
200
+ flex: 0 0 auto;
201
+ transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
202
+ }
203
+
204
+ .brand:hover .brand-mark {
205
+ transform: rotate(15deg) scale(1.12);
206
+ }
207
+
208
+ .brand-context { color: var(--ink-muted); font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: .04em; text-transform: uppercase; }
209
+ .brand-separator { width: 1px; height: 17px; background: var(--line-strong); }
210
+
211
+ .eyebrow {
212
+ display: inline-flex;
213
+ align-items: center;
214
+ gap: 8px;
215
+ color: var(--blue-dark);
216
+ font: 600 11px/1 var(--font-mono);
217
+ letter-spacing: .12em;
218
+ text-transform: uppercase;
219
+ }
220
+
221
+ .eyebrow::before { width: 22px; height: 1px; content: ""; background: var(--blue); }
222
+
223
+ h1 { margin: 13px 0 8px; color: var(--ink); font-size: clamp(32px, 4vw, 50px); letter-spacing: -.045em; }
224
+ .path { max-width: 760px; overflow-wrap: anywhere; color: var(--ink-muted); font: 12px/1.6 var(--font-mono); }
225
+
226
+ /* Live status dot animation */
227
+ .live {
228
+ display: inline-flex;
229
+ align-items: center;
230
+ gap: 8px;
231
+ padding: 9px 14px;
232
+ border: 1px solid var(--line-strong);
233
+ border-radius: 999px;
234
+ background: rgba(255, 255, 255, .92);
235
+ color: var(--ink-soft);
236
+ font-size: 12px;
237
+ font-weight: 600;
238
+ white-space: nowrap;
239
+ box-shadow: 0 1px 3px rgba(18, 24, 23, .06);
240
+ transition: border-color .3s ease, background-color .3s ease;
241
+ }
242
+
243
+ @keyframes livePulse {
244
+ 0% { transform: scale(1); opacity: 0.7; }
245
+ 70% { transform: scale(2.4); opacity: 0; }
246
+ 100% { transform: scale(2.4); opacity: 0; }
247
+ }
248
+
249
+ @keyframes livePulseBox {
250
+ 0% { box-shadow: 0 0 0 0 rgba(11, 122, 83, 0.5); }
251
+ 70% { box-shadow: 0 0 0 8px rgba(11, 122, 83, 0); }
252
+ 100% { box-shadow: 0 0 0 0 rgba(11, 122, 83, 0); }
253
+ }
254
+
255
+ .live-dot {
256
+ position: relative;
257
+ display: inline-block;
258
+ width: 8px;
259
+ height: 8px;
260
+ border-radius: 50%;
261
+ background: var(--green);
262
+ transition: background-color .3s ease;
263
+ }
264
+
265
+ .live-dot::after {
266
+ content: "";
267
+ position: absolute;
268
+ display: block;
269
+ inset: -4px;
270
+ border-radius: 50%;
271
+ background: var(--green);
272
+ opacity: 0;
273
+ pointer-events: none;
274
+ }
275
+
276
+ .live-dot.is-live {
277
+ animation: livePulseBox 2.2s infinite;
278
+ }
279
+
280
+ .live-dot.is-live::after {
281
+ animation: livePulse 2.2s ease-out infinite;
282
+ }
283
+
284
+ .grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; }
285
+
286
+ /* Initial Load Card Entrance Animation */
287
+ @keyframes cardEntrance {
288
+ from { opacity: 0; transform: translateY(10px); }
289
+ to { opacity: 1; transform: translateY(0); }
290
+ }
291
+
292
+ .card {
293
+ grid-column: span 4;
294
+ min-width: 0;
295
+ padding: 22px;
296
+ border: 1px solid rgba(189, 199, 193, .78);
297
+ border-radius: 14px;
298
+ background: var(--surface);
299
+ box-shadow: 0 1px 2px rgba(18, 24, 23, .05), 0 10px 30px rgba(18, 24, 23, .04);
300
+ backdrop-filter: blur(14px);
301
+ transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
302
+ }
303
+
304
+ .card-enter {
305
+ animation: cardEntrance 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
306
+ }
307
+
308
+ .card:hover {
309
+ transform: translateY(-2px);
310
+ box-shadow: 0 12px 36px rgba(18, 24, 23, .08), 0 2px 6px rgba(18, 24, 23, .04);
311
+ border-color: rgba(21, 94, 239, 0.4);
312
+ }
313
+
314
+ .card.wide { grid-column: span 8; }
315
+ .card.full { grid-column: 1 / -1; }
316
+
317
+ .card-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
318
+ .label { color: var(--blue-dark); font: 600 11px/1 var(--font-mono); letter-spacing: .1em; text-transform: uppercase; }
319
+
320
+ .value {
321
+ color: var(--ink);
322
+ font-size: 20px;
323
+ font-weight: 700;
324
+ letter-spacing: -.02em;
325
+ transition: color .2s ease;
326
+ }
327
+
328
+ .muted { color: var(--ink-muted); font-size: 13px; line-height: 1.6; }
329
+
330
+ /* Pill Transitions and Scale Pop */
331
+ @keyframes pillPop {
332
+ 0% { transform: scale(1); }
333
+ 50% { transform: scale(1.06); }
334
+ 100% { transform: scale(1); }
335
+ }
336
+
337
+ .pill {
338
+ display: inline-block;
339
+ padding: 5px 10px;
340
+ border: 1px solid var(--line);
341
+ border-radius: 999px;
342
+ color: var(--ink-muted);
343
+ background: var(--surface-muted);
344
+ font: 600 10px/1 var(--font-mono);
345
+ letter-spacing: .04em;
346
+ text-transform: uppercase;
347
+ transition: background-color .3s ease, border-color .3s ease, color .3s ease, transform .2s ease;
348
+ }
349
+
350
+ .pill-pop {
351
+ animation: pillPop 300ms cubic-bezier(0.16, 1, 0.3, 1);
352
+ }
353
+
354
+ .pill.ok, .pill.ready, .pill.active { border-color: #b9dfce; background: var(--green-soft); color: var(--green); }
355
+ .pill.warning, .pill.blocked, .pill.needs_verification { border-color: #efd5a5; background: var(--amber-soft); color: var(--amber); }
356
+
357
+ .facts { display: grid; gap: 12px; }
358
+ .fact { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; padding-bottom: 11px; border-bottom: 1px solid var(--line); }
359
+ .fact:last-child { padding-bottom: 0; border-bottom: 0; }
360
+ .fact span:first-child { color: var(--ink-muted); font-size: 12px; }
361
+ .fact span:last-child { max-width: 70%; overflow-wrap: anywhere; color: var(--ink-soft); font: 12px/1.45 var(--font-mono); text-align: right; }
362
+
363
+ /* Progress bar with Shimmer and Increase Glow */
364
+ .progress { height: 8px; margin: 16px 0 10px; overflow: hidden; border-radius: 999px; background: var(--surface-muted); }
365
+
366
+ @keyframes shimmer {
367
+ 0% { background-position: -200% 0; }
368
+ 100% { background-position: 200% 0; }
369
+ }
370
+
371
+ @keyframes progressGlow {
372
+ 0% { box-shadow: 0 0 0 0 rgba(21, 94, 239, 0.6); }
373
+ 50% { box-shadow: 0 0 12px 2px rgba(21, 94, 239, 0.7); }
374
+ 100% { box-shadow: 0 0 0 0 rgba(21, 94, 239, 0); }
375
+ }
376
+
377
+ .progress-bar {
378
+ display: block;
379
+ width: 0;
380
+ height: 100%;
381
+ border-radius: inherit;
382
+ background: var(--blue);
383
+ transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
384
+ }
385
+
386
+ .progress-bar.active-shimmer {
387
+ background: linear-gradient(90deg, #155eef 0%, #4785ff 50%, #155eef 100%);
388
+ background-size: 200% 100%;
389
+ animation: shimmer 3s infinite linear;
390
+ }
391
+
392
+ .progress-glow {
393
+ animation: progressGlow 400ms ease-out;
394
+ }
395
+
396
+ /* Highlight Flash for values */
397
+ @keyframes flashGreen {
398
+ 0% { background-color: rgba(11, 122, 83, 0.2); }
399
+ 100% { background-color: transparent; }
400
+ }
401
+
402
+ @keyframes flashAmber {
403
+ 0% { background-color: rgba(154, 87, 0, 0.2); }
404
+ 100% { background-color: transparent; }
405
+ }
406
+
407
+ .flash-green { animation: flashGreen 600ms ease-out; border-radius: 4px; padding: 0 4px; }
408
+ .flash-amber { animation: flashAmber 600ms ease-out; border-radius: 4px; padding: 0 4px; }
409
+
410
+ /* Code Panel & Next Action Flip/Glow */
411
+ @keyframes commandFlip {
412
+ 0% { opacity: 0; transform: translateY(-6px); }
413
+ 100% { opacity: 1; transform: translateY(0); }
414
+ }
415
+
416
+ @keyframes panelGlowPulse {
417
+ 0% { border-color: rgba(118, 168, 255, 0.9); box-shadow: 0 0 24px rgba(21, 94, 239, 0.35); }
418
+ 100% { border-color: var(--code-line); box-shadow: 0 24px 80px rgba(18, 24, 23, .12); }
419
+ }
420
+
421
+ .code-panel {
422
+ color: var(--code-text);
423
+ border-color: var(--code-line);
424
+ background: var(--code);
425
+ box-shadow: 0 24px 80px rgba(18, 24, 23, .12);
426
+ backdrop-filter: none;
427
+ transition: border-color .3s ease, box-shadow .3s ease;
428
+ }
429
+
430
+ .code-panel .label { color: var(--code-blue); }
431
+ .code-panel .muted { color: var(--code-muted); }
432
+
433
+ .next-action { padding: 24px; }
434
+
435
+ .command {
436
+ margin: 13px 0 8px;
437
+ color: var(--code-blue);
438
+ font: 600 clamp(20px, 3vw, 29px)/1.3 var(--font-mono);
439
+ overflow-wrap: anywhere;
440
+ }
441
+
442
+ .command-flip { animation: commandFlip 300ms ease-out; }
443
+ .next-action-pulse { animation: panelGlowPulse 800ms ease-out; }
444
+
445
+ /* Staggered List Items Fade-in */
446
+ @keyframes fadeSlideIn {
447
+ from { opacity: 0; transform: translateY(6px); }
448
+ to { opacity: 1; transform: translateY(0); }
449
+ }
450
+
451
+ ul { margin: 0; padding: 0; list-style: none; }
452
+ li {
453
+ padding: 11px 10px;
454
+ border-bottom: 1px solid var(--line);
455
+ color: var(--ink-soft);
456
+ font-size: 13px;
457
+ line-height: 1.5;
458
+ border-radius: 6px;
459
+ transition: background-color 0.2s ease, transform 0.2s ease;
460
+ }
461
+ li:hover {
462
+ background-color: rgba(21, 94, 239, 0.04);
463
+ transform: translateX(3px);
464
+ }
465
+ li:last-child { border-bottom: 0; }
466
+ li.empty { color: var(--ink-muted); }
467
+ li.empty:hover { background-color: transparent; transform: none; }
468
+ li.fade-slide-in { animation: fadeSlideIn 250ms ease-out both; }
469
+
470
+ footer { margin-top: 24px; color: var(--ink-muted); font: 11px/1.6 var(--font-mono); }
471
+
472
+ @media (max-width: 860px) {
473
+ .card, .card.wide { grid-column: span 6; }
474
+ .card.full { grid-column: 1 / -1; }
475
+ }
476
+
477
+ @media (max-width: 620px) {
478
+ .shell { width: min(100% - 24px, 1180px); padding-top: 24px; }
479
+ header { flex-direction: column; }
480
+ .brand { margin-bottom: 22px; }
481
+ .card, .card.wide, .card.full { grid-column: 1 / -1; }
482
+ }
483
+
484
+ /* Accessibility Reduced Motion Rule */
485
+ @media (prefers-reduced-motion: reduce) {
486
+ *, *::before, *::after {
487
+ animation-duration: 0.001ms !important;
488
+ animation-iteration-count: 1 !important;
489
+ transition-duration: 0.001ms !important;
490
+ }
491
+ }
492
+ </style>
493
+ </head>
494
+ <body>
495
+ <main class="shell">
496
+ <div class="brand">
497
+ <svg class="brand-mark" viewBox="0 0 48 48" role="img" aria-label="Nexus-DevFlow">
498
+ <path fill="#155eef" d="M4 4h25.2L16.3 44H4zM39.7 4H44v40H26.8z"></path>
499
+ </svg>
500
+ <span>Nexus-DevFlow</span>
501
+ <span class="brand-separator" aria-hidden="true"></span>
502
+ <span class="brand-context">Dashboard</span>
503
+ </div>
504
+ <header>
505
+ <div>
506
+ <div class="eyebrow">Local 3-Pillars Workspace Status</div>
507
+ <h1 id="project-name">Loading project...</h1>
508
+ <div class="path" id="project-path">...</div>
509
+ </div>
510
+ <div class="live"><span class="live-dot is-live" id="live-dot"></span><span id="live-label">Connecting</span></div>
511
+ </header>
512
+
513
+ <section class="grid">
514
+ <article class="card card-enter" style="animation-delay: 0.04s;">
515
+ <div class="card-head"><span class="label">Nexus-DevFlow</span><span class="pill" id="health-pill">Loading</span></div>
516
+ <div class="facts">
517
+ <div class="fact"><span>Version</span><span id="val-version">-</span></div>
518
+ <div class="fact"><span>Adapters</span><span id="val-adapters">-</span></div>
519
+ <div class="fact"><span>Architecture</span><span>3-Pillars Model</span></div>
520
+ </div>
521
+ </article>
522
+
523
+ <article class="card wide card-enter" style="animation-delay: 0.08s;">
524
+ <div class="card-head"><span class="label">Current Work</span><span class="pill" id="work-pill">Loading</span></div>
525
+ <div class="value" id="work-title">Reading living spec...</div>
526
+ <div class="progress"><span class="progress-bar" id="work-progress-bar"></span></div>
527
+ <div class="muted" id="work-meta">Loading checklist steps...</div>
528
+ </article>
529
+
530
+ <article class="card card-enter" style="animation-delay: 0.12s;">
531
+ <div class="card-head"><span class="label">Git Status</span><span class="pill" id="git-pill">Loading</span></div>
532
+ <div class="facts">
533
+ <div class="fact"><span>Branch</span><span id="git-branch">-</span></div>
534
+ <div class="fact"><span>Working Tree</span><span id="git-changed">-</span></div>
535
+ <div class="fact"><span>Upstream</span><span id="git-upstream">-</span></div>
536
+ </div>
537
+ </article>
538
+
539
+ <article class="card card-enter" style="animation-delay: 0.16s;">
540
+ <div class="card-head"><span class="label">Findings</span><span class="value" id="findings-count">-</span></div>
541
+ <ul id="findings-list"><li class="empty">Loading findings...</li></ul>
542
+ </article>
543
+
544
+ <article class="card card-enter" style="animation-delay: 0.20s;">
545
+ <div class="card-head"><span class="label">Completion</span><span class="pill" id="completion-pill">Loading</span></div>
546
+ <ul id="completion-list"><li class="empty">Checking readiness...</li></ul>
547
+ </article>
548
+
549
+ <article class="card card-enter" style="animation-delay: 0.24s;">
550
+ <div class="card-head"><span class="label">Attention</span><span class="value" id="warnings-count">-</span></div>
551
+ <ul id="warnings-list"><li class="empty">Loading warnings...</li></ul>
552
+ </article>
553
+
554
+ <article class="card full card-enter" style="animation-delay: 0.28s;">
555
+ <div class="card-head"><span class="label">Completed Work Archive</span><span class="value" id="history-count">-</span></div>
556
+ <div class="muted">Categorized history of features, fixes, and rollbacks.</div>
557
+ <ul id="history-list"><li class="empty">Loading history archive...</li></ul>
558
+ </article>
559
+
560
+ <article class="card full code-panel next-action card-enter" style="animation-delay: 0.32s;" id="next-panel">
561
+ <span class="label">Next Action</span>
562
+ <div class="command" id="next-command">Loading...</div>
563
+ <div class="muted" id="next-reason"></div>
564
+ </article>
565
+ </section>
566
+
567
+ <footer>Read-only local DevFlow 2.0 dashboard. Refreshes every 2 seconds while process is active.</footer>
568
+ </main>
569
+
570
+ <script>
571
+ const byId = (id) => document.getElementById(id);
572
+ let prevData = null;
573
+ let isFirstLoad = true;
574
+ let refreshing = false;
575
+
576
+ function setPill(id, value) {
577
+ const node = byId(id);
578
+ if (!node) return;
579
+ const formatted = String(value || '').replaceAll("_", " ");
580
+ const newClass = "pill " + (value || 'idle');
581
+
582
+ if (!isFirstLoad && (node.textContent !== formatted || !node.className.includes(value))) {
583
+ node.classList.remove('pill-pop');
584
+ void node.offsetWidth;
585
+ node.classList.add('pill-pop');
586
+ }
587
+
588
+ node.textContent = formatted;
589
+ node.className = newClass + (node.classList.contains('pill-pop') ? ' pill-pop' : '');
590
+ }
591
+
592
+ function animateCount(node, start, end, duration = 400) {
593
+ if (!node) return;
594
+ if (start === end) {
595
+ node.textContent = end;
596
+ return;
597
+ }
598
+ const startTime = performance.now();
599
+ function step(currentTime) {
600
+ const elapsed = currentTime - startTime;
601
+ const progress = Math.min(elapsed / duration, 1);
602
+ const currentVal = Math.round(start + (end - start) * progress);
603
+ node.textContent = currentVal;
604
+ if (progress < 1) {
605
+ requestAnimationFrame(step);
606
+ } else {
607
+ node.textContent = end;
608
+ }
609
+ }
610
+ requestAnimationFrame(step);
611
+ }
612
+
613
+ function triggerFlash(node, type) {
614
+ if (!node) return;
615
+ const className = type === 'green' ? 'flash-green' : 'flash-amber';
616
+ node.classList.remove('flash-green', 'flash-amber');
617
+ void node.offsetWidth;
618
+ node.classList.add(className);
619
+ setTimeout(() => node.classList.remove(className), 600);
620
+ }
621
+
622
+ function setList(id, values, emptyMessage) {
623
+ const list = byId(id);
624
+ if (!list) return;
625
+ const prevItemsText = Array.from(list.children).map(c => c.textContent);
626
+ list.replaceChildren();
627
+ const items = values.length > 0 ? values : [emptyMessage];
628
+
629
+ items.forEach((value, idx) => {
630
+ const item = document.createElement("li");
631
+ item.textContent = value;
632
+ if (values.length === 0) {
633
+ item.className = "empty";
634
+ } else if (!isFirstLoad && !prevItemsText.includes(value)) {
635
+ item.className = "fade-slide-in";
636
+ item.style.animationDelay = (idx * 40) + 'ms';
637
+ }
638
+ list.append(item);
639
+ });
640
+ }
641
+
642
+ async function refreshStatus() {
643
+ if (refreshing) return;
644
+ refreshing = true;
645
+ try {
646
+ const [resStatus, resHistory] = await Promise.all([
647
+ fetch('/api/status'),
648
+ fetch('/api/history').catch(() => null)
649
+ ]);
650
+
651
+ if (!resStatus.ok) throw new Error('HTTP ' + resStatus.status);
652
+ const data = await resStatus.json();
653
+ const historyData = resHistory && resHistory.ok ? await resHistory.json() : { items: [] };
654
+
655
+ byId('live-dot').style.background = '#0b7a53';
656
+ byId('live-dot').classList.add('is-live');
657
+ byId('live-label').textContent = 'Live';
658
+
659
+ byId('project-name').textContent = data.project?.name || 'Nexus-DevFlow';
660
+ byId('project-path').textContent = data.project?.root || '';
661
+
662
+ setPill('health-pill', data.health || 'ok');
663
+ byId('val-version').textContent = data.devflow?.version ? 'v' + data.devflow.version : '-';
664
+ byId('val-adapters').textContent = (data.devflow?.adapters || []).join(', ') || 'none';
665
+
666
+ const work = data.currentWork || {};
667
+ setPill('work-pill', work.state || 'idle');
668
+ byId('work-title').textContent = work.title || 'No active run in progress';
669
+ byId('work-meta').textContent = work.state === 'active'
670
+ ? (work.completed + ' of ' + work.total + ' steps completed')
671
+ : 'Workspace is idle. Run /feature or /fix to start a new delivery run.';
672
+
673
+ const pct = work.total > 0 ? Math.round((work.completed / work.total) * 100) : 0;
674
+ const progressBar = byId('work-progress-bar');
675
+
676
+ const prevPct = prevData?.currentWork ? (prevData.currentWork.total > 0 ? Math.round((prevData.currentWork.completed / prevData.currentWork.total) * 100) : 0) : 0;
677
+ if (!isFirstLoad && pct > prevPct) {
678
+ progressBar.classList.remove('progress-glow');
679
+ void progressBar.offsetWidth;
680
+ progressBar.classList.add('progress-glow');
681
+ setTimeout(() => progressBar.classList.remove('progress-glow'), 400);
682
+ }
683
+
684
+ if (work.state === 'active') {
685
+ progressBar.classList.add('active-shimmer');
686
+ } else {
687
+ progressBar.classList.remove('active-shimmer');
688
+ }
689
+ progressBar.style.width = pct + '%';
690
+
691
+ const git = data.git || {};
692
+ setPill('git-pill', git.clean ? 'ok' : 'warning');
693
+ byId('git-branch').textContent = git.branch || 'unknown';
694
+ byId('git-changed').textContent = git.clean ? 'clean' : (git.changedFiles + ' changed files');
695
+ byId('git-upstream').textContent = git.upstream || 'none';
696
+
697
+ const findings = data.findings || { total: 0, blockers: [] };
698
+ const prevFindingsCount = prevData?.findings?.total ?? 0;
699
+ const findingsNode = byId('findings-count');
700
+ if (!isFirstLoad && findings.total !== prevFindingsCount) {
701
+ animateCount(findingsNode, prevFindingsCount, findings.total);
702
+ triggerFlash(findingsNode, findings.total < prevFindingsCount ? 'green' : 'amber');
703
+ } else {
704
+ findingsNode.textContent = findings.total || '0';
705
+ }
706
+ setList('findings-list', findings.blockers.map(b => b.id + ': ' + b.title), 'No blocking findings');
707
+
708
+ const comp = data.completion || { state: 'ready', blockers: [] };
709
+ setPill('completion-pill', comp.state || 'ready');
710
+ setList('completion-list', comp.blockers || [], 'All readiness checks passed');
711
+
712
+ const warnings = data.warnings || [];
713
+ const prevWarningsCount = prevData?.warnings?.length ?? 0;
714
+ const warningsNode = byId('warnings-count');
715
+ if (!isFirstLoad && warnings.length !== prevWarningsCount) {
716
+ animateCount(warningsNode, prevWarningsCount, warnings.length);
717
+ triggerFlash(warningsNode, warnings.length < prevWarningsCount ? 'green' : 'amber');
718
+ } else {
719
+ warningsNode.textContent = warnings.length;
720
+ }
721
+ setList('warnings-list', warnings.map(w => w.message), 'No active warnings or drift');
722
+
723
+ const historyItems = historyData.items || [];
724
+ const prevHistoryCount = prevData?.historyCount ?? 0;
725
+ const historyNode = byId('history-count');
726
+ if (!isFirstLoad && historyItems.length !== prevHistoryCount) {
727
+ animateCount(historyNode, prevHistoryCount, historyItems.length);
728
+ triggerFlash(historyNode, 'green');
729
+ } else {
730
+ historyNode.textContent = historyItems.length;
731
+ }
732
+ setList('history-list', historyItems.map(h => h.type.toUpperCase() + ': ' + h.title + (h.status ? ' (' + h.status + ')' : '')), 'No completed work in history archive');
733
+
734
+ const next = data.nextAction || {};
735
+ const cmdNode = byId('next-command');
736
+ const nextPanelNode = byId('next-panel');
737
+ if (!isFirstLoad && prevData?.nextAction?.command !== next.command) {
738
+ cmdNode.classList.remove('command-flip');
739
+ nextPanelNode.classList.remove('next-action-pulse');
740
+ void cmdNode.offsetWidth;
741
+ cmdNode.classList.add('command-flip');
742
+ nextPanelNode.classList.add('next-action-pulse');
743
+ setTimeout(() => {
744
+ cmdNode.classList.remove('command-flip');
745
+ nextPanelNode.classList.remove('next-action-pulse');
746
+ }, 800);
747
+ }
748
+ cmdNode.textContent = next.command || '/feature';
749
+ byId('next-reason').textContent = next.reason || 'Ready for next feature';
750
+
751
+ prevData = {
752
+ ...data,
753
+ historyCount: historyItems.length
754
+ };
755
+ } catch (err) {
756
+ byId('live-dot').style.background = '#a5333f';
757
+ byId('live-dot').classList.remove('is-live');
758
+ byId('live-label').textContent = 'Disconnected';
759
+ } finally {
760
+ isFirstLoad = false;
761
+ refreshing = false;
762
+ }
763
+ }
764
+
765
+ refreshStatus();
766
+ setInterval(refreshStatus, 2000);
767
+ </script>
768
+ </body>
769
+ </html>`;
770
+ export { openDashboard, startDashboardServer };
771
+ //# sourceMappingURL=dashboard.js.map