@monoes/monomindcli 1.10.46 → 1.10.54

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 (144) hide show
  1. package/.claude/commands/monomind/do.md +52 -0
  2. package/.claude/commands/monomind/improve.md +2 -0
  3. package/.claude/helpers/handlers/route-handler.cjs +61 -11
  4. package/.claude/helpers/skill-registry.json +99 -51
  5. package/.claude/skills/agent-browser-testing/SKILL.md +522 -152
  6. package/.claude/skills/github-issue-triage/SKILL.md +354 -0
  7. package/.claude/skills/github-repo-recap/SKILL.md +207 -0
  8. package/.claude/skills/mastermind/_delegation.md +83 -0
  9. package/.claude/skills/mastermind/_protocol.md +14 -0
  10. package/dist/src/init/executor.d.ts.map +1 -1
  11. package/dist/src/init/executor.js +18 -19
  12. package/dist/src/init/executor.js.map +1 -1
  13. package/dist/src/init/helpers-generator.js +3 -3
  14. package/dist/src/init/helpers-generator.js.map +1 -1
  15. package/dist/src/ui/dashboard-v2.html +782 -42
  16. package/dist/src/ui/dashboard.html +1 -0
  17. package/dist/src/ui/data/agent-avatars.html +763 -0
  18. package/dist/src/ui/data/agent-avatars.json +966 -0
  19. package/dist/src/ui/data/avatars/account-strategist.svg +58 -0
  20. package/dist/src/ui/data/avatars/accounts-payable.svg +54 -0
  21. package/dist/src/ui/data/avatars/adaptive-coordinator.svg +55 -0
  22. package/dist/src/ui/data/avatars/adaptive-coordinator2.svg +54 -0
  23. package/dist/src/ui/data/avatars/ai-citation.svg +57 -0
  24. package/dist/src/ui/data/avatars/ai-engineer.svg +61 -0
  25. package/dist/src/ui/data/avatars/analytics-reporter.svg +53 -0
  26. package/dist/src/ui/data/avatars/api-tester.svg +53 -0
  27. package/dist/src/ui/data/avatars/architecture.svg +54 -0
  28. package/dist/src/ui/data/avatars/automation-governance.svg +55 -0
  29. package/dist/src/ui/data/avatars/backend-dev.svg +53 -0
  30. package/dist/src/ui/data/avatars/benchmarker.svg +54 -0
  31. package/dist/src/ui/data/avatars/blockchain-auditor.svg +53 -0
  32. package/dist/src/ui/data/avatars/byzantine-coord.svg +57 -0
  33. package/dist/src/ui/data/avatars/case-analyst.svg +57 -0
  34. package/dist/src/ui/data/avatars/cicd-engineer.svg +55 -0
  35. package/dist/src/ui/data/avatars/cloud-architect.svg +54 -0
  36. package/dist/src/ui/data/avatars/code-review-swarm.svg +57 -0
  37. package/dist/src/ui/data/avatars/coder-v119.svg +57 -0
  38. package/dist/src/ui/data/avatars/coder.svg +58 -0
  39. package/dist/src/ui/data/avatars/collective-coord.svg +54 -0
  40. package/dist/src/ui/data/avatars/compliance-auditor.svg +58 -0
  41. package/dist/src/ui/data/avatars/consensus-coordinator.svg +54 -0
  42. package/dist/src/ui/data/avatars/content-creator.svg +54 -0
  43. package/dist/src/ui/data/avatars/crdt-synchronizer.svg +53 -0
  44. package/dist/src/ui/data/avatars/cro-specialist.svg +58 -0
  45. package/dist/src/ui/data/avatars/data-consolidator.svg +54 -0
  46. package/dist/src/ui/data/avatars/data-engineer.svg +53 -0
  47. package/dist/src/ui/data/avatars/database-optimizer.svg +61 -0
  48. package/dist/src/ui/data/avatars/deal-strategist.svg +54 -0
  49. package/dist/src/ui/data/avatars/defender.svg +53 -0
  50. package/dist/src/ui/data/avatars/devops-automator.svg +56 -0
  51. package/dist/src/ui/data/avatars/discovery-coach.svg +54 -0
  52. package/dist/src/ui/data/avatars/email-marketing.svg +57 -0
  53. package/dist/src/ui/data/avatars/embedded-firmware.svg +61 -0
  54. package/dist/src/ui/data/avatars/evidence-collector.svg +57 -0
  55. package/dist/src/ui/data/avatars/experiment-tracker.svg +53 -0
  56. package/dist/src/ui/data/avatars/feedback-synthesizer.svg +54 -0
  57. package/dist/src/ui/data/avatars/finance-tracker.svg +54 -0
  58. package/dist/src/ui/data/avatars/frontend-developer.svg +54 -0
  59. package/dist/src/ui/data/avatars/game-audio-engineer.svg +59 -0
  60. package/dist/src/ui/data/avatars/game-designer.svg +54 -0
  61. package/dist/src/ui/data/avatars/gossip-coordinator.svg +54 -0
  62. package/dist/src/ui/data/avatars/hierarchical-coord.svg +54 -0
  63. package/dist/src/ui/data/avatars/incident-commander.svg +57 -0
  64. package/dist/src/ui/data/avatars/infrastructure.svg +54 -0
  65. package/dist/src/ui/data/avatars/input-validator.svg +53 -0
  66. package/dist/src/ui/data/avatars/ios-developer.svg +54 -0
  67. package/dist/src/ui/data/avatars/issue-tracker.svg +53 -0
  68. package/dist/src/ui/data/avatars/judge.svg +55 -0
  69. package/dist/src/ui/data/avatars/launch-strategist.svg +54 -0
  70. package/dist/src/ui/data/avatars/legal-compliance.svg +53 -0
  71. package/dist/src/ui/data/avatars/level-designer.svg +53 -0
  72. package/dist/src/ui/data/avatars/load-balancer.svg +57 -0
  73. package/dist/src/ui/data/avatars/mcp-builder.svg +53 -0
  74. package/dist/src/ui/data/avatars/memory-coordinator.svg +55 -0
  75. package/dist/src/ui/data/avatars/mesh-coordinator.svg +55 -0
  76. package/dist/src/ui/data/avatars/ml-developer.svg +58 -0
  77. package/dist/src/ui/data/avatars/mobile-app-builder.svg +53 -0
  78. package/dist/src/ui/data/avatars/mobile-dev.svg +54 -0
  79. package/dist/src/ui/data/avatars/model-qa.svg +58 -0
  80. package/dist/src/ui/data/avatars/narrative-designer.svg +58 -0
  81. package/dist/src/ui/data/avatars/outbound-strategist.svg +55 -0
  82. package/dist/src/ui/data/avatars/path-validator.svg +54 -0
  83. package/dist/src/ui/data/avatars/payment-agent.svg +53 -0
  84. package/dist/src/ui/data/avatars/perf-analyzer.svg +58 -0
  85. package/dist/src/ui/data/avatars/pipeline-analyst.svg +54 -0
  86. package/dist/src/ui/data/avatars/planner.svg +55 -0
  87. package/dist/src/ui/data/avatars/pr-manager.svg +54 -0
  88. package/dist/src/ui/data/avatars/pricing-strategist.svg +54 -0
  89. package/dist/src/ui/data/avatars/product-manager.svg +54 -0
  90. package/dist/src/ui/data/avatars/production-validator.svg +54 -0
  91. package/dist/src/ui/data/avatars/project-shepherd.svg +54 -0
  92. package/dist/src/ui/data/avatars/proposal-strategist.svg +54 -0
  93. package/dist/src/ui/data/avatars/prosecutor.svg +57 -0
  94. package/dist/src/ui/data/avatars/pseudocode.svg +53 -0
  95. package/dist/src/ui/data/avatars/queen-coordinator.svg +55 -0
  96. package/dist/src/ui/data/avatars/quorum-manager.svg +53 -0
  97. package/dist/src/ui/data/avatars/raft-manager.svg +53 -0
  98. package/dist/src/ui/data/avatars/reality-checker.svg +58 -0
  99. package/dist/src/ui/data/avatars/recruitment.svg +58 -0
  100. package/dist/src/ui/data/avatars/refinement.svg +53 -0
  101. package/dist/src/ui/data/avatars/release-manager.svg +54 -0
  102. package/dist/src/ui/data/avatars/repo-architect.svg +54 -0
  103. package/dist/src/ui/data/avatars/researcher.svg +58 -0
  104. package/dist/src/ui/data/avatars/resource-allocator.svg +53 -0
  105. package/dist/src/ui/data/avatars/reviewer.svg +53 -0
  106. package/dist/src/ui/data/avatars/safe-executor.svg +53 -0
  107. package/dist/src/ui/data/avatars/sales-coach.svg +53 -0
  108. package/dist/src/ui/data/avatars/sales-engineer.svg +58 -0
  109. package/dist/src/ui/data/avatars/scout-explorer.svg +58 -0
  110. package/dist/src/ui/data/avatars/security-architect.svg +54 -0
  111. package/dist/src/ui/data/avatars/security-auditor.svg +55 -0
  112. package/dist/src/ui/data/avatars/senior-developer.svg +58 -0
  113. package/dist/src/ui/data/avatars/senior-pm.svg +58 -0
  114. package/dist/src/ui/data/avatars/seo-specialist.svg +57 -0
  115. package/dist/src/ui/data/avatars/social-media.svg +54 -0
  116. package/dist/src/ui/data/avatars/solidity-engineer.svg +58 -0
  117. package/dist/src/ui/data/avatars/sparc-coder.svg +58 -0
  118. package/dist/src/ui/data/avatars/sparc-coord.svg +56 -0
  119. package/dist/src/ui/data/avatars/specification.svg +57 -0
  120. package/dist/src/ui/data/avatars/sprint-prioritizer.svg +53 -0
  121. package/dist/src/ui/data/avatars/sre.svg +54 -0
  122. package/dist/src/ui/data/avatars/studio-operations.svg +53 -0
  123. package/dist/src/ui/data/avatars/studio-producer.svg +55 -0
  124. package/dist/src/ui/data/avatars/support-responder.svg +56 -0
  125. package/dist/src/ui/data/avatars/system-architect.svg +54 -0
  126. package/dist/src/ui/data/avatars/task-orchestrator.svg +56 -0
  127. package/dist/src/ui/data/avatars/technical-artist.svg +53 -0
  128. package/dist/src/ui/data/avatars/technical-writer.svg +59 -0
  129. package/dist/src/ui/data/avatars/tester.svg +53 -0
  130. package/dist/src/ui/data/avatars/threat-detection.svg +61 -0
  131. package/dist/src/ui/data/avatars/trend-researcher.svg +54 -0
  132. package/dist/src/ui/data/avatars/trial-director.svg +55 -0
  133. package/dist/src/ui/data/avatars/unity-architect.svg +54 -0
  134. package/dist/src/ui/data/avatars/visionos-engineer.svg +57 -0
  135. package/dist/src/ui/data/avatars/worker-specialist.svg +55 -0
  136. package/dist/src/ui/data/avatars/workflow-architect.svg +57 -0
  137. package/dist/src/ui/data/avatars/workflow-automation.svg +54 -0
  138. package/dist/src/ui/data/avatars/zk-steward.svg +54 -0
  139. package/dist/src/ui/data/known-projects.json +1 -1
  140. package/dist/src/ui/data/mastermind-events.jsonl +28 -0
  141. package/dist/src/ui/orgs.html +1171 -0
  142. package/dist/src/ui/server.mjs +25 -8
  143. package/dist/tsconfig.tsbuildinfo +1 -1
  144. package/package.json +1 -1
