@jakkrichm/create-nexus-devflow 2.13.0 → 2.15.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 (65) 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/branch-context.d.ts +1 -0
  6. package/dist/lib/branch-context.js +8 -3
  7. package/dist/lib/branch-context.js.map +1 -1
  8. package/dist/lib/dashboard-page.d.ts +5 -2
  9. package/dist/lib/dashboard-page.js +7 -388
  10. package/dist/lib/dashboard-page.js.map +1 -1
  11. package/dist/lib/dashboard-snapshot.d.ts +22 -3
  12. package/dist/lib/dashboard-snapshot.js +154 -3
  13. package/dist/lib/dashboard-snapshot.js.map +1 -1
  14. package/dist/lib/delivery-lifecycle-engine.d.ts +1 -1
  15. package/dist/lib/delivery-lifecycle-engine.js +1 -1
  16. package/dist/lib/delivery-lifecycle-engine.js.map +1 -1
  17. package/dist/lib/mcp-server-engine.d.ts +57 -0
  18. package/dist/lib/mcp-server-engine.js +678 -0
  19. package/dist/lib/mcp-server-engine.js.map +1 -0
  20. package/dist/lib/mcp.d.ts +15 -27
  21. package/dist/lib/mcp.js +20 -646
  22. package/dist/lib/mcp.js.map +1 -1
  23. package/dist/lib/project-status-engine.d.ts +110 -0
  24. package/dist/lib/project-status-engine.js +493 -0
  25. package/dist/lib/project-status-engine.js.map +1 -0
  26. package/dist/lib/skill-manager.d.ts +2 -82
  27. package/dist/lib/skill-manager.js +15 -856
  28. package/dist/lib/skill-manager.js.map +1 -1
  29. package/dist/lib/skill-registry-engine.d.ts +121 -0
  30. package/dist/lib/skill-registry-engine.js +824 -0
  31. package/dist/lib/skill-registry-engine.js.map +1 -0
  32. package/dist/lib/status.d.ts +1 -90
  33. package/dist/lib/status.js +7 -467
  34. package/dist/lib/status.js.map +1 -1
  35. package/dist/lib/studio-view-renderer.d.ts +17 -1
  36. package/dist/lib/studio-view-renderer.js +855 -3
  37. package/dist/lib/studio-view-renderer.js.map +1 -1
  38. package/dist/lib/tooling/commands/scan-doc-contract.js +1 -0
  39. package/dist/lib/tooling/commands/scan-doc-contract.js.map +1 -1
  40. package/dist/lib/update.js +1 -1
  41. package/dist/lib/update.js.map +1 -1
  42. package/package.json +1 -1
  43. package/template/.agents/skills/complete/SKILL.md +14 -13
  44. package/template/.agents/skills/implement/SKILL.md +36 -2
  45. package/template/.claude/skills/complete/SKILL.md +14 -13
  46. package/template/.claude/skills/implement/SKILL.md +36 -2
  47. package/template/AGENTS.md +1 -1
  48. package/template/devflow/build-plan.md +5 -4
  49. package/template/devflow/context/ai-interaction.md +4 -2
  50. package/template/devflow/docs/USAGE.md +145 -0
  51. package/template/devflow/docs/diagrams/nexus-devflow-architecture.html +14821 -0
  52. package/template/devflow/docs/diagrams/nexus-devflow-architecture.json +80 -0
  53. package/template/devflow/docs/diagrams/nexus-devflow-lifecycle.html +14860 -0
  54. package/template/devflow/docs/diagrams/nexus-devflow-lifecycle.json +88 -0
  55. package/template/devflow/docs/examples/living-spec/adr.example.md +43 -0
  56. package/template/devflow/docs/examples/living-spec/current-feature.example.md +113 -0
  57. package/template/devflow/docs/examples/living-spec/discovery.example.md +45 -0
  58. package/template/devflow/docs/examples/living-spec/ideas.example.md +22 -0
  59. package/template/devflow/docs/governance-rules.md +39 -0
  60. package/template/devflow/docs/manual-review-workflow-spec.md +116 -0
  61. package/template/devflow/docs/markdown-metadata-contract.md +140 -0
  62. package/template/devflow/docs/quickstart.md +119 -0
  63. package/template/devflow/docs/release-process.md +52 -0
  64. package/template/devflow/docs/skill-selection-policy.md +64 -0
  65. package/template/devflow/docs/workflow-surface-map.md +85 -0
