@jakkrichm/create-nexus-devflow 2.12.4 → 2.14.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.
Files changed (97) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/create-nexus-devflow.d.ts +1 -1
  3. package/dist/bin/create-nexus-devflow.js +4 -3
  4. package/dist/bin/create-nexus-devflow.js.map +1 -1
  5. package/dist/lib/active-context-engine.d.ts +47 -0
  6. package/dist/lib/active-context-engine.js +174 -0
  7. package/dist/lib/active-context-engine.js.map +1 -0
  8. package/dist/lib/branch-context.d.ts +1 -0
  9. package/dist/lib/branch-context.js +1 -0
  10. package/dist/lib/branch-context.js.map +1 -1
  11. package/dist/lib/current-work.js +3 -28
  12. package/dist/lib/current-work.js.map +1 -1
  13. package/dist/lib/dashboard-engine.d.ts +54 -0
  14. package/dist/lib/dashboard-engine.js +109 -0
  15. package/dist/lib/dashboard-engine.js.map +1 -0
  16. package/dist/lib/dashboard-page.d.ts +8 -1
  17. package/dist/lib/dashboard-page.js +10 -1
  18. package/dist/lib/dashboard-page.js.map +1 -1
  19. package/dist/lib/dashboard.d.ts +2 -0
  20. package/dist/lib/dashboard.js +78 -1068
  21. package/dist/lib/dashboard.js.map +1 -1
  22. package/dist/lib/delivery-lifecycle-engine.d.ts +96 -0
  23. package/dist/lib/delivery-lifecycle-engine.js +388 -0
  24. package/dist/lib/delivery-lifecycle-engine.js.map +1 -0
  25. package/dist/lib/gatekeeper-engine.d.ts +65 -0
  26. package/dist/lib/gatekeeper-engine.js +155 -0
  27. package/dist/lib/gatekeeper-engine.js.map +1 -0
  28. package/dist/lib/gatekeeper.d.ts +3 -22
  29. package/dist/lib/gatekeeper.js +4 -67
  30. package/dist/lib/gatekeeper.js.map +1 -1
  31. package/dist/lib/history.d.ts +3 -1
  32. package/dist/lib/history.js +2 -1
  33. package/dist/lib/history.js.map +1 -1
  34. package/dist/lib/skill-manager.js +319 -7
  35. package/dist/lib/skill-manager.js.map +1 -1
  36. package/dist/lib/studio-view-renderer.d.ts +50 -0
  37. package/dist/lib/studio-view-renderer.js +773 -0
  38. package/dist/lib/studio-view-renderer.js.map +1 -0
  39. package/dist/lib/tooling/commands/link-project.d.ts +2 -0
  40. package/dist/lib/tooling/commands/link-project.js +22 -0
  41. package/dist/lib/tooling/commands/link-project.js.map +1 -0
  42. package/dist/lib/tooling/commands/migrate-stage-artifacts.d.ts +2 -0
  43. package/dist/lib/tooling/commands/migrate-stage-artifacts.js +61 -0
  44. package/dist/lib/tooling/commands/migrate-stage-artifacts.js.map +1 -0
  45. package/dist/lib/tooling/commands/render-html.d.ts +2 -0
  46. package/dist/lib/tooling/commands/render-html.js +78 -0
  47. package/dist/lib/tooling/commands/render-html.js.map +1 -0
  48. package/dist/lib/tooling/commands/report-html.d.ts +2 -0
  49. package/dist/lib/tooling/commands/report-html.js +27 -0
  50. package/dist/lib/tooling/commands/report-html.js.map +1 -0
  51. package/dist/lib/tooling/commands/scan-doc-contract.d.ts +2 -0
  52. package/dist/lib/tooling/commands/scan-doc-contract.js +78 -0
  53. package/dist/lib/tooling/commands/scan-doc-contract.js.map +1 -0
  54. package/dist/lib/tooling/commands/scan-security-hygiene.d.ts +2 -0
  55. package/dist/lib/tooling/commands/scan-security-hygiene.js +93 -0
  56. package/dist/lib/tooling/commands/scan-security-hygiene.js.map +1 -0
  57. package/dist/lib/tooling/commands/skill-descriptions.d.ts +2 -0
  58. package/dist/lib/tooling/commands/skill-descriptions.js +50 -0
  59. package/dist/lib/tooling/commands/skill-descriptions.js.map +1 -0
  60. package/dist/lib/tooling/commands/summarize-run-status.d.ts +2 -0
  61. package/dist/lib/tooling/commands/summarize-run-status.js +65 -0
  62. package/dist/lib/tooling/commands/summarize-run-status.js.map +1 -0
  63. package/dist/lib/tooling/commands/switch-artifact-language.d.ts +2 -0
  64. package/dist/lib/tooling/commands/switch-artifact-language.js +87 -0
  65. package/dist/lib/tooling/commands/switch-artifact-language.js.map +1 -0
  66. package/dist/lib/tooling/index.d.ts +4 -0
  67. package/dist/lib/tooling/index.js +23 -0
  68. package/dist/lib/tooling/index.js.map +1 -0
  69. package/dist/lib/tooling/registry.d.ts +6 -0
  70. package/dist/lib/tooling/registry.js +56 -0
  71. package/dist/lib/tooling/registry.js.map +1 -0
  72. package/dist/lib/tooling/types.d.ts +19 -0
  73. package/dist/lib/tooling/types.js +10 -0
  74. package/dist/lib/tooling/types.js.map +1 -0
  75. package/dist/lib/update.js +1 -1
  76. package/dist/lib/update.js.map +1 -1
  77. package/dist/lib/webview-studio.d.ts +5 -0
  78. package/dist/lib/webview-studio.js +11 -577
  79. package/dist/lib/webview-studio.js.map +1 -1
  80. package/package.json +1 -1
  81. package/template/AGENTS.md +1 -1
  82. package/template/devflow/docs/USAGE.md +145 -0
  83. package/template/devflow/docs/diagrams/nexus-devflow-architecture.html +14821 -0
  84. package/template/devflow/docs/diagrams/nexus-devflow-architecture.json +80 -0
  85. package/template/devflow/docs/diagrams/nexus-devflow-lifecycle.html +14860 -0
  86. package/template/devflow/docs/diagrams/nexus-devflow-lifecycle.json +88 -0
  87. package/template/devflow/docs/examples/living-spec/adr.example.md +43 -0
  88. package/template/devflow/docs/examples/living-spec/current-feature.example.md +113 -0
  89. package/template/devflow/docs/examples/living-spec/discovery.example.md +45 -0
  90. package/template/devflow/docs/examples/living-spec/ideas.example.md +22 -0
  91. package/template/devflow/docs/governance-rules.md +39 -0
  92. package/template/devflow/docs/manual-review-workflow-spec.md +116 -0
  93. package/template/devflow/docs/markdown-metadata-contract.md +140 -0
  94. package/template/devflow/docs/quickstart.md +119 -0
  95. package/template/devflow/docs/release-process.md +52 -0
  96. package/template/devflow/docs/skill-selection-policy.md +64 -0
  97. package/template/devflow/docs/workflow-surface-map.md +85 -0
@@ -1,17 +1,18 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import http from "node:http";
3
- import { DASHBOARD_PAGE_HTML } from "./dashboard-page.js";
4
- import { readDashboardSnapshot } from "./dashboard-snapshot.js";
3
+ import { renderDashboardPage } from "./dashboard-page.js";
4
+ import { DashboardStateEngine } from "./dashboard-engine.js";
5
5
  import { readHistory } from "./history.js";