@@ -34,7 +34,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
34
34
  #sb-logo { padding: 18px 16px 14px; border-bottom: 1px solid var(--border); }
35
35
  #sb-logo .mark { font-size: 13px; font-weight: 600; letter-spacing: 0.05em; color: var(--text-hi); }
36
36
  #sb-logo .proj { font-size: 11px; color: var(--accent); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
37
- #sb-nav { flex: 1; padding: 10px 8px; overflow-y: auto; }
37
+ #sb-nav { flex: 1; padding: 10px 8px; overflow-y: auto; display: flex; flex-direction: column; }
38
38
  .nav-sect { margin-bottom: 8px; }
39
39
  .nav-lbl { font-size: 10px; letter-spacing: 0.08em; color: var(--text-xs); padding: 8px 8px 4px; text-transform: uppercase; }
40
40
  .nav-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: var(--r); cursor: pointer; color: var(--text-mid); transition: background 0.1s, color 0.1s; user-select: none; }
@@ -44,6 +44,15 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
44
44
  .nav-item .lbl { font-size: 13px; }
45
45
  .nav-item .bdg { margin-left: auto; font-size: 10px; background: var(--surface-hi); color: var(--text-lo); border-radius: 8px; padding: 1px 6px; min-width: 18px; text-align: center; }
46
46
  .nav-item.active .bdg { background: var(--accent-dim); color: var(--accent); }
47
+ /* project context section */
48
+ #nav-project-ctx { display: none; }
49
+ #nav-project-ctx.visible { display: block; }
50
+ .nav-proj-hdr { display: flex; align-items: center; gap: 7px; padding: 10px 8px 5px; cursor: pointer; }
51
+ .nav-proj-hdr:hover .nav-proj-name { color: var(--text-hi); }
52
+ .nav-proj-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
53
+ .nav-proj-name { font-size: 12px; font-weight: 600; color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
54
+ .nav-proj-items { padding-left: 12px; border-left: 1px solid oklch(72% 0.18 75 / 0.2); margin-left: 11px; }
55
+ .nav-no-proj { display: flex; align-items: center; gap: 8px; padding: 8px 8px 6px 10px; font-size: 11px; color: var(--text-xs); font-style: italic; }
47
56
  #sb-footer { padding: 10px 14px; border-top: 1px solid var(--border); }
48
57
  #sb-user { font-size: 12px; font-weight: 500; color: var(--text-mid); }
49
58
  #sb-path { font-size: 10px; color: var(--text-lo); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); }
@@ -211,12 +220,103 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
211
220
  .dr-ts { font-size: 10px; color: var(--text-lo); margin-top: 2px; }
212
221
  .drawer-item.hidden { display: none; }
213
222
 
214
- /* orgs */
215
- .org-list { display: flex; flex-direction: column; gap: 6px; }
216
- .org-row { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 11px 14px; cursor: pointer; transition: border-color 0.12s; }
217
- .org-row:hover { border-color: var(--accent); }
218
- .org-name { font-size: 13px; color: var(--text-hi); font-weight: 500; }
219
- .org-meta { font-size: 11px; color: var(--text-lo); margin-top: 3px; }
223
+ /* orgs ── two-panel layout */
224
+ #view-orgs { flex-direction: row; }
225
+ #orgs-list-pane { width: 280px; flex-shrink: 0; border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
226
+ #orgs-list-head { padding: 14px 16px 12px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
227
+ #orgs-list-scroll { flex: 1; overflow-y: auto; padding: 8px; }
228
+ #orgs-list-scroll::-webkit-scrollbar { width: 3px; }
229
+ #orgs-list-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
230
+ .orgs-view-title { font-size: 14px; font-weight: 600; color: var(--text-hi); }
231
+ .orgs-view-sub { font-size: 11px; color: var(--text-lo); margin-top: 2px; }
232
+
233
+ .org-item { display: flex; align-items: flex-start; gap: 9px; padding: 9px 10px; border-radius: var(--r); cursor: pointer; transition: background 0.09s; border: 1px solid transparent; }
234
+ .org-item:hover { background: var(--surface-hi); }
235
+ .org-item.active { background: var(--accent-dim); border-color: oklch(72% 0.18 75 / 0.25); }
236
+ .oi-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border); flex-shrink: 0; margin-top: 4px; transition: background 0.3s; }
237
+ .oi-dot.running { background: var(--green); box-shadow: 0 0 5px oklch(65% 0.15 150 / 0.5); }
238
+ @media (prefers-reduced-motion: no-preference) { .oi-dot.running { animation: blink 2s ease-in-out infinite; } }
239
+ .oi-body { flex: 1; min-width: 0; }
240
+ .oi-name { font-size: 13px; font-weight: 500; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
241
+ .oi-goal { font-size: 11px; color: var(--text-lo); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
242
+ .oi-chips { display: flex; gap: 5px; margin-top: 5px; flex-wrap: wrap; }
243
+ .oi-chip { font-size: 10px; padding: 1px 6px; border-radius: 8px; background: var(--surface-hi); color: var(--text-lo); }
244
+ .oi-chip.live { background: oklch(65% 0.15 150 / 0.12); color: var(--green); }
245
+
246
+ /* detail pane */
247
+ #org-detail-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
248
+ #org-detail-pane.empty-state { align-items: center; justify-content: center; }
249
+ .orgs-no-sel { color: var(--text-lo); font-size: 13px; text-align: center; }
250
+ .orgs-no-sel-ico { font-size: 28px; margin-bottom: 10px; opacity: 0.3; }
251
+
252
+ #org-detail-head { padding: 12px 18px 11px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
253
+ .odh-name { font-size: 16px; font-weight: 600; color: var(--text-hi); }
254
+ .odh-badge { font-size: 11px; padding: 2px 8px; border-radius: 8px; }
255
+ .odh-badge.idle { background: var(--surface-hi); color: var(--text-lo); }
256
+ .odh-badge.live { background: oklch(65% 0.15 150 / 0.12); color: var(--green); }
257
+ .odh-pill { font-size: 11px; padding: 2px 8px; border-radius: 8px; background: var(--surface); color: var(--text-lo); border: 1px solid var(--border); }
258
+ .odh-right { margin-left: auto; display: flex; gap: 6px; }
259
+
260
+ #org-detail-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); flex-shrink: 0; padding: 0 16px; }
261
+ .odt-btn { font-size: 12px; padding: 8px 12px; background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer; color: var(--text-lo); font-family: var(--sans); transition: color 0.1s; }
262
+ .odt-btn:hover { color: var(--text-hi); }
263
+ .odt-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
264
+
265
+ #org-detail-body { flex: 1; overflow-y: auto; padding: 18px 20px; }
266
+ #org-detail-body::-webkit-scrollbar { width: 3px; }
267
+ #org-detail-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
268
+ .odt-pane { display: none; }
269
+ .odt-pane.active { display: block; }
270
+
271
+ /* org meta strip */
272
+ .org-meta-strip { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 16px; }
273
+ .oms-cell { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 10px 12px; }
274
+ .oms-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-lo); margin-bottom: 4px; }
275
+ .oms-val { font-size: 12px; color: var(--text-hi); word-break: break-word; line-height: 1.4; }
276
+
277
+ /* SVG org chart */
278
+ .org-chart-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; margin-bottom: 12px; }
279
+ .org-chart-svg { width: 100%; display: block; }
280
+ .org-chart-legend { display: flex; gap: 14px; flex-wrap: wrap; padding: 0 2px; }
281
+ .ocl-item { display: flex; align-items: center; gap: 5px; font-size: 10px; color: var(--text-lo); }
282
+
283
+ /* roles in v2 */
284
+ .roles-v2-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; }
285
+ .role-v2 { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 12px 14px; }
286
+ .rv2-head { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 6px; }
287
+ .rv2-avatar { width: 48px; height: 48px; border-radius: 50%; flex-shrink: 0; object-fit: cover; }
288
+ .rv2-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; padding-top: 2px; }
289
+ .rv2-title { font-size: 13px; font-weight: 500; color: var(--text-hi); line-height: 1.3; }
290
+ .rv2-agent { font-size: 10px; padding: 1px 7px; border-radius: 8px; background: var(--accent-dim); color: var(--accent); white-space: nowrap; align-self: flex-start; }
291
+ .rv2-id { font-size: 10px; color: var(--text-lo); font-family: var(--mono); margin-bottom: 4px; }
292
+ .rv2-reports { font-size: 11px; color: var(--text-lo); margin-bottom: 6px; }
293
+ .rv2-reports span { color: var(--text-mid); }
294
+ .rv2-resps { }
295
+ .rv2-resp { font-size: 11px; color: var(--text-lo); line-height: 1.6; display: flex; gap: 6px; }
296
+ .rv2-resp::before { content: '–'; opacity: 0.4; flex-shrink: 0; }
297
+ /* role avatars in org list */
298
+ .oi-avatars { display: flex; margin-top: 5px; }
299
+ .oi-av { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; margin-right: -5px; border: 1.5px solid var(--bg); }
300
+ .oi-av-more { width: 22px; height: 22px; border-radius: 50%; background: var(--surface); border: 1.5px solid var(--border); font-size: 9px; color: var(--text-lo); display: flex; align-items: center; justify-content: center; margin-right: -5px; }
301
+ /* chart avatar clip */
302
+ .v2oc-node image { pointer-events: none; }
303
+
304
+ /* activity in v2 */
305
+ .act-v2-list { display: flex; flex-direction: column; }
306
+ .av2-row { display: grid; grid-template-columns: 56px 90px 1fr; gap: 8px; padding: 5px 0; border-bottom: 1px solid oklch(25% 0.008 55 / 0.5); font-size: 11px; align-items: baseline; }
307
+ .av2-row:last-child { border-bottom: none; }
308
+ .av2-time { color: var(--text-xs); font-family: var(--mono); font-size: 10px; }
309
+ .av2-type { color: var(--accent); font-weight: 500; }
310
+ .av2-msg { color: var(--text-lo); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
311
+
312
+ /* health in v2 */
313
+ .health-v2-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 8px; margin-bottom: 12px; }
314
+ .hv2-cell { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 10px 12px; }
315
+ .hv2-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-lo); margin-bottom: 4px; }
316
+ .hv2-val { font-size: 20px; font-weight: 700; color: var(--text-hi); font-family: var(--mono); }
317
+ .hv2-val.green { color: var(--green); }
318
+ .hv2-val.amber { color: var(--accent); }
319
+ .hv2-val.red { color: var(--red); }
220
320
 