@@ -1,9 +1,12 @@
1
- declare const DASHBOARD_PAGE_HTML = "<!doctype html>\n<html lang=\"th\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Nexus-DevFlow Enterprise Dashboard</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n <link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Noto+Sans+Thai:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap\" rel=\"stylesheet\">\n <style>\n :root{\n --font-display:\"Google Sans Thai\",\"Google Sans\",\"Noto Sans Thai\",\"Space Grotesk\",sans-serif;\n --font-sans:\"Google Sans Thai\",\"Google Sans\",\"Noto Sans Thai\",sans-serif;\n --font-mono:\"IBM Plex Mono\",monospace;\n --bg:#0a2540;--bg-deep:#061a2e;--panel:rgba(255,255,255,.04);--panel-strong:#0c2b49;\n --ink:#edf6ff;--soft:#b7d3ef;--muted:#7598ba;--line:rgba(191,224,255,.17);--line-strong:rgba(191,224,255,.36);\n --cyan:#7dd8ff;--mint:#6fe3b4;--gold:#f2c14e;--red:#ff7061;--violet:#b8a4ff;\n --mint-soft:rgba(111,227,180,.14);--gold-soft:rgba(242,193,78,.14);--red-soft:rgba(255,112,97,.14);--violet-soft:rgba(184,164,255,.14);\n }\n *{box-sizing:border-box}\n html{color-scheme:dark}\n body{margin:0;min-height:100vh;background-color:var(--bg);color:var(--ink);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;background-image:linear-gradient(rgba(125,216,255,.065) 1px,transparent 1px),linear-gradient(90deg,rgba(125,216,255,.065) 1px,transparent 1px),radial-gradient(circle at 8% -5%,rgba(125,216,255,.16),transparent 42rem);background-size:34px 34px,34px 34px,auto}\n button,input{font:inherit}\n code{font-family:var(--font-mono);color:var(--cyan)}\n .sheet{width:min(1180px,calc(100% - 40px));margin:0 auto;padding:34px 0 64px}\n .titleblock{display:flex;align-items:flex-end;justify-content:space-between;gap:26px;padding-bottom:20px;border-bottom:1px solid var(--line-strong);flex-wrap:wrap}\n .eyebrow,.label{font:600 10px/1 var(--font-mono);letter-spacing:.13em;text-transform:uppercase;color:var(--cyan)}\n .eyebrow{display:flex;align-items:center;gap:8px;margin-bottom:12px}.eyebrow:before{content:\"\";width:20px;height:1px;background:var(--cyan)}\n h1{margin:0 0 6px;font:700 clamp(32px,4vw,48px)/1.05 var(--font-display);letter-spacing:-.03em}\n .path,.muted{color:var(--muted);font-size:12px;line-height:1.65}.path{font-family:var(--font-mono)}\n .badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.badge,.pill{padding:4px 9px;border:1px solid var(--line-strong);border-radius:3px;background:rgba(255,255,255,.025);color:var(--muted);font:600 10px/1.5 var(--font-mono);text-transform:uppercase}\n .ok,.healthy,.current,.ready,.idle{border-color:rgba(111,227,180,.42)!important;color:var(--mint)!important;background:var(--mint-soft)!important}\n .warning,.available,.offline,.blocked,.fail{border-color:rgba(255,112,97,.42)!important;color:var(--red)!important;background:var(--red-soft)!important}\n .active,.deep,.fast,.configured{border-color:rgba(184,164,255,.42)!important;color:var(--violet)!important;background:var(--violet-soft)!important}\n .meta{display:grid;grid-template-columns:auto auto;gap:4px 20px;margin:0;font:11px/1.55 var(--font-mono)}.meta dt{color:var(--muted);text-transform:uppercase}.meta dd{margin:0;color:var(--soft);text-align:right}\n .live{display:flex;align-items:center;gap:8px;margin:14px 0 24px;font:11px var(--font-mono);color:var(--muted)}.live-dot{width:7px;height:7px;border-radius:50%;background:var(--gold)}.live-dot.on{background:var(--mint);box-shadow:0 0 0 4px rgba(111,227,180,.12)}\n .panel,.card{position:relative;min-width:0;border:1px solid var(--line);border-radius:4px;background:var(--panel)}\n #dual-track{overflow:hidden}\n .stepper{justify-content:center}.step{display:flex;flex-direction:column;align-items:center;text-align:center}.step-icon{display:grid;place-items:center;width:30px;height:30px;margin:10px auto 8px;border:1px solid currentColor;border-radius:8px;background:rgba(255,255,255,.035)}.icon-glyph{display:block;font-size:17px;line-height:1}.step.flow-0,.step.flow-4{color:var(--cyan)}.step.flow-1,.step.flow-5{color:var(--violet)}.step.flow-2,.step.flow-6{color:var(--gold)}.step.flow-3,.step.flow-7{color:var(--mint)}.step .step-name{color:currentColor}.step .step-cmd{color:var(--muted)}\n .panel{padding:24px 26px;margin-bottom:20px}.panel-head,.card-head{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}.panel-head{margin-bottom:20px}.card-head{margin-bottom:17px}\n .track-tabs{display:flex;gap:7px;flex-wrap:wrap}.tab{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;border:1px solid var(--line-strong);border-radius:99px;background:transparent;color:var(--muted);font:600 10px var(--font-mono);cursor:pointer}.tab:hover,.tab:focus-visible{border-color:var(--cyan);color:var(--cyan);outline:none}.tab.on{background:var(--cyan);border-color:var(--cyan);color:var(--bg-deep)}.tab-active-dot{display:none;padding:2px 6px;border-radius:99px;font:700 8px/1 var(--font-mono);letter-spacing:.05em;text-transform:uppercase;background:var(--mint);color:var(--bg-deep)}.tab.is-active-track .tab-active-dot{display:inline-flex;align-items:center;gap:3px;box-shadow:0 0 0 2px rgba(111,227,180,.25)}.tab.on.is-active-track .tab-active-dot{background:var(--bg-deep);color:var(--mint)}\n .track-view{display:none;min-width:0}.track-view.on{display:block}.stepper{display:flex;width:100%;max-width:100%;overflow-x:auto;padding:4px 0 7px}.step{flex:1 1 0;min-width:104px;position:relative;padding-right:12px}.step-line{position:absolute;top:10px;left:21px;right:0;height:1px;background:var(--line-strong)}.step:last-child .step-line{display:none}.step.done .step-line{background:var(--mint)}\n .step-dot{position:relative;z-index:1;width:21px;height:21px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--line-strong);background:var(--bg-deep);color:var(--muted);font:700 8px var(--font-mono)}.step.done .step-dot{border-color:var(--mint);color:var(--mint)}.step.active .step-dot{border-color:var(--gold);color:var(--gold);box-shadow:0 0 0 4px rgba(242,193,78,.16)}\n .step-name{margin-top:9px;color:var(--soft);font:600 10px/1.35 var(--font-mono)}.step.active .step-name{color:var(--gold)}.step.done .step-name{color:var(--mint)}.step-cmd{margin-top:3px;color:var(--muted);font:9px/1.4 var(--font-mono)}.track-note{margin-top:12px;padding-top:12px;border-top:1px dashed var(--line);color:var(--muted);font:11px/1.65 var(--font-mono)}\n .next-action{background:var(--bg-deep);border-color:var(--line-strong)}.next-head{display:flex;justify-content:space-between;gap:12px;align-items:center}.command{margin:8px 0 6px;color:var(--gold);font:700 clamp(22px,3vw,30px)/1.3 var(--font-mono)}.copy{padding:7px 12px;border:1px solid var(--cyan);border-radius:3px;background:transparent;color:var(--cyan);font:600 10px var(--font-mono);cursor:pointer}.copy:hover,.copy:focus-visible{background:var(--cyan);color:var(--bg-deep);outline:none}\n .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:1px;margin:0 0 20px;border:1px solid var(--line);border-radius:4px;overflow:hidden;background:var(--line)}.stat{padding:16px 18px;background:var(--bg-deep)}.stat-value{font:700 25px/1 var(--font-display)}.stat-label{margin-top:6px;color:var(--muted);font:10px var(--font-mono);text-transform:uppercase;letter-spacing:.08em}\n .grid{display:grid;grid-template-columns:repeat(12,1fr);gap:20px;margin-bottom:20px}.card{grid-column:span 4;min-width:0;padding:clamp(20px,2.4vw,28px)}.card.wide{grid-column:span 8}.card.full{grid-column:1/-1}.value{font:700 18px/1.4 var(--font-display)}\n .facts{display:grid;gap:10px}.fact{display:flex;justify-content:space-between;gap:16px;padding-bottom:10px;border-bottom:1px dashed var(--line)}.fact:last-child{padding-bottom:0;border:0}.fact span:first-child{color:var(--muted);font-size:11px}.fact span:last-child{color:var(--soft);font:11px var(--font-mono);text-align:right;overflow-wrap:anywhere}\n .progress{height:6px;margin:14px 0 9px;border-radius:99px;background:rgba(255,255,255,.07);overflow:hidden}.progress span{display:block;height:100%;width:0;background:var(--gold);transition:width .35s ease}\n ul{list-style:none;margin:0;padding:0}li{padding:10px 0;border-bottom:1px dashed var(--line);color:var(--soft);font-size:12px;line-height:1.55}li:last-child{border:0}.empty{color:var(--muted)}\n .severity{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.sev{padding:9px 6px;border:1px solid var(--line);border-radius:3px;text-align:center}.sev b{display:block;font:700 17px var(--font-display)}.sev small{color:var(--muted);font:9px var(--font-mono)}\n .commands{display:flex;flex-wrap:wrap;gap:10px}.cmd{position:relative;padding:8px 13px;border:1px solid var(--line-strong);border-radius:3px;background:rgba(255,255,255,.025);color:var(--soft);font:11px var(--font-mono);cursor:pointer}.cmd:hover,.cmd:focus-visible,.cmd:focus{border-color:var(--cyan);color:var(--cyan);outline:2px solid rgba(125,216,255,.2);outline-offset:2px}.cmd:after{content:attr(data-tip);position:absolute;z-index:30;left:50%;bottom:calc(100% + 10px);width:max-content;max-width:min(300px,80vw);padding:9px 11px;border:1px solid var(--line-strong);border-radius:3px;background:#031421;color:var(--soft);font:10px/1.5 var(--font-mono);white-space:normal;text-align:left;opacity:0;visibility:hidden;transform:translate(-50%,5px);box-shadow:0 12px 30px rgba(0,0,0,.4);transition:.15s;pointer-events:none}.cmd:hover:after,.cmd:focus-visible:after,.cmd:focus:after{opacity:1;visibility:visible;transform:translate(-50%,0)}\n .adapters,.roster-grid,.mcp-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.adapter,.roster-card,.mcp-card{padding:15px;border:1px solid var(--line);border-radius:4px;background:rgba(255,255,255,.02)}.adapter strong,.roster-card strong,.mcp-card strong{display:block;margin-bottom:6px;text-transform:capitalize}.adapter-path,.roster-desc,.mcp-desc{color:var(--muted);font:10px var(--font-mono);line-height:1.4}.adapter-state{margin-top:9px;font:10px var(--font-mono)}\n .mcp-grid{grid-template-columns:repeat(3,1fr)}\n .search-row{display:flex;gap:10px;margin-bottom:16px}.search-input{flex:1;padding:10px 14px;background:var(--bg-deep);border:1px solid var(--line-strong);border-radius:4px;color:var(--ink);font-family:var(--font-mono);font-size:12px}.search-btn{padding:10px 18px;border:1px solid var(--cyan);border-radius:4px;background:var(--cyan);color:var(--bg-deep);font-weight:700;cursor:pointer}\n .blast-result{padding:16px;border:1px solid var(--line-strong);border-radius:4px;background:var(--bg-deep);margin-top:12px;display:none}\n .section-note{margin:-2px 0 14px;color:var(--muted);font:10px/1.5 var(--font-mono)}.record-list{display:grid;gap:8px}.record-item{display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:13px;align-items:center;padding:13px 15px;border:1px solid var(--line);border-left:3px solid var(--record-color,var(--cyan));border-radius:3px;background:rgba(255,255,255,.02)}.record-icon{display:grid;place-items:center;width:31px;height:31px;border-radius:8px;background:color-mix(in srgb,var(--record-color,var(--cyan)) 14%,transparent)}.record-body{min-width:0}.record-meta{color:var(--record-color,var(--cyan));font:700 10px var(--font-mono);letter-spacing:.06em;text-transform:uppercase}.record-title{margin-top:3px;color:var(--ink);font-weight:650}.record-desc{margin-top:3px;color:var(--soft);font-size:12px}.record-desc:empty{display:none}.record-title strong,.record-desc strong{color:var(--record-color,var(--cyan));font-weight:750}.record-title em,.record-desc em{color:var(--gold)}.record-title code,.record-desc code{padding:1px 4px;border:1px solid var(--line);border-radius:3px;color:var(--cyan);font:10px var(--font-mono)}.record-title a,.record-desc a{color:var(--cyan);text-decoration:underline}.record-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.record-action{border:1px solid var(--record-color,var(--cyan));border-radius:3px;padding:8px 11px;background:transparent;color:var(--record-color,var(--cyan));font:700 10px var(--font-mono);cursor:pointer}.record-action:hover,.record-action:focus-visible{background:var(--record-color,var(--cyan));color:var(--bg)}.record-badge{align-self:start;border:1px solid rgba(111,227,180,.36);border-radius:99px;padding:4px 8px;background:var(--mint-soft);color:var(--mint);font:700 9px var(--font-mono);letter-spacing:.04em;text-transform:uppercase}.record-item.tone-cyan{--record-color:var(--cyan)}.record-item.tone-mint{--record-color:var(--mint)}.record-item.tone-gold{--record-color:var(--gold)}.record-item.tone-red{--record-color:var(--red)}.record-item.tone-violet{--record-color:var(--violet)}.stepper{gap:0;justify-content:center}.step{padding-right:0;overflow:visible}.step-line{left:calc(50% + 10px);right:calc(-50% + 10px);width:auto}#sev-p0{color:var(--red)}#sev-p1{color:var(--gold)}#sev-p2{color:var(--violet)}#sev-p3{color:var(--cyan)}#stat-runs.tone{color:var(--mint)}#stat-findings.tone-ok{color:var(--mint)}#stat-findings.tone-alert{color:var(--red)}#stat-ideas.tone-ok{color:var(--mint)}#stat-ideas.tone-pending{color:var(--gold)}#stat-adapters.tone-ok{color:var(--mint)}#stat-adapters.tone-partial{color:var(--gold)}#stat-state.tone-idle{color:var(--cyan)}#stat-state.tone-active{color:var(--violet)}#stat-state.tone-blocked{color:var(--red)}\n .multitask-panel{border-color:rgba(56,189,248,.35)}.workspace-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:14px;margin-top:10px}.workspace-card{background:rgba(255,255,255,.025);border:1px solid var(--line);border-radius:6px;padding:16px;display:flex;flex-direction:column;gap:8px;transition:all .2s ease}.workspace-card:hover{border-color:var(--cyan);background:rgba(255,255,255,.04)}.ws-header{display:flex;align-items:center;justify-content:space-between;gap:8px}.ws-id{font-family:var(--font-mono);font-weight:700;font-size:11px;color:var(--cyan);background:var(--cyan-soft);padding:2px 8px;border-radius:3px}.ws-stage{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;padding:2px 6px;border-radius:3px;background:var(--gold-soft);color:var(--gold);border:1px solid rgba(242,193,78,.3)}.ws-stage.stage-check{background:var(--mint-soft);color:var(--mint);border-color:rgba(111,227,180,.3)}.ws-title{font-weight:650;font-size:13px;color:var(--ink);line-height:1.4}.ws-progress-bar{height:6px;background:rgba(0,0,0,.3);border-radius:99px;overflow:hidden;margin:4px 0}.ws-progress-fill{height:100%;background:linear-gradient(135deg,var(--cyan),var(--violet));transition:width .3s}.ws-meta{display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--muted)}.ws-actions{display:flex;align-items:center;gap:6px;padding-top:8px;border-top:1px dashed var(--line);flex-wrap:wrap}.btn-ws{padding:4px 9px;border-radius:3px;font-family:var(--font-mono);font-size:10px;font-weight:700;background:rgba(255,255,255,.04);border:1px solid var(--line);color:var(--soft);cursor:pointer;transition:all .15s}.btn-ws:hover{background:var(--cyan-soft);border-color:var(--cyan);color:var(--cyan)}\n footer{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-top:26px;padding-top:17px;border-top:1px solid var(--line);color:var(--muted);font:10px var(--font-mono)}\n @media(max-width:920px){.grid .card,.grid .card.wide{grid-column:1/-1}.stats{grid-template-columns:repeat(2,1fr)}.adapters,.roster-grid,.mcp-grid{grid-template-columns:repeat(2,1fr)}.titleblock{align-items:flex-start;flex-direction:column}.meta dd{text-align:left}.stepper{gap:0}.cmd:after{position:fixed;left:12px;right:12px;bottom:16px;width:auto;max-width:none;transform:translateY(5px)}.cmd:hover:after,.cmd:focus-visible:after,.cmd:focus:after{transform:none}}\n @media(max-width:560px){.sheet{width:calc(100% - 24px);padding-top:22px}.panel,.card{padding:18px}.grid{gap:14px;margin-bottom:14px}.stats{margin-bottom:14px}.adapters,.roster-grid,.mcp-grid{grid-template-columns:1fr}.cmd:after{position:fixed;left:12px;right:12px;bottom:16px;width:auto;max-width:none;transform:translateY(5px)}.cmd:hover:after,.cmd:focus-visible:after,.cmd:focus:after{transform:none}.meta{grid-template-columns:1fr}.track-tabs{width:100%}.tab{flex:1}.fact{align-items:flex-start}.record-item{grid-template-columns:34px minmax(0,1fr)}.record-action{grid-column:2;justify-self:start}}\n @media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.001ms!important;transition-duration:.001ms!important}}\n </style>\n</head>\n<body>\n <main class=\"sheet\">\n <header class=\"titleblock\">\n <div>\n <div class=\"eyebrow\">Nexus-DevFlow \u00B7 Enterprise Workspace Status</div>\n <h1 id=\"project-name\">Nexus-DevFlow</h1>\n <div class=\"path\" id=\"project-path\">Connecting...</div>\n <div class=\"badges\">\n <span class=\"badge\" id=\"version-badge\">v2.5.0</span>\n <span class=\"badge\" id=\"health-badge\">health</span>\n <span class=\"badge\" id=\"gate-badge\">Gate: checking</span>\n <span class=\"badge\" id=\"drift-badge\">Drift: checking</span>\n <span class=\"badge\" id=\"track-badge\">track</span>\n </div>\n </div>\n <dl class=\"meta\">\n <dt>Package</dt><dd id=\"package-name\">@jakkrichm/create-nexus-devflow</dd>\n <dt>Adapters</dt><dd id=\"adapter-count\">-</dd>\n <dt>Branch</dt><dd id=\"header-branch\">-</dd>\n <dt>Snapshot</dt><dd id=\"generated-at\">-</dd>\n </dl>\n </header>\n <div class=\"live\"><span class=\"live-dot\" id=\"live-dot\"></span><span id=\"live-label\">Connecting to local dashboard...</span></div>\n\n <section class=\"panel\" id=\"dual-track\">\n <div class=\"panel-head\">\n <span class=\"label\">Unified Living Spec Model (DevFlow 2.5.0)</span>\n <div class=\"track-tabs\" role=\"tablist\" aria-label=\"Delivery tracks\">\n <button class=\"tab on\" id=\"tab-preflight\" role=\"tab\" aria-selected=\"true\" aria-controls=\"view-preflight\" data-track=\"preflight\"><span>\uD83D\uDD2E Pre-Flight Discovery</span><span class=\"tab-active-dot\">\u25CF ACTIVE</span></button>\n <button class=\"tab\" id=\"tab-fast\" role=\"tab\" aria-selected=\"false\" aria-controls=\"view-fast\" data-track=\"fast\"><span>\u26A1 Living Spec \u00B7 4 steps</span><span class=\"tab-active-dot\">\u25CF ACTIVE</span></button>\n <button class=\"tab\" id=\"tab-swarm\" role=\"tab\" aria-selected=\"false\" aria-controls=\"view-swarm\" data-track=\"swarm\"><span>\uD83E\uDD16 Multi-Agent Swarm</span></button>\n <button class=\"tab\" id=\"tab-graph\" role=\"tab\" aria-selected=\"false\" aria-controls=\"view-graph\" data-track=\"graph\"><span>\uD83D\uDDFA\uFE0F Code Graph</span></button>\n </div>\n </div>\n <div class=\"track-view on\" id=\"view-preflight\" role=\"tabpanel\" aria-labelledby=\"tab-preflight\">\n <div class=\"stepper\" id=\"pipeline-preflight\"></div>\n <div class=\"track-note\" id=\"note-preflight\">Pre-Flight Discovery & Architectural Alignment (/idea, /grill, /brainstorm, /discovery).</div>\n </div>\n <div class=\"track-view\" id=\"view-fast\" role=\"tabpanel\" aria-labelledby=\"tab-fast\">\n <div class=\"stepper\" id=\"pipeline-fast\"></div>\n <div class=\"track-note\" id=\"note-fast\">Single Living Spec (/feature, /implement, /check, /complete) with TDD Discipline & QA Matrix.</div>\n </div>\n <div class=\"track-view\" id=\"view-swarm\" role=\"tabpanel\" aria-labelledby=\"tab-swarm\">\n <div class=\"section-note\">Role-based specialized AI Subagents roster & execution matrix</div>\n <div class=\"roster-grid\" id=\"swarm-roster\"></div>\n </div>\n <div class=\"track-view\" id=\"view-graph\" role=\"tabpanel\" aria-labelledby=\"tab-graph\">\n <div class=\"section-note\">Semantic dependency graph & instant Blast Radius analyzer</div>\n <div class=\"search-row\">\n <input class=\"search-input\" id=\"graph-file-input\" placeholder=\"Type file path (e.g. packages/create-nexus-devflow/lib/code-graph.ts)...\" />\n <button class=\"search-btn\" id=\"graph-search-btn\" type=\"button\">Analyze Impact</button>\n </div>\n <div class=\"blast-result\" id=\"blast-result\"></div>\n </div>\n </section>\n\n <section class=\"panel next-action\" id=\"next-panel\">\n <div class=\"next-head\"><span class=\"label\">Next Action</span><button class=\"copy\" id=\"next-copy\" type=\"button\">Copy command</button></div>\n <div class=\"command\" id=\"next-command\" aria-live=\"polite\">Loading...</div>\n <div class=\"muted\" id=\"next-reason\"></div>\n </section>\n\n <section class=\"stats\" aria-label=\"Workspace summary\">\n <div class=\"stat\"><div class=\"stat-value\" id=\"stat-runs\">-</div><div class=\"stat-label\">Runs released</div></div>\n <div class=\"stat\"><div class=\"stat-value\" id=\"stat-findings\">-</div><div class=\"stat-label\">Active findings</div></div>\n <div class=\"stat\"><div class=\"stat-value\" id=\"stat-ideas\">-</div><div class=\"stat-label\">Pending ideas</div></div>\n <div class=\"stat\"><div class=\"stat-value\" id=\"stat-mcp\">-</div><div class=\"stat-label\">MCP Tools Ready</div></div>\n <div class=\"stat\"><div class=\"stat-value\" id=\"stat-state\">-</div><div class=\"stat-label\">Current state</div></div>\n </section>\n\n <!-- Multi-Task Living Spec Workspaces Grid -->\n <section class=\"card full panel multitask-panel\" id=\"active-workspaces-panel\">\n <div class=\"panel-head\">\n <div>\n <span class=\"label\">Active Living Specs (Pure Task-Isolated Workspaces)</span>\n <div class=\"muted\" id=\"active-workspaces-summary\" style=\"margin-top:3px;font-size:11px;\">Scanned from devflow/context/{xxx-slug}/</div>\n </div>\n <span class=\"pill active\" id=\"active-workspaces-pill\">0 Active</span>\n </div>\n <div class=\"workspace-grid\" id=\"active-workspaces-list\"></div>\n </section>\n\n <section class=\"grid\">\n <article class=\"card\"><div class=\"card-head\"><span class=\"label\">Nexus-DevFlow</span><span class=\"pill\" id=\"update-pill\">checking</span></div><div class=\"facts\"><div class=\"fact\"><span>Installed / latest</span><span id=\"installed-latest\">-</span></div><div class=\"fact\"><span>Health</span><span id=\"system-health\">-</span></div><div class=\"fact\"><span>Architecture</span><span>3-Pillars + Swarm RAG</span></div></div></article>\n <article class=\"card wide\"><div class=\"card-head\"><span class=\"label\">Current Work</span><span class=\"pill\" id=\"work-pill\">loading</span></div><div class=\"value\" id=\"work-title\">Loading...</div><div class=\"progress\"><span id=\"work-progress\"></span></div><div class=\"muted\" id=\"work-meta\"></div></article>\n <article class=\"card\"><div class=\"card-head\"><span class=\"label\">Git & Drift</span><span class=\"pill\" id=\"git-pill\">loading</span></div><div class=\"facts\"><div class=\"fact\"><span>Branch</span><span id=\"git-branch\">-</span></div><div class=\"fact\"><span>Working tree</span><span id=\"git-changed\">-</span></div><div class=\"fact\"><span>Drift Status</span><span id=\"git-drift\">-</span></div><div class=\"fact\"><span>Last commit</span><span id=\"git-commit\">-</span></div></div></article>\n <article class=\"card\"><div class=\"card-head\"><span class=\"label\">Findings Ledger</span><span class=\"value\" id=\"findings-count\">-</span></div><div class=\"severity\"><div class=\"sev\"><b id=\"sev-p0\">0</b><small>P0</small></div><div class=\"sev\"><b id=\"sev-p1\">0</b><small>P1</small></div><div class=\"sev\"><b id=\"sev-p2\">0</b><small>P2</small></div><div class=\"sev\"><b id=\"sev-p3\">0</b><small>P3</small></div></div><ul id=\"findings-list\"></ul></article>\n <article class=\"card\"><div class=\"card-head\"><span class=\"label\">Completion & Gate</span><span class=\"pill\" id=\"completion-pill\">-</span></div><ul id=\"completion-list\"></ul></article>\n <article class=\"card\"><div class=\"card-head\"><span class=\"label\">Independent Review</span><span class=\"pill\" id=\"review-state\">none</span></div><div class=\"facts\"><div class=\"fact\"><span>Verdict</span><span id=\"review-verdict\">-</span></div><div class=\"fact\"><span>Freshness</span><span id=\"review-freshness\">-</span></div><div class=\"fact\"><span>Check</span><span id=\"review-check\">-</span></div></div><ul id=\"review-warnings\"></ul></article>\n <article class=\"card\"><div class=\"card-head\"><span class=\"label\">Warnings</span><span class=\"value\" id=\"warnings-count\">-</span></div><ul id=\"warnings-list\"></ul></article>\n </section>\n\n <section class=\"card full panel\"><div class=\"card-head\"><span class=\"label\">Model Context Protocol (MCP) Tools Hub</span><span class=\"value\" id=\"mcp-count\">12 Tools</span></div><div class=\"section-note\">Typed JSON-RPC tools exposed to AI Coding Assistants</div><div class=\"mcp-grid\" id=\"mcp-list\"></div></section>\n <section class=\"card full panel\"><div class=\"card-head\"><span class=\"label\">Quick Commands</span><span class=\"muted\">hover or focus for help \u00B7 click to copy</span></div><div class=\"commands\" id=\"command-list\"></div></section>\n <section class=\"grid\"><article class=\"card full\"><div class=\"card-head\"><span class=\"label\">Doctor \u00B7 Health Check</span><span class=\"pill\" id=\"doctor-pill\">-</span></div><div class=\"record-list\" id=\"doctor-list\"></div></article></section>\n <section class=\"card full panel\"><div class=\"card-head\"><span class=\"label\">Discoveries</span><span class=\"value\" id=\"discovery-count\">-</span></div><div class=\"record-list\" id=\"discovery-list\"></div></section>\n <section class=\"card full panel\"><div class=\"card-head\"><span class=\"label\">Idea Inbox & Backlog</span><span class=\"value\" id=\"ideas-count\">-</span></div><div class=\"section-note\">From devflow/ideas.md \u00B7 start a selected item with /feature IDEA-xxx</div><div class=\"record-list\" id=\"ideas-list\"></div></section>\n <section class=\"card full panel\"><div class=\"card-head\"><span class=\"label\">Completed Work Archive</span><span class=\"value\" id=\"history-count\">-</span></div><div class=\"section-note\">Categorized delivery history \u00B7 features, fixes and rollbacks</div><div class=\"record-list\" id=\"history-list\"></div></section>\n <footer><span>Nexus-DevFlow 2.5.0 Enterprise Dashboard \u00B7 refreshes every 2 seconds</span><span id=\"footer-revision\">snapshot pending</span></footer>\n </main>\n <script>\n const byId = (id) => document.getElementById(id);\n const escapeHtml = (str) => String(str == null ? '' : str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/'/g, '&#039;');\n const text = (id, value) => { const node = byId(id); if (node) node.textContent = value == null ? '-' : String(value); };\n const pill = (id, state, label) => { const node = byId(id); if (!node) return; node.className = 'pill ' + (state || 'configured'); node.textContent = label || state || '-'; };\n const list = (id, values, empty) => { const node = byId(id); if (!node) return; node.replaceChildren(); const rows = values.length ? values : [empty]; rows.forEach((value) => { const li = document.createElement('li'); li.textContent = value; if (!values.length) li.className = 'empty'; node.append(li); }); };\n const titleCase = (value) => String(value || '').replaceAll('_',' ').replace(/\bw/g, (c) => c.toUpperCase());\n const FLOW_ICON_NAMES = ['target','code','shield','package'];\n const EMOJI_ICONS = { compass:'\uD83D\uDD0E',discovery:'\uD83D\uDD0E',target:'\uD83C\uDFAF',document:'\uD83D\uDCC4',list:'\uD83E\uDDE9',code:'\u2699\uFE0F',shield:'\uD83E\uDDEA',report:'\uD83D\uDCCA',package:'\uD83D\uDCE6',bulb:'\uD83D\uDCA1',check:'\u2705',wrench:'\uD83D\uDEE0\uFE0F',rollback:'\u21A9\uFE0F',search:'\uD83D\uDD0D',alert:'\u26A0\uFE0F',grill:'\uD83D\uDD25',brainstorm:'\uD83D\uDCAD',idea:'\uD83D\uDCA1' };\n function iconImage(name) { const icon = document.createElement('span'); icon.className = 'icon-glyph'; icon.setAttribute('aria-hidden','true'); icon.textContent = EMOJI_ICONS[name] || '\uD83D\uDCC4'; return icon; }\n function decoratePipeline(id) { byId(id)?.querySelectorAll('.step').forEach((step,index) => { step.classList.add('flow-' + index); const icon = document.createElement('span'); icon.className = 'step-icon'; icon.setAttribute('aria-hidden','true'); const cmd = step.querySelector('.step-cmd')?.textContent?.replace('/','').split(' ')[0] || ''; const iconKey = id.includes('preflight') ? (cmd || ['idea','grill','brainstorm','discovery'][index] || 'search') : (FLOW_ICON_NAMES[index] || 'document'); icon.append(iconImage(iconKey)); const name = step.querySelector('.step-name'); step.insertBefore(icon,name || null); }); }\n function richText(root,value) { const source = String(value || ''); let cursor = 0; const plain = (end) => { if (end > cursor) root.append(document.createTextNode(source.slice(cursor,end))); cursor = end; }; while (cursor < source.length) { if (source.startsWith('**',cursor)) { const end = source.indexOf('**',cursor + 2); if (end > cursor + 2) { const node = document.createElement('strong'); node.textContent = source.slice(cursor + 2,end); root.append(node); cursor = end + 2; continue; } } if (source[cursor] === '*') { const end = source.indexOf('*',cursor + 1); if (end > cursor + 1) { const node = document.createElement('em'); node.textContent = source.slice(cursor + 1,end); root.append(node); cursor = end + 1; continue; } } if (source.charCodeAt(cursor) === 96) { const end = source.indexOf(String.fromCharCode(96),cursor + 1); if (end > cursor + 1) { const node = document.createElement('code'); node.textContent = source.slice(cursor + 1,end); root.append(node); cursor = end + 1; continue; } } const next = [source.indexOf('**',cursor + 1),source.indexOf('*',cursor + 1),source.indexOf(String.fromCharCode(96),cursor + 1)].filter((index) => index >= 0).sort((a,b) => a-b)[0] ?? source.length; plain(next > cursor ? next : cursor + 1); } }\n function recordCard(options) { const card = document.createElement('article'); card.className = 'record-item tone-' + (options.tone || 'cyan'); const icon = document.createElement('span'); icon.className = 'record-icon'; icon.append(iconImage(options.icon || 'document')); const body = document.createElement('div'); body.className = 'record-body'; const meta = document.createElement('div'); meta.className = 'record-meta'; richText(meta,options.meta); const title = document.createElement('div'); title.className = 'record-title'; richText(title,options.title); const desc = document.createElement('div'); desc.className = 'record-desc'; richText(desc,options.description); body.append(meta,title,desc); card.append(icon,body); if (options.badge) { const badge = document.createElement('span'); badge.className = 'record-badge'; badge.textContent = options.badge; card.append(badge); } return card; }\n function renderRecords(id,items,empty,mapper) { const root = byId(id); root.replaceChildren(); if (!items?.length) { const node = document.createElement('div'); node.className = 'empty'; node.textContent = empty; root.append(node); return; } items.forEach((item,index) => root.append(recordCard(mapper(item,index)))); }\n function renderIdeas(items) { renderRecords('ideas-list',items,'No pending ideas',(item) => ({ icon:'bulb',tone:'gold',meta:item.id,title:item.title,description:(item.description || '\u26A1') + ' \u00B7 ' + (item.feasibility || 'Not scored') })); }\n function renderHistory(items) { const map = { feature:{icon:'package',tone:'cyan'},fix:{icon:'wrench',tone:'gold'},rollback:{icon:'rollback',tone:'red'} }; renderRecords('history-list',(items || []).slice(0,10),'No completed work in history archive',(item) => { const type = String(item.type || 'other').toLowerCase(); const style = map[type] || {icon:'report',tone:'violet'}; return { ...style,meta:type + ' ' + (item.buildPlanItem || ''),title:item.title,description:'',badge:'Released' }; }); }\n function renderDoctor(items) { renderRecords('doctor-list',items,'No doctor checks available',(item) => ({ icon:item.status === 'pass' ? 'check' : 'alert',tone:item.status === 'pass' ? 'mint' : item.status === 'warn' ? 'gold' : 'red',meta:item.status,title:item.name,description:item.message })); }\n function renderDiscoveries(items) { renderRecords('discovery-list',items,'No discoveries recorded',(item) => ({ icon:'search',tone:'mint',meta:item.id,title:item.title,description:'',badge:'Decision: ' + (item.decision || 'Pending') })); }\n\n function renderPipeline(id, nodes) {\n const root = byId(id); root.replaceChildren();\n (nodes || []).forEach((node, index) => {\n const step = document.createElement('div'); step.className = 'step ' + node.state;\n const line = document.createElement('span'); line.className = 'step-line';\n const dot = document.createElement('span'); dot.className = 'step-dot'; dot.textContent = node.state === 'done' ? '\u2713' : String(index + 1).padStart(nodes.length > 4 ? 2 : 1, '0');\n const name = document.createElement('div'); name.className = 'step-name'; name.textContent = node.label;\n const command = document.createElement('div'); command.className = 'step-cmd'; command.textContent = node.command;\n step.append(line, dot, name, command); root.append(step);\n });\n }\n\n let userHasSwitchedTab = false;\n function selectTrack(track) {\n document.querySelectorAll('.tab').forEach((node) => { const on = node.dataset.track === track; node.classList.toggle('on', on); node.setAttribute('aria-selected', String(on)); });\n document.querySelectorAll('.track-view').forEach((node) => node.classList.toggle('on', node.id === 'view-' + track));\n }\n\n function wireCopy(button, getValue) {\n button.addEventListener('click', async () => {\n const original = button.textContent;\n button.textContent = 'Copied!';\n try { await navigator.clipboard.writeText(getValue()); }\n catch { button.textContent = 'Copy unavailable'; }\n setTimeout(() => { button.textContent = original; }, 1100);\n });\n }\n\n document.querySelectorAll('.tab').forEach((node) => node.addEventListener('click', () => { userHasSwitchedTab = true; selectTrack(node.dataset.track); }));\n wireCopy(byId('next-copy'), () => byId('next-command').textContent.trim());\n\n function renderCommands(commands) {\n const root = byId('command-list'); root.replaceChildren();\n const allCommands = [...(commands || [])];\n ['nexus-devflow check-gate', 'nexus-devflow drift', 'nexus-devflow reconcile --fix', 'nexus-devflow slice --stage implement', 'nexus-devflow swarm', 'nexus-devflow studio'].forEach(c => {\n if (!allCommands.some(item => item.command === c)) {\n allCommands.push({ name: c.replace('nexus-devflow ', ''), command: c, family: 'companion', description: 'Run ' + c });\n }\n });\n allCommands.forEach((item) => {\n const button = document.createElement('button'); button.type = 'button'; button.className = 'cmd'; button.textContent = item.name; button.dataset.tip = item.description; wireCopy(button, () => item.command); root.append(button);\n });\n }\n\n function renderSwarm(swarm) {\n const root = byId('swarm-roster'); if (!root) return; root.replaceChildren();\n (swarm?.agentRoster || []).forEach(agent => {\n const card = document.createElement('div'); card.className = 'roster-card';\n card.innerHTML = '<strong>' + agent.avatar + ' ' + agent.name + '</strong><div class=\"roster-desc\">' + agent.responsibility + '</div>';\n root.append(card);\n });\n }\n\n function renderMcp(tools) {\n const root = byId('mcp-list'); if (!root) return; root.replaceChildren();\n (tools || []).forEach(t => {\n const card = document.createElement('div'); card.className = 'mcp-card';\n card.innerHTML = '<strong>\u2699\uFE0F ' + t.name + '</strong><div class=\"mcp-desc\">' + t.description + '</div>';\n root.append(card);\n });\n }\n\n byId('graph-search-btn')?.addEventListener('click', async () => {\n const input = byId('graph-file-input'); const res = byId('blast-result');\n if (!input || !res) return;\n const file = input.value.trim();\n if (!file) return;\n res.style.display = 'block'; res.innerHTML = 'Analyzing Blast Radius...';\n try {\n const resp = await fetch('/api/graph?file=' + encodeURIComponent(file));\n const data = await resp.json();\n if (data.error) { res.innerHTML = '<span style=\"color:var(--red)\">' + data.error + '</span>'; return; }\n let html = '<strong>Impact Score: <span style=\"color:var(--gold)\">' + data.impactScore + '</span> | Total Affected: ' + data.totalAffected + ' files</strong><br/><br/>';\n if (data.directDependents?.length) {\n html += '<strong>Direct Dependents:</strong><ul>' + data.directDependents.map(d => '<li>' + d + '</li>').join('') + '</ul>';\n }\n if (data.transitiveDependents?.length) {\n html += '<br/><strong>Transitive Dependents:</strong><ul>' + data.transitiveDependents.map(t => '<li>' + t + '</li>').join('') + '</ul>';\n }\n res.innerHTML = html;\n } catch (e) {\n res.innerHTML = '<span style=\"color:var(--red)\">Error: ' + e.message + '</span>';\n }\n });\n\n function renderActiveWorkspaces(activeRuns, work) {\n const root = byId('active-workspaces-list');\n if (!root) return;\n root.replaceChildren();\n const runs = (activeRuns && activeRuns.length > 0) ? activeRuns : (work && work.state === 'active' ? [{\n runId: work.runId || 'ACTIVE',\n title: work.title || 'Active Spec',\n status: work.status || 'implementing',\n branch: 'current',\n totalTasks: work.total || 0,\n completedTasks: work.completed || 0,\n remainingTasks: work.remaining || 0,\n hasOpenFindings: false\n }] : []);\n\n pill('active-workspaces-pill', runs.length > 0 ? 'active' : 'idle', runs.length + ' Active');\n text('active-workspaces-summary', runs.length > 0 ? 'Found ' + runs.length + ' active workspace(s) in devflow/context/' : 'No active workspace folders. Run /feature or /fix to start.');\n\n if (runs.length === 0) {\n const empty = document.createElement('div');\n empty.className = 'empty';\n empty.style.padding = '18px';\n empty.textContent = 'No active delivery runs in progress. Use /feature or /fix to create a living spec.';\n root.append(empty);\n return;\n }\n\n runs.forEach((run) => {\n const card = document.createElement('article');\n card.className = 'workspace-card';\n const review = run.review || { state: 'none', freshness: 'not-applicable', verdict: null, checkResult: null };\n const pct = run.totalTasks > 0 ? Math.round((run.completedTasks / run.totalTasks) * 100) : 0;\n const stageClass = (run.status || '').includes('check') ? 'ws-stage stage-check' : 'ws-stage';\n const findingTag = run.hasOpenFindings ? '<span style=\"color:var(--red);font-size:10px;font-weight:700;\">\u26A0 Open Findings</span>' : '<span style=\"color:var(--mint);font-size:10px;font-weight:700;\">\u2714 0 Blockers</span>';\n\n card.innerHTML = '<div class=\"ws-header\">' +\n '<span class=\"ws-id\">' + escapeHtml(run.runId) + '</span>' +\n '<span class=\"' + stageClass + '\">Stage: ' + escapeHtml(run.status || 'active') + '</span>' +\n '</div>' +\n '<div class=\"ws-title\">' + escapeHtml(run.title || 'Living Spec') + '</div>' +\n '<div class=\"ws-progress-bar\"><div class=\"ws-progress-fill\" style=\"width:' + pct + '%;\"></div></div>' +\n '<div class=\"ws-meta\">' +\n '<span>\uD83C\uDF3F <code>' + escapeHtml(run.branch || 'main') + '</code></span>' +\n '<span><strong>' + pct + '%</strong> (' + run.completedTasks + '/' + run.totalTasks + ' Tasks)</span>' +\n '</div>' +\n '<div style=\"display:flex;justify-content:space-between;align-items:center;margin-top:2px;\">' +\n findingTag +\n '<span style=\"font-size:10px;color:var(--muted);\">' + run.remainingTasks + ' remaining</span>' +\n '</div>' +\n '<div style=\"font-size:10px;color:var(--muted);margin-top:6px;\">Review: <strong>' + escapeHtml(review.verdict || review.state) + '</strong> \u00B7 ' + escapeHtml(review.freshness) + '</div>' +\n '<div class=\"ws-actions\">' +\n '<button class=\"btn-ws\" type=\"button\" data-cmd=\"/implement ' + escapeHtml(run.runId) + '\">\u25B6 /implement</button>' +\n '<button class=\"btn-ws\" type=\"button\" data-cmd=\"/check ' + escapeHtml(run.runId) + '\">\uD83E\uDDEA /check</button>' +\n '<button class=\"btn-ws\" type=\"button\" data-cmd=\"/complete ' + escapeHtml(run.runId) + '\">\uD83D\uDCE6 /complete</button>' +\n '</div>';\n\n card.querySelectorAll('.btn-ws').forEach(btn => {\n wireCopy(btn, () => btn.getAttribute('data-cmd'));\n });\n root.append(card);\n });\n }\n\n function renderSnapshot(data) {\n const status = data.status || {}; const project = status.project || {}; const devflow = status.devflow || {}; const work = status.currentWork || {}; const git = status.git || {}; const findings = status.findings || { active: [], blockers: [] }; const review = status.review || { state: 'none', freshness: 'not-applicable', verdict: null, checkResult: null, warnings: [] }; const workflow = data.workflow || {}; const update = data.update || {}; const doctor = data.doctor || { checks: [] }; const discoveries = data.discoveries || { recent: [] }; const history = data.history || { items: [], total: 0 }; const ideas = status.ideas || { pending: [], totalPending: 0 }; const gate = data.gatekeeper || { passed: true }; const drift = data.drift || { hasDrift: false };\n\n text('project-name', project.name || 'Nexus-DevFlow'); text('project-path', project.root || ''); text('version-badge', 'v' + (devflow.version || '2.5.0'));\n pill('health-badge', status.health, status.health === 'ok' ? 'Health OK' : 'Health warning');\n pill('gate-badge', gate.passed ? 'ok' : 'blocked', gate.passed ? '\u2714 Gate Passed' : '\u2716 Gate Blocked');\n pill('drift-badge', !drift.hasDrift ? 'ok' : 'warning', !drift.hasDrift ? '\u2714 In Sync' : '\u26A0 Drift Detected');\n pill('track-badge', workflow.track, 'Track ' + titleCase(workflow.track || 'idle'));\n\n text('adapter-count', (devflow.adapters || []).length + ' configured'); text('header-branch', git.branch || 'unknown'); text('generated-at', new Date(data.generatedAt).toLocaleTimeString()); text('footer-revision', 'snapshot ' + data.generatedAt);\n renderPipeline('pipeline-preflight', workflow.deep || workflow.preflight); renderPipeline('pipeline-fast', workflow.fast); decoratePipeline('pipeline-preflight'); decoratePipeline('pipeline-fast');\n if (!userHasSwitchedTab) {\n if (work.state === 'active' || workflow.track === 'fast') {\n selectTrack('fast');\n } else {\n selectTrack('preflight');\n }\n }\n renderSwarm(data.swarm); renderMcp(data.mcpTools); text('stat-mcp', (data.mcpTools || []).length + ' Ready');\n renderActiveWorkspaces(status.activeRuns, work);\n\n const nextCommand = data.nextAction?.command || status.nextAction?.command || '/feature'; text('next-command', nextCommand); text('next-reason', data.nextAction?.reason || 'Ready for new work.');\n text('stat-runs', history.total || 0); text('stat-findings', (findings.active || []).length); text('stat-ideas', ideas.totalPending || 0); text('stat-state', titleCase(work.state || 'idle'));\n pill('update-pill', update.state, update.state === 'current' ? 'up to date' : update.state); text('installed-latest', (update.installedVersion || '2.5.0') + ' / ' + (update.latestVersion || '2.5.0')); text('system-health', status.health || 'ok');\n pill('work-pill', work.state, work.state || 'idle'); text('work-title', work.title || 'No active delivery run'); const pct = work.total > 0 ? Math.round((work.completed / work.total) * 100) : 0; byId('work-progress').style.width = pct + '%'; text('work-meta', work.state === 'active' ? work.completed + ' of ' + work.total + ' steps completed' : 'Ready');\n pill('git-pill', git.clean ? 'ok' : 'warning', git.clean ? 'clean' : (git.changedFiles || 0) + ' changed'); text('git-branch', git.branch || 'unknown'); text('git-changed', git.clean ? 'clean' : (git.changedFiles || 0) + ' changed files'); text('git-drift', drift.clean ? 'In Sync' : 'Drift detected'); text('git-commit', git.lastCommit || '-');\n text('findings-count', findings.total || 0); ['P0','P1','P2','P3'].forEach((severity) => text('sev-' + severity.toLowerCase(), (findings.active || []).filter((item) => item.severity === severity).length)); list('findings-list', (findings.active || []).map((item) => item.id + ' \u00B7 ' + item.title), 'No active findings');\n pill('completion-pill', status.completion?.state, status.completion?.state || 'unknown'); list('completion-list', status.completion?.blockers || [], work.state === 'idle' ? 'No active run to complete' : 'All readiness checks passed'); text('warnings-count', (status.warnings || []).length); list('warnings-list', (status.warnings || []).map((item) => item.message), 'No active warnings or drift');\n pill('review-state', review.state, review.state); text('review-verdict', review.verdict || '-'); text('review-freshness', review.freshness); text('review-check', review.checkResult || '-'); list('review-warnings', (review.warnings || []).map((item) => item.message), 'No review warnings');\n pill('doctor-pill', doctor.failCount ? 'fail' : doctor.warnCount ? 'warning' : 'ok', doctor.passCount + '/' + doctor.totalChecks + ' pass'); renderDoctor(doctor.checks || []);\n text('discovery-count', discoveries.total || 0); renderDiscoveries(discoveries.recent || []); renderCommands(data.commands);\n text('ideas-count', (ideas.totalPending || 0) + ' pending'); renderIdeas(ideas.pending || []); text('history-count', (history.total || 0) + ' released'); renderHistory(history.items || []);\n }\n\n window.__INITIAL_SNAPSHOT__ = null;\n if (window.__INITIAL_SNAPSHOT__) {\n try {\n renderSnapshot(window.__INITIAL_SNAPSHOT__);\n byId('live-dot').classList.add('on');\n text('live-label','Connected \u00B7 live snapshot every 2 seconds');\n } catch (e) {\n console.error('Initial hydration error:', e);\n }\n }\n\n let refreshing = false;\n async function refresh() { if (refreshing) return; refreshing = true; try { const response = await fetch('/api/dashboard'); if (!response.ok) throw new Error('HTTP ' + response.status); renderSnapshot(await response.json()); byId('live-dot').classList.add('on'); text('live-label','Connected \u00B7 live snapshot every 2 seconds'); } catch (error) { byId('live-dot').classList.remove('on'); text('live-label','Disconnected \u00B7 ' + error.message); } finally { refreshing = false; } }\n if (!window.__INITIAL_SNAPSHOT__) { refresh(); }\n setInterval(refresh, 2000);\n </script>\n</body>\n</html>";
2
1
  import type { DashboardSnapshot } from "./dashboard-snapshot.js";