6
- import { readProjectStatus } from "./status.js";
7
- import { buildCodeGraph, calculateBlastRadius } from "./code-graph.js";
8
- import { reconcileState } from "./drift-reconciler.js";
9
6
  const DASHBOARD_HOST = "127.0.0.1";
7
+ const CSP_HEADER = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
10
8
  async function startDashboardServer(startPath = process.cwd(), options = {}) {
11
- const initialStatus = await readProjectStatus(startPath);
12
- const projectRoot = initialStatus.project.root;
9
+ const engine = options.engine ?? new DashboardStateEngine(startPath, {
10
+ snapshotOptions: options.snapshotOptions
11
+ });
12
+ const status = await engine.getStatus();
13
+ const projectRoot = status.project.root;
13
14
  const server = http.createServer((request, response) => {
14
- void handleRequest(projectRoot, request, response, options.snapshotOptions);
15
+ void handleRequest(engine, projectRoot, request, response, options.snapshotOptions);
15
16
  });
16
17
  await new Promise((resolve, reject) => {
17
18
  const onError = (error) => {
@@ -32,101 +33,106 @@ async function startDashboardServer(startPath = process.cwd(), options = {}) {
32
33
  throw new Error("Nexus-DevFlow dashboard could not determine its local address.");
33
34
  }
34
35
  // Pre-warm dashboard snapshot in the background on startup
35
- void readDashboardSnapshot(projectRoot, options.snapshotOptions).catch(() => { });
36
+ engine.prewarm();
36
37
  return {
37
38
  url: `http://${DASHBOARD_HOST}:${address.port}`,
38
39
  close: () => closeServer(server)
39
40
  };
40
41
  }
41
- async function handleRequest(projectRoot, request, response, snapshotOptions) {
42
+ async function handleRequest(engine, projectRoot, request, response, snapshotOptions) {
42
43
  const method = request.method || "GET";
43
- if (method !== "GET" && method !== "HEAD") {
44
- response.setHeader("Allow", "GET, HEAD");
44
+ if (method !== "GET" && method !== "HEAD" && method !== "POST") {
45
+ response.setHeader("Allow", "GET, HEAD, POST");
45
46
  sendResponse(response, method, 405, "text/plain; charset=utf-8", "Method not allowed.\n");
46
47
  return;
47
48
  }
48
- const pathname = new URL(request.url || "/", `http://${DASHBOARD_HOST}`).pathname;
49
+ const urlObj = new URL(request.url || "/", `http://${DASHBOARD_HOST}`);
50
+ const pathname = urlObj.pathname;
49
51
  if (pathname === "/") {
50
- response.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'");
52
+ response.setHeader("Content-Security-Policy", CSP_HEADER);
51
53
  try {
52
- const snapshot = await readDashboardSnapshot(projectRoot, snapshotOptions);
53
- const injectedHtml = DASHBOARD_PAGE_HTML.replace("window.__INITIAL_SNAPSHOT__ = null;", `window.__INITIAL_SNAPSHOT__ = ${JSON.stringify(snapshot).replace(/</g, "\\u003c")};`);
54
- sendResponse(response, method, 200, "text/html; charset=utf-8", injectedHtml);
54
+ const snapshot = await engine.getSnapshot(snapshotOptions);
55
+ sendResponse(response, method, 200, "text/html; charset=utf-8", renderDashboardPage(snapshot));
55
56
  }
56
57
  catch {
57
- sendResponse(response, method, 200, "text/html; charset=utf-8", DASHBOARD_PAGE_HTML);
58
+ sendResponse(response, method, 200, "text/html; charset=utf-8", renderDashboardPage(null));
58
59
  }
59
60
  return;
60
61
  }
61
62
  if (pathname === "/api/status") {
62
63
  try {
63
- const status = await readProjectStatus(projectRoot);
64
- sendResponse(response, method, 200, "application/json; charset=utf-8", `${JSON.stringify(status)}\n`);
64
+ const status = await engine.getStatus();
65
+ sendJsonResponse(response, method, 200, status);
65
66
  }
66
67
  catch (error) {
67
- sendResponse(response, method, 500, "application/json; charset=utf-8", `${JSON.stringify({
68
- error: error instanceof Error ? error.message : "Unable to read Nexus-DevFlow status."
69
- })}\n`);
68
+ sendErrorResponse(response, method, error, "Unable to read Nexus-DevFlow status.");
70
69
  }
71
70
  return;
72
71
  }
73
- if (pathname === "/api/dashboard") {
72
+ if (pathname === "/api/snapshot" || pathname === "/api/dashboard") {
74
73
  try {
75
- const snapshot = await readDashboardSnapshot(projectRoot, snapshotOptions);
76
- sendResponse(response, method, 200, "application/json; charset=utf-8", `${JSON.stringify(snapshot)}\n`);
74
+ const snapshot = await engine.getSnapshot(snapshotOptions);
75
+ sendJsonResponse(response, method, 200, snapshot);
77
76
  }
78
77
  catch (error) {
79
- sendResponse(response, method, 500, "application/json; charset=utf-8", `${JSON.stringify({
80
- error: error instanceof Error ? error.message : "Unable to read dashboard snapshot."
81
- })}\n`);
78
+ sendErrorResponse(response, method, error, "Unable to read dashboard snapshot.");
82
79
  }
83
80
  return;
84
81
  }
85
82
  if (pathname === "/api/history") {
86
83
  try {
87
84
  const history = await readHistory(projectRoot);
88
- sendResponse(response, method, 200, "application/json; charset=utf-8", `${JSON.stringify(history)}\n`);
85
+ sendJsonResponse(response, method, 200, history);
89
86
  }
90
87
  catch (error) {
91
- sendResponse(response, method, 500, "application/json; charset=utf-8", `${JSON.stringify({
92
- error: error instanceof Error ? error.message : "Unable to read DevFlow history."
93
- })}\n`);
88
+ sendErrorResponse(response, method, error, "Unable to read DevFlow history.");
94
89
  }
95
90
  return;
96
91
  }
97
- if (pathname === "/api/graph") {
92
+ if (pathname === "/api/codegraph" || pathname === "/api/graph") {
98
93
  try {
99
- const urlObj = new URL(request.url || "/", `http://${DASHBOARD_HOST}`);
100
94
  const targetFile = urlObj.searchParams.get("file");
101
- const graph = await buildCodeGraph(projectRoot);
102
95
  if (targetFile && targetFile.trim().length > 0) {
103
- const blast = calculateBlastRadius(graph, targetFile.trim());
104
- sendResponse(response, method, 200, "application/json; charset=utf-8", `${JSON.stringify(blast)}\n`);
96
+ const blast = await engine.getBlastRadius(targetFile.trim());
97
+ sendJsonResponse(response, method, 200, blast);
105
98
  }
106
99
  else {
107
- sendResponse(response, method, 200, "application/json; charset=utf-8", `${JSON.stringify({
100
+ const graph = await engine.getCodeGraph();
101
+ sendJsonResponse(response, method, 200, {
108
102
  totalFiles: graph.totalFiles,
109
103
  totalEdges: graph.totalEdges,
110
104
  files: Object.keys(graph.nodes)
111
- })}\n`);
105
+ });
112
106
  }
113
107
  }
114
108
  catch (error) {
115
- sendResponse(response, method, 500, "application/json; charset=utf-8", `${JSON.stringify({
116
- error: error instanceof Error ? error.message : "Unable to query code graph."
117
- })}\n`);
109
+ sendErrorResponse(response, method, error, "Unable to query code graph.");
118
110
  }
119
111
  return;
120
112
  }
121
- if (pathname === "/api/reconcile") {
113
+ if (pathname === "/api/action" || pathname === "/api/reconcile") {
122
114
  try {
123
- const result = await reconcileState(projectRoot, { healStage: true });
124
- sendResponse(response, method, 200, "application/json; charset=utf-8", `${JSON.stringify(result)}\n`);
115
+ let actionType = pathname === "/api/reconcile" ? "reconcile" : urlObj.searchParams.get("type") || "";
116
+ let payload = {};
117
+ if (method === "POST") {
118
+ const bodyText = await readBody(request);
119
+ if (bodyText.trim().length > 0) {
120
+ try {
121
+ payload = JSON.parse(bodyText);
122
+ if (payload.type && typeof payload.type === "string") {
123
+ actionType = payload.type;
124
+ }
125
+ }
126
+ catch {
127
+ // Invalid JSON body
128
+ }
129
+ }
130
+ }
131
+ const result = await engine.dispatchAction({ type: actionType, ...payload });
132
+ sendJsonResponse(response, method, 200, result);
125
133
  }
126
134
  catch (error) {
127
- sendResponse(response, method, 500, "application/json; charset=utf-8", `${JSON.stringify({
128
- error: error instanceof Error ? error.message : "Unable to reconcile state."
129
- })}\n`);
135
+ sendErrorResponse(response, method, error, "Unable to execute action.");
130
136
  }
131
137
  return;
132
138
  }
@@ -136,26 +142,33 @@ async function handleRequest(projectRoot, request, response, snapshotOptions) {
136
142
  }
137
143
  sendResponse(response, method, 404, "text/plain; charset=utf-8", "Not found.\n");
138
144
  }
139
- function sendResponse(response, method, statusCode, contentType, body) {
140
- response.statusCode = statusCode;
141
- response.setHeader("Cache-Control", "no-store");
142
- response.setHeader("Content-Type", contentType);
143
- response.setHeader("Access-Control-Allow-Origin", "*");
144
- response.setHeader("X-Content-Type-Options", "nosniff");
145
- response.end(method === "HEAD" ? undefined : body);
145
+ function readBody(request) {
146
+ return new Promise((resolve, reject) => {
147
+ let data = "";
148
+ request.on("data", (chunk) => { data += chunk; });
149
+ request.on("end", () => resolve(data));
150
+ request.on("error", reject);
151
+ });
152
+ }
153
+ function sendJsonResponse(res, method, status, data) {
154
+ sendResponse(res, method, status, "application/json; charset=utf-8", `${JSON.stringify(data)}\n`);
155
+ }
156
+ function sendErrorResponse(res, method, err, fallback) {
157
+ sendJsonResponse(res, method, 500, { error: err instanceof Error ? err.message : fallback });
158
+ }
159
+ function sendResponse(res, method, status, type, body) {
160
+ res.statusCode = status;
161
+ res.setHeader("Cache-Control", "no-store");
162
+ res.setHeader("Content-Type", type);
163
+ res.setHeader("Access-Control-Allow-Origin", "*");
164
+ res.setHeader("X-Content-Type-Options", "nosniff");
165
+ res.end(method === "HEAD" ? undefined : body);
146
166
  }
147
167
  async function closeServer(server) {
148
- if (!server.listening) {
168
+ if (!server.listening)
149
169
  return;
150
- }
151
170
  await new Promise((resolve, reject) => {
152
- server.close((error) => {
153
- if (error) {
154
- reject(error);
155
- return;
156
- }
157
- resolve();
158
- });
171
+ server.close((err) => (err ? reject(err) : resolve()));
159
172
  });
160
173
  }
161
174
  async function openDashboard(url) {
@@ -179,1008 +192,5 @@ async function openDashboard(url) {
179
192
  });
180
193
  });
181
194
  }
182
- const DASHBOARD_HTML = `<!doctype html>
183
- <html lang="en">
184
- <head>
185
- <meta charset="utf-8">
186
- <meta name="viewport" content="width=device-width, initial-scale=1">
187
- <title>Nexus-DevFlow Dashboard</title>
188
- <style>
189
- :root {
190
- color-scheme: light;
191
- --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
192
- --font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
193
- --paper: #f5f6f3;
194
- --surface: rgba(255, 255, 255, .84);
195
- --surface-solid: #ffffff;
196
- --surface-muted: #eef1ed;
197
- --ink: #121817;
198
- --ink-soft: #45504d;
199
- --ink-muted: #717b78;
200
- --line: #d9ded9;
201
- --line-strong: #bdc7c1;
202
- --blue: #155eef;
203
- --blue-dark: #0b43ba;
204
- --green: #0b7a53;
205
- --green-soft: #e7f5ee;
206
- --amber: #9a5700;
207
- --amber-soft: #fff2d9;
208
- --red: #a5333f;
209
- --red-soft: #fdebed;
210
- --code: #111715;
211
- --code-line: #2c3532;
212
- --code-text: #d9dfdc;
213
- --code-muted: #9ba7a2;
214
- --code-blue: #76a8ff;
215
- --code-green: #70d5a9;
216
- font-family: var(--font-sans);
217
- background: var(--paper);
218
- color: var(--ink);
219
- }
220
-
221
- * { box-sizing: border-box; }
222
-
223
- body {
224
- margin: 0;
225
- min-width: 320px;
226
- min-height: 100vh;
227
- background:
228
- linear-gradient(rgba(21, 94, 239, .09) 1px, transparent 1px),
229
- linear-gradient(90deg, rgba(21, 94, 239, .09) 1px, transparent 1px),
230
- radial-gradient(circle at 12% 0%, rgba(21, 94, 239, .08), transparent 34rem),
231
- var(--paper);
232
- background-size: 40px 40px, 40px 40px, auto, auto;
233
- -webkit-font-smoothing: antialiased;
234
- }
235
-
236
- .shell { width: min(1180px, calc(100% - 40px)); margin: 0 auto; padding: 38px 0 64px; }
237
-
238
- header {
239
- display: flex;
240
- align-items: flex-start;
241
- justify-content: space-between;
242
- gap: 24px;
243
- margin-bottom: 30px;
244
- }
245
-
246
- .brand {
247
- display: flex;
248
- align-items: center;
249
- gap: 11px;
250
- margin-bottom: 28px;
251
- color: var(--ink);
252
- font-size: 16px;
253
- font-weight: 700;
254
- letter-spacing: -.02em;
255
- cursor: pointer;
256
- }
257
-
258
- .brand-mark {
259
- width: 28px;
260
- height: 28px;
261
- flex: 0 0 auto;
262
- transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
263
- }
264
-
265
- .brand:hover .brand-mark {
266
- transform: rotate(15deg) scale(1.12);
267
- }
268
-
269
- .brand-context { color: var(--ink-muted); font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: .04em; text-transform: uppercase; }
270
- .brand-separator { width: 1px; height: 17px; background: var(--line-strong); }
271
-
272
- .eyebrow {
273
- display: inline-flex;
274
- align-items: center;
275
- gap: 8px;
276
- color: var(--blue-dark);
277
- font: 600 11px/1 var(--font-mono);
278
- letter-spacing: .12em;
279
- text-transform: uppercase;
280
- }
281
-
282
- .eyebrow::before { width: 22px; height: 1px; content: ""; background: var(--blue); }
283
-
284
- h1 { margin: 13px 0 8px; color: var(--ink); font-size: clamp(32px, 4vw, 50px); letter-spacing: -.045em; }
285
- .path { max-width: 760px; overflow-wrap: anywhere; color: var(--ink-muted); font: 12px/1.6 var(--font-mono); }
286
-
287
- /* Live status dot animation */
288
- .live {
289
- display: inline-flex;
290
- align-items: center;
291
- gap: 8px;
292
- padding: 9px 14px;
293
- border: 1px solid var(--line-strong);
294
- border-radius: 999px;
295
- background: rgba(255, 255, 255, .92);
296
- color: var(--ink-soft);
297
- font-size: 12px;
298
- font-weight: 600;
299
- white-space: nowrap;
300
- box-shadow: 0 1px 3px rgba(18, 24, 23, .06);
301
- transition: border-color .3s ease, background-color .3s ease;
302
- }
303
-
304
- @keyframes livePulse {
305
- 0% { transform: scale(1); opacity: 0.7; }
306
- 70% { transform: scale(2.4); opacity: 0; }
307
- 100% { transform: scale(2.4); opacity: 0; }
308
- }
309
-
310
- @keyframes livePulseBox {
311
- 0% { box-shadow: 0 0 0 0 rgba(11, 122, 83, 0.5); }
312
- 70% { box-shadow: 0 0 0 8px rgba(11, 122, 83, 0); }
313
- 100% { box-shadow: 0 0 0 0 rgba(11, 122, 83, 0); }
314
- }
315
-
316
- .live-dot {
317
- position: relative;
318
- display: inline-block;
319
- width: 8px;
320
- height: 8px;
321
- border-radius: 50%;
322
- background: var(--green);
323
- transition: background-color .3s ease;
324
- }
325
-
326
- .live-dot::after {
327
- content: "";
328
- position: absolute;
329
- display: block;
330
- inset: -4px;
331
- border-radius: 50%;
332
- background: var(--green);
333
- opacity: 0;
334
- pointer-events: none;
335
- }
336
-
337
- .live-dot.is-live {
338
- animation: livePulseBox 2.2s infinite;
339
- }
340
-
341
- .live-dot.is-live::after {
342
- animation: livePulse 2.2s ease-out infinite;
343
- }
344
-
345
- .grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; }
346
-
347
- /* Initial Load Card Entrance Animation */
348
- @keyframes cardEntrance {
349
- from { opacity: 0; transform: translateY(10px); }
350
- to { opacity: 1; transform: translateY(0); }
351
- }
352
-
353
- .card {
354
- grid-column: span 4;
355
- min-width: 0;
356
- padding: 22px;
357
- border: 1px solid rgba(189, 199, 193, .78);
358
- border-radius: 14px;
359
- background: var(--surface);
360
- box-shadow: 0 1px 2px rgba(18, 24, 23, .05), 0 10px 30px rgba(18, 24, 23, .04);
361
- backdrop-filter: blur(14px);
362
- transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
363
- }
364
-
365
- .card-enter {
366
- animation: cardEntrance 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
367
- }
368
-
369
- .card:hover {
370
- transform: translateY(-2px);
371
- box-shadow: 0 12px 36px rgba(18, 24, 23, .08), 0 2px 6px rgba(18, 24, 23, .04);
372
- border-color: rgba(21, 94, 239, 0.4);
373
- }
374
-
375
- .card.wide { grid-column: span 8; }
376
- .card.full { grid-column: 1 / -1; }
377
-
378
- .card-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
379
- .label { color: var(--blue-dark); font: 600 11px/1 var(--font-mono); letter-spacing: .1em; text-transform: uppercase; }
380
-
381
- .value {
382
- color: var(--ink);
383
- font-size: 20px;
384
- font-weight: 700;
385
- letter-spacing: -.02em;
386
- transition: color .2s ease;
387
- }
388
-
389
- .muted { color: var(--ink-muted); font-size: 13px; line-height: 1.6; }
390
-
391
- /* Pill Transitions and Scale Pop */
392
- @keyframes pillPop {
393
- 0% { transform: scale(1); }
394
- 50% { transform: scale(1.06); }
395
- 100% { transform: scale(1); }
396
- }
397
-
398
- .pill {
399
- display: inline-block;
400
- padding: 5px 10px;
401
- border: 1px solid var(--line);
402
- border-radius: 999px;
403
- color: var(--ink-muted);
404
- background: var(--surface-muted);
405
- font: 600 10px/1 var(--font-mono);
406
- letter-spacing: .04em;
407
- text-transform: uppercase;
408
- transition: background-color .3s ease, border-color .3s ease, color .3s ease, transform .2s ease;
409
- }
410
-
411
- .pill-pop {
412
- animation: pillPop 300ms cubic-bezier(0.16, 1, 0.3, 1);
413
- }
414
-
415
- .pill.ok, .pill.ready, .pill.active { border-color: #b9dfce; background: var(--green-soft); color: var(--green); }
416
- .pill.warning, .pill.blocked, .pill.needs_verification { border-color: #efd5a5; background: var(--amber-soft); color: var(--amber); }
417
-
418
- .facts { display: grid; gap: 12px; }
419
- .fact { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; padding-bottom: 11px; border-bottom: 1px solid var(--line); }
420
- .fact:last-child { padding-bottom: 0; border-bottom: 0; }
421
- .fact span:first-child { color: var(--ink-muted); font-size: 12px; }
422
- .fact span:last-child { max-width: 70%; overflow-wrap: anywhere; color: var(--ink-soft); font: 12px/1.45 var(--font-mono); text-align: right; }
423
-
424
- /* Progress bar with Shimmer and Increase Glow */
425
- .progress { height: 8px; margin: 16px 0 10px; overflow: hidden; border-radius: 999px; background: var(--surface-muted); }
426
-
427
- @keyframes shimmer {
428
- 0% { background-position: -200% 0; }
429
- 100% { background-position: 200% 0; }
430
- }
431
-
432
- @keyframes progressGlow {
433
- 0% { box-shadow: 0 0 0 0 rgba(21, 94, 239, 0.6); }
434
- 50% { box-shadow: 0 0 12px 2px rgba(21, 94, 239, 0.7); }
435
- 100% { box-shadow: 0 0 0 0 rgba(21, 94, 239, 0); }
436
- }
437
-
438
- .progress-bar {
439
- display: block;
440
- width: 0;
441
- height: 100%;
442
- border-radius: inherit;
443
- background: var(--blue);
444
- transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
445
- }
446
-
447
- .progress-bar.active-shimmer {
448
- background: linear-gradient(90deg, #155eef 0%, #4785ff 50%, #155eef 100%);
449
- background-size: 200% 100%;
450
- animation: shimmer 3s infinite linear;
451
- }
452
-
453
- .progress-glow {
454
- animation: progressGlow 400ms ease-out;
455
- }
456
-
457
- /* Highlight Flash for values */
458
- @keyframes flashGreen {
459
- 0% { background-color: rgba(11, 122, 83, 0.2); }
460
- 100% { background-color: transparent; }
461
- }
462
-
463
- @keyframes flashAmber {
464
- 0% { background-color: rgba(154, 87, 0, 0.2); }
465
- 100% { background-color: transparent; }
466
- }
467
-
468
- .flash-green { animation: flashGreen 600ms ease-out; border-radius: 4px; padding: 0 4px; }
469
- .flash-amber { animation: flashAmber 600ms ease-out; border-radius: 4px; padding: 0 4px; }
470
-
471
- /* Code Panel & Next Action Flip/Glow */
472
- @keyframes commandFlip {
473
- 0% { opacity: 0; transform: translateY(-6px); }
474
- 100% { opacity: 1; transform: translateY(0); }
475
- }
476
-
477
- @keyframes panelGlowPulse {
478
- 0% { border-color: rgba(118, 168, 255, 0.9); box-shadow: 0 0 24px rgba(21, 94, 239, 0.35); }
479
- 100% { border-color: var(--code-line); box-shadow: 0 24px 80px rgba(18, 24, 23, .12); }
480
- }
481
-
482
- .code-panel {
483
- color: var(--code-text);
484
- border-color: var(--code-line);
485
- background: var(--code);
486
- box-shadow: 0 24px 80px rgba(18, 24, 23, .12);
487
- backdrop-filter: none;
488
- transition: border-color .3s ease, box-shadow .3s ease;
489
- }
490
-
491
- .code-panel .label { color: var(--code-blue); }
492
- .code-panel .muted { color: var(--code-muted); }
493
-
494
- .next-action { padding: 24px; }
495
-
496
- .next-action-header {
497
- display: flex;
498
- align-items: center;
499
- justify-content: space-between;
500
- gap: 12px;
501
- }
502
-
503
- .next-copy-btn {
504
- display: inline-flex;
505
- align-items: center;
506
- gap: 6px;
507
- padding: 5px 12px;
508
- border-radius: 6px;
509
- background: rgba(118, 168, 255, 0.12);
510
- color: var(--code-blue);
511
- border: 1px solid rgba(118, 168, 255, 0.3);
512
- font: 600 11px/1.3 var(--font-mono);
513
- cursor: pointer;
514
- transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.15s ease, color 0.2s ease;
515
- user-select: none;
516
- }
517
- .next-copy-btn:hover {
518
- background: var(--blue);
519
- color: #ffffff;
520
- border-color: var(--blue);
521
- transform: translateY(-1px);
522
- }
523
- .next-copy-btn:active {
524
- transform: translateY(0);
525
- }
526
- .next-copy-btn.copied {
527
- background: var(--green);
528
- color: #ffffff;
529
- border-color: var(--green);
530
- }
531
-
532
- .command {
533
- margin: 13px 0 8px;
534
- color: var(--code-blue);
535
- font: 600 clamp(20px, 3vw, 29px)/1.3 var(--font-mono);
536
- overflow-wrap: anywhere;
537
- cursor: pointer;
538
- border-radius: 6px;
539
- padding: 4px 6px;
540
- margin-left: -6px;
541
- transition: background-color 0.2s ease;
542
- }
543
- .command:hover {
544
- background: rgba(118, 168, 255, 0.08);
545
- }
546
-
547
- .command-flip { animation: commandFlip 300ms ease-out; }
548
- .next-action-pulse { animation: panelGlowPulse 800ms ease-out; }
549
-
550
- /* Staggered List Items Fade-in */
551
- @keyframes fadeSlideIn {
552
- from { opacity: 0; transform: translateY(6px); }
553
- to { opacity: 1; transform: translateY(0); }
554
- }
555
-
556
- ul { margin: 0; padding: 0; list-style: none; }
557
- li {
558
- padding: 11px 10px;
559
- border-bottom: 1px solid var(--line);
560
- color: var(--ink-soft);
561
- font-size: 13px;
562
- line-height: 1.5;
563
- border-radius: 6px;
564
- transition: background-color 0.2s ease, transform 0.2s ease;
565
- }
566
- li:hover {
567
- background-color: rgba(21, 94, 239, 0.04);
568
- transform: translateX(3px);
569
- }
570
- li:last-child { border-bottom: 0; }
571
- li.empty { color: var(--ink-muted); }
572
- li.empty:hover { background-color: transparent; transform: none; }
573
- /* Markdown Inline Formatting */
574
- .inline-code, code.inline-code {
575
- display: inline-block;
576
- padding: 1.5px 6px;
577
- margin: 0 2px;
578
- border-radius: 5px;
579
- background: var(--surface-muted);
580
- color: var(--blue-dark);
581
- font-family: var(--font-mono);
582
- font-size: 0.88em;
583
- font-weight: 600;
584
- border: 1px solid var(--line);
585
- line-height: 1.35;
586
- vertical-align: baseline;
587
- word-break: break-word;
588
- }
589
- strong {
590
- color: var(--ink);
591
- font-weight: 700;
592
- }
593
- em {
594
- font-style: italic;
595
- color: var(--ink-soft);
596
- }
597
- del {
598
- text-decoration: line-through;
599
- color: var(--ink-muted);
600
- }
601
- .inline-link {
602
- color: var(--blue);
603
- text-decoration: none;
604
- font-weight: 600;
605
- }
606
- .inline-link:hover {
607
- text-decoration: underline;
608
- }
609
-
610
- /* Idea Inbox Item Styling */
611
- .idea-item {
612
- display: flex;
613
- flex-direction: column;
614
- gap: 8px;
615
- padding: 14px 16px;
616
- border: 1px solid var(--line);
617
- border-radius: 10px;
618
- background: var(--surface-solid);
619
- margin-bottom: 12px;
620
- transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
621
- }
622
- .idea-item:hover {
623
- border-color: var(--blue);
624
- box-shadow: 0 4px 16px rgba(21, 94, 239, 0.09);
625
- transform: translateY(-1px);
626
- }
627
- .idea-header {
628
- display: flex;
629
- align-items: center;
630
- justify-content: space-between;
631
- gap: 12px;
632
- }
633
- .idea-id-badge {
634
- display: inline-flex;
635
- align-items: center;
636
- padding: 4px 9px;
637
- border-radius: 6px;
638
- background: rgba(21, 94, 239, 0.08);
639
- color: var(--blue-dark);
640
- font: 700 11px/1 var(--font-mono);
641
- letter-spacing: .04em;
642
- }
643
- .idea-title-text {
644
- color: var(--ink);
645
- font-size: 14px;
646
- font-weight: 700;
647
- letter-spacing: -.01em;
648
- line-height: 1.45;
649
- }
650
- .idea-title-text .inline-code {
651
- font-size: 0.85em;
652
- background: rgba(21, 94, 239, 0.06);
653
- border-color: rgba(21, 94, 239, 0.2);
654
- }
655
- .idea-desc-text {
656
- color: var(--ink-soft);
657
- font-size: 13px;
658
- line-height: 1.55;
659
- }
660
- .idea-meta-row {
661
- display: flex;
662
- align-items: center;
663
- justify-content: space-between;
664
- flex-wrap: wrap;
665
- gap: 10px;
666
- margin-top: 2px;
667
- padding-top: 8px;
668
- border-top: 1px dashed var(--line);
669
- }
670
- .idea-feasibility-tag {
671
- color: var(--ink-soft);
672
- font-size: 12px;
673
- background: var(--surface-muted);
674
- padding: 4px 9px;
675
- border-radius: 6px;
676
- line-height: 1.4;
677
- }
678
- .idea-feasibility-tag strong {
679
- color: var(--ink);
680
- }
681
- .idea-feasibility-tag .inline-code {
682
- font-size: 0.85em;
683
- background: rgba(0, 0, 0, 0.04);
684
- }
685
- .idea-cmd-code {
686
- display: inline-block;
687
- padding: 4px 10px;
688
- border-radius: 6px;
689
- background: var(--surface-muted);
690
- color: var(--blue-dark);
691
- font: 600 11px/1.3 var(--font-mono);
692
- cursor: pointer;
693
- border: 1px solid var(--line);
694
- transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
695
- user-select: none;
696
- }
697
- .idea-cmd-code:hover {
698
- background: var(--blue);
699
- color: #ffffff;
700
- border-color: var(--blue);
701
- }
702
-
703
- footer { margin-top: 24px; color: var(--ink-muted); font: 11px/1.6 var(--font-mono); }
704
-
705
- @media (max-width: 860px) {
706
- .card, .card.wide { grid-column: span 6; }
707
- .card.full { grid-column: 1 / -1; }
708
- }
709
-
710
- @media (max-width: 620px) {
711
- .shell { width: min(100% - 24px, 1180px); padding-top: 24px; }
712
- header { flex-direction: column; }
713
- .brand { margin-bottom: 22px; }
714
- .card, .card.wide, .card.full { grid-column: 1 / -1; }
715
- }
716
-
717
- /* Accessibility Reduced Motion Rule */
718
- @media (prefers-reduced-motion: reduce) {
719
- *, *::before, *::after {
720
- animation-duration: 0.001ms !important;
721
- animation-iteration-count: 1 !important;
722
- transition-duration: 0.001ms !important;
723
- }
724
- }
725
- </style>
726
- </head>
727
- <body>
728
- <main class="shell">
729
- <div class="brand">
730
- <svg class="brand-mark" viewBox="0 0 48 48" role="img" aria-label="Nexus-DevFlow" aria-hidden="true">
731
- <path fill="#155eef" d="M4 4h25.2L16.3 44H4zM39.7 4H44v40H26.8z"></path>
732
- </svg>
733
- <span>Nexus-DevFlow</span>
734
- <span class="brand-separator" aria-hidden="true"></span>
735
- <span class="brand-context">Dashboard</span>
736
- </div>
737
- <header>
738
- <div>
739
- <div class="eyebrow">Local 3-Pillars Workspace Status</div>
740
- <h1 id="project-name">Loading project...</h1>
741
- <div class="path" id="project-path">...</div>
742
- </div>
743
- <div class="live"><span class="live-dot is-live" id="live-dot"></span><span id="live-label">Connecting</span></div>
744
- </header>
745
-
746
- <section class="grid">
747
- <article class="card card-enter" style="animation-delay: 0.04s;">
748
- <div class="card-head"><span class="label">Nexus-DevFlow</span><span class="pill" id="health-pill">Loading</span></div>
749
- <div class="facts">
750
- <div class="fact"><span>Version</span><span id="val-version">-</span></div>
751
- <div class="fact"><span>Adapters</span><span id="val-adapters">-</span></div>
752
- <div class="fact"><span>Architecture</span><span>3-Pillars Model</span></div>
753
- </div>
754
- </article>
755
-
756
- <article class="card wide card-enter" style="animation-delay: 0.08s;">
757
- <div class="card-head"><span class="label">Current Work</span><span class="pill" id="work-pill">Loading</span></div>
758
- <div class="value" id="work-title">Reading living spec...</div>
759
- <div class="progress"><span class="progress-bar" id="work-progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-valuetext="Loading checklist steps..."></span></div>
760
- <div class="muted" id="work-meta">Loading checklist steps...</div>
761
- </article>
762
-
763
- <article class="card card-enter" style="animation-delay: 0.12s;">
764
- <div class="card-head"><span class="label">Git Status</span><span class="pill" id="git-pill">Loading</span></div>
765
- <div class="facts">
766
- <div class="fact"><span>Branch</span><span id="git-branch">-</span></div>
767
- <div class="fact"><span>Working Tree</span><span id="git-changed">-</span></div>
768
- <div class="fact"><span>Upstream</span><span id="git-upstream">-</span></div>
769
- </div>
770
- </article>
771
-
772
- <article class="card card-enter" style="animation-delay: 0.16s;">
773
- <div class="card-head"><span class="label">Findings</span><span class="value" id="findings-count">-</span></div>
774
- <ul id="findings-list" aria-live="polite"><li class="empty">Loading findings...</li></ul>
775
- </article>
776
-
777
- <article class="card card-enter" style="animation-delay: 0.20s;">
778
- <div class="card-head"><span class="label">Completion</span><span class="pill" id="completion-pill">Loading</span></div>
779
- <ul id="completion-list" aria-live="polite"><li class="empty">Checking readiness...</li></ul>
780
- </article>
781
-
782
- <article class="card card-enter" style="animation-delay: 0.24s;">
783
- <div class="card-head"><span class="label">Attention</span><span class="value" id="warnings-count">-</span></div>
784
- <ul id="warnings-list" aria-live="polite"><li class="empty">Loading warnings...</li></ul>
785
- </article>
786
-
787
- <article class="card full card-enter" style="animation-delay: 0.26s;">
788
- <div class="card-head"><span class="label">Idea Inbox & Backlog</span><span class="value" id="ideas-count">-</span></div>
789
- <div class="muted">Pending ideas from devflow/ideas.md. Start any idea with /feature IDEA-xxx or /discovery IDEA-xxx.</div>
790
- <ul id="ideas-list" tabindex="0" aria-label="Idea Inbox"><li class="empty">Loading ideas...</li></ul>
791
- </article>
792
-
793
- <article class="card full card-enter" style="animation-delay: 0.28s;">
794
- <div class="card-head"><span class="label">Completed Work Archive</span><span class="value" id="history-count">-</span></div>
795
- <div class="muted">Categorized history of features, fixes, and rollbacks.</div>
796
- <ul id="history-list" tabindex="0" aria-label="Completed work archive"><li class="empty">Loading history archive...</li></ul>
797
- </article>
798
-
799
- <article class="card full code-panel next-action card-enter" style="animation-delay: 0.32s;" id="next-panel">
800
- <div class="next-action-header">
801
- <span class="label">Next Action</span>
802
- <button class="next-copy-btn" id="next-copy-btn" type="button" aria-label="Copy next command" title="Click to copy next action command">
803
- <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
804
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
805
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
806
- </svg>
807
- <span id="next-copy-text">Copy Command</span>
808
- </button>
809
- </div>
810
- <div class="command" id="next-command" aria-live="polite" title="Click to copy command">Loading...</div>
811
- <div class="muted" id="next-reason"></div>
812
- </article>
813
- </section>
814
-
815
- <footer>Read-only local DevFlow 2.0 dashboard. Refreshes every 2 seconds while process is active.</footer>
816
- </main>
817
-
818
- <script>
819
- const byId = (id) => document.getElementById(id);
820
- let prevData = null;
821
- let isFirstLoad = true;
822
- let refreshing = false;
823
-
824
- function setPill(id, value, label = value) {
825
- const node = byId(id);
826
- if (!node) return;
827
- const formatted = String(label || '').replaceAll("_", " ");
828
- const newClass = "pill " + (value || 'idle');
829
-
830
- if (!isFirstLoad && (node.textContent !== formatted || !node.className.includes(value))) {
831
- node.classList.remove('pill-pop');
832
- void node.offsetWidth;
833
- node.classList.add('pill-pop');
834
- }
835
-
836
- node.textContent = formatted;
837
- node.className = newClass + (node.classList.contains('pill-pop') ? ' pill-pop' : '');
838
- }
839
-
840
- function animateCount(node, start, end, duration = 400) {
841
- if (!node) return;
842
- if (start === end) {
843
- node.textContent = end;
844
- return;
845
- }
846
- const startTime = performance.now();
847
- function step(currentTime) {
848
- const elapsed = currentTime - startTime;
849
- const progress = Math.min(elapsed / duration, 1);
850
- const currentVal = Math.round(start + (end - start) * progress);
851
- node.textContent = currentVal;
852
- if (progress < 1) {
853
- requestAnimationFrame(step);
854
- } else {
855
- node.textContent = end;
856
- }
857
- }
858
- requestAnimationFrame(step);
859
- }
860
-
861
- function triggerFlash(node, type) {
862
- if (!node) return;
863
- const className = type === 'green' ? 'flash-green' : 'flash-amber';
864
- node.classList.remove('flash-green', 'flash-amber');
865
- void node.offsetWidth;
866
- node.classList.add(className);
867
- setTimeout(() => node.classList.remove(className), 600);
868
- }
869
-
870
- function escapeHtml(str) {
871
- if (typeof str !== 'string') return '';
872
- return str
873
- .replace(/&/g, '&amp;')
874
- .replace(/</g, '&lt;')
875
- .replace(/>/g, '&gt;')
876
- .replace(/"/g, '&quot;')
877
- .replace(/'/g, '&#039;');
878
- }
879
-
880
- function renderMarkdownInline(text) {
881
- if (!text) return '';
882
- var str = escapeHtml(text);
883
-
884
- // Inline code
885
- str = str.replace(new RegExp('\\x60([^\\x60]+)\\x60', 'g'), '<code class="inline-code">$1</code>');
886
-
887
- // Bold
888
- str = str.replace(new RegExp('\\\\*\\\\*(.+?)\\\\*\\\\*', 'g'), '<strong>$1</strong>');
889
- str = str.replace(new RegExp('__(.+?)__', 'g'), '<strong>$1</strong>');
890
-
891
- // Strikethrough
892
- str = str.replace(new RegExp('~~(.+?)~~', 'g'), '<del>$1</del>');
893
-
894
- // Italic
895
- str = str.replace(new RegExp('(^|[^*])\\\\*([^*\\\\r\\\\n]+)\\\\*([^*]|$)', 'g'), '$1<em>$2</em>$3');
896
- str = str.replace(new RegExp('(^|[^_])_([^_\\\\r\\\\n]+)_([^_]|$)', 'g'), '$1<em>$2</em>$3');
897
-
898
- // Links
899
- str = str.replace(new RegExp('\\\\[([^\\\\]]+)\\\\]\\\\((https?:\\\\/\\\\/[^\\\\s\\\\)]+|#[^\\\\s\\\\)]+)\\\\)', 'g'), '<a href="$2" target="_blank" rel="noopener noreferrer" class="inline-link">$1</a>');
900
-
901
- return str;
902
- }
903
-
904
- function setList(id, values, emptyMessage) {
905
- const list = byId(id);
906
- if (!list) return;
907
- const prevItemsText = Array.from(list.children).map(c => c.textContent);
908
- list.replaceChildren();
909
- const items = values.length > 0 ? values : [emptyMessage];
910
-
911
- items.forEach((value, idx) => {
912
- const item = document.createElement("li");
913
- item.innerHTML = renderMarkdownInline(value);
914
- if (values.length === 0) {
915
- item.className = "empty";
916
- } else if (!isFirstLoad && !prevItemsText.includes(value)) {
917
- item.className = "fade-slide-in";
918
- item.style.animationDelay = (idx * 40) + 'ms';
919
- }
920
- list.append(item);
921
- });
922
- }
923
-
924
- function setIdeas(ideas) {
925
- const list = byId('ideas-list');
926
- if (!list) return;
927
- list.replaceChildren();
928
-
929
- const items = ideas && ideas.pending && ideas.pending.length > 0 ? ideas.pending : [];
930
- if (items.length === 0) {
931
- const emptyItem = document.createElement('li');
932
- emptyItem.className = 'empty';
933
- emptyItem.textContent = 'No pending ideas in devflow/ideas.md';
934
- list.append(emptyItem);
935
- return;
936
- }
937
-
938
- items.forEach((idea, idx) => {
939
- const li = document.createElement('li');
940
- li.className = 'idea-item';
941
- if (!isFirstLoad) {
942
- li.classList.add('fade-slide-in');
943
- li.style.animationDelay = (idx * 40) + 'ms';
944
- }
945
-
946
- const headerDiv = document.createElement('div');
947
- headerDiv.className = 'idea-header';
948
-
949
- const badgeSpan = document.createElement('span');
950
- badgeSpan.className = 'idea-id-badge';
951
- badgeSpan.textContent = idea.id;
952
-
953
- const cmdCode = document.createElement('code');
954
- cmdCode.className = 'idea-cmd-code';
955
- cmdCode.textContent = '/feature ' + idea.id;
956
- cmdCode.title = 'Click to copy command';
957
- cmdCode.onclick = () => {
958
- if (navigator.clipboard) {
959
- navigator.clipboard.writeText('/feature ' + idea.id);
960
- const orig = cmdCode.textContent;
961
- cmdCode.textContent = 'Copied!';
962
- setTimeout(() => { cmdCode.textContent = orig; }, 1200);
963
- }
964
- };
965
-
966
- headerDiv.append(badgeSpan, cmdCode);
967
-
968
- const titleDiv = document.createElement('div');
969
- titleDiv.className = 'idea-title-text';
970
- titleDiv.innerHTML = renderMarkdownInline(idea.title);
971
-
972
- li.append(headerDiv, titleDiv);
973
-
974
- if (idea.rawInput) {
975
- const descDiv = document.createElement('div');
976
- descDiv.className = 'idea-desc-text';
977
- descDiv.innerHTML = renderMarkdownInline(idea.rawInput);
978
- li.append(descDiv);
979
- }
980
-
981
- if (idea.feasibility || idea.value || idea.date) {
982
- const metaDiv = document.createElement('div');
983
- metaDiv.className = 'idea-meta-row';
984
- if (idea.feasibility) {
985
- const feasSpan = document.createElement('span');
986
- feasSpan.className = 'idea-feasibility-tag';
987
- feasSpan.innerHTML = '⚡ ' + renderMarkdownInline(idea.feasibility);
988
- metaDiv.append(feasSpan);
989
- }
990
- if (idea.date) {
991
- const dateSpan = document.createElement('span');
992
- dateSpan.className = 'muted';
993
- dateSpan.style.fontSize = '11px';
994
- dateSpan.textContent = idea.date;
995
- metaDiv.append(dateSpan);
996
- }
997
- li.append(metaDiv);
998
- }
999
-
1000
- list.append(li);
1001
- });
1002
- }
1003
-
1004
- async function refreshStatus() {
1005
- if (refreshing) return;
1006
- refreshing = true;
1007
- try {
1008
- const [resStatus, resHistory] = await Promise.all([
1009
- fetch('/api/status'),
1010
- fetch('/api/history').catch(() => null)
1011
- ]);
1012
-
1013
- if (!resStatus.ok) throw new Error('HTTP ' + resStatus.status);
1014
- const data = await resStatus.json();
1015
- const historyData = resHistory && resHistory.ok ? await resHistory.json() : { items: [] };
1016
-
1017
- byId('live-dot').style.background = '#0b7a53';
1018
- byId('live-dot').classList.add('is-live');
1019
- byId('live-label').textContent = 'Connected';
1020
-
1021
- byId('project-name').textContent = data.project?.name || 'Nexus-DevFlow';
1022
- byId('project-path').textContent = data.project?.root || '';
1023
-
1024
- const healthIssues = (data.warnings || []).map((warning) => warning.message).concat(
1025
- (data.findings?.blockers || []).map((finding) => "Blocking finding " + finding.id + ": " + finding.title)
1026
- );
1027
- const healthCount = healthIssues.length;
1028
- setPill(
1029
- 'health-pill',
1030
- data.health || 'ok',
1031
- healthCount === 0 ? 'Clear' : healthCount + (healthCount === 1 ? ' issue' : ' issues')
1032
- );
1033
-
1034
- byId('val-version').textContent = data.devflow?.version ? 'v' + data.devflow.version : '-';
1035
- byId('val-adapters').textContent = (data.devflow?.adapters || []).join(', ') || 'none';
1036
-
1037
- const work = data.currentWork || {};
1038
- setPill('work-pill', work.state || 'idle');
1039
- byId('work-title').textContent = work.title || 'No active run in progress';
1040
- const workSummary = work.state === 'active'
1041
- ? (work.completed + ' of ' + work.total + ' steps completed')
1042
- : 'Workspace is idle. Run /feature or /fix to start a new delivery run.';
1043
- byId('work-meta').textContent = workSummary;
1044
-
1045
- const pct = work.total > 0 ? Math.round((work.completed / work.total) * 100) : 0;
1046
- const progressBar = byId('work-progress-bar');
1047
- progressBar.setAttribute('aria-valuenow', String(pct));
1048
- progressBar.setAttribute('aria-valuetext', workSummary);
1049
-
1050
- const prevPct = prevData?.currentWork ? (prevData.currentWork.total > 0 ? Math.round((prevData.currentWork.completed / prevData.currentWork.total) * 100) : 0) : 0;
1051
- if (!isFirstLoad && pct > prevPct) {
1052
- progressBar.classList.remove('progress-glow');
1053
- void progressBar.offsetWidth;
1054
- progressBar.classList.add('progress-glow');
1055
- setTimeout(() => progressBar.classList.remove('progress-glow'), 400);
1056
- }
1057
-
1058
- if (work.state === 'active') {
1059
- progressBar.classList.add('active-shimmer');
1060
- } else {
1061
- progressBar.classList.remove('active-shimmer');
1062
- }
1063
- progressBar.style.width = pct + '%';
1064
-
1065
- const git = data.git || {};
1066
- setPill('git-pill', git.clean ? 'ok' : 'warning', !git.branch ? 'Unavailable' : git.clean ? 'Clean' : (git.changedFiles + ' changed'));
1067
- byId('git-branch').textContent = git.branch || 'unknown';
1068
- byId('git-changed').textContent = git.clean ? 'clean' : (git.changedFiles + ' changed files');
1069
- byId('git-upstream').textContent = git.upstream || 'none';
1070
-
1071
- const findings = data.findings || { total: 0, blockers: [] };
1072
- const prevFindingsCount = prevData?.findings?.total ?? 0;
1073
- const findingsNode = byId('findings-count');
1074
- if (!isFirstLoad && findings.total !== prevFindingsCount) {
1075
- animateCount(findingsNode, prevFindingsCount, findings.total);
1076
- triggerFlash(findingsNode, findings.total < prevFindingsCount ? 'green' : 'amber');
1077
- } else {
1078
- findingsNode.textContent = findings.total || '0';
1079
- }
1080
- setList('findings-list', findings.blockers.map(b => b.id + ': ' + b.title), 'No blocking findings');
1081
-
1082
- const comp = data.completion || { state: 'ready', blockers: [] };
1083
- setPill('completion-pill', comp.state || 'ready');
1084
- setList('completion-list', comp.blockers || [], 'All readiness checks passed');
1085
-
1086
- const warnings = data.warnings || [];
1087
- const prevWarningsCount = prevData?.warnings?.length ?? 0;
1088
- const warningsNode = byId('warnings-count');
1089
- if (!isFirstLoad && warnings.length !== prevWarningsCount) {
1090
- animateCount(warningsNode, prevWarningsCount, warnings.length);
1091
- triggerFlash(warningsNode, warnings.length < prevWarningsCount ? 'green' : 'amber');
1092
- } else {
1093
- warningsNode.textContent = warnings.length;
1094
- }
1095
- setList('warnings-list', warnings.map(w => w.message), 'No active warnings or drift');
1096
-
1097
- const ideas = data.ideas || { totalPending: 0, pending: [] };
1098
- const prevIdeasCount = prevData?.ideas?.totalPending ?? 0;
1099
- const ideasNode = byId('ideas-count');
1100
- if (!isFirstLoad && ideas.totalPending !== prevIdeasCount) {
1101
- animateCount(ideasNode, prevIdeasCount, ideas.totalPending);
1102
- triggerFlash(ideasNode, 'green');
1103
- } else {
1104
- ideasNode.textContent = ideas.totalPending + ' pending';
1105
- }
1106
- setIdeas(ideas);
1107
-
1108
- const historyItems = historyData.items || [];
1109
- const prevHistoryCount = prevData?.historyCount ?? 0;
1110
- const historyNode = byId('history-count');
1111
- if (!isFirstLoad && historyItems.length !== prevHistoryCount) {
1112
- animateCount(historyNode, prevHistoryCount, historyItems.length);
1113
- triggerFlash(historyNode, 'green');
1114
- } else {
1115
- historyNode.textContent = historyItems.length;
1116
- }
1117
- setList('history-list', historyItems.map(h => h.type.toUpperCase() + ': ' + h.title + (h.status ? ' (' + h.status + ')' : '')), 'No completed work in history archive');
1118
-
1119
- const next = data.nextAction || {};
1120
- const cmdNode = byId('next-command');
1121
- const nextPanelNode = byId('next-panel');
1122
- if (!isFirstLoad && prevData?.nextAction?.command !== next.command) {
1123
- cmdNode.classList.remove('command-flip');
1124
- nextPanelNode.classList.remove('next-action-pulse');
1125
- void cmdNode.offsetWidth;
1126
- cmdNode.classList.add('command-flip');
1127
- nextPanelNode.classList.add('next-action-pulse');
1128
- setTimeout(() => {
1129
- cmdNode.classList.remove('command-flip');
1130
- nextPanelNode.classList.remove('next-action-pulse');
1131
- }, 800);
1132
- }
1133
- cmdNode.textContent = next.command || '/feature';
1134
- byId('next-reason').textContent = next.reason || 'Ready for next feature';
1135
-
1136
- if (isFirstLoad) {
1137
- requestAnimationFrame(() => document.body.classList.add('hydrated'));
1138
- }
1139
-
1140
- prevData = {
1141
- ...data,
1142
- historyCount: historyItems.length
1143
- };
1144
- } catch (err) {
1145
- byId('live-dot').style.background = '#a5333f';
1146
- byId('live-dot').classList.remove('is-live');
1147
- byId('live-label').textContent = 'Disconnected';
1148
- } finally {
1149
- isFirstLoad = false;
1150
- refreshing = false;
1151
- }
1152
- }
1153
-
1154
- function setupNextActionCopy() {
1155
- const copyBtn = byId('next-copy-btn');
1156
- const cmdNode = byId('next-command');
1157
- const copyText = byId('next-copy-text');
1158
-
1159
- const performCopy = () => {
1160
- const textToCopy = cmdNode ? cmdNode.textContent.trim() : '';
1161
- if (!textToCopy || textToCopy === 'Loading...') return;
1162
- if (navigator.clipboard) {
1163
- navigator.clipboard.writeText(textToCopy);
1164
- if (copyBtn && copyText) {
1165
- copyBtn.classList.add('copied');
1166
- copyText.textContent = '✓ Copied!';
1167
- setTimeout(() => {
1168
- copyBtn.classList.remove('copied');
1169
- copyText.textContent = 'Copy Command';
1170
- }, 1400);
1171
- }
1172
- }
1173
- };
1174
-
1175
- if (copyBtn) copyBtn.onclick = performCopy;
1176
- if (cmdNode) cmdNode.onclick = performCopy;
1177
- }
1178
-
1179
- setupNextActionCopy();
1180
- refreshStatus();
1181
- setInterval(refreshStatus, 2000);
1182
- </script>
1183
- </body>
1184
- </html>`;
1185
195
  export { openDashboard, startDashboardServer };
1186
196
  //# sourceMappingURL=dashboard.js.map