221
321
  /* ── alerts rail ─────────────────────────────────────────── */
222
322
  #alerts-rail { display: none; flex-shrink: 0; border-bottom: 1px solid var(--border); padding: 0 16px; min-height: 36px; flex-direction: row; gap: 8px; align-items: center; overflow-x: auto; }
@@ -832,7 +932,12 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
832
932
  .ctx-pressure-fill.warn { background:oklch(70% 0.18 80); }
833
933
  .ctx-pressure-fill.crit { background:oklch(65% 0.2 25); }
834
934
  .ctx-pressure-lbl { font-size:9px; color:var(--text-xs); white-space:nowrap; }
935
+ /* org chart nodes — CSS fallback animation */
936
+ .v2oc-node { transform-box: fill-box; transform-origin: center center; }
937
+ @keyframes v2nodeIn { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } }
938
+ @keyframes v2edgeIn { from { opacity: 0; } to { opacity: 1; } }
835
939
  </style>
940
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
836
941
  </head>
837
942
  <body>
838
943
  <div id="app">
@@ -848,30 +953,36 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
848
953
  <div class="nav-item active" data-view="now">
849
954
  <span class="ico">◉</span><span class="lbl">Now</span>
850
955
  </div>
851
- </div>
852
- <div class="nav-sect">
853
- <div class="nav-lbl">Workspace</div>
854
956
  <div class="nav-item" data-view="projects">
855
957
  <span class="ico">⊞</span><span class="lbl">Projects</span>
856
958
  <span class="bdg" id="bdg-projects">—</span>
857
959
  </div>
858
- <div class="nav-item" data-view="sessions">
859
- <span class="ico">◫</span><span class="lbl">Sessions</span>
860
- <span class="bdg" id="bdg-sessions">—</span>
861
- </div>
862
- <div class="nav-item" data-view="loops">
863
- <span class="ico">↺</span><span class="lbl">Loops</span>
864
- <span class="bdg" id="bdg-loops">—</span>
865
- </div>
866
960
  </div>
867
- <div class="nav-sect">
868
- <div class="nav-lbl">Intelligence</div>
869
- <div class="nav-item" data-view="memory">
870
- <span class="ico">◈</span><span class="lbl">Memory</span>
961
+ <!-- project-scoped section: hidden until a project is selected -->
962
+ <div id="nav-project-ctx">
963
+ <div class="nav-proj-hdr" onclick="switchView('projects')" title="Change project">
964
+ <div class="nav-proj-dot"></div>
965
+ <div class="nav-proj-name" id="nav-proj-name">—</div>
871
966
  </div>
872
- <div class="nav-item" data-view="orgs">
873
- <span class="ico">⬡</span><span class="lbl">Orgs</span>
967
+ <div class="nav-proj-items">
968
+ <div class="nav-item" data-view="sessions">
969
+ <span class="ico">◫</span><span class="lbl">Sessions</span>
970
+ <span class="bdg" id="bdg-sessions">—</span>
971
+ </div>
972
+ <div class="nav-item" data-view="loops">
973
+ <span class="ico">↺</span><span class="lbl">Loops</span>
974
+ <span class="bdg" id="bdg-loops">—</span>
975
+ </div>
976
+ <div class="nav-item" data-view="memory">
977
+ <span class="ico">◈</span><span class="lbl">Memory</span>
978
+ </div>
979
+ <div class="nav-item" data-view="orgs">
980
+ <span class="ico">⬡</span><span class="lbl">Orgs</span>
981
+ </div>
874
982
  </div>
983
+ </div>
984
+ <div class="nav-no-proj" id="nav-no-proj-hint">Select a project above</div>
985
+ <div class="nav-sect" style="margin-top:auto;padding-top:8px;">
875
986
  <div class="nav-item" data-view="global">
876
987
  <span class="ico">⊕</span><span class="lbl">Global Feed</span>
877
988
  </div>
@@ -1180,10 +1291,47 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1180
1291
 
1181
1292
  <!-- ORGS -->
1182
1293
  <div class="view" id="view-orgs">