3
2
  import { StudioViewRenderer } from "./studio-view-renderer.js";
3
+ /**
4
+ * Base static HTML template string for web dashboard.
5
+ */
6
+ export declare const DASHBOARD_PAGE_HTML: string;
4
7
  /**
5
8
  * Safely renders the dashboard HTML page with an optional embedded snapshot.
6
9
  * Delegates to StudioViewRenderer.
7
10
  */
8
11
  export declare function renderDashboardPage(snapshot?: DashboardSnapshot | null): string;
9
- export { DASHBOARD_PAGE_HTML, StudioViewRenderer };
12
+ export { StudioViewRenderer };
@@ -1,396 +1,15 @@
1
- const DASHBOARD_PAGE_HTML = `<!doctype html>
2
- <html lang="th">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Nexus-DevFlow Enterprise Dashboard</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link rel="preconnect" href="https://fonts.googleapis.com" />
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Noto+Sans+Thai:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
12
- <style>
13
- :root{
14
- --font-display:"Google Sans Thai","Google Sans","Noto Sans Thai","Space Grotesk",sans-serif;
15
- --font-sans:"Google Sans Thai","Google Sans","Noto Sans Thai",sans-serif;
16
- --font-mono:"IBM Plex Mono",monospace;
17
- --bg:#0a2540;--bg-deep:#061a2e;--panel:rgba(255,255,255,.04);--panel-strong:#0c2b49;
18
- --ink:#edf6ff;--soft:#b7d3ef;--muted:#7598ba;--line:rgba(191,224,255,.17);--line-strong:rgba(191,224,255,.36);
19
- --cyan:#7dd8ff;--mint:#6fe3b4;--gold:#f2c14e;--red:#ff7061;--violet:#b8a4ff;
20
- --mint-soft:rgba(111,227,180,.14);--gold-soft:rgba(242,193,78,.14);--red-soft:rgba(255,112,97,.14);--violet-soft:rgba(184,164,255,.14);
21
- }
22
- *{box-sizing:border-box}
23
- html{color-scheme:dark}
24
- body{margin:0;min-height:100vh;background-color:var(--bg);color:var(--ink);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;background-image:linear-gradient(rgba(125,216,255,.065) 1px,transparent 1px),linear-gradient(90deg,rgba(125,216,255,.065) 1px,transparent 1px),radial-gradient(circle at 8% -5%,rgba(125,216,255,.16),transparent 42rem);background-size:34px 34px,34px 34px,auto}
25
- button,input{font:inherit}
26
- code{font-family:var(--font-mono);color:var(--cyan)}
27
- .sheet{width:min(1180px,calc(100% - 40px));margin:0 auto;padding:34px 0 64px}
28
- .titleblock{display:flex;align-items:flex-end;justify-content:space-between;gap:26px;padding-bottom:20px;border-bottom:1px solid var(--line-strong);flex-wrap:wrap}
29
- .eyebrow,.label{font:600 10px/1 var(--font-mono);letter-spacing:.13em;text-transform:uppercase;color:var(--cyan)}
30
- .eyebrow{display:flex;align-items:center;gap:8px;margin-bottom:12px}.eyebrow:before{content:"";width:20px;height:1px;background:var(--cyan)}
31
- h1{margin:0 0 6px;font:700 clamp(32px,4vw,48px)/1.05 var(--font-display);letter-spacing:-.03em}
32
- .path,.muted{color:var(--muted);font-size:12px;line-height:1.65}.path{font-family:var(--font-mono)}
33
- .badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.badge,.pill{padding:4px 9px;border:1px solid var(--line-strong);border-radius:3px;background:rgba(255,255,255,.025);color:var(--muted);font:600 10px/1.5 var(--font-mono);text-transform:uppercase}
34
- .ok,.healthy,.current,.ready,.idle{border-color:rgba(111,227,180,.42)!important;color:var(--mint)!important;background:var(--mint-soft)!important}
35
- .warning,.available,.offline,.blocked,.fail{border-color:rgba(255,112,97,.42)!important;color:var(--red)!important;background:var(--red-soft)!important}
36
- .active,.deep,.fast,.configured{border-color:rgba(184,164,255,.42)!important;color:var(--violet)!important;background:var(--violet-soft)!important}
37
- .meta{display:grid;grid-template-columns:auto auto;gap:4px 20px;margin:0;font:11px/1.55 var(--font-mono)}.meta dt{color:var(--muted);text-transform:uppercase}.meta dd{margin:0;color:var(--soft);text-align:right}
38
- .live{display:flex;align-items:center;gap:8px;margin:14px 0 24px;font:11px var(--font-mono);color:var(--muted)}.live-dot{width:7px;height:7px;border-radius:50%;background:var(--gold)}.live-dot.on{background:var(--mint);box-shadow:0 0 0 4px rgba(111,227,180,.12)}
39
- .panel,.card{position:relative;min-width:0;border:1px solid var(--line);border-radius:4px;background:var(--panel)}
40
- #dual-track{overflow:hidden}
41
- .stepper{justify-content:center}.step{display:flex;flex-direction:column;align-items:center;text-align:center}.step-icon{display:grid;place-items:center;width:30px;height:30px;margin:10px auto 8px;border:1px solid currentColor;border-radius:8px;background:rgba(255,255,255,.035)}.icon-glyph{display:block;font-size:17px;line-height:1}.step.flow-0,.step.flow-4{color:var(--cyan)}.step.flow-1,.step.flow-5{color:var(--violet)}.step.flow-2,.step.flow-6{color:var(--gold)}.step.flow-3,.step.flow-7{color:var(--mint)}.step .step-name{color:currentColor}.step .step-cmd{color:var(--muted)}
42
- .panel{padding:24px 26px;margin-bottom:20px}.panel-head,.card-head{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}.panel-head{margin-bottom:20px}.card-head{margin-bottom:17px}
43
- .track-tabs{display:flex;gap:7px;flex-wrap:wrap}.tab{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;border:1px solid var(--line-strong);border-radius:99px;background:transparent;color:var(--muted);font:600 10px var(--font-mono);cursor:pointer}.tab:hover,.tab:focus-visible{border-color:var(--cyan);color:var(--cyan);outline:none}.tab.on{background:var(--cyan);border-color:var(--cyan);color:var(--bg-deep)}.tab-active-dot{display:none;padding:2px 6px;border-radius:99px;font:700 8px/1 var(--font-mono);letter-spacing:.05em;text-transform:uppercase;background:var(--mint);color:var(--bg-deep)}.tab.is-active-track .tab-active-dot{display:inline-flex;align-items:center;gap:3px;box-shadow:0 0 0 2px rgba(111,227,180,.25)}.tab.on.is-active-track .tab-active-dot{background:var(--bg-deep);color:var(--mint)}
44
- .track-view{display:none;min-width:0}.track-view.on{display:block}.stepper{display:flex;width:100%;max-width:100%;overflow-x:auto;padding:4px 0 7px}.step{flex:1 1 0;min-width:104px;position:relative;padding-right:12px}.step-line{position:absolute;top:10px;left:21px;right:0;height:1px;background:var(--line-strong)}.step:last-child .step-line{display:none}.step.done .step-line{background:var(--mint)}
45
- .step-dot{position:relative;z-index:1;width:21px;height:21px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--line-strong);background:var(--bg-deep);color:var(--muted);font:700 8px var(--font-mono)}.step.done .step-dot{border-color:var(--mint);color:var(--mint)}.step.active .step-dot{border-color:var(--gold);color:var(--gold);box-shadow:0 0 0 4px rgba(242,193,78,.16)}
46
- .step-name{margin-top:9px;color:var(--soft);font:600 10px/1.35 var(--font-mono)}.step.active .step-name{color:var(--gold)}.step.done .step-name{color:var(--mint)}.step-cmd{margin-top:3px;color:var(--muted);font:9px/1.4 var(--font-mono)}.track-note{margin-top:12px;padding-top:12px;border-top:1px dashed var(--line);color:var(--muted);font:11px/1.65 var(--font-mono)}
47
- .next-action{background:var(--bg-deep);border-color:var(--line-strong)}.next-head{display:flex;justify-content:space-between;gap:12px;align-items:center}.command{margin:8px 0 6px;color:var(--gold);font:700 clamp(22px,3vw,30px)/1.3 var(--font-mono)}.copy{padding:7px 12px;border:1px solid var(--cyan);border-radius:3px;background:transparent;color:var(--cyan);font:600 10px var(--font-mono);cursor:pointer}.copy:hover,.copy:focus-visible{background:var(--cyan);color:var(--bg-deep);outline:none}
48
- .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:1px;margin:0 0 20px;border:1px solid var(--line);border-radius:4px;overflow:hidden;background:var(--line)}.stat{padding:16px 18px;background:var(--bg-deep)}.stat-value{font:700 25px/1 var(--font-display)}.stat-label{margin-top:6px;color:var(--muted);font:10px var(--font-mono);text-transform:uppercase;letter-spacing:.08em}
49
- .grid{display:grid;grid-template-columns:repeat(12,1fr);gap:20px;margin-bottom:20px}.card{grid-column:span 4;min-width:0;padding:clamp(20px,2.4vw,28px)}.card.wide{grid-column:span 8}.card.full{grid-column:1/-1}.value{font:700 18px/1.4 var(--font-display)}
50
- .facts{display:grid;gap:10px}.fact{display:flex;justify-content:space-between;gap:16px;padding-bottom:10px;border-bottom:1px dashed var(--line)}.fact:last-child{padding-bottom:0;border:0}.fact span:first-child{color:var(--muted);font-size:11px}.fact span:last-child{color:var(--soft);font:11px var(--font-mono);text-align:right;overflow-wrap:anywhere}
51
- .progress{height:6px;margin:14px 0 9px;border-radius:99px;background:rgba(255,255,255,.07);overflow:hidden}.progress span{display:block;height:100%;width:0;background:var(--gold);transition:width .35s ease}
52
- ul{list-style:none;margin:0;padding:0}li{padding:10px 0;border-bottom:1px dashed var(--line);color:var(--soft);font-size:12px;line-height:1.55}li:last-child{border:0}.empty{color:var(--muted)}
53
- .severity{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.sev{padding:9px 6px;border:1px solid var(--line);border-radius:3px;text-align:center}.sev b{display:block;font:700 17px var(--font-display)}.sev small{color:var(--muted);font:9px var(--font-mono)}
54
- .commands{display:flex;flex-wrap:wrap;gap:10px}.cmd{position:relative;padding:8px 13px;border:1px solid var(--line-strong);border-radius:3px;background:rgba(255,255,255,.025);color:var(--soft);font:11px var(--font-mono);cursor:pointer}.cmd:hover,.cmd:focus-visible,.cmd:focus{border-color:var(--cyan);color:var(--cyan);outline:2px solid rgba(125,216,255,.2);outline-offset:2px}.cmd:after{content:attr(data-tip);position:absolute;z-index:30;left:50%;bottom:calc(100% + 10px);width:max-content;max-width:min(300px,80vw);padding:9px 11px;border:1px solid var(--line-strong);border-radius:3px;background:#031421;color:var(--soft);font:10px/1.5 var(--font-mono);white-space:normal;text-align:left;opacity:0;visibility:hidden;transform:translate(-50%,5px);box-shadow:0 12px 30px rgba(0,0,0,.4);transition:.15s;pointer-events:none}.cmd:hover:after,.cmd:focus-visible:after,.cmd:focus:after{opacity:1;visibility:visible;transform:translate(-50%,0)}
55
- .adapters,.roster-grid,.mcp-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.adapter,.roster-card,.mcp-card{padding:15px;border:1px solid var(--line);border-radius:4px;background:rgba(255,255,255,.02)}.adapter strong,.roster-card strong,.mcp-card strong{display:block;margin-bottom:6px;text-transform:capitalize}.adapter-path,.roster-desc,.mcp-desc{color:var(--muted);font:10px var(--font-mono);line-height:1.4}.adapter-state{margin-top:9px;font:10px var(--font-mono)}
56
- .mcp-grid{grid-template-columns:repeat(3,1fr)}
57
- .search-row{display:flex;gap:10px;margin-bottom:16px}.search-input{flex:1;padding:10px 14px;background:var(--bg-deep);border:1px solid var(--line-strong);border-radius:4px;color:var(--ink);font-family:var(--font-mono);font-size:12px}.search-btn{padding:10px 18px;border:1px solid var(--cyan);border-radius:4px;background:var(--cyan);color:var(--bg-deep);font-weight:700;cursor:pointer}
58
- .blast-result{padding:16px;border:1px solid var(--line-strong);border-radius:4px;background:var(--bg-deep);margin-top:12px;display:none}
59
- .section-note{margin:-2px 0 14px;color:var(--muted);font:10px/1.5 var(--font-mono)}.record-list{display:grid;gap:8px}.record-item{display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:13px;align-items:center;padding:13px 15px;border:1px solid var(--line);border-left:3px solid var(--record-color,var(--cyan));border-radius:3px;background:rgba(255,255,255,.02)}.record-icon{display:grid;place-items:center;width:31px;height:31px;border-radius:8px;background:color-mix(in srgb,var(--record-color,var(--cyan)) 14%,transparent)}.record-body{min-width:0}.record-meta{color:var(--record-color,var(--cyan));font:700 10px var(--font-mono);letter-spacing:.06em;text-transform:uppercase}.record-title{margin-top:3px;color:var(--ink);font-weight:650}.record-desc{margin-top:3px;color:var(--soft);font-size:12px}.record-desc:empty{display:none}.record-title strong,.record-desc strong{color:var(--record-color,var(--cyan));font-weight:750}.record-title em,.record-desc em{color:var(--gold)}.record-title code,.record-desc code{padding:1px 4px;border:1px solid var(--line);border-radius:3px;color:var(--cyan);font:10px var(--font-mono)}.record-title a,.record-desc a{color:var(--cyan);text-decoration:underline}.record-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.record-action{border:1px solid var(--record-color,var(--cyan));border-radius:3px;padding:8px 11px;background:transparent;color:var(--record-color,var(--cyan));font:700 10px var(--font-mono);cursor:pointer}.record-action:hover,.record-action:focus-visible{background:var(--record-color,var(--cyan));color:var(--bg)}.record-badge{align-self:start;border:1px solid rgba(111,227,180,.36);border-radius:99px;padding:4px 8px;background:var(--mint-soft);color:var(--mint);font:700 9px var(--font-mono);letter-spacing:.04em;text-transform:uppercase}.record-item.tone-cyan{--record-color:var(--cyan)}.record-item.tone-mint{--record-color:var(--mint)}.record-item.tone-gold{--record-color:var(--gold)}.record-item.tone-red{--record-color:var(--red)}.record-item.tone-violet{--record-color:var(--violet)}.stepper{gap:0;justify-content:center}.step{padding-right:0;overflow:visible}.step-line{left:calc(50% + 10px);right:calc(-50% + 10px);width:auto}#sev-p0{color:var(--red)}#sev-p1{color:var(--gold)}#sev-p2{color:var(--violet)}#sev-p3{color:var(--cyan)}#stat-runs.tone{color:var(--mint)}#stat-findings.tone-ok{color:var(--mint)}#stat-findings.tone-alert{color:var(--red)}#stat-ideas.tone-ok{color:var(--mint)}#stat-ideas.tone-pending{color:var(--gold)}#stat-adapters.tone-ok{color:var(--mint)}#stat-adapters.tone-partial{color:var(--gold)}#stat-state.tone-idle{color:var(--cyan)}#stat-state.tone-active{color:var(--violet)}#stat-state.tone-blocked{color:var(--red)}
60
- .multitask-panel{border-color:rgba(56,189,248,.35)}.workspace-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:14px;margin-top:10px}.workspace-card{background:rgba(255,255,255,.025);border:1px solid var(--line);border-radius:6px;padding:16px;display:flex;flex-direction:column;gap:8px;transition:all .2s ease}.workspace-card:hover{border-color:var(--cyan);background:rgba(255,255,255,.04)}.ws-header{display:flex;align-items:center;justify-content:space-between;gap:8px}.ws-id{font-family:var(--font-mono);font-weight:700;font-size:11px;color:var(--cyan);background:var(--cyan-soft);padding:2px 8px;border-radius:3px}.ws-stage{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;padding:2px 6px;border-radius:3px;background:var(--gold-soft);color:var(--gold);border:1px solid rgba(242,193,78,.3)}.ws-stage.stage-check{background:var(--mint-soft);color:var(--mint);border-color:rgba(111,227,180,.3)}.ws-title{font-weight:650;font-size:13px;color:var(--ink);line-height:1.4}.ws-progress-bar{height:6px;background:rgba(0,0,0,.3);border-radius:99px;overflow:hidden;margin:4px 0}.ws-progress-fill{height:100%;background:linear-gradient(135deg,var(--cyan),var(--violet));transition:width .3s}.ws-meta{display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--muted)}.ws-actions{display:flex;align-items:center;gap:6px;padding-top:8px;border-top:1px dashed var(--line);flex-wrap:wrap}.btn-ws{padding:4px 9px;border-radius:3px;font-family:var(--font-mono);font-size:10px;font-weight:700;background:rgba(255,255,255,.04);border:1px solid var(--line);color:var(--soft);cursor:pointer;transition:all .15s}.btn-ws:hover{background:var(--cyan-soft);border-color:var(--cyan);color:var(--cyan)}
61
- footer{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-top:26px;padding-top:17px;border-top:1px solid var(--line);color:var(--muted);font:10px var(--font-mono)}
62
- @media(max-width:920px){.grid .card,.grid .card.wide{grid-column:1/-1}.stats{grid-template-columns:repeat(2,1fr)}.adapters,.roster-grid,.mcp-grid{grid-template-columns:repeat(2,1fr)}.titleblock{align-items:flex-start;flex-direction:column}.meta dd{text-align:left}.stepper{gap:0}.cmd:after{position:fixed;left:12px;right:12px;bottom:16px;width:auto;max-width:none;transform:translateY(5px)}.cmd:hover:after,.cmd:focus-visible:after,.cmd:focus:after{transform:none}}
63
- @media(max-width:560px){.sheet{width:calc(100% - 24px);padding-top:22px}.panel,.card{padding:18px}.grid{gap:14px;margin-bottom:14px}.stats{margin-bottom:14px}.adapters,.roster-grid,.mcp-grid{grid-template-columns:1fr}.cmd:after{position:fixed;left:12px;right:12px;bottom:16px;width:auto;max-width:none;transform:translateY(5px)}.cmd:hover:after,.cmd:focus-visible:after,.cmd:focus:after{transform:none}.meta{grid-template-columns:1fr}.track-tabs{width:100%}.tab{flex:1}.fact{align-items:flex-start}.record-item{grid-template-columns:34px minmax(0,1fr)}.record-action{grid-column:2;justify-self:start}}
64
- @media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.001ms!important;transition-duration:.001ms!important}}
65
- </style>
66
- </head>
67
- <body>
68
- <main class="sheet">
69
- <header class="titleblock">
70
- <div>
71
- <div class="eyebrow">Nexus-DevFlow · Enterprise Workspace Status</div>
72
- <h1 id="project-name">Nexus-DevFlow</h1>
73
- <div class="path" id="project-path">Connecting...</div>
74
- <div class="badges">
75
- <span class="badge" id="version-badge">v2.5.0</span>
76
- <span class="badge" id="health-badge">health</span>
77
- <span class="badge" id="gate-badge">Gate: checking</span>
78
- <span class="badge" id="drift-badge">Drift: checking</span>
79
- <span class="badge" id="track-badge">track</span>
80
- </div>
81
- </div>
82
- <dl class="meta">
83
- <dt>Package</dt><dd id="package-name">@jakkrichm/create-nexus-devflow</dd>
84
- <dt>Adapters</dt><dd id="adapter-count">-</dd>
85
- <dt>Branch</dt><dd id="header-branch">-</dd>
86
- <dt>Snapshot</dt><dd id="generated-at">-</dd>
87
- </dl>
88
- </header>
89
- <div class="live"><span class="live-dot" id="live-dot"></span><span id="live-label">Connecting to local dashboard...</span></div>
90
-
91
- <section class="panel" id="dual-track">
92
- <div class="panel-head">
93
- <span class="label">Unified Living Spec Model (DevFlow 2.5.0)</span>
94
- <div class="track-tabs" role="tablist" aria-label="Delivery tracks">
95
- <button class="tab on" id="tab-preflight" role="tab" aria-selected="true" aria-controls="view-preflight" data-track="preflight"><span>🔮 Pre-Flight Discovery</span><span class="tab-active-dot">● ACTIVE</span></button>
96
- <button class="tab" id="tab-fast" role="tab" aria-selected="false" aria-controls="view-fast" data-track="fast"><span>⚡ Living Spec · 4 steps</span><span class="tab-active-dot">● ACTIVE</span></button>
97
- <button class="tab" id="tab-swarm" role="tab" aria-selected="false" aria-controls="view-swarm" data-track="swarm"><span>🤖 Multi-Agent Swarm</span></button>
98
- <button class="tab" id="tab-graph" role="tab" aria-selected="false" aria-controls="view-graph" data-track="graph"><span>🗺️ Code Graph</span></button>
99
- </div>
100
- </div>
101
- <div class="track-view on" id="view-preflight" role="tabpanel" aria-labelledby="tab-preflight">
102
- <div class="stepper" id="pipeline-preflight"></div>
103
- <div class="track-note" id="note-preflight">Pre-Flight Discovery & Architectural Alignment (/idea, /grill, /brainstorm, /discovery).</div>
104
- </div>
105
- <div class="track-view" id="view-fast" role="tabpanel" aria-labelledby="tab-fast">
106
- <div class="stepper" id="pipeline-fast"></div>
107
- <div class="track-note" id="note-fast">Single Living Spec (/feature, /implement, /check, /complete) with TDD Discipline & QA Matrix.</div>
108
- </div>
109
- <div class="track-view" id="view-swarm" role="tabpanel" aria-labelledby="tab-swarm">
110
- <div class="section-note">Role-based specialized AI Subagents roster & execution matrix</div>
111
- <div class="roster-grid" id="swarm-roster"></div>
112
- </div>
113
- <div class="track-view" id="view-graph" role="tabpanel" aria-labelledby="tab-graph">
114
- <div class="section-note">Semantic dependency graph & instant Blast Radius analyzer</div>
115
- <div class="search-row">
116
- <input class="search-input" id="graph-file-input" placeholder="Type file path (e.g. packages/create-nexus-devflow/lib/code-graph.ts)..." />
117
- <button class="search-btn" id="graph-search-btn" type="button">Analyze Impact</button>
118
- </div>
119
- <div class="blast-result" id="blast-result"></div>
120
- </div>
121
- </section>
122
-
123
- <section class="panel next-action" id="next-panel">
124
- <div class="next-head"><span class="label">Next Action</span><button class="copy" id="next-copy" type="button">Copy command</button></div>
125
- <div class="command" id="next-command" aria-live="polite">Loading...</div>
126
- <div class="muted" id="next-reason"></div>
127
- </section>
128
-
129
- <section class="stats" aria-label="Workspace summary">
130
- <div class="stat"><div class="stat-value" id="stat-runs">-</div><div class="stat-label">Runs released</div></div>
131
- <div class="stat"><div class="stat-value" id="stat-findings">-</div><div class="stat-label">Active findings</div></div>
132
- <div class="stat"><div class="stat-value" id="stat-ideas">-</div><div class="stat-label">Pending ideas</div></div>
133
- <div class="stat"><div class="stat-value" id="stat-mcp">-</div><div class="stat-label">MCP Tools Ready</div></div>
134
- <div class="stat"><div class="stat-value" id="stat-state">-</div><div class="stat-label">Current state</div></div>
135
- </section>
136
-
137
- <!-- Multi-Task Living Spec Workspaces Grid -->
138
- <section class="card full panel multitask-panel" id="active-workspaces-panel">
139
- <div class="panel-head">
140
- <div>
141
- <span class="label">Active Living Specs (Pure Task-Isolated Workspaces)</span>
142
- <div class="muted" id="active-workspaces-summary" style="margin-top:3px;font-size:11px;">Scanned from devflow/context/{xxx-slug}/</div>
143
- </div>
144
- <span class="pill active" id="active-workspaces-pill">0 Active</span>
145
- </div>
146
- <div class="workspace-grid" id="active-workspaces-list"></div>
147
- </section>
148
-
149
- <section class="grid">
150
- <article class="card"><div class="card-head"><span class="label">Nexus-DevFlow</span><span class="pill" id="update-pill">checking</span></div><div class="facts"><div class="fact"><span>Installed / latest</span><span id="installed-latest">-</span></div><div class="fact"><span>Health</span><span id="system-health">-</span></div><div class="fact"><span>Architecture</span><span>3-Pillars + Swarm RAG</span></div></div></article>
151
- <article class="card wide"><div class="card-head"><span class="label">Current Work</span><span class="pill" id="work-pill">loading</span></div><div class="value" id="work-title">Loading...</div><div class="progress"><span id="work-progress"></span></div><div class="muted" id="work-meta"></div></article>
152
- <article class="card"><div class="card-head"><span class="label">Git & Drift</span><span class="pill" id="git-pill">loading</span></div><div class="facts"><div class="fact"><span>Branch</span><span id="git-branch">-</span></div><div class="fact"><span>Working tree</span><span id="git-changed">-</span></div><div class="fact"><span>Drift Status</span><span id="git-drift">-</span></div><div class="fact"><span>Last commit</span><span id="git-commit">-</span></div></div></article>
153
- <article class="card"><div class="card-head"><span class="label">Findings Ledger</span><span class="value" id="findings-count">-</span></div><div class="severity"><div class="sev"><b id="sev-p0">0</b><small>P0</small></div><div class="sev"><b id="sev-p1">0</b><small>P1</small></div><div class="sev"><b id="sev-p2">0</b><small>P2</small></div><div class="sev"><b id="sev-p3">0</b><small>P3</small></div></div><ul id="findings-list"></ul></article>
154
- <article class="card"><div class="card-head"><span class="label">Completion & Gate</span><span class="pill" id="completion-pill">-</span></div><ul id="completion-list"></ul></article>
155
- <article class="card"><div class="card-head"><span class="label">Independent Review</span><span class="pill" id="review-state">none</span></div><div class="facts"><div class="fact"><span>Verdict</span><span id="review-verdict">-</span></div><div class="fact"><span>Freshness</span><span id="review-freshness">-</span></div><div class="fact"><span>Check</span><span id="review-check">-</span></div></div><ul id="review-warnings"></ul></article>
156
- <article class="card"><div class="card-head"><span class="label">Warnings</span><span class="value" id="warnings-count">-</span></div><ul id="warnings-list"></ul></article>
157
- </section>
158
-
159
- <section class="card full panel"><div class="card-head"><span class="label">Model Context Protocol (MCP) Tools Hub</span><span class="value" id="mcp-count">12 Tools</span></div><div class="section-note">Typed JSON-RPC tools exposed to AI Coding Assistants</div><div class="mcp-grid" id="mcp-list"></div></section>
160
- <section class="card full panel"><div class="card-head"><span class="label">Quick Commands</span><span class="muted">hover or focus for help · click to copy</span></div><div class="commands" id="command-list"></div></section>
161
- <section class="grid"><article class="card full"><div class="card-head"><span class="label">Doctor · Health Check</span><span class="pill" id="doctor-pill">-</span></div><div class="record-list" id="doctor-list"></div></article></section>
162
- <section class="card full panel"><div class="card-head"><span class="label">Discoveries</span><span class="value" id="discovery-count">-</span></div><div class="record-list" id="discovery-list"></div></section>
163
- <section class="card full panel"><div class="card-head"><span class="label">Idea Inbox & Backlog</span><span class="value" id="ideas-count">-</span></div><div class="section-note">From devflow/ideas.md · start a selected item with /feature IDEA-xxx</div><div class="record-list" id="ideas-list"></div></section>
164
- <section class="card full panel"><div class="card-head"><span class="label">Completed Work Archive</span><span class="value" id="history-count">-</span></div><div class="section-note">Categorized delivery history · features, fixes and rollbacks</div><div class="record-list" id="history-list"></div></section>
165
- <footer><span>Nexus-DevFlow 2.5.0 Enterprise Dashboard · refreshes every 2 seconds</span><span id="footer-revision">snapshot pending</span></footer>
166
- </main>
167
- <script>
168
- const byId = (id) => document.getElementById(id);
169
- const escapeHtml = (str) => String(str == null ? '' : str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
170
- const text = (id, value) => { const node = byId(id); if (node) node.textContent = value == null ? '-' : String(value); };
171
- const pill = (id, state, label) => { const node = byId(id); if (!node) return; node.className = 'pill ' + (state || 'configured'); node.textContent = label || state || '-'; };
172
- const list = (id, values, empty) => { const node = byId(id); if (!node) return; node.replaceChildren(); const rows = values.length ? values : [empty]; rows.forEach((value) => { const li = document.createElement('li'); li.textContent = value; if (!values.length) li.className = 'empty'; node.append(li); }); };
173
- const titleCase = (value) => String(value || '').replaceAll('_',' ').replace(/\b\w/g, (c) => c.toUpperCase());
174
- const FLOW_ICON_NAMES = ['target','code','shield','package'];
175
- const EMOJI_ICONS = { compass:'🔎',discovery:'🔎',target:'🎯',document:'📄',list:'🧩',code:'⚙️',shield:'🧪',report:'📊',package:'📦',bulb:'💡',check:'✅',wrench:'🛠️',rollback:'↩️',search:'🔍',alert:'⚠️',grill:'🔥',brainstorm:'💭',idea:'💡' };
176
- function iconImage(name) { const icon = document.createElement('span'); icon.className = 'icon-glyph'; icon.setAttribute('aria-hidden','true'); icon.textContent = EMOJI_ICONS[name] || '📄'; return icon; }
177
- function decoratePipeline(id) { byId(id)?.querySelectorAll('.step').forEach((step,index) => { step.classList.add('flow-' + index); const icon = document.createElement('span'); icon.className = 'step-icon'; icon.setAttribute('aria-hidden','true'); const cmd = step.querySelector('.step-cmd')?.textContent?.replace('/','').split(' ')[0] || ''; const iconKey = id.includes('preflight') ? (cmd || ['idea','grill','brainstorm','discovery'][index] || 'search') : (FLOW_ICON_NAMES[index] || 'document'); icon.append(iconImage(iconKey)); const name = step.querySelector('.step-name'); step.insertBefore(icon,name || null); }); }
178
- function richText(root,value) { const source = String(value || ''); let cursor = 0; const plain = (end) => { if (end > cursor) root.append(document.createTextNode(source.slice(cursor,end))); cursor = end; }; while (cursor < source.length) { if (source.startsWith('**',cursor)) { const end = source.indexOf('**',cursor + 2); if (end > cursor + 2) { const node = document.createElement('strong'); node.textContent = source.slice(cursor + 2,end); root.append(node); cursor = end + 2; continue; } } if (source[cursor] === '*') { const end = source.indexOf('*',cursor + 1); if (end > cursor + 1) { const node = document.createElement('em'); node.textContent = source.slice(cursor + 1,end); root.append(node); cursor = end + 1; continue; } } if (source.charCodeAt(cursor) === 96) { const end = source.indexOf(String.fromCharCode(96),cursor + 1); if (end > cursor + 1) { const node = document.createElement('code'); node.textContent = source.slice(cursor + 1,end); root.append(node); cursor = end + 1; continue; } } const next = [source.indexOf('**',cursor + 1),source.indexOf('*',cursor + 1),source.indexOf(String.fromCharCode(96),cursor + 1)].filter((index) => index >= 0).sort((a,b) => a-b)[0] ?? source.length; plain(next > cursor ? next : cursor + 1); } }
179
- function recordCard(options) { const card = document.createElement('article'); card.className = 'record-item tone-' + (options.tone || 'cyan'); const icon = document.createElement('span'); icon.className = 'record-icon'; icon.append(iconImage(options.icon || 'document')); const body = document.createElement('div'); body.className = 'record-body'; const meta = document.createElement('div'); meta.className = 'record-meta'; richText(meta,options.meta); const title = document.createElement('div'); title.className = 'record-title'; richText(title,options.title); const desc = document.createElement('div'); desc.className = 'record-desc'; richText(desc,options.description); body.append(meta,title,desc); card.append(icon,body); if (options.badge) { const badge = document.createElement('span'); badge.className = 'record-badge'; badge.textContent = options.badge; card.append(badge); } return card; }
180
- function renderRecords(id,items,empty,mapper) { const root = byId(id); root.replaceChildren(); if (!items?.length) { const node = document.createElement('div'); node.className = 'empty'; node.textContent = empty; root.append(node); return; } items.forEach((item,index) => root.append(recordCard(mapper(item,index)))); }
181
- function renderIdeas(items) { renderRecords('ideas-list',items,'No pending ideas',(item) => ({ icon:'bulb',tone:'gold',meta:item.id,title:item.title,description:(item.description || '⚡') + ' · ' + (item.feasibility || 'Not scored') })); }
182
- function renderHistory(items) { const map = { feature:{icon:'package',tone:'cyan'},fix:{icon:'wrench',tone:'gold'},rollback:{icon:'rollback',tone:'red'} }; renderRecords('history-list',(items || []).slice(0,10),'No completed work in history archive',(item) => { const type = String(item.type || 'other').toLowerCase(); const style = map[type] || {icon:'report',tone:'violet'}; return { ...style,meta:type + ' ' + (item.buildPlanItem || ''),title:item.title,description:'',badge:'Released' }; }); }
183
- function renderDoctor(items) { renderRecords('doctor-list',items,'No doctor checks available',(item) => ({ icon:item.status === 'pass' ? 'check' : 'alert',tone:item.status === 'pass' ? 'mint' : item.status === 'warn' ? 'gold' : 'red',meta:item.status,title:item.name,description:item.message })); }
184
- function renderDiscoveries(items) { renderRecords('discovery-list',items,'No discoveries recorded',(item) => ({ icon:'search',tone:'mint',meta:item.id,title:item.title,description:'',badge:'Decision: ' + (item.decision || 'Pending') })); }
185
-
186
- function renderPipeline(id, nodes) {
187
- const root = byId(id); root.replaceChildren();
188
- (nodes || []).forEach((node, index) => {
189
- const step = document.createElement('div'); step.className = 'step ' + node.state;
190
- const line = document.createElement('span'); line.className = 'step-line';
191
- const dot = document.createElement('span'); dot.className = 'step-dot'; dot.textContent = node.state === 'done' ? '✓' : String(index + 1).padStart(nodes.length > 4 ? 2 : 1, '0');
192
- const name = document.createElement('div'); name.className = 'step-name'; name.textContent = node.label;
193
- const command = document.createElement('div'); command.className = 'step-cmd'; command.textContent = node.command;
194
- step.append(line, dot, name, command); root.append(step);
195
- });
196
- }
197
-
198
- let userHasSwitchedTab = false;
199
- function selectTrack(track) {
200
- document.querySelectorAll('.tab').forEach((node) => { const on = node.dataset.track === track; node.classList.toggle('on', on); node.setAttribute('aria-selected', String(on)); });
201
- document.querySelectorAll('.track-view').forEach((node) => node.classList.toggle('on', node.id === 'view-' + track));
202
- }
203
-
204
- function wireCopy(button, getValue) {
205
- button.addEventListener('click', async () => {
206
- const original = button.textContent;
207
- button.textContent = 'Copied!';
208
- try { await navigator.clipboard.writeText(getValue()); }
209
- catch { button.textContent = 'Copy unavailable'; }
210
- setTimeout(() => { button.textContent = original; }, 1100);
211
- });
212
- }
213
-
214
- document.querySelectorAll('.tab').forEach((node) => node.addEventListener('click', () => { userHasSwitchedTab = true; selectTrack(node.dataset.track); }));
215
- wireCopy(byId('next-copy'), () => byId('next-command').textContent.trim());
216
-
217
- function renderCommands(commands) {
218
- const root = byId('command-list'); root.replaceChildren();
219
- const allCommands = [...(commands || [])];
220
- ['nexus-devflow check-gate', 'nexus-devflow drift', 'nexus-devflow reconcile --fix', 'nexus-devflow slice --stage implement', 'nexus-devflow swarm', 'nexus-devflow studio'].forEach(c => {
221
- if (!allCommands.some(item => item.command === c)) {
222
- allCommands.push({ name: c.replace('nexus-devflow ', ''), command: c, family: 'companion', description: 'Run ' + c });
223
- }
224
- });
225
- allCommands.forEach((item) => {
226
- const button = document.createElement('button'); button.type = 'button'; button.className = 'cmd'; button.textContent = item.name; button.dataset.tip = item.description; wireCopy(button, () => item.command); root.append(button);
227
- });
228
- }
229
-
230
- function renderSwarm(swarm) {
231
- const root = byId('swarm-roster'); if (!root) return; root.replaceChildren();
232
- (swarm?.agentRoster || []).forEach(agent => {
233
- const card = document.createElement('div'); card.className = 'roster-card';
234
- card.innerHTML = '<strong>' + agent.avatar + ' ' + agent.name + '</strong><div class="roster-desc">' + agent.responsibility + '</div>';
235
- root.append(card);
236
- });
237
- }
238
-
239
- function renderMcp(tools) {
240
- const root = byId('mcp-list'); if (!root) return; root.replaceChildren();
241
- (tools || []).forEach(t => {
242
- const card = document.createElement('div'); card.className = 'mcp-card';
243
- card.innerHTML = '<strong>⚙️ ' + t.name + '</strong><div class="mcp-desc">' + t.description + '</div>';
244
- root.append(card);
245
- });
246
- }
247
-
248
- byId('graph-search-btn')?.addEventListener('click', async () => {
249
- const input = byId('graph-file-input'); const res = byId('blast-result');
250
- if (!input || !res) return;
251
- const file = input.value.trim();
252
- if (!file) return;
253
- res.style.display = 'block'; res.innerHTML = 'Analyzing Blast Radius...';
254
- try {
255
- const resp = await fetch('/api/graph?file=' + encodeURIComponent(file));
256
- const data = await resp.json();
257
- if (data.error) { res.innerHTML = '<span style="color:var(--red)">' + data.error + '</span>'; return; }
258
- let html = '<strong>Impact Score: <span style="color:var(--gold)">' + data.impactScore + '</span> | Total Affected: ' + data.totalAffected + ' files</strong><br/><br/>';
259
- if (data.directDependents?.length) {
260
- html += '<strong>Direct Dependents:</strong><ul>' + data.directDependents.map(d => '<li>' + d + '</li>').join('') + '</ul>';
261
- }
262
- if (data.transitiveDependents?.length) {
263
- html += '<br/><strong>Transitive Dependents:</strong><ul>' + data.transitiveDependents.map(t => '<li>' + t + '</li>').join('') + '</ul>';
264
- }
265
- res.innerHTML = html;
266
- } catch (e) {
267
- res.innerHTML = '<span style="color:var(--red)">Error: ' + e.message + '</span>';
268
- }
269
- });
270
-
271
- function renderActiveWorkspaces(activeRuns, work) {
272
- const root = byId('active-workspaces-list');
273
- if (!root) return;
274
- root.replaceChildren();
275
- const runs = (activeRuns && activeRuns.length > 0) ? activeRuns : (work && work.state === 'active' ? [{
276
- runId: work.runId || 'ACTIVE',
277
- title: work.title || 'Active Spec',
278
- status: work.status || 'implementing',
279
- branch: 'current',
280
- totalTasks: work.total || 0,
281
- completedTasks: work.completed || 0,
282
- remainingTasks: work.remaining || 0,
283
- hasOpenFindings: false
284
- }] : []);
285
-
286
- pill('active-workspaces-pill', runs.length > 0 ? 'active' : 'idle', runs.length + ' Active');
287
- text('active-workspaces-summary', runs.length > 0 ? 'Found ' + runs.length + ' active workspace(s) in devflow/context/' : 'No active workspace folders. Run /feature or /fix to start.');
288
-
289
- if (runs.length === 0) {
290
- const empty = document.createElement('div');
291
- empty.className = 'empty';
292
- empty.style.padding = '18px';
293
- empty.textContent = 'No active delivery runs in progress. Use /feature or /fix to create a living spec.';
294
- root.append(empty);
295
- return;
296
- }
297
-
298
- runs.forEach((run) => {
299
- const card = document.createElement('article');
300
- card.className = 'workspace-card';
301
- const review = run.review || { state: 'none', freshness: 'not-applicable', verdict: null, checkResult: null };
302
- const pct = run.totalTasks > 0 ? Math.round((run.completedTasks / run.totalTasks) * 100) : 0;
303
- const stageClass = (run.status || '').includes('check') ? 'ws-stage stage-check' : 'ws-stage';
304
- const findingTag = run.hasOpenFindings ? '<span style="color:var(--red);font-size:10px;font-weight:700;">⚠ Open Findings</span>' : '<span style="color:var(--mint);font-size:10px;font-weight:700;">✔ 0 Blockers</span>';
305
-
306
- card.innerHTML = '<div class="ws-header">' +
307
- '<span class="ws-id">' + escapeHtml(run.runId) + '</span>' +
308
- '<span class="' + stageClass + '">Stage: ' + escapeHtml(run.status || 'active') + '</span>' +
309
- '</div>' +
310
- '<div class="ws-title">' + escapeHtml(run.title || 'Living Spec') + '</div>' +
311
- '<div class="ws-progress-bar"><div class="ws-progress-fill" style="width:' + pct + '%;"></div></div>' +
312
- '<div class="ws-meta">' +
313
- '<span>🌿 <code>' + escapeHtml(run.branch || 'main') + '</code></span>' +
314
- '<span><strong>' + pct + '%</strong> (' + run.completedTasks + '/' + run.totalTasks + ' Tasks)</span>' +
315
- '</div>' +
316
- '<div style="display:flex;justify-content:space-between;align-items:center;margin-top:2px;">' +
317
- findingTag +
318
- '<span style="font-size:10px;color:var(--muted);">' + run.remainingTasks + ' remaining</span>' +
319
- '</div>' +
320
- '<div style="font-size:10px;color:var(--muted);margin-top:6px;">Review: <strong>' + escapeHtml(review.verdict || review.state) + '</strong> · ' + escapeHtml(review.freshness) + '</div>' +
321
- '<div class="ws-actions">' +
322
- '<button class="btn-ws" type="button" data-cmd="/implement ' + escapeHtml(run.runId) + '">▶ /implement</button>' +
323
- '<button class="btn-ws" type="button" data-cmd="/check ' + escapeHtml(run.runId) + '">🧪 /check</button>' +
324
- '<button class="btn-ws" type="button" data-cmd="/complete ' + escapeHtml(run.runId) + '">📦 /complete</button>' +
325
- '</div>';
326
-
327
- card.querySelectorAll('.btn-ws').forEach(btn => {
328
- wireCopy(btn, () => btn.getAttribute('data-cmd'));
329
- });
330
- root.append(card);
331
- });
332
- }
333
-
334
- function renderSnapshot(data) {
335
- const status = data.status || {}; const project = status.project || {}; const devflow = status.devflow || {}; const work = status.currentWork || {}; const git = status.git || {}; const findings = status.findings || { active: [], blockers: [] }; const review = status.review || { state: 'none', freshness: 'not-applicable', verdict: null, checkResult: null, warnings: [] }; const workflow = data.workflow || {}; const update = data.update || {}; const doctor = data.doctor || { checks: [] }; const discoveries = data.discoveries || { recent: [] }; const history = data.history || { items: [], total: 0 }; const ideas = status.ideas || { pending: [], totalPending: 0 }; const gate = data.gatekeeper || { passed: true }; const drift = data.drift || { hasDrift: false };
336
-
337
- text('project-name', project.name || 'Nexus-DevFlow'); text('project-path', project.root || ''); text('version-badge', 'v' + (devflow.version || '2.5.0'));
338
- pill('health-badge', status.health, status.health === 'ok' ? 'Health OK' : 'Health warning');
339
- pill('gate-badge', gate.passed ? 'ok' : 'blocked', gate.passed ? '✔ Gate Passed' : '✖ Gate Blocked');
340
- pill('drift-badge', !drift.hasDrift ? 'ok' : 'warning', !drift.hasDrift ? '✔ In Sync' : '⚠ Drift Detected');
341
- pill('track-badge', workflow.track, 'Track ' + titleCase(workflow.track || 'idle'));
342
-
343
- text('adapter-count', (devflow.adapters || []).length + ' configured'); text('header-branch', git.branch || 'unknown'); text('generated-at', new Date(data.generatedAt).toLocaleTimeString()); text('footer-revision', 'snapshot ' + data.generatedAt);
344
- renderPipeline('pipeline-preflight', workflow.deep || workflow.preflight); renderPipeline('pipeline-fast', workflow.fast); decoratePipeline('pipeline-preflight'); decoratePipeline('pipeline-fast');
345
- if (!userHasSwitchedTab) {
346
- if (work.state === 'active' || workflow.track === 'fast') {
347
- selectTrack('fast');
348
- } else {
349
- selectTrack('preflight');
350
- }
351
- }
352
- renderSwarm(data.swarm); renderMcp(data.mcpTools); text('stat-mcp', (data.mcpTools || []).length + ' Ready');
353
- renderActiveWorkspaces(status.activeRuns, work);
354
-
355
- const nextCommand = data.nextAction?.command || status.nextAction?.command || '/feature'; text('next-command', nextCommand); text('next-reason', data.nextAction?.reason || 'Ready for new work.');
356
- text('stat-runs', history.total || 0); text('stat-findings', (findings.active || []).length); text('stat-ideas', ideas.totalPending || 0); text('stat-state', titleCase(work.state || 'idle'));
357
- pill('update-pill', update.state, update.state === 'current' ? 'up to date' : update.state); text('installed-latest', (update.installedVersion || '2.5.0') + ' / ' + (update.latestVersion || '2.5.0')); text('system-health', status.health || 'ok');
358
- pill('work-pill', work.state, work.state || 'idle'); text('work-title', work.title || 'No active delivery run'); const pct = work.total > 0 ? Math.round((work.completed / work.total) * 100) : 0; byId('work-progress').style.width = pct + '%'; text('work-meta', work.state === 'active' ? work.completed + ' of ' + work.total + ' steps completed' : 'Ready');
359
- pill('git-pill', git.clean ? 'ok' : 'warning', git.clean ? 'clean' : (git.changedFiles || 0) + ' changed'); text('git-branch', git.branch || 'unknown'); text('git-changed', git.clean ? 'clean' : (git.changedFiles || 0) + ' changed files'); text('git-drift', drift.clean ? 'In Sync' : 'Drift detected'); text('git-commit', git.lastCommit || '-');
360
- text('findings-count', findings.total || 0); ['P0','P1','P2','P3'].forEach((severity) => text('sev-' + severity.toLowerCase(), (findings.active || []).filter((item) => item.severity === severity).length)); list('findings-list', (findings.active || []).map((item) => item.id + ' · ' + item.title), 'No active findings');
361
- pill('completion-pill', status.completion?.state, status.completion?.state || 'unknown'); list('completion-list', status.completion?.blockers || [], work.state === 'idle' ? 'No active run to complete' : 'All readiness checks passed'); text('warnings-count', (status.warnings || []).length); list('warnings-list', (status.warnings || []).map((item) => item.message), 'No active warnings or drift');
362
- pill('review-state', review.state, review.state); text('review-verdict', review.verdict || '-'); text('review-freshness', review.freshness); text('review-check', review.checkResult || '-'); list('review-warnings', (review.warnings || []).map((item) => item.message), 'No review warnings');
363
- pill('doctor-pill', doctor.failCount ? 'fail' : doctor.warnCount ? 'warning' : 'ok', doctor.passCount + '/' + doctor.totalChecks + ' pass'); renderDoctor(doctor.checks || []);
364
- text('discovery-count', discoveries.total || 0); renderDiscoveries(discoveries.recent || []); renderCommands(data.commands);
365
- text('ideas-count', (ideas.totalPending || 0) + ' pending'); renderIdeas(ideas.pending || []); text('history-count', (history.total || 0) + ' released'); renderHistory(history.items || []);
366
- }
367
-
368
- window.__INITIAL_SNAPSHOT__ = null;
369
- if (window.__INITIAL_SNAPSHOT__) {
370
- try {
371
- renderSnapshot(window.__INITIAL_SNAPSHOT__);
372
- byId('live-dot').classList.add('on');
373
- text('live-label','Connected · live snapshot every 2 seconds');
374
- } catch (e) {
375
- console.error('Initial hydration error:', e);
376
- }
377
- }
378
-
379
- let refreshing = false;
380
- async function refresh() { if (refreshing) return; refreshing = true; try { const response = await fetch('/api/dashboard'); if (!response.ok) throw new Error('HTTP ' + response.status); renderSnapshot(await response.json()); byId('live-dot').classList.add('on'); text('live-label','Connected · live snapshot every 2 seconds'); } catch (error) { byId('live-dot').classList.remove('on'); text('live-label','Disconnected · ' + error.message); } finally { refreshing = false; } }
381
- if (!window.__INITIAL_SNAPSHOT__) { refresh(); }
382
- setInterval(refresh, 2000);
383
- </script>
384
- </body>
385
- </html>`;
386
1
  import { StudioViewRenderer } from "./studio-view-renderer.js";
2
+ const defaultRenderer = new StudioViewRenderer();
3
+ /**
4
+ * Base static HTML template string for web dashboard.
5
+ */
6
+ export const DASHBOARD_PAGE_HTML = defaultRenderer.getBaseDashboardHtml();
387
7
  /**
388
8
  * Safely renders the dashboard HTML page with an optional embedded snapshot.
389
9
  * Delegates to StudioViewRenderer.
390
10
  */
391
11
  export function renderDashboardPage(snapshot) {
392
- const renderer = new StudioViewRenderer();
393
- return renderer.renderWebDashboard(snapshot);
12
+ return defaultRenderer.renderWebDashboard(snapshot);
394
13
  }
395
- export { DASHBOARD_PAGE_HTML, StudioViewRenderer };
14
+ export { StudioViewRenderer };
396
15
  //# sourceMappingURL=dashboard-page.js.map