1183
- <div class="vscroll">
1184
- <div class="pg-title">Orgs</div>
1185
- <div class="pg-sub">MASTERMIND organizations and swarms</div>
1186
- <div id="orgs-content"><div class="loading-txt">Loading…</div></div>
1294
+ <!-- List sidebar -->
1295
+ <div id="orgs-list-pane">
1296
+ <div id="orgs-list-head">
1297
+ <div class="orgs-view-title">Orgs <span id="orgs-proj-label" style="font-size:11px;font-weight:400;opacity:0.5;margin-left:6px;"></span></div>
1298
+ <div class="orgs-view-sub" id="orgs-view-sub">MASTERMIND organizations</div>
1299
+ </div>
1300
+ <div id="orgs-list-scroll">
1301
+ <div class="loading-txt" id="orgs-list-content">Loading…</div>
1302
+ </div>
1303
+ </div>
1304
+ <!-- Detail pane -->
1305
+ <div id="org-detail-pane" class="empty-state">
1306
+ <div class="orgs-no-sel">
1307
+ <div class="orgs-no-sel-ico">⬡</div>
1308
+ Select an org
1309
+ </div>
1310
+ <div id="org-detail-inner" style="display:none;width:100%;height:100%;display:none;flex-direction:column;overflow:hidden;">
1311
+ <div id="org-detail-head">
1312
+ <div>
1313
+ <div class="odh-name" id="odh-name">—</div>
1314
+ </div>
1315
+ <span class="odh-badge idle" id="odh-badge">IDLE</span>
1316
+ <span class="odh-pill" id="odh-topo">—</span>
1317
+ <span class="odh-pill" id="odh-roles">0 roles</span>
1318
+ <div class="odh-right">
1319
+ <button class="btn" id="org-stop-btn" onclick="v2StopOrg()" style="display:none;color:var(--red);border-color:oklch(60% 0.18 25 / 0.4)">Stop</button>
1320
+ </div>
1321
+ </div>
1322
+ <div id="org-detail-tabs">
1323
+ <button class="odt-btn active" data-tab="chart" onclick="v2SwitchOrgTab('chart')">Chart</button>
1324
+ <button class="odt-btn" data-tab="roles" onclick="v2SwitchOrgTab('roles')">Roles</button>
1325
+ <button class="odt-btn" data-tab="activity" onclick="v2SwitchOrgTab('activity')">Activity</button>
1326
+ <button class="odt-btn" data-tab="health" onclick="v2SwitchOrgTab('health')">Health</button>
1327
+ </div>
1328
+ <div id="org-detail-body">
1329
+ <div class="odt-pane active" id="odt-chart"></div>
1330
+ <div class="odt-pane" id="odt-roles"></div>
1331
+ <div class="odt-pane" id="odt-activity"></div>
1332
+ <div class="odt-pane" id="odt-health"></div>
1333
+ </div>
1334
+ </div>
1187
1335
  </div>
1188
1336
  </div>
1189
1337
 
@@ -1325,6 +1473,7 @@ async function init() {
1325
1473
  document.getElementById('sb-user').textContent = gu.name || gu.email || '—';
1326
1474
  document.getElementById('sb-path').textContent = DIR;
1327
1475
  document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
1476
+ _showNavProjectCtx(DIR);
1328
1477
  } catch (_) {}
1329
1478
  // deep-link: ?sess=<id>&proj=<path>
1330
1479
  const params = new URLSearchParams(window.location.search);
@@ -1334,6 +1483,7 @@ async function init() {
1334
1483
  DIR = projParam;
1335
1484
  document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
1336
1485
  document.getElementById('sb-path').textContent = DIR;
1486
+ _showNavProjectCtx(DIR);
1337
1487
  }
1338
1488
  restoreURLParams();
1339
1489
  viewRendered['now'] = true;
@@ -1369,13 +1519,36 @@ async function apiFetch(url) {
1369
1519
  }
1370
1520
 
1371
1521
  // ── project switching ──────────────────────────────────────
1522
+ function _showNavProjectCtx(path) {
1523
+ const projName = (path || '').split('/').filter(Boolean).pop() || '';
1524
+ const ctx = document.getElementById('nav-project-ctx');
1525
+ const hint = document.getElementById('nav-no-proj-hint');
1526
+ if (projName) {
1527
+ document.getElementById('nav-proj-name').textContent = projName;
1528
+ ctx.classList.add('visible');
1529
+ hint.style.display = 'none';
1530
+ } else {
1531
+ ctx.classList.remove('visible');
1532
+ hint.style.display = '';
1533
+ }
1534
+ }
1535
+
1372
1536
  function switchProject(path) {
1373
1537
  if (DIR === path) { switchView('sessions'); return; }
1374
1538
  DIR = path;
1375
1539
  document.getElementById('sb-proj').textContent = path.split('/').filter(Boolean).pop() || '—';
1376
1540
  document.getElementById('sb-path').textContent = path;
1541
+ _showNavProjectCtx(path);
1377
1542
  viewRendered = {};
1378
1543
  allSessions = [];
1544
+ _v2SelOrg = null;
1545
+ _v2OrgData = null;
1546
+ _v2Orgs = [];
1547
+ Object.keys(_v2OrgEventLog).forEach(k => delete _v2OrgEventLog[k]);
1548
+ document.getElementById('org-detail-pane')?.classList.add('empty-state');
1549
+ document.getElementById('org-detail-inner') && (document.getElementById('org-detail-inner').style.display = 'none');
1550
+ const _noSel = document.querySelector('#org-detail-pane .orgs-no-sel');
1551
+ if (_noSel) _noSel.style.display = '';
1379
1552
  closeDetail();
1380
1553
  switchView('sessions');
1381
1554
  }
@@ -3204,26 +3377,593 @@ function filterMemoryNs(ns) {
3204
3377
  }
3205
3378
 
3206
3379
  // ── orgs ───────────────────────────────────────────────────
3380
+ let _v2Orgs = [];
3381
+ let _v2SelOrg = null;
3382
+ let _v2OrgData = null;
3383
+ let _v2OrgTab = 'chart';
3384
+ const _v2OrgEventLog = {};
3385
+
3207
3386
  async function renderOrgs() {
3208
- const el = document.getElementById('orgs-content');
3209
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
3210
3387
  try {
3211
- const data = await apiFetch('/api/orgs');
3212
- const orgs = Array.isArray(data) ? data : (data.orgs || []);
3213
- if (!orgs.length) {
3214
- el.innerHTML = '<div class="empty"><div class="empty-ico">⬡</div><div>No MASTERMIND orgs found</div></div>';
3215
- return;
3388
+ const q = DIR ? '?dir=' + encodeURIComponent(DIR) : '';
3389
+ const data = await apiFetch('/api/orgs' + q);
3390
+ _v2Orgs = Array.isArray(data) ? data : (data.orgs || []);
3391
+ } catch (_) { _v2Orgs = []; }
3392
+ // Show current project name in sub-header
3393
+ const orgProjLabel = document.getElementById('orgs-proj-label');
3394
+ if (orgProjLabel) orgProjLabel.textContent = DIR ? DIR.split('/').filter(Boolean).pop() : '';
3395
+ try { v2RenderOrgList(); } catch (_) {}
3396
+ }
3397
+
3398
+ function v2RenderOrgList() {
3399
+ const wrap = document.getElementById('orgs-list-scroll');
3400
+ const sub = document.getElementById('orgs-view-sub');
3401
+ if (!wrap || !sub) return;
3402
+ const running = _v2Orgs.filter(o => o.running).length;
3403
+ sub.textContent = running > 0
3404
+ ? `${_v2Orgs.length} orgs · ${running} running`
3405
+ : `${_v2Orgs.length} org${_v2Orgs.length !== 1 ? 's' : ''}`;
3406
+
3407
+ if (!_v2Orgs.length) {
3408
+ wrap.innerHTML = '<div class="empty"><div class="empty-ico">⬡</div><div>No orgs found<br><span style="font-size:11px;color:var(--text-xs)">/mastermind:createorg</span></div></div>';
3409
+ return;
3410
+ }
3411
+
3412
+ wrap.innerHTML = _v2Orgs.map(o => {
3413
+ const rolesArr = Array.isArray(o.roles) ? o.roles : [];
3414
+ const rolesN = rolesArr.length || (typeof o.roles === 'number' ? o.roles : 0);
3415
+ const active = _v2SelOrg === o.name ? ' active' : '';
3416
+ const goalSnip = (o.goal || '').slice(0, 60) + ((o.goal || '').length > 60 ? '…' : '');
3417
+ const MAX_SHOW = 4;
3418
+ const shownRoles = rolesArr.slice(0, MAX_SHOW);
3419
+ const extraCount = rolesN - shownRoles.length;
3420
+ const avatarsHtml = shownRoles.length ? `<div class="oi-avatars">
3421
+ ${shownRoles.map(r => `<img class="oi-av" src="${v2RoleAvatar(r)}" alt="${esc(r.name||r.id||'')}" loading="lazy" title="${esc(r.name||r.id||'')}" onerror="this.src='data/avatars/coder.svg'"/>`).join('')}
3422
+ ${extraCount > 0 ? `<div class="oi-av-more">+${extraCount}</div>` : ''}
3423
+ </div>` : '';
3424
+ return `<div class="org-item${active}" data-org="${esc(o.name)}" onclick="v2SelectOrg(this.dataset.org)">
3425
+ <div class="oi-dot ${o.running ? 'running' : ''}"></div>
3426
+ <div class="oi-body">
3427
+ <div class="oi-name">${esc(o.name)}</div>
3428
+ ${goalSnip ? `<div class="oi-goal">${esc(goalSnip)}</div>` : ''}
3429
+ <div class="oi-chips">
3430
+ ${o.running ? '<span class="oi-chip live">LIVE</span>' : ''}
3431
+ <span class="oi-chip">${rolesN} roles</span>
3432
+ <span class="oi-chip">${esc(o.topology || 'hierarchical')}</span>
3433
+ </div>
3434
+ ${avatarsHtml}
3435
+ </div>
3436
+ </div>`;
3437
+ }).join('');
3438
+ }
3439
+
3440
+ async function v2SelectOrg(name) {
3441
+ _v2SelOrg = name;
3442
+ const requested = name;
3443
+ const requestedDir = DIR;
3444
+ document.querySelectorAll('.org-item').forEach(el => {
3445
+ el.classList.toggle('active', el.dataset.org === name);
3446
+ });
3447
+
3448
+ // Show detail inner, hide placeholder
3449
+ const pane = document.getElementById('org-detail-pane');
3450
+ if (!pane) return;
3451
+ pane.classList.remove('empty-state');
3452
+ const noSel = pane.querySelector('.orgs-no-sel');
3453
+ if (noSel) noSel.style.display = 'none';
3454
+ const inner = document.getElementById('org-detail-inner');
3455
+ if (!inner) return;
3456
+ inner.style.display = 'flex';
3457
+
3458
+ // Populate header from list data
3459
+ const listOrg = _v2Orgs.find(o => o.name === name) || {};
3460
+ v2UpdateOrgHeader(listOrg, null);
3461
+
3462
+ // Fetch full data
3463
+ _v2OrgData = null;
3464
+ const _enc = encodeURIComponent(name);
3465
+ try {
3466
+ const [mainR, actR, healthR] = await Promise.all([
3467
+ apiFetch(`/api/org/${_enc}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`),
3468
+ fetch(`/api/org/${_enc}/activity${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]),
3469
+ fetch(`/api/org/${_enc}/health${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():null).catch(()=>null),
3470
+ ]);
3471
+ if (_v2SelOrg !== requested || DIR !== requestedDir) return;
3472
+ // API returns {config, state, goals, ...} — flatten config; config wins for schema fields
3473
+ _v2OrgData = mainR.config ? { ...mainR, ...mainR.config, running: mainR.running } : mainR;
3474
+ _v2OrgData._activity = Array.isArray(actR) ? actR : [];
3475
+ _v2OrgData._health = healthR;
3476
+ } catch (_) {
3477
+ if (_v2SelOrg !== requested || DIR !== requestedDir) return;
3478
+ _v2OrgData = { ...listOrg, communication: [], _activity: [], _health: null };
3479
+ }
3480
+
3481
+ v2UpdateOrgHeader(listOrg, _v2OrgData);
3482
+ v2RenderOrgTab(_v2OrgTab);
3483
+ }
3484
+
3485
+ function v2UpdateOrgHeader(listOrg, data) {
3486
+ document.getElementById('odh-name').textContent = listOrg.name || _v2SelOrg || '—';
3487
+ const running = data ? (data.running ?? listOrg.running) : listOrg.running;
3488
+ const badge = document.getElementById('odh-badge');
3489
+ badge.textContent = running ? 'LIVE' : 'IDLE';
3490
+ badge.className = 'odh-badge ' + (running ? 'live' : 'idle');
3491
+ const roles = data ? (Array.isArray(data.roles) ? data.roles.length : (data.roles || 0)) : (listOrg.roles || 0);
3492
+ document.getElementById('odh-roles').textContent = roles + ' role' + (roles !== 1 ? 's' : '');
3493
+ document.getElementById('odh-topo').textContent = (data?.topology || listOrg.topology || '—');
3494
+ document.getElementById('org-stop-btn').style.display = running ? '' : 'none';
3495
+ }
3496
+
3497
+ window.v2SwitchOrgTab = function(tab) {
3498
+ _v2OrgTab = tab;
3499
+ document.querySelectorAll('.odt-btn').forEach(b => {
3500
+ b.classList.toggle('active', b.dataset.tab === tab);
3501
+ });
3502
+ document.querySelectorAll('.odt-pane').forEach(p => p.classList.remove('active'));
3503
+ document.getElementById('odt-' + tab)?.classList.add('active');
3504
+ v2RenderOrgTab(tab);
3505
+ };
3506
+
3507
+ function v2RenderOrgTab(tab) {
3508
+ if (!_v2OrgData) {
3509
+ const p = document.getElementById('odt-' + tab);
3510
+ if (p) p.innerHTML = '<div class="empty">Loading…</div>';
3511
+ return;
3512
+ }
3513
+ if (tab === 'chart') v2RenderOrgChart();
3514
+ else if (tab === 'roles') v2RenderOrgRoles();
3515
+ else if (tab === 'activity') v2RenderOrgActivity();
3516
+ else if (tab === 'health') v2RenderOrgHealth();
3517
+ }
3518
+
3519
+ // ── org chart ───────────────────────────────
3520
+ function _v2OrgIsLeader(r) {
3521
+ return r.id === 'boss' || r.type === 'coordinator' || r.type === 'planner';
3522
+ }
3523
+
3524
+ function v2RenderOrgChart() {
3525
+ if (!_v2SelOrg) return;
3526
+ const d = _v2OrgData;
3527
+ if (!d) return;
3528
+ const roles = Array.isArray(d.roles) ? d.roles : [];
3529
+ const listOrg = _v2Orgs.find(o => o.name === _v2SelOrg) || {};
3530
+ const isRunning = listOrg.running || d.running;
3531
+ const goalText = d.goal || '—';
3532
+ const topo = (d.topology || 'hierarchical').toLowerCase();
3533
+ const created = d.created_at ? new Date(d.created_at).toLocaleDateString('en', {month:'short',day:'numeric',year:'numeric'}) : '—';
3534
+ const pane = document.getElementById('odt-chart');
3535
+
3536
+ // ── color palette ─────────────────────────────────────
3537
+ const COLORS = [
3538
+ 'oklch(72% 0.18 75)', // amber — leader
3539
+ 'oklch(65% 0.12 240)', // blue
3540
+ 'oklch(65% 0.15 150)', // green
3541
+ 'oklch(65% 0.13 290)', // purple
3542
+ 'oklch(65% 0.14 35)', // orange
3543
+ 'oklch(62% 0.12 195)', // cyan
3544
+ ];
3545
+ // fix: reserve COLORS[0] for leaders; non-leaders start at index 1
3546
+ const colorOf = (role, idx) => COLORS[_v2OrgIsLeader(role) ? 0 : (idx % (COLORS.length - 1)) + 1];
3547
+
3548
+ // ── auto-generate edges ───────────────────────────────
3549
+ let comms = Array.isArray(d.communication) ? [...d.communication] : [];
3550
+ if (!comms.length && roles.length > 1) {
3551
+ const _lr0 = roles.find(_v2OrgIsLeader) || roles[0];
3552
+ comms = roles.filter(r => r.id !== _lr0.id).flatMap(r => [
3553
+ { from: _lr0.id, to: r.id, type: 'command' },
3554
+ { from: r.id, to: _lr0.id, type: 'report' },
3555
+ ]);
3556
+ }
3557
+
3558
+ // ── layout positions ──────────────────────────────────
3559
+ const W = 720, R = 42, PAD_X = R + 20, PAD_Y = R + 24;
3560
+ const roleIds = new Set(roles.map(r => r.id));
3561
+ // all leaders at depth 0 (supports co-lead orgs)
3562
+ const leaders = roles.filter(_v2OrgIsLeader);
3563
+ const lr = leaders[0] || roles[0];
3564
+ const hasSubs = lr && roles.length > 1;
3565
+
3566
+ const pos = {};
3567
+ let _layoutH = null; // separate from pos to avoid key collision
3568
+
3569
+ const _hubAndSpoke = () => {
3570
+ if (leaders.length > 0) {
3571
+ leaders.forEach((r, i) => {
3572
+ const x = leaders.length === 1 ? W / 2 : PAD_X + ((W - PAD_X * 2) / (leaders.length - 1)) * i;
3573
+ pos[r.id] = { x, y: PAD_Y };
3574
+ });
3216
3575
  }
3217
- el.innerHTML = '<div class="org-list">' + orgs.map(o =>
3218
- `<div class="org-row">
3219
- <div class="org-name">${esc(o.name || o.id || '')}</div>
3220
- <div class="org-meta">${esc(o.description || (o.agents != null ? o.agents + ' agents' : ''))}</div>
3221
- </div>`).join('') + '</div>';
3222
- } catch (err) {
3223
- el.innerHTML = '<div class="empty">Could not load orgs: ' + esc(err.message) + '</div>';
3576
+ const subs = roles.filter(r => !_v2OrgIsLeader(r));
3577
+ const usableW = W - PAD_X * 2;
3578
+ // collapse to single row when no leaders avoids blank 220px upper band
3579
+ const subY = leaders.length > 0 ? PAD_Y + 220 : PAD_Y;
3580
+ subs.forEach((r, i) => {
3581
+ const x = subs.length === 1 ? W / 2 : PAD_X + (usableW / (subs.length - 1)) * i;
3582
+ pos[r.id] = { x, y: subY };
3583
+ });
3584
+ const _hasBothRows = leaders.length > 0 && subs.length > 0;
3585
+ _layoutH = _hasBothRows ? PAD_Y + 220 + R + 8 : PAD_Y + R + 8;
3586
+ };
3587
+
3588
+ if (!roles.length) {
3589
+ // nothing
3590
+ } else if (!hasSubs) {
3591
+ pos[roles[0].id] = { x: W / 2, y: 90 };
3592
+ } else if (topo === 'mesh') {
3593
+ // circular — radius and cy both scale with n to prevent overlap and top-clip
3594
+ const n = roles.length, cx = W / 2;
3595
+ const minRad = (2 * R + 6) * n / (2 * Math.PI);
3596
+ const rad = Math.max(minRad, Math.min((W - PAD_X * 2) / 2, 130) * 0.82);
3597
+ const cy = Math.max(170, rad + PAD_Y); // shift center down so top nodes stay in viewBox
3598
+ roles.forEach((r, i) => {
3599
+ const a = (2 * Math.PI * i / n) - Math.PI / 2;
3600
+ pos[r.id] = { x: cx + rad * Math.cos(a), y: cy + rad * Math.sin(a) };
3601
+ });
3602
+ _layoutH = Math.round(cy + rad + PAD_Y);
3603
+ } else if (topo === 'ring' || topo === 'pipeline') {
3604
+ // linear sequence
3605
+ const usableW = W - PAD_X * 2;
3606
+ roles.forEach((r, i) => {
3607
+ const x = roles.length === 1 ? W / 2 : PAD_X + (usableW / (roles.length - 1)) * i;
3608
+ pos[r.id] = { x, y: 90 };
3609
+ });
3610
+ _layoutH = 180;
3611
+ } else if (Array.isArray(d.communication) && d.communication.length > 0) {
3612
+ // BFS multi-level layout — only traverse forward (non-report) edges
3613
+ const fwdEdges = comms.filter(e => e.type !== 'report');
3614
+ if (!fwdEdges.length) {
3615
+ // all-report graph: render as hub-and-spoke
3616
+ _hubAndSpoke();
3617
+ } else {
3618
+ const depth = {};
3619
+ leaders.forEach(r => { depth[r.id] = 0; });
3620
+ const queue = [...leaders.map(r => r.id)];
3621
+ while (queue.length) {
3622
+ const cur = queue.shift();
3623
+ fwdEdges.forEach(e => {
3624
+ // fix: skip ghost ids not present in roles
3625
+ if (e.from === cur && roleIds.has(e.to) && depth[e.to] === undefined) {
3626
+ depth[e.to] = depth[cur] + 1;
3627
+ queue.push(e.to);
3628
+ }
3629
+ });
3630
+ }
3631
+ roles.forEach(r => { if (depth[r.id] === undefined) depth[r.id] = 1; });
3632
+ // normalize: shift all depths so the shallowest layer is 0 — avoids blank top band when leaders=[]
3633
+ const minDepth = Math.min(...roles.map(r => depth[r.id]));
3634
+ if (minDepth > 0) roles.forEach(r => { depth[r.id] -= minDepth; });
3635
+ const maxDepth = Math.max(...roles.map(r => depth[r.id]));
3636
+ const layerOf = {};
3637
+ for (let d2 = 0; d2 <= maxDepth; d2++) layerOf[d2] = [];
3638
+ roles.forEach(r => layerOf[depth[r.id]].push(r));
3639
+
3640
+ const LAYER_H = 110;
3641
+ _layoutH = PAD_Y + maxDepth * LAYER_H + R + 24; // bottom row center + radius + margin
3642
+
3643
+ for (let d2 = 0; d2 <= maxDepth; d2++) {
3644
+ const layer = layerOf[d2];
3645
+ const usableW = W - PAD_X * 2;
3646
+ layer.forEach((r, i) => {
3647
+ const x = layer.length === 1 ? W / 2 : PAD_X + (usableW / (layer.length - 1)) * i;
3648
+ pos[r.id] = { x, y: PAD_Y + d2 * LAYER_H };
3649
+ });
3650
+ }
3651
+ }
3652
+ } else {
3653
+ _hubAndSpoke();
3654
+ }
3655
+
3656
+ const H = _layoutH || (hasSubs ? 320 : 180);
3657
+
3658
+ // ── build edge SVG strings with offset parallel arrows ─
3659
+ let edgesHTML = '', particlesHTML = '';
3660
+ const OFFSET = 8; // perpendicular offset px between cmd/report arrows
3661
+
3662
+ comms.forEach((edge, ei) => {
3663
+ if (edge.from === edge.to) return; // skip self-loops — zero-length path parks a static particle
3664
+ const fp = pos[edge.from], tp = pos[edge.to];
3665
+ if (!fp || !tp) return;
3666
+ const dx = tp.x - fp.x, dy = tp.y - fp.y;
3667
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
3668
+ // Perpendicular unit vector — derived from canonical direction to stay
3669
+ // consistent for both halves of an anti-parallel pair (A→B and B→A),
3670
+ // so command and report arrows always offset to opposite sides.
3671
+ const canonDir = String(edge.from) < String(edge.to) ? 1 : -1;
3672
+ const px = (-dy / len) * canonDir, py = (dx / len) * canonDir;
3673
+ const edgeSide = edge.type === 'report' ? -1 : 1;
3674
+ const ox = px * OFFSET * edgeSide, oy = py * OFFSET * edgeSide;
3675
+ // Trim from circle edges
3676
+ const trimS = R + 3, trimE = R + 8;
3677
+ const x1 = (fp.x + ox + dx / len * trimS).toFixed(1);
3678
+ const y1 = (fp.y + oy + dy / len * trimS).toFixed(1);
3679
+ const x2 = (tp.x + ox - dx / len * trimE).toFixed(1);
3680
+ const y2 = (tp.y + oy - dy / len * trimE).toFixed(1);
3681
+ const pid = `v2ep${ei}`;
3682
+
3683
+ let stroke, dash, marker, opacity, sw;
3684
+ if (edge.type === 'command') {
3685
+ stroke='oklch(72% 0.18 75)'; dash=''; marker='v2ac'; opacity=0.7; sw=1.8;
3686
+ } else if (edge.type === 'report') {
3687
+ stroke='oklch(65% 0.12 240)'; dash='5 4'; marker='v2ar'; opacity=0.55; sw=1.2;
3688
+ } else {
3689
+ stroke='oklch(65% 0.15 150)'; dash=''; marker='v2ah'; opacity=0.6; sw=1.5;
3690
+ }
3691
+ edgesHTML += `<path id="${pid}" class="v2oc-edge" d="M${x1},${y1} L${x2},${y2}" stroke="${stroke}" stroke-opacity="${opacity}" stroke-width="${sw}" ${dash?`stroke-dasharray="${dash}"`:''} fill="none" marker-end="url(#${marker})"/>`;
3692
+
3693
+ // Animated flow particle on command edges
3694
+ if (edge.type === 'command') {
3695
+ const delay = (ei * 0.4).toFixed(2);
3696
+ particlesHTML += `<circle class="v2oc-particle" r="3.5" fill="${stroke}" opacity="0.7">
3697
+ <animateMotion dur="2s" repeatCount="indefinite" begin="${delay}s"><mpath href="#${pid}" xlink:href="#${pid}"/></animateMotion>
3698
+ </circle>`;
3699
+ }
3700
+ });
3701
+
3702
+ // ── build node SVG strings ─────────────────────────────
3703
+ // Pre-build per-node clipPath ids for avatar images
3704
+ const nodeClipIds = roles.map((r, i) => `v2ac-${_v2SelOrg.replace(/\W/g,'')}-${i}`);
3705
+ const avatarClipDefs = roles.map((r, i) =>
3706
+ `<clipPath id="${nodeClipIds[i]}"><circle r="${Math.round(R * 0.52)}"/></clipPath>`
3707
+ ).join('');
3708
+
3709
+ let nodesHTML = '';
3710
+ roles.forEach((role, i) => {
3711
+ const p = pos[role.id];
3712
+ if (!p) return;
3713
+ const leader = _v2OrgIsLeader(role);
3714
+ const color = colorOf(role, i);
3715
+ const displayName = role.name || role.title || role.id;
3716
+ const subType = role.type || role.agent_type || '';
3717
+ const MAX_LBL = Math.floor(R / 4.2);
3718
+ const nameText = displayName.length > MAX_LBL ? displayName.slice(0, MAX_LBL - 1) + '…' : displayName;
3719
+ const subTypeText = subType.length > MAX_LBL ? subType.slice(0, MAX_LBL - 1) + '…' : subType;
3720
+ const outerRing = leader ? `<circle r="${R + 9}" fill="none" stroke="${color}" stroke-width="0.6" opacity="0.18"/>` : '';
3721
+ const nameY = subType ? (R * 0.55 + 2).toFixed(0) : (R * 0.55 + 8).toFixed(0);
3722
+ const avR = Math.round(R * 0.52);
3723
+ const avatarSrc = v2RoleAvatar(role);
3724
+ nodesHTML += `<g class="v2oc-node" transform="translate(${p.x.toFixed(1)},${p.y.toFixed(1)})">
3725
+ <title>${esc(displayName)}${subType ? ` · ${esc(subType)}` : ''}</title>
3726
+ ${outerRing}
3727
+ <circle r="${R}" fill="oklch(12% 0.008 55)" stroke="${color}" stroke-width="${leader ? 2.5 : 1.8}" filter="url(#v2glow)"/>
3728
+ <image href="${avatarSrc}" x="${-avR}" y="${-R + 2}" width="${avR * 2}" height="${avR * 2}" clip-path="url(#${nodeClipIds[i]})" preserveAspectRatio="xMidYMid meet" onerror="this.setAttribute('href','data/avatars/coder.svg')"/>
3729
+ <text text-anchor="middle" y="${nameY}" font-size="9" font-weight="${leader?'600':'500'}" font-family="'Inter',system-ui,sans-serif" fill="${color}">${esc(nameText)}</text>
3730
+ ${subTypeText ? `<text text-anchor="middle" y="${+nameY + 11}" font-size="7.5" font-family="'Inter',system-ui,sans-serif" fill="oklch(48% 0.005 75)">${esc(subTypeText)}</text>` : ''}
3731
+ </g>`;
3732
+ });
3733
+
3734
+ // ── assemble HTML ─────────────────────────────────────
3735
+ pane.innerHTML = `
3736
+ <div class="org-meta-strip">
3737
+ <div class="oms-cell"><div class="oms-lbl">Goal</div><div class="oms-val" style="color:var(--text-mid);font-size:11px;line-height:1.4">${esc(goalText)}</div></div>
3738
+ <div class="oms-cell"><div class="oms-lbl">Topology</div><div class="oms-val">${esc(topo)}</div></div>
3739
+ <div class="oms-cell"><div class="oms-lbl">Created</div><div class="oms-val" style="color:var(--text-mid)">${esc(created)}</div></div>
3740
+ </div>
3741
+ <div class="org-chart-wrap">
3742
+ <svg class="org-chart-svg" id="v2-org-svg" viewBox="0 0 ${W} ${H}" style="min-height:${Math.round(H * 0.5)}px">
3743
+ <defs>
3744
+ <marker id="v2ac" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0.5 L0,6.5 L6,3.5 z" fill="oklch(72% 0.18 75 / 0.8)"/></marker>
3745
+ <marker id="v2ar" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0.5 L0,6.5 L6,3.5 z" fill="oklch(65% 0.12 240 / 0.7)"/></marker>
3746
+ <marker id="v2ah" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0.5 L0,6.5 L6,3.5 z" fill="oklch(65% 0.15 150 / 0.75)"/></marker>
3747
+ <filter id="v2glow" x="-50%" y="-50%" width="200%" height="200%">
3748
+ <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b"/>
3749
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
3750
+ </filter>
3751
+ ${avatarClipDefs}
3752
+ </defs>
3753
+ <rect width="${W}" height="${H}" fill="oklch(12% 0.008 55)"/>
3754
+ <g id="v2oc-edges">${edgesHTML}</g>
3755
+ <g id="v2oc-nodes">${nodesHTML}</g>
3756
+ <g id="v2oc-particles" opacity="${isRunning ? 1 : 0}">${particlesHTML}</g>
3757
+ </svg>
3758
+ </div>
3759
+ <div class="org-chart-legend" style="margin-top:10px">
3760
+ <div class="ocl-item"><span style="display:inline-block;width:20px;height:2px;background:oklch(72% 0.18 75);vertical-align:middle;margin-right:3px;border-radius:1px"></span>Command</div>
3761
+ <div class="ocl-item"><span style="display:inline-block;width:20px;height:1px;background:oklch(65% 0.12 240);vertical-align:middle;margin-right:3px;border-top:1px dashed oklch(65% 0.12 240)"></span>Report</div>
3762
+ <div class="ocl-item"><span style="display:inline-block;width:20px;height:2px;background:oklch(65% 0.15 150);vertical-align:middle;margin-right:3px;border-radius:1px"></span>Handoff</div>
3763
+ </div>`;
3764
+
3765
+ // ── GSAP entrance animation ───────────────────────────
3766
+ if (typeof gsap !== 'undefined') {
3767
+ const nodes = pane.querySelectorAll('.v2oc-node');
3768
+ const edges = pane.querySelectorAll('.v2oc-edge');
3769
+ // Nodes: scale + fade in, staggered
3770
+ gsap.fromTo(nodes,
3771
+ { scale: 0, autoAlpha: 0 },
3772
+ { scale: 1, autoAlpha: 1, duration: 0.5, ease: 'back.out(1.7)', stagger: 0.08 }
3773
+ );
3774
+ // Edges: draw along path after nodes appear
3775
+ edges.forEach((path, i) => {
3776
+ const pathLen = path.getTotalLength ? path.getTotalLength() : 200;
3777
+ gsap.fromTo(path,
3778
+ { strokeDasharray: pathLen, strokeDashoffset: pathLen, autoAlpha: 0 },
3779
+ { strokeDashoffset: 0, autoAlpha: 1, duration: 0.65, ease: 'power2.out', delay: 0.3 + i * 0.04 }
3780
+ );
3781
+ });
3782
+ } else {
3783
+ // CSS fallback: simple fade-in stagger
3784
+ pane.querySelectorAll('.v2oc-node').forEach((el, i) => {
3785
+ el.style.animation = `v2nodeIn 0.45s cubic-bezier(0.34,1.56,0.64,1) ${(i * 0.08).toFixed(2)}s forwards`;
3786
+ el.style.opacity = '0';
3787
+ });
3788
+ }
3789
+ }
3790
+
3791
+
3792
+ // ── avatar lookup ────────────────────────────
3793
+ const _v2AvatarKnown = new Set([
3794
+ 'coder','senior-developer','reviewer','tester','planner','researcher',
3795
+ 'security-architect','security-auditor','threat-detection','input-validator',
3796
+ 'path-validator','safe-executor','hierarchical-coord','mesh-coordinator',
3797
+ 'adaptive-coordinator','collective-coord','queen-coordinator','worker-specialist',
3798
+ 'byzantine-coord','raft-manager','gossip-coordinator','crdt-synchronizer',
3799
+ 'quorum-manager','consensus-coordinator','perf-analyzer','benchmarker',
3800
+ 'task-orchestrator','memory-coordinator','load-balancer','resource-allocator',
3801
+ 'pr-manager','code-review-swarm','issue-tracker','release-manager','repo-architect',
3802
+ 'workflow-automation','sparc-coord','sparc-coder','specification','pseudocode',
3803
+ 'architecture','refinement','backend-dev','frontend-developer','mobile-dev',
3804
+ 'ml-developer','cicd-engineer','system-architect','ai-engineer','model-qa',
3805
+ 'data-engineer','analytics-reporter','experiment-tracker','data-consolidator',
3806
+ 'devops-automator','sre','incident-commander','infrastructure','database-optimizer',
3807
+ 'cloud-architect','prosecutor','defender','judge','case-analyst','trial-director',
3808
+ 'legal-compliance','technical-writer','content-creator','seo-specialist',
3809
+ 'social-media','email-marketing','ai-citation','product-manager','sprint-prioritizer',
3810
+ 'launch-strategist','pricing-strategist','feedback-synthesizer','cro-specialist',
3811
+ 'sales-engineer','deal-strategist','account-strategist','outbound-strategist',
3812
+ 'pipeline-analyst','sales-coach','support-responder','discovery-coach',
3813
+ 'proposal-strategist','game-designer','narrative-designer','level-designer',
3814
+ 'game-audio-engineer','technical-artist','unity-architect','blockchain-auditor',
3815
+ 'solidity-engineer','zk-steward','studio-producer','project-shepherd',
3816
+ 'senior-pm','studio-operations','workflow-architect','adaptive-coordinator2',
3817
+ 'api-tester','evidence-collector','reality-checker','production-validator',
3818
+ 'finance-tracker','accounts-payable','recruitment','visionos-engineer',
3819
+ 'embedded-firmware','ios-developer','mobile-app-builder','mcp-builder',
3820
+ 'automation-governance','payment-agent','compliance-auditor','trend-researcher',
3821
+ 'scout-explorer'
3822
+ ]);
3823
+ const _v2TypeFallback = {
3824
+ planner:'planner', coordinator:'hierarchical-coord', researcher:'researcher',
3825
+ analyst:'researcher', reviewer:'reviewer', coder:'coder', tester:'tester',
3826
+ architect:'system-architect', engineer:'backend-dev', developer:'coder',
3827
+ manager:'product-manager', security:'security-architect',
3828
+ reporter:'analytics-reporter', strategist:'product-manager',
3829
+ designer:'game-designer', writer:'technical-writer', auditor:'security-auditor',
3830
+ };
3831
+ function v2RoleAvatar(role) {
3832
+ const id = ((role && role.id) || '').toLowerCase().replace(/\s+/g,'-');
3833
+ const type = ((role && (role.type || role.agent_type)) || '').toLowerCase().replace(/\s+/g,'-');
3834
+ if (_v2AvatarKnown.has(id)) return `data/avatars/${id}.svg`;
3835
+ if (_v2AvatarKnown.has(type)) return `data/avatars/${type}.svg`;
3836
+ for (const k of _v2AvatarKnown) {
3837
+ if (id && (id.includes(k) || k.includes(id))) return `data/avatars/${k}.svg`;
3838
+ }
3839
+ for (const [t, av] of Object.entries(_v2TypeFallback)) {
3840
+ if ((type && type.includes(t)) || (id && id.includes(t))) return `data/avatars/${av}.svg`;
3841
+ }
3842
+ return 'data/avatars/coder.svg';
3843
+ }
3844
+
3845
+ // ── org roles ───────────────────────────────
3846
+ function v2RenderOrgRoles() {
3847
+ if (!_v2SelOrg) return;
3848
+ const roles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
3849
+ const V2_ROLE_COLORS = ['oklch(72% 0.18 75)','oklch(65% 0.12 240)','oklch(65% 0.15 150)','oklch(65% 0.13 290)','oklch(65% 0.14 35)','oklch(62% 0.12 195)'];
3850
+ const pane = document.getElementById('odt-roles');
3851
+ if (!pane) return;
3852
+ if (!roles.length) {
3853
+ pane.innerHTML = '<div class="empty">No roles defined</div>';
3854
+ return;
3224
3855
  }
3856
+ // Determine leader: explicit reports_to=undefined + type=planner/coordinator, or first role, or id=boss
3857
+ const leaderIds = new Set(roles.filter(r => r.id === 'boss' || r.type === 'coordinator' || r.type === 'planner').map(r => r.id));
3858
+ if (!leaderIds.size) leaderIds.add(roles[0]?.id);
3859
+ pane.innerHTML = `<div class="roles-v2-grid">${roles.map((r, i) => {
3860
+ const color = V2_ROLE_COLORS[i % V2_ROLE_COLORS.length];
3861
+ const isLeader = leaderIds.has(r.id);
3862
+ const resps = Array.isArray(r.responsibilities) ? r.responsibilities : [];
3863
+ const displayName = r.name || r.title || r.id;
3864
+ const roleType = r.type || r.agent_type || '';
3865
+ const avatarSrc = v2RoleAvatar(r);
3866
+ return `<div class="role-v2" style="border-top:2px solid ${color}">
3867
+ <div class="rv2-head">
3868
+ <img class="rv2-avatar" src="${avatarSrc}" alt="${esc(displayName)}" loading="lazy" onerror="this.src='data/avatars/coder.svg'"/>
3869
+ <div class="rv2-info">
3870
+ <div class="rv2-title">${esc(displayName)}${isLeader ? ' <span style="font-size:10px;color:var(--accent);font-weight:400;opacity:0.8">lead</span>' : ''}</div>
3871
+ ${roleType ? `<span class="rv2-agent">${esc(roleType)}</span>` : ''}
3872
+ </div>
3873
+ </div>
3874
+ <div class="rv2-id">${esc(r.id)}</div>
3875
+ ${r.reports_to ? `<div class="rv2-reports">reports to <span>${esc(r.reports_to)}</span></div>` : ''}
3876
+ ${resps.length ? `<div class="rv2-resps">${resps.map(s=>`<div class="rv2-resp">${esc(s)}</div>`).join('')}</div>` : ''}
3877
+ </div>`;
3878
+ }).join('')}</div>`;
3879
+ }
3880
+
3881
+ // ── org activity ────────────────────────────
3882
+ function v2RenderOrgActivity() {
3883
+ if (!_v2SelOrg) return;
3884
+ const activity = _v2OrgData?._activity || [];
3885
+ const orgEvents = _v2OrgEventLog[_v2SelOrg] || [];
3886
+ const events = [...activity, ...orgEvents].sort((a,b) => (b.ts||0)-(a.ts||0)).slice(0,80);
3887
+ const pane = document.getElementById('odt-activity');
3888
+ if (!pane) return;
3889
+ const fmtOrgEvType = t => {
3890
+ const m={'org:start':'start','org:stop':'stop','org:complete':'done','org:create':'create','org:heartbeat':'hb','org:agent:online':'agent-on','org:comms':'comms'};
3891
+ return m[t]||(t||'').replace(/^org:/,'');
3892
+ };
3893
+ if (!events.length) {
3894
+ pane.innerHTML = '<div class="empty">No activity recorded</div>';
3895
+ return;
3896
+ }
3897
+ pane.innerHTML = `<div class="act-v2-list">${events.map(ev => {
3898
+ const t = ev.ts ? new Date(ev.ts).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
3899
+ const detail = ev.role||ev.msg||ev.agent||'';
3900
+ return `<div class="av2-row"><span class="av2-time">${esc(t)}</span><span class="av2-type">${esc(fmtOrgEvType(ev.type))}</span><span class="av2-msg">${esc(detail)}</span></div>`;
3901
+ }).join('')}</div>`;
3902
+ }
3903
+
3904
+ // ── org health ──────────────────────────────
3905
+ function v2RenderOrgHealth() {
3906
+ if (!_v2SelOrg) return;
3907
+ const listOrg = _v2Orgs.find(o => o.name === _v2SelOrg) || {};
3908
+ const health = _v2OrgData?._health;
3909
+ const roles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles.length : 0;
3910
+ const pane = document.getElementById('odt-health');
3911
+ if (!pane) return;
3912
+ pane.innerHTML = `
3913
+ <div class="health-v2-grid">
3914
+ <div class="hv2-cell"><div class="hv2-lbl">Status</div><div class="hv2-val ${(_v2OrgData?.running??listOrg.running)?'green':''}">${(_v2OrgData?.running??listOrg.running)?'LIVE':'IDLE'}</div></div>
3915
+ <div class="hv2-cell"><div class="hv2-lbl">Roles</div><div class="hv2-val">${roles}</div></div>
3916
+ <div class="hv2-cell"><div class="hv2-lbl">Topology</div><div class="hv2-val" style="font-size:14px">${esc((_v2OrgData?.topology||'—').toUpperCase())}</div></div>
3917
+ ${health?.agents_active!=null?`<div class="hv2-cell"><div class="hv2-lbl">Agents</div><div class="hv2-val ${health.agents_active>0?'green':''}">${health.agents_active}</div></div>`:''}
3918
+ ${health?.tasks_pending!=null?`<div class="hv2-cell"><div class="hv2-lbl">Tasks</div><div class="hv2-val ${health.tasks_pending>5?'amber':''}">${health.tasks_pending}</div></div>`:''}
3919
+ </div>
3920
+ ${health?.errors?.length?`<div style="margin-top:12px"><div class="m-group-title">Errors</div>${health.errors.slice(0,5).map(e=>`<div style="font-size:11px;color:var(--red);padding:4px 0;border-bottom:1px solid var(--border)">${esc(e)}</div>`).join('')}</div>`:`<div style="margin-top:10px;font-size:12px;color:var(--text-lo)">${listOrg.running?'Running — no errors detected':'Org is idle'}</div>`}
3921
+ `;
3225
3922
  }
3226
3923
 
3924
+ // stop org
3925
+ window.v2StopOrg = async function() {
3926
+ if (!_v2SelOrg) return;
3927
+ const stopped = _v2SelOrg;
3928
+ try { await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop`, {method:'POST'}); } catch(_) {}
3929
+ setTimeout(async () => { await renderOrgs(); if (_v2SelOrg === stopped) v2SelectOrg(stopped); }, 600);
3930
+ };
3931
+
3932
+ // live SSE for org events
3933
+ (function v2OrgSSE() {
3934
+ let src;
3935
+ function connect() {
3936
+ if (src) src.close();
3937
+ src = new EventSource('/api/events');
3938
+ src.onmessage = e => {
3939
+ try {
3940
+ const ev = JSON.parse(e.data);
3941
+ if (!ev?.org || !ev?.type?.startsWith('org:')) return;
3942
+ const n = ev.org;
3943
+ const evDir = ev.dir || '';
3944
+ if (!evDir || (DIR && evDir !== DIR)) return;
3945
+ if (!_v2OrgEventLog[n]) _v2OrgEventLog[n] = [];
3946
+ _v2OrgEventLog[n].push(ev);
3947
+ if (_v2OrgEventLog[n].length > 50) _v2OrgEventLog[n].shift();
3948
+ if (n === _v2SelOrg && _v2OrgTab === 'activity') v2RenderOrgActivity();
3949
+ if (ev.type === 'org:start' || ev.type === 'org:stop') {
3950
+ setTimeout(async () => {
3951
+ await renderOrgs();
3952
+ if (_v2SelOrg === n) {
3953
+ const listOrg = _v2Orgs.find(o => o.name === _v2SelOrg) || {};
3954
+ if (_v2OrgData) _v2OrgData.running = listOrg.running;
3955
+ v2UpdateOrgHeader(listOrg, _v2OrgData);
3956
+ if (_v2OrgTab === 'health') v2RenderOrgHealth();
3957
+ }
3958
+ }, 400);
3959
+ }
3960
+ } catch(_) {}
3961
+ };
3962
+ src.onerror = () => setTimeout(connect, 5000);
3963
+ }
3964
+ connect();
3965
+ })();
3966
+
3227
3967
  // ── density toggle ─────────────────────────────────────────
3228
3968
  let compactMode = false;
3229
3969
  function toggleDensity() {