agentix-cli 0.32.0 → 0.34.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 (93) hide show
  1. package/apps/mac-voice/Sources/AgentXVoice/AgentClient.swift +43 -2
  2. package/apps/mac-voice/Sources/AgentXVoice/Config.swift +6 -1
  3. package/apps/mac-voice/Sources/AgentXVoice/Progress.swift +5 -3
  4. package/apps/mac-voice/Sources/AgentXVoice/Speech.swift +5 -4
  5. package/apps/mac-voice/Sources/AgentXVoice/main.swift +27 -5
  6. package/dist/{App-2LAWZNMG.js → App-QHPZXA42.js} +2 -2
  7. package/dist/{ChatApp-2KRDHMTV.js → ChatApp-EAHHH365.js} +2 -2
  8. package/dist/{agent-ZRRIMLTX.js → agent-KBBO7OZS.js} +2 -2
  9. package/dist/{board-dashboard-LL3YVODL.js → board-dashboard-74OLMLAD.js} +35 -35
  10. package/dist/board-dashboard-74OLMLAD.js.map +1 -0
  11. package/dist/{chunk-WFIBOAXN.js → chunk-33G4TIZT.js} +148 -141
  12. package/dist/chunk-33G4TIZT.js.map +1 -0
  13. package/dist/{chunk-5FEQI22L.js → chunk-5H5L75RN.js} +2 -2
  14. package/dist/chunk-6WGTLDME.js +551 -0
  15. package/dist/chunk-6WGTLDME.js.map +1 -0
  16. package/dist/{chunk-G5MFPNGJ.js → chunk-CY24RXE4.js} +2 -2
  17. package/dist/{chunk-JGJE5Y2J.js → chunk-IUNCX6BG.js} +2 -2
  18. package/dist/{chunk-4CR463C6.js → chunk-IVAGDL5M.js} +2 -2
  19. package/dist/{chunk-K6E7WJ7O.js → chunk-JDC35PWM.js} +2 -2
  20. package/dist/{chunk-JWKIFAXA.js → chunk-MJYE5ADH.js} +2 -2
  21. package/dist/{chunk-MFVI5FOM.js → chunk-PG7BAZIZ.js} +2 -2
  22. package/dist/{chunk-W3YGESNK.js → chunk-QKNAGTJK.js} +2 -2
  23. package/dist/{chunk-2DBGOCCW.js → chunk-QXAUKNMW.js} +2 -2
  24. package/dist/chunk-SPJWXIAV.js +13 -0
  25. package/dist/chunk-SPJWXIAV.js.map +1 -0
  26. package/dist/{chunk-B2XTYYXV.js → chunk-U5P7XGCB.js} +2 -2
  27. package/dist/{chunk-A4YR3PQY.js → chunk-UJF5JUFJ.js} +2 -2
  28. package/dist/{chunk-GWQIMAJ7.js → chunk-X6HLI6XB.js} +2 -2
  29. package/dist/{chunk-6VQKHMEQ.js → chunk-ZMFUBIEQ.js} +5 -5
  30. package/dist/cli.js +1 -1
  31. package/dist/client-CAGQYJML.js +2 -0
  32. package/dist/config-NPLE42XY.js +2 -0
  33. package/dist/config-mutator-FFTODESU.js +2 -0
  34. package/dist/doctor-X4KHQFRF.js +2 -0
  35. package/dist/index.d.ts +110 -0
  36. package/dist/index.js +1 -1
  37. package/dist/program-ISXECD4J.js +2 -0
  38. package/dist/query-XLJ6I34S.js +2 -0
  39. package/dist/{registry-OB6S66XX.js → registry-3RN7I6YW.js} +2 -2
  40. package/dist/{request-planner-F5NVL2G7.js → request-planner-JX7M3PTD.js} +2 -2
  41. package/dist/{review-candidates-IRPKFWL2.js → review-candidates-UTS4RX4U.js} +2 -2
  42. package/dist/{routing-OSPRKRAY.js → routing-D7APOAP6.js} +2 -2
  43. package/dist/scheduler-EGR5AJAQ.js +2 -0
  44. package/dist/{seat-2MHKVMW2.js → seat-UA2ROH3V.js} +2 -2
  45. package/dist/{select-7LCHJVPK.js → select-WQXHYEPD.js} +2 -2
  46. package/dist/web/activity-graph.global.js +11 -11
  47. package/dist/web/activity-graph.global.js.map +1 -1
  48. package/dist/wiki-RNANPNUX.js +2 -0
  49. package/package.json +1 -1
  50. package/dist/board-dashboard-LL3YVODL.js.map +0 -1
  51. package/dist/chunk-2DMVKLLE.js +0 -13
  52. package/dist/chunk-2DMVKLLE.js.map +0 -1
  53. package/dist/chunk-H57XXGMF.js +0 -550
  54. package/dist/chunk-H57XXGMF.js.map +0 -1
  55. package/dist/chunk-WFIBOAXN.js.map +0 -1
  56. package/dist/client-IP643GRO.js +0 -2
  57. package/dist/config-IVDP46ZG.js +0 -2
  58. package/dist/config-mutator-UCGAV3SP.js +0 -2
  59. package/dist/doctor-MMT4SREV.js +0 -2
  60. package/dist/program-7ULXLTD6.js +0 -2
  61. package/dist/query-RHVR6QUZ.js +0 -2
  62. package/dist/scheduler-JL6K5HHI.js +0 -2
  63. package/dist/wiki-MY32MON3.js +0 -2
  64. /package/dist/{App-2LAWZNMG.js.map → App-QHPZXA42.js.map} +0 -0
  65. /package/dist/{ChatApp-2KRDHMTV.js.map → ChatApp-EAHHH365.js.map} +0 -0
  66. /package/dist/{agent-ZRRIMLTX.js.map → agent-KBBO7OZS.js.map} +0 -0
  67. /package/dist/{chunk-5FEQI22L.js.map → chunk-5H5L75RN.js.map} +0 -0
  68. /package/dist/{chunk-G5MFPNGJ.js.map → chunk-CY24RXE4.js.map} +0 -0
  69. /package/dist/{chunk-JGJE5Y2J.js.map → chunk-IUNCX6BG.js.map} +0 -0
  70. /package/dist/{chunk-4CR463C6.js.map → chunk-IVAGDL5M.js.map} +0 -0
  71. /package/dist/{chunk-K6E7WJ7O.js.map → chunk-JDC35PWM.js.map} +0 -0
  72. /package/dist/{chunk-JWKIFAXA.js.map → chunk-MJYE5ADH.js.map} +0 -0
  73. /package/dist/{chunk-MFVI5FOM.js.map → chunk-PG7BAZIZ.js.map} +0 -0
  74. /package/dist/{chunk-W3YGESNK.js.map → chunk-QKNAGTJK.js.map} +0 -0
  75. /package/dist/{chunk-2DBGOCCW.js.map → chunk-QXAUKNMW.js.map} +0 -0
  76. /package/dist/{chunk-B2XTYYXV.js.map → chunk-U5P7XGCB.js.map} +0 -0
  77. /package/dist/{chunk-A4YR3PQY.js.map → chunk-UJF5JUFJ.js.map} +0 -0
  78. /package/dist/{chunk-GWQIMAJ7.js.map → chunk-X6HLI6XB.js.map} +0 -0
  79. /package/dist/{chunk-6VQKHMEQ.js.map → chunk-ZMFUBIEQ.js.map} +0 -0
  80. /package/dist/{client-IP643GRO.js.map → client-CAGQYJML.js.map} +0 -0
  81. /package/dist/{config-IVDP46ZG.js.map → config-NPLE42XY.js.map} +0 -0
  82. /package/dist/{config-mutator-UCGAV3SP.js.map → config-mutator-FFTODESU.js.map} +0 -0
  83. /package/dist/{doctor-MMT4SREV.js.map → doctor-X4KHQFRF.js.map} +0 -0
  84. /package/dist/{program-7ULXLTD6.js.map → program-ISXECD4J.js.map} +0 -0
  85. /package/dist/{query-RHVR6QUZ.js.map → query-XLJ6I34S.js.map} +0 -0
  86. /package/dist/{registry-OB6S66XX.js.map → registry-3RN7I6YW.js.map} +0 -0
  87. /package/dist/{request-planner-F5NVL2G7.js.map → request-planner-JX7M3PTD.js.map} +0 -0
  88. /package/dist/{review-candidates-IRPKFWL2.js.map → review-candidates-UTS4RX4U.js.map} +0 -0
  89. /package/dist/{routing-OSPRKRAY.js.map → routing-D7APOAP6.js.map} +0 -0
  90. /package/dist/{scheduler-JL6K5HHI.js.map → scheduler-EGR5AJAQ.js.map} +0 -0
  91. /package/dist/{seat-2MHKVMW2.js.map → seat-UA2ROH3V.js.map} +0 -0
  92. /package/dist/{select-7LCHJVPK.js.map → select-WQXHYEPD.js.map} +0 -0
  93. /package/dist/{wiki-MY32MON3.js.map → wiki-RNANPNUX.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/daemon/monitor-capacity.ts","../src/daemon/ui/pages/monitor.ts","../src/daemon/activity-timeline.ts","../src/daemon/ui/asset-url.ts","../src/daemon/ui/pages/activity.ts","../src/daemon/ui/pages/workflow-editor.ts","../src/daemon/board-dashboard.ts","../src/daemon/vendor/sortable.ts","../src/daemon/admin-panel.ts","../src/daemon/ui/pages/admin.ts","../src/daemon/graph-panel.ts","../src/daemon/ui/pages/graph.ts","../src/daemon/observability-panel.ts","../src/daemon/ui/pages/observability.ts","../src/daemon/ledger-panel.ts","../src/daemon/ui/pages/cost.ts","../src/daemon/ui/pages/projects.ts","../src/daemon/activity-graph-panel.ts","../src/daemon/activity-graph-attribution.ts","../src/daemon/activity-graph-forge.ts","../src/daemon/agent-panel.ts","../src/daemon/ui/pages/agent.ts","../src/daemon/ui-labels.ts","../src/daemon/ui/pages/live.ts","../src/daemon/ui/pages/mesh.css.ts","../src/daemon/ui/pages/mesh-shared.client.ts","../src/daemon/ui/pages/mesh-drill.client.ts","../src/daemon/ui/pages/mesh-analytics.client.ts","../src/daemon/ui/pages/mesh-ops.client.ts","../src/daemon/ui/pages/mesh.ts","../src/daemon/mesh-analytics-api.ts","../src/daemon/ui/pages/boards.ts","../src/daemon/ui/pages/glossary.ts","../src/daemon/ui/pages/workflows.ts","../src/daemon/ui/pages/procedures.ts","../src/daemon/ui/pages/task.ts","../src/daemon/ui/pages/history.ts","../src/daemon/workflows-api.ts"],"sourcesContent":["// --- Ordering the backlog -------------------------------------------------\n//\n// This used to fit actions into a stated minute budget. That asked the\n// reviewer to estimate durations it could not know, and asked the operator to\n// know how many minutes they had. What actually decides order is how fast a\n// delay costs something, and that is knowable: a client with a stated\n// `respondWithin` accrues against it, and everything else is flat.\n//\n// A view over the backlog, never a deletion or automatic dismissal.\n\nexport interface DecayAction {\n needsHuman: boolean\n /** When the review that produced this action last changed, ms. */\n updatedAt?: number\n clientId?: string\n}\n\n/** clientId -> minutes before a wait starts costing. Absent = no clock. */\nexport type ClientClocks = Record<string, number | undefined>\n\nexport interface Decay {\n /** Higher is more urgent. Only meaningful relative to other actions. */\n score: number\n ageMs: number\n /** True when a client is actually waiting, so the cost grows with time. */\n rising: boolean\n /** True once the wait has passed the client's stated clock. */\n overdue: boolean\n}\n\nexport function decayOf(action: DecayAction, clocks: ClientClocks, now = Date.now()): Decay {\n const ageMs = Math.max(0, now - (action.updatedAt || now))\n const clockMinutes = action.clientId ? clocks[action.clientId] : undefined\n if (!clockMinutes) {\n // Flat cost. Age still breaks ties, but this can never outrank someone\n // who is genuinely waiting on an answer.\n // One day in ms, inlined: decayOf is stringified and shipped to the\n // browser, where a module-scope constant would be unbound.\n return { score: ageMs / 86_400_000, ageMs, rising: false, overdue: false }\n }\n const ratio = ageMs / (clockMinutes * 60_000)\n return { score: 1000 + ratio, ageMs, rising: true, overdue: ratio >= 1 }\n}\n\n/** Indices of `actions`, most costly to leave first. Stable for equal scores\n * so the list does not reshuffle under the reader between refreshes. */\nexport function rankByDecay(actions: DecayAction[], clocks: ClientClocks, now = Date.now()): number[] {\n return actions\n .map((a, i) => ({ i, s: decayOf(a, clocks, now).score }))\n .sort((x, y) => y.s - x.s || x.i - y.i)\n .map(x => x.i)\n}\n","import { rankByDecay, decayOf } from \"../../monitor-capacity\"\nimport { renderShell, type TopbarPeer } from \"..\"\nimport { injectFns } from \"../inject\"\n\n/** Feather-style 24x24 stroked glyphs, sized at the call site. */\nconst ICON = {\n refresh: '<path d=\"M21 2v6h-6\"/><path d=\"M3 12a9 9 0 0 1 15-6.7L21 8\"/><path d=\"M3 22v-6h6\"/><path d=\"M21 12a9 9 0 0 1-15 6.7L3 16\"/>',\n clock: '<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/>',\n alert: '<path d=\"M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/>',\n gavel: '<path d=\"M12 3 6 9\"/><path d=\"m9 6 6 6\"/><path d=\"M15 9 9 15\"/><path d=\"m12 12 6 6\"/><path d=\"M4 21h10\"/>',\n loop: '<path d=\"M17 2l4 4-4 4\"/><path d=\"M3 11V9a4 4 0 0 1 4-4h14\"/><path d=\"M7 22l-4-4 4-4\"/><path d=\"M21 13v2a4 4 0 0 1-4 4H3\"/>',\n broom: '<path d=\"M3 21h18\"/><path d=\"M12 3v9\"/><path d=\"m7 12 5-3 5 3-1.5 6h-7Z\"/>',\n server: '<rect x=\"2\" y=\"3\" width=\"20\" height=\"7\" rx=\"2\"/><rect x=\"2\" y=\"14\" width=\"20\" height=\"7\" rx=\"2\"/><path d=\"M6 6.5h.01\"/><path d=\"M6 17.5h.01\"/>',\n inbox: '<path d=\"M21 12h-6l-2 3h-2l-2-3H3\"/><path d=\"M5 5h14l2 7v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-5Z\"/>',\n cross: '<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"m15 9-6 6\"/><path d=\"m9 9 6 6\"/>',\n file: '<path d=\"M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v5h5\"/>',\n search: '<circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/>',\n chev: '<path d=\"m6 9 6 6 6-6\"/>',\n chevr: '<path d=\"m9 6 6 6-6 6\"/>',\n ext: '<path d=\"M15 3h6v6\"/><path d=\"M10 14 21 3\"/><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/>',\n pulse: '<path d=\"M3 12h4l3-8 4 16 3-8h4\"/>',\n plug: '<path d=\"M9 2v6\"/><path d=\"M15 2v6\"/><path d=\"M6 8h12v3a6 6 0 0 1-12 0Z\"/><path d=\"M12 17v5\"/>',\n check: '<path d=\"M20 6 9 17l-5-5\"/>',\n route: '<circle cx=\"6\" cy=\"19\" r=\"3\"/><circle cx=\"18\" cy=\"5\" r=\"3\"/><path d=\"M9 19h6a4 4 0 0 0 0-8H9a4 4 0 0 1 0-8h6\"/>',\n // --- channel glyphs (generic, never brand marks) ---\n branch: '<circle cx=\"6\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><circle cx=\"18\" cy=\"8\" r=\"3\"/><path d=\"M18 11v1a3 3 0 0 1-3 3H9\"/><path d=\"M6 9v6\"/>',\n send: '<path d=\"M22 2 11 13\"/><path d=\"M22 2l-7 20-4-9-9-4Z\"/>',\n msg: '<path d=\"M21 11.5a8.4 8.4 0 0 1-12.9 7.5L3 21l2-5.1A8.4 8.4 0 1 1 21 11.5Z\"/>',\n code: '<path d=\"m9 18-6-6 6-6\"/><path d=\"m15 6 6 6-6 6\"/>',\n hash: '<path d=\"M4 9h16\"/><path d=\"M4 15h16\"/><path d=\"M10 3 8 21\"/><path d=\"M16 3l-2 18\"/>',\n x: '<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>',\n trace: '<path d=\"M4 4v16h16\"/><path d=\"m7 15 4-4 3 3 5-6\"/>',\n}\nconst svg = (d: string, size = 14) =>\n '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"' + size + '\" height=\"' + size + '\" aria-hidden=\"true\">' + d + \"</svg>\"\n\n/** Segmented radio group. Values carry the payload; labels stay human. */\nconst secLabel = (icon: string, text: string, countId?: string, rightHtml = \"\") =>\n '<div class=\"ax-sec-label\"><h3>' + svg(icon, 13) + text +\n (countId ? '<span class=\"ax-sec-label__n\" id=\"' + countId + '\"></span>' : \"\") + \"</h3>\" + rightHtml + \"</div>\"\n\nexport function renderMonitorPage(opts: { peers?: TopbarPeer[] } = {}): string {\n return renderShell({\n title: \"AgentX · Monitor\", activeTab: \"monitor\", subtitle: \"Monitor\", peers: opts.peers,\n body: `<div class=\"bf\">\n\n<div class=\"bf-head\">\n <div>\n <span class=\"bf-kicker\">Your attention, protected</span>\n <h1>What needs you.</h1>\n </div>\n <div class=\"bf-sync\">\n <span class=\"bf-sync__t\" id=\"synced\">Gathering&hellip;</span>\n <a class=\"ax-btn ax-btn--sm\" href=\"/activity\" title=\"What ran, and when\">${svg(ICON.pulse, 13)} Activity</a>\n <button class=\"ax-btn\" id=\"refresh\" title=\"Refresh now\">${svg(ICON.refresh)} Refresh</button>\n </div>\n</div>\n\n<p id=\"coverage\" class=\"bf-sr\" role=\"status\">Gathering session reviews across your mesh&hellip;</p>\n<div class=\"bf-strip\" id=\"strip\"></div>\n<div id=\"coverage-note\"></div>\n<div id=\"principals\"></div>\n\n<p id=\"notice\" role=\"status\" aria-live=\"polite\"></p>\n\n<div class=\"bf-grid\">\n <section class=\"bf-col\">\n ${secLabel(ICON.alert, \"Only you\", \"you-count\",\n '<span class=\"bf-why\">needs your authority, your relationship, or something the system cannot know</span>'\n + '<button class=\"ax-btn ax-btn--sm\" id=\"clear-actions\" title=\"Mark every open action done on every node\">Clear all</button>')}\n <div id=\"you\" class=\"bf-col\"></div>\n\n ${secLabel(ICON.plug, \"Agents can handle\", \"agents-count\", '<span class=\"bf-why\">no human needed &mdash; not yet automatic</span>')}\n <div id=\"agents\" class=\"bf-col\"></div>\n <button class=\"ax-btn ax-btn--sm\" id=\"more-actions\" hidden></button>\n\n <div id=\"handled\"></div>\n </section>\n <aside class=\"bf-col\">\n ${secLabel(ICON.pulse, \"In motion\", \"run-count\")}\n <div id=\"running\" class=\"bf-col\"></div>\n </aside>\n</div>\n\n${secLabel(ICON.file, \"Session briefings\", \"brief-count\")}\n<div class=\"bf-toolbar\">\n <div class=\"bf-search\">${svg(ICON.search)}\n <input id=\"search\" type=\"search\" placeholder=\"Search sessions&hellip;\" aria-label=\"Search sessions\">\n </div>\n <div class=\"bf-filters\" id=\"filters\" role=\"group\" aria-label=\"Filter briefings\"></div>\n</div>\n<div id=\"reviews\" class=\"bf-col\"></div>\n\n<section class=\"ax-panel bf-connect\">\n <div class=\"bf-connect__head\">${svg(ICON.plug, 17)}<h2>Connect a CLI session</h2>\n <span class=\"bf-connect__note\">Daemon-owned processes only</span></div>\n <div class=\"bf-discover\">\n <div class=\"ax-field\"><label for=\"node\">Node</label><select id=\"node\"></select></div>\n <button class=\"ax-btn\" id=\"discover\">${svg(ICON.search)} Discover CLIs</button>\n </div>\n <div class=\"bf-cli\" id=\"discovered\"></div>\n <details class=\"bf-manual\">\n <summary>${svg(ICON.chev, 12)} Register manually</summary>\n <div class=\"bf-forms\">\n <form id=\"register\" class=\"bf-form\">\n <div class=\"ax-sec-label\"><h3>Register a session</h3></div>\n <div class=\"bf-form__row\">\n <div class=\"ax-field\"><label for=\"r-id\">Session ID</label>\n <input id=\"r-id\" name=\"id\" required maxlength=\"200\"></div>\n <div class=\"ax-field\"><label for=\"r-rt\">Runtime</label>\n <select id=\"r-rt\" name=\"runtime\"><option>claude</option><option>codex</option>\n <option>gemini</option><option>opencode</option><option>other</option></select></div>\n </div>\n <div class=\"ax-field\"><label for=\"r-lb\">Label</label>\n <input id=\"r-lb\" name=\"label\" required maxlength=\"200\"></div>\n <button class=\"ax-btn ax-btn--primary\">Register session</button>\n </form>\n <form id=\"ended\" class=\"bf-form\">\n <div class=\"ax-sec-label\"><h3>Submit an ended run</h3></div>\n <div class=\"bf-form__row\">\n <div class=\"ax-field\"><label for=\"registered\">Registered session</label>\n <select id=\"registered\" name=\"sessionId\" required></select></div>\n <div class=\"ax-field\"><label for=\"e-run\">Run ID</label>\n <input id=\"e-run\" name=\"runId\" required maxlength=\"200\" placeholder=\"unique per turn\"></div>\n </div>\n <div class=\"ax-field\"><label for=\"e-tr\">Run transcript</label>\n <textarea id=\"e-tr\" name=\"transcript\" required maxlength=\"100000\" rows=\"3\"\n placeholder=\"Last 90 KB is submitted\"></textarea></div>\n <button class=\"ax-btn ax-btn--primary\">Review ended run</button>\n </form>\n </div>\n </details>\n</section>\n</div>`,\n css: MONITOR_CSS,\n scripts: `<script>${injectFns({ decayOf, rankByDecay })}const ICON = ${JSON.stringify(ICON)};${MONITOR_SCRIPT}</script>`,\n })\n}\n\nexport const MONITOR_CSS = `\n.bf{max-width:1240px;margin:0 auto;padding:22px 24px 48px}\n.bf-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n.bf :focus-visible{outline:2px solid var(--ax-accent);outline-offset:2px;border-radius:4px}\n\n/* Page head */\n.bf-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:16px}\n.bf-kicker{display:inline-block;font-size:10px;letter-spacing:0.1em;color:var(--ax-muted);\n text-transform:uppercase;margin-bottom:6px;font-family:var(--ax-mono)}\n.bf h1{font-size:22px;font-weight:600;letter-spacing:-0.015em;margin:0}\n.bf-sync{display:flex;align-items:center;gap:12px;flex-shrink:0}\n.bf-sync__t{font-size:11px;color:var(--ax-muted);font-family:var(--ax-mono);text-align:right;line-height:1.5}\n.bf .ax-btn{display:inline-flex;align-items:center;gap:7px}\n.bf .ax-btn--sm{padding:5px 12px;font-size:var(--ax-fs-sm);border-radius:var(--ax-radius-sm)}\n\n/* Coverage counters */\n.bf-strip{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}\n.bf-si--warn .ax-stat-inline__icon{background:color-mix(in oklch,var(--ax-warn) 14%,var(--ax-surface-2));color:var(--ax-warn)}\n.bf-si--warn .ax-stat-inline__v{color:var(--ax-warn)}\n.bf-si--err .ax-stat-inline__icon{background:color-mix(in oklch,var(--ax-err) 14%,var(--ax-surface-2));color:var(--ax-err)}\n.bf-si--err .ax-stat-inline__v{color:var(--ax-err)}\n#coverage-note:not(:empty){margin-bottom:16px}\n.bf-callout--err{border-color:color-mix(in oklch,var(--ax-err) 30%,var(--ax-border));\n background:color-mix(in oklch,var(--ax-err) 5%,var(--ax-bg-elev))}\n.bf-callout--err .ax-callout__icon{background:color-mix(in oklch,var(--ax-err) 18%,transparent);color:var(--ax-err)}\n\n#notice{margin:0 0 14px;font-size:12.5px;color:var(--ax-text-2)}\n#notice:not(:empty){padding:9px 13px;border-radius:var(--ax-radius-sm);background:var(--ax-surface-2);\n border:var(--ax-border-w) solid var(--ax-border)}\n\n/* Cost of delay — replaces the old minutes estimate */\n.bf-cost{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:600;\n color:var(--ax-text-2)}\n.bf-cost--rise{color:var(--ax-err)}\n.bf-cost--rise.is-over{padding:2px 9px;border-radius:var(--ax-radius-pill);\n background:color-mix(in oklch,var(--ax-err) 9%,var(--ax-surface));\n border:1px solid color-mix(in oklch,var(--ax-err) 32%,var(--ax-border))}\n.bf-act.is-snoozed{opacity:0.62}\n.bf-why{font-size:12px;color:var(--ax-text-2);font-weight:400;margin-left:2px}\n.ax-sec-label h3 .bf-why{margin-left:8px}\n/* The hint wraps under the heading on a narrow column instead of running\n beneath the button that used to sit on top of it. */\n.bf .ax-sec-label{flex-wrap:wrap;gap:8px 12px;align-items:baseline}\n.bf .ax-sec-label h3{flex-wrap:wrap;row-gap:2px;min-width:0}\n.bf .ax-sec-label h3 .bf-why{flex:1 1 260px;min-width:0;white-space:normal}\n#clear-actions{flex:none;margin-left:auto}\n\n/* Handled — a count, not a list: it shows the backlog moving, nothing to decide */\n.bf-done{display:flex;align-items:center;gap:10px;margin-top:14px;padding:11px 15px;\n border-radius:var(--ax-radius-lg);border:var(--ax-border-w) solid var(--ax-border);\n background:var(--ax-surface-2);font-size:12.5px;color:var(--ax-text-2)}\n.bf-done b{font-family:var(--ax-mono);font-size:15px;color:var(--ax-text);font-weight:600}\n.bf-done svg{color:var(--ax-ok);flex:none}\n.bf-done span{margin-left:auto}\n\n/* Principals */\n.bf-pr{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:14px}\n.bf-pr__c{min-width:0}\n.bf-pr__n,.bf-pr__k,.bf-pr__o{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.bf-pr__c{display:flex;flex-direction:column;gap:4px;text-align:left;font:inherit;cursor:pointer;\n background:var(--ax-surface);border:var(--ax-border-w) solid var(--ax-border);\n border-radius:var(--ax-radius-lg);box-shadow:var(--ax-shadow);padding:11px 14px;color:var(--ax-text)}\n.bf-pr__c:hover{background:var(--ax-surface-2)}\n.bf-pr__c.is-on{border-color:var(--ax-accent);box-shadow:var(--ax-shadow-accent)}\n.bf-pr__c--client{border-left:4px solid var(--ax-accent)}\n.bf-pr__n{font-size:13.5px;font-weight:600;letter-spacing:-0.005em}\n.bf-pr__k{font-size:10.5px;color:var(--ax-text-2);font-family:var(--ax-mono)}\n.bf-pr__s{display:flex;align-items:baseline;gap:8px;font-size:11.5px;color:var(--ax-text-2);margin-top:3px}\n.bf-pr__s b{font-family:var(--ax-mono);font-size:17px;font-weight:600;color:var(--ax-text-2)}\n.bf-pr__s b.bf-pr__you{color:var(--ax-err)}\n.bf-pr__s i{font-style:normal}\n.bf-pr__o{font-size:10.5px;color:var(--ax-text-2)}\n\n/* Triage columns */\n.bf-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(300px,1fr);gap:22px;\n align-items:flex-start;margin-bottom:24px}\n.bf-col{display:flex;flex-direction:column;gap:10px;min-width:0}\n.bf .ax-sec-label h3{display:flex;align-items:center;gap:8px}\n.ax-sec-label__n{font-family:var(--ax-mono);font-size:11px;padding:1px 7px;border-radius:var(--ax-radius);\n background:var(--ax-surface-3);color:var(--ax-text-2);letter-spacing:0}\n.ax-sec-label__n:empty{display:none}\n\n/* --- Identity: who, and where ------------------------------------------\n Every card leads with the agent avatar and a row of place tags, so the\n question \"who did this, and where\" is answered before any prose. */\n.bf-id{display:flex;align-items:center;gap:11px;min-width:0}\n.bf-id .ax-avatar{width:32px;height:32px;font-size:12px;text-transform:uppercase}\n.bf-who{flex:1;min-width:0}\n.bf-who__n{font-size:13.5px;font-weight:600;letter-spacing:-0.005em;display:flex;align-items:center;\n gap:7px;flex-wrap:wrap}\n.bf-where{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:3px}\n.bf-tag{display:inline-flex;align-items:center;gap:4px;font-size:10.5px;font-family:var(--ax-mono);\n color:var(--ax-muted);background:var(--ax-surface-2);border:1px solid var(--ax-border);\n border-radius:var(--ax-radius-pill);padding:1px 8px;line-height:16px;max-width:100%;\n overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\nbutton.bf-tag{cursor:pointer;font-weight:600}\nbutton.bf-tag:hover{border-color:var(--ax-accent);color:var(--ax-accent)}\nbutton.bf-tag.is-on{background:color-mix(in oklch,var(--ax-accent) 14%,transparent);\n border-color:color-mix(in oklch,var(--ax-accent) 35%,transparent);color:var(--ax-accent)}\n.bf-tag svg{flex-shrink:0}\n.bf-time{font-size:10.5px;color:var(--ax-muted);font-family:var(--ax-mono);white-space:nowrap;\n align-self:flex-start;padding-top:2px}\n\n/* --- Quick links: always visible, never buried in a disclosure --------- */\n.bf-links{display:flex;gap:6px;flex-wrap:wrap}\n.bf-lnk{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:600;\n padding:4px 10px;border-radius:var(--ax-radius-sm);border:var(--ax-border-w) solid var(--ax-border);\n background:var(--ax-surface-2);color:var(--ax-text-2);text-decoration:none;max-width:320px;\n overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.bf-lnk:hover{border-color:var(--ax-accent);color:var(--ax-accent);text-decoration:none}\n.bf-lnk svg{flex-shrink:0;opacity:0.75}\n.bf-lnk--go{color:var(--ax-accent);border-color:color-mix(in oklch,var(--ax-accent) 32%,var(--ax-border));\n background:color-mix(in oklch,var(--ax-accent) 8%,transparent)}\n\n/* Action card */\n.bf-act{background:var(--ax-surface);border:var(--ax-border-w) solid var(--ax-border);\n border-radius:var(--ax-radius-lg);padding:14px 16px;box-shadow:var(--ax-shadow);\n display:flex;flex-direction:column;gap:10px}\n.bf-act--now{border-color:color-mix(in oklch,var(--ax-warn) 34%,var(--ax-border))}\n.bf-act__cost{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--ax-text-2);\n font-family:var(--ax-mono)}\n.bf-act__cost svg{color:var(--ax-muted)}\n/* One job, re-derived by N session reviews. Marking it done clears all N. */\n.bf-dupe{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:600;\n padding:2px 9px;border-radius:var(--ax-radius-pill);color:var(--ax-red);\n background:color-mix(in oklch,var(--ax-err) 8%,var(--ax-surface));\n border:1px solid color-mix(in oklch,var(--ax-err) 30%,var(--ax-border))}\n.bf-eff{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--ax-muted)}\n.bf-eff i{width:4px;height:10px;border-radius:1px;background:var(--ax-border-2);display:block}\n.bf-eff--low i:nth-child(1),.bf-eff--medium i:nth-child(-n+2),.bf-eff--high i{background:var(--ax-text-2)}\n.bf-act__text{font-size:var(--ax-fs);line-height:1.55;color:var(--ax-text);overflow-wrap:anywhere;margin:0}\n.bf-act__foot{display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n.bf-why{margin-top:2px}\n.bf-why summary{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--ax-muted);\n cursor:pointer;list-style:none;font-weight:600}\n.bf-why summary::-webkit-details-marker{display:none}\n.bf-why[open] summary svg{transform:rotate(180deg)}\n.bf-ev{margin:8px 0 0;font-size:12px;line-height:1.6;color:var(--ax-text-2);\n border-left:2px solid var(--ax-border-2);padding-left:12px;overflow-wrap:anywhere}\n\n/* Deferred band */\n.bf-defer{background:var(--ax-surface-2);border:var(--ax-border-w) solid var(--ax-border);\n border-radius:var(--ax-radius-lg);padding:12px 16px;display:flex;align-items:center;gap:10px;\n flex-wrap:wrap;cursor:pointer}\n.bf-defer:hover{border-color:var(--ax-border-2)}\n.bf-defer__t{font-size:var(--ax-fs-sm);font-weight:600;color:var(--ax-text-2)}\n.bf-defer__n{margin-left:auto;display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.bf-defer svg{color:var(--ax-muted);transition:transform 140ms ease}\n#later{margin-top:10px}\n#more-actions{align-self:flex-start;margin-top:10px}\n\n/* In motion */\n.bf-run{background:var(--ax-surface);border:var(--ax-border-w) solid var(--ax-border);\n border-radius:var(--ax-radius-sm);padding:11px 13px;display:flex;flex-direction:column;gap:8px}\n.bf-run__dot{width:7px;height:7px;border-radius:50%;background:var(--ax-accent);flex-shrink:0;\n box-shadow:0 0 0 3px color-mix(in oklch,var(--ax-accent) 18%,transparent)}\n.bf-run__msg{font-size:12.5px;line-height:1.5;color:var(--ax-text-2);margin:0;overflow:hidden;\n display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}\n\n/* Briefings toolbar + filters */\n.bf-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}\n.bf-search{position:relative;flex:0 1 300px}\n.bf-search svg{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--ax-muted);\n pointer-events:none}\n.bf-search input{width:100%;background:var(--ax-surface);color:var(--ax-text);\n border:var(--ax-border-w) solid var(--ax-border-2);border-radius:var(--ax-radius);\n padding:8px 14px 8px 34px;font:inherit;font-size:var(--ax-fs-sm)}\n.bf-filters{display:flex;gap:6px;flex-wrap:wrap;align-items:center}\n.bf-filter{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:600;\n padding:5px 11px;border-radius:var(--ax-radius-pill);border:var(--ax-border-w) solid var(--ax-border);\n background:var(--ax-surface);color:var(--ax-text-2);cursor:pointer;font-family:inherit}\n.bf-filter:hover{border-color:var(--ax-border-2);color:var(--ax-text)}\n.bf-filter.is-on{background:color-mix(in oklch,var(--ax-accent) 14%,var(--ax-surface));\n border-color:color-mix(in oklch,var(--ax-accent) 40%,var(--ax-border));color:var(--ax-accent)}\n.bf-filter__n{font-family:var(--ax-mono);font-size:10px;opacity:0.8}\n.bf-filter--clear{color:var(--ax-muted);border-style:dashed}\n\n/* Briefing card */\n.bf-brief{background:var(--ax-surface);border:var(--ax-border-w) solid var(--ax-border);\n border-radius:var(--ax-radius-lg);padding:14px 16px;box-shadow:var(--ax-shadow);\n display:flex;flex-direction:column;gap:10px}\n.bf-brief--failed{border-color:color-mix(in oklch,var(--ax-err) 30%,var(--ax-border))}\n.bf-brief__top{display:flex;align-items:flex-start;gap:10px}\n.bf-brief__sum{font-size:13px;line-height:1.6;color:var(--ax-text);margin:0;overflow-wrap:anywhere}\n.bf-brief__fail{display:flex;align-items:center;gap:12px;flex-wrap:wrap}\n.bf-brief__fail .bf-brief__sum{flex:1;min-width:220px;color:var(--ax-text-2)}\n\n/* Finding chips */\n.bf-finds{display:flex;gap:6px;flex-wrap:wrap}\n.bf-find{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:3px 9px;\n border-radius:var(--ax-radius-pill);border:var(--ax-border-w) solid var(--ax-border);\n background:var(--ax-surface-2);color:var(--ax-muted)}\n.bf-find--warn{color:var(--ax-warn);background:color-mix(in oklch,var(--ax-warn) 12%,transparent);\n border-color:color-mix(in oklch,var(--ax-warn) 32%,transparent)}\n.bf-find--info{color:var(--ax-info);background:color-mix(in oklch,var(--ax-info) 12%,transparent);\n border-color:color-mix(in oklch,var(--ax-info) 32%,transparent)}\n.bf-find--zero{display:none}\n.bf-brief details>summary{cursor:pointer;font-size:11.5px;color:var(--ax-muted);font-weight:600;\n list-style:none;display:inline-flex;align-items:center;gap:5px}\n.bf-brief details>summary::-webkit-details-marker{display:none}\n.bf-brief details[open]>summary svg{transform:rotate(180deg)}\n.bf-detail{border-top:1px dashed var(--ax-border);margin-top:10px;padding-top:12px;\n display:flex;flex-direction:column;gap:13px}\n.bf-fgroup h4{margin:0 0 7px;font-size:10.5px;font-family:var(--ax-mono);text-transform:uppercase;\n letter-spacing:0.06em;color:var(--ax-muted);font-weight:600;display:flex;align-items:center;gap:6px}\n.bf-fgroup--warn h4{color:var(--ax-warn)}\n.bf-fgroup--info h4{color:var(--ax-info)}\n.bf-item{display:flex;flex-direction:column;gap:4px;padding-bottom:9px}\n.bf-item>p{font-size:12.5px;line-height:1.55;color:var(--ax-text);margin:0;overflow-wrap:anywhere}\n\n/* Inline trace panel — a readable summary in place, not a JSON tab */\n.bf-trace{border:var(--ax-border-w) solid var(--ax-border);border-radius:var(--ax-radius-sm);\n background:var(--ax-surface-2);padding:8px 12px}\n.bf-trace>summary{cursor:pointer;font-size:11.5px;color:var(--ax-text-2);font-weight:600;\n list-style:none;display:inline-flex;align-items:center;gap:6px}\n.bf-trace>summary::-webkit-details-marker{display:none}\n.bf-trace>summary:hover{color:var(--ax-accent)}\n.bf-trace[open]>summary{margin-bottom:10px}\n.bf-trace__body{font-size:12px;display:flex;flex-direction:column;gap:10px;align-items:flex-start}\n.bf-kv{display:grid;grid-template-columns:auto 1fr;gap:5px 14px;margin:0;width:100%}\n.bf-kv dt{font-family:var(--ax-mono);font-size:10.5px;text-transform:uppercase;letter-spacing:0.06em;\n color:var(--ax-muted);align-self:center}\n.bf-kv dd{margin:0;font-size:12.5px;color:var(--ax-text);overflow-wrap:anywhere}\n\n.bf-tool{display:inline-flex;align-items:center;gap:4px;font-family:var(--ax-mono);font-size:11px;\n background:var(--ax-surface-3);border-radius:var(--ax-radius-sm);padding:1px 7px;margin-right:4px}\n\n/* Connect a CLI session */\n.bf-connect{margin-top:24px}\n.bf-connect__head{display:flex;align-items:center;gap:11px;margin-bottom:14px}\n.bf-connect h2{margin:0;font-size:16px;font-weight:600;letter-spacing:-0.01em}\n.bf-connect__note{margin-left:auto;font-size:10.5px;color:var(--ax-muted);font-family:var(--ax-mono)}\n.bf-discover{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;margin-bottom:14px}\n.bf-discover .ax-field{min-width:220px;flex:0 1 300px}\n.bf-cli{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:10px}\n.bf-cli:not(:empty){margin-bottom:14px}\n.bf-cli-card{background:var(--ax-surface-2);border:var(--ax-border-w) solid var(--ax-border);\n border-radius:var(--ax-radius-sm);padding:12px 13px;display:flex;flex-direction:column;gap:9px}\n.bf-cli-card__top{display:flex;align-items:center;gap:10px;min-width:0}\n.bf-cli-card__n{font-size:13px;font-weight:600}\n.bf-cli-card__m{font-size:10.5px;color:var(--ax-muted);font-family:var(--ax-mono);margin-top:2px;\n overflow-wrap:anywhere}\n.bf-cli-card .ax-btn{justify-content:center}\n.bf-manual>summary{cursor:pointer;font-size:12px;color:var(--ax-muted);font-weight:600;list-style:none;\n display:inline-flex;align-items:center;gap:6px}\n.bf-manual>summary::-webkit-details-marker{display:none}\n.bf-manual[open]>summary svg{transform:rotate(180deg)}\n.bf-forms{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:14px}\n.bf-form{display:flex;flex-direction:column;gap:12px}\n.bf-form .ax-sec-label{margin:0}\n.bf-form__row{display:grid;grid-template-columns:1fr 1fr;gap:12px}\n.bf-form .ax-btn{align-self:flex-start}\n\n/* Empty + loading */\n.bf-empty{text-align:center;padding:30px 22px;background:var(--ax-surface);\n border:var(--ax-border-w) dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);\n color:var(--ax-muted);display:flex;flex-direction:column;align-items:center;gap:7px}\n.bf-empty svg{color:var(--ax-border-2)}\n.bf-empty h4{margin:0;color:var(--ax-text);font-size:14px;font-weight:600}\n.bf-empty p{font-size:12.5px;line-height:1.55;max-width:380px;margin:0}\n.bf-skel{background:var(--ax-surface);border:var(--ax-border-w) solid var(--ax-border);\n border-radius:var(--ax-radius-lg);padding:14px 16px;display:flex;flex-direction:column;gap:10px}\n.bf-bar{display:block;height:10px;border-radius:var(--ax-radius-pill);background:var(--ax-surface-3);\n animation:bf-pulse 1.4s ease-in-out infinite}\n@keyframes bf-pulse{0%,100%{opacity:1}50%{opacity:0.45}}\n@media (prefers-reduced-motion:reduce){.bf-bar{animation:none}}\n\n@media(max-width:980px){\n .bf-grid{grid-template-columns:1fr}\n .bf-forms{grid-template-columns:1fr}\n .bf-strip{grid-template-columns:1fr 1fr}\n}\n@media(max-width:600px){\n .bf{padding:16px 14px 36px}\n .bf-head{flex-direction:column;gap:14px;align-items:stretch}\n .bf-sync{justify-content:space-between}\n }`\n\nexport const MONITOR_SCRIPT = String.raw`\n(function(){\nconst $=id=>document.getElementById(id), esc=s=>String(s??'').replace(/[&<>\"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst ic=(n,s)=>'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"'+(s||14)+'\" height=\"'+(s||14)+'\" aria-hidden=\"true\">'+ICON[n]+'</svg>';\nlet nodes=[], actions=[], busy=false, expandedActions=false;\nlet filter={kind:'all',agent:null,node:null};\nasync function api(op,node,body){const r=await fetch('/api/monitor/'+op+'?node='+encodeURIComponent(node),{method:body?'POST':'GET',headers:{'Content-Type':'application/json'},...(body?{body:JSON.stringify(body)}:{})});const data=await r.json();if(!r.ok)throw Error(data.error||'Request failed');return data;}\nfunction message(s){$('notice').textContent=s;}\nlet clientFilter='';\nconst PAGE=25;\nlet shown={you:PAGE,agents:PAGE};\n\n/* --- Who and where ------------------------------------------------------\n session_id is \"<agent>:<channel>:<target>\" for trace-sourced reviews, e.g.\n \"umbrella-coding:gitlab:umbrella-lab/umbrella-v1:issue:94\". Native CLI session\n ids carry no place, so the tags simply fall away. */\nconst CHANNEL_ICON={gitlab:'branch',github:'branch',telegram:'send',whatsapp:'msg',discord:'msg',slack:'hash',api:'code',cron:'clock',mesh:'route'};\nfunction place(sessionId){\nconst parts=String(sessionId||'').split(':');\nif(parts.length<3)return null;\nconst channel=parts[1];\nlet target=parts.slice(2).join(':')\n .replace(/:issue:/,' #').replace(/:mr:/,' !').replace(/:pr:/,' #').replace(/:/g,' · ');\nreturn {channel:channel,target:target};\n}\nfunction initials(name){const w=String(name||'?').split(/[-_\\s.]+/).filter(Boolean);return ((w[0]||'?')[0]+(w[1]?w[1][0]:(w[0]||'?')[1]||'')).slice(0,2);}\nconst AV=['blue','teal','amber','coral'];\nfunction avatarClass(name){let h=0;for(const c of String(name||'')) h=(h*31+c.charCodeAt(0))>>>0;return AV[h%AV.length];}\nfunction tag(icon,text,attrs){return '<'+(attrs?'button type=\"button\" '+attrs:'span')+' class=\"bf-tag\">'+(icon?ic(icon,11):'')+esc(text)+'</'+(attrs?'button':'span')+'>';}\nfunction idBlock(agent,nodeName,nodeUrl,sessionId,model,when,whenExact){\nconst p=place(sessionId);\nconst on=(k,v)=>filter[k]===v?' is-on':'';\nreturn '<div class=\"bf-id\"><div class=\"ax-avatar ax-avatar--'+avatarClass(agent)+'\">'+esc(initials(agent))+'</div>'\n +'<div class=\"bf-who\"><div class=\"bf-who__n\">'\n +'<button type=\"button\" class=\"bf-tag'+on('agent',agent)+'\" data-fagent=\"'+esc(agent)+'\" style=\"font-size:13.5px;font-family:inherit;background:none;border:none;padding:0;color:inherit\">'+esc(agent)+'</button>'\n +(model?'<span class=\"ax-badge ax-badge--mono\">'+esc(model)+'</span>':'')+'</div>'\n +'<div class=\"bf-where\">'\n +'<button type=\"button\" class=\"bf-tag'+on('node',nodeName)+'\" data-fnode=\"'+esc(nodeName)+'\" title=\"Filter by node\">'+ic('server',11)+esc(nodeName)+'</button>'\n +(p?tag(CHANNEL_ICON[p.channel]||'hash',p.channel):'')\n +(p&&p.target?tag('',p.target):'')\n +'</div></div>'\n +(when?'<span class=\"bf-time\" title=\"'+esc(whenExact||'')+'\">'+esc(when)+'</span>':'')+'</div>';\n}\nfunction lnk(href,icon,text,go){return '<a class=\"bf-lnk'+(go?' bf-lnk--go':'')+'\" href=\"'+esc(href)+'\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"'+esc(text)+'\">'+ic(icon,12)+esc(text)+'</a>';}\n/* Human-friendly time. Relative on the face, exact on hover. */\nfunction relTime(ms){\nif(!ms)return '';\nconst d=Math.round((Date.now()-ms)/1000);\nif(d<45)return 'just now';\nif(d<90)return 'a minute ago';\nconst m=Math.round(d/60);\nif(m<60)return m+' min ago';\nconst h=Math.round(m/60);\nif(h<24)return h===1?'an hour ago':h+' hours ago';\nconst day=Math.round(h/24);\nif(day===1)return 'yesterday';\nif(day<7)return day+' days ago';\nreturn new Date(ms).toLocaleDateString([], {month:'short',day:'numeric'});\n}\nfunction absTime(ms){return ms?new Date(ms).toLocaleString():'';}\nfunction fmtDur(ms){\nif(ms==null)return '';\nif(ms<1000)return ms+' ms';\nconst s=Math.round(ms/1000);\nif(s<60)return s+' s';\nconst m=Math.floor(s/60),r=s%60;\nif(m<60)return m+'m'+(r?' '+r+'s':'');\nconst h=Math.floor(m/60);\nreturn h+'h'+(m%60?' '+(m%60)+'m':'');\n}\nfunction tracePanel(nodeUrl,taskId){\nreturn '<details class=\"bf-trace\" data-node=\"'+esc(nodeUrl)+'\" data-task=\"'+esc(taskId)+'\">'\n +'<summary>'+ic('trace',12)+'Trace</summary>'\n +'<div class=\"bf-trace__body\"><span class=\"ax-muted\">Loading&hellip;</span></div></details>';\n}\nfunction renderTrace(el,d){\nif(!d||d.found===false)return '<span class=\"ax-muted\">No trace stored for this run.</span>';\nconst kv=[];\nconst add=(k,v)=>{if(v!==''&&v!=null)kv.push('<dt>'+k+'</dt><dd>'+v+'</dd>');};\nadd('Status','<span class=\"ax-pill ax-pill--'+(d.status==='ok'?'ok':d.status==='error'?'err':'off')+'\"><span class=\"ax-pill__dot\"></span>'+esc(d.status||'unknown')+'</span>');\nadd('Agent',esc(d.agent||''));\nif(d.channel)add('Where',esc(d.channel)+(d.chatId?' &middot; '+esc(String(d.chatId).replace(/:issue:/,' #').replace(/:mr:/,' !')):''));\nadd('Started',d.startedAt?'<span title=\"'+esc(absTime(d.startedAt))+'\">'+esc(relTime(d.startedAt))+'</span>':'');\nadd('Took',d.durationMs!=null?esc(fmtDur(d.durationMs)):'');\nif(d.model)add('Model',esc(d.model));\nif(d.cause)add('Cause',esc(d.cause));\n// Counters are derived from steps; pruned traces report zeros that mean\n// \"unknown\", not \"none\". Say so rather than showing a wall of 0s.\nif(d.stepsPruned)kv.push('<dt>Detail</dt><dd class=\"ax-muted\">Step detail was pruned &mdash; tool, token and I/O counters are no longer available.</dd>');\nelse{\n add('Steps',String((d.steps||[]).length));\n if(d.tools&&d.tools.length)add('Tools',d.tools.map(t=>{\n const name=esc(typeof t==='string'?t:(t.tool||t.name||'tool'));\n const used=(t&&t.used>1)?' &times;'+t.used:'';\n const failed=(t&&t.failed)?' <span class=\"ax-pill ax-pill--err\" style=\"padding:0 5px\">'+t.failed+' failed</span>':'';\n return '<span class=\"bf-tool\">'+name+used+failed+'</span>';\n }).join(' '));\n if(d.outputTokens)add('Output tokens',String(d.outputTokens));\n const io=[d.reads?d.reads+' read':'',d.writes?d.writes+' write':'',d.sends?d.sends+' send':''].filter(Boolean).join(' &middot; ');\n if(io)add('I/O',io);\n}\nreturn '<dl class=\"bf-kv\">'+kv.join('')+'</dl>'\n +'<a class=\"bf-lnk\" href=\"/api/mesh/run?node='+encodeURIComponent(el.dataset.node)+'&task='+encodeURIComponent(el.dataset.task)+'\" target=\"_blank\" rel=\"noopener noreferrer\">'+ic('ext',12)+'Raw JSON</a>';\n}\n/* The human task page, not the raw JSON endpoint. archived=1 reads the stored\n record; a live task streams instead. Falls back to the JSON API only when no\n agent can be resolved, because /tasks refuses a request without one. */\nfunction taskHref(nodeUrl,taskId,agentId,archived){\nreturn agentId\n ?'/tasks/'+encodeURIComponent(taskId)+'?agent='+encodeURIComponent(agentId)+'&node='+encodeURIComponent(nodeUrl)+(archived?'&archived=1':'')\n :'/api/mesh/run?node='+encodeURIComponent(nodeUrl)+'&task='+encodeURIComponent(taskId);\n}\n/* A related task usually has its own review (or is still running); either one\n tells us which agent owns it. */\nfunction ownerOf(n,taskId){\nconst run=(n.data.running||[]).find(t=>t.taskId===taskId);\nif(run)return run.agentId;\nconst rev=(n.data.reviews||[]).find(r=>r.id===taskId);\nreturn rev?rev.agent:'';\n}\n\n/* --- Loading skeleton --------------------------------------------------- */\nfunction skeleton(){\nconst bar=w=>'<i class=\"bf-bar\" style=\"width:'+w+'\"></i>';\nconst card='<div class=\"bf-skel\">'+bar('40%')+bar('100%')+bar('64%')+'</div>';\n$('strip').innerHTML=('<div class=\"ax-stat-inline\"><div class=\"ax-stat-inline__icon\"></div>'\n +'<div class=\"bf-skel\" style=\"flex:1;gap:6px\">'+bar('46px')+bar('76px')+'</div></div>').repeat(4);\n$('you').innerHTML=card+card;$('agents').innerHTML=card;$('reviews').innerHTML=card;$('running').innerHTML=card;\n}\n\n/* --- Coverage ----------------------------------------------------------- */\nfunction si(icon,value,label,kind){return '<div class=\"ax-stat-inline'+(kind?' bf-si--'+kind:'')+'\"><div class=\"ax-stat-inline__icon\">'+ic(icon,14)+'</div><div style=\"min-width:0\"><div class=\"ax-stat-inline__v\">'+esc(value)+'</div><div class=\"ax-stat-inline__l\">'+esc(label)+'</div></div></div>';}\nfunction renderCoverage(){\nconst ok=nodes.filter(n=>n.ok), failed=nodes.filter(n=>!n.ok);\nconst reviews=ok.reduce((a,n)=>a+(n.data.reviews||[]).length,0);\nconst queue=ok.flatMap(n=>n.data.counts||[]);\nconst waiting=queue.filter(c=>c.status==='pending'||c.status==='reviewing').reduce((a,c)=>a+c.count,0);\nconst failures=queue.filter(c=>c.status==='failed').reduce((a,c)=>a+c.count,0);\n$('strip').innerHTML=si('server',ok.length+' / '+nodes.length,'Nodes reporting',failed.length?'warn':'')\n +si('inbox',String(waiting),'Awaiting review')\n +si('cross',String(failures),failures===1?'Review failed':'Reviews failed',failures?'err':'')\n +si('file',String(reviews),'Recent briefings');\n$('coverage').textContent=reviews+' recent reviews, '+waiting+' awaiting review'+(failures?', '+failures+' review failures':'')+', '+ok.length+' of '+nodes.length+' nodes reporting'+(failed.length?'. Unavailable: '+failed.map(n=>n.name+' ('+n.error+')').join(', '):'');\n$('coverage-note').innerHTML=failed.length\n ?'<div class=\"ax-callout'+(ok.length?'':' bf-callout--err')+'\"><span class=\"ax-callout__icon\">!</span><div><b>'\n +esc(failed.map(n=>n.name).join(', '))+'</b> not reporting &mdash; '\n +failed.map(n=>'<code>'+esc(n.error)+'</code>').join(' ')+'</div></div>':'';\n}\n\n/* --- Actions ------------------------------------------------------------ */\nfunction card(a,i,clocks){\n/* The cost of waiting, not an estimate of the work. A client with a stated\n clock accrues against it; everything else is flat and says so. */\nconst d=decayOf(a,clocks||{});\nconst age=a.updatedAt?relTime(a.updatedAt).replace(' ago',''):'';\nconst cost=d.rising\n ?'<span class=\"bf-cost bf-cost--rise'+(d.overdue?' is-over':'')+'\">'+ic('pulse',12)+esc(age)+(d.overdue?' &middot; past the clock':' &middot; rising')+'</span>'\n :'<span class=\"bf-cost\">'+ic('clock',12)+esc(age)+' &middot; flat</span>';\nconst pill=a.needsHuman\n ?'<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>Needs you</span>'\n :'<span class=\"ax-pill ax-pill--info\"><span class=\"ax-pill__dot\"></span>Agent</span>';\nconst done='<button class=\"ax-btn ax-btn--sm ax-btn--primary\" data-action=\"done\" data-index=\"'+i+'\">'+ic('check',13)+' Done</button>';\nconst foot=a.snoozed\n ?done+'<button class=\"ax-btn ax-btn--sm ax-btn--ghost\" data-action=\"open\" data-index=\"'+i+'\">Bring back</button>'\n :done+'<button class=\"ax-btn ax-btn--sm ax-btn--ghost\" data-action=\"later\" data-index=\"'+i+'\">Snooze</button>';\nconst src=a.sources[0];\nconst links=(src && !String(src.reviewId).startsWith('external:'))?tracePanel(src.node,src.reviewId):'';\nreturn '<article class=\"bf-act'+(d.overdue&&a.needsHuman?' bf-act--now':'')+(a.snoozed?' is-snoozed':'')+'\">'\n +idBlock(a.agent,a.nodes.join(' / '),src?src.node:'',a.sessionId,'',relTime(a.updatedAt),absTime(a.updatedAt))\n +'<p class=\"bf-act__text\">'+esc(a.text)+'</p>'\n +'<div class=\"bf-links\">'+pill\n +(a.sources.length>1?'<span class=\"bf-dupe\" title=\"Re-derived by this many session reviews\">'+ic('loop',12)+'seen '+a.sources.length+'&times;</span>':'')\n +cost\n +(a.snoozed?'<span class=\"ax-pill ax-pill--off\">snoozed</span>':'')+'</div>'\n +links\n +'<div class=\"bf-act__foot\">'+foot+'</div>'\n +(a.evidence?'<details class=\"bf-why\"><summary>'+ic('chev',12)+'Evidence</summary><p class=\"bf-ev\">'+esc(a.evidence)+'</p></details>':'')\n +'</article>';\n}\n/* --- Principals ---------------------------------------------------------- */\n/* Who the work is for. Clients are derived from business.projects and\n business.contactMap, so this fills itself in; it is only empty when no\n project or contact has ever been configured. Clicking one filters the\n action list — the fastest way to answer \"what does this client need\". */\nconst KIND_LABEL={client:'client',own:'own product',internal:'internal'};\nfunction renderPrincipals(){\nconst policy={};\nfor(const n of nodes.filter(n=>n.ok))for(const c of n.data.clients||[])policy[c.id]=c;\nconst by={};\nfor(const a of actions){\n const id=a.clientId||'unmapped';\n const g=by[id]||(by[id]={id:id,you:0,agents:0,oldest:0});\n if(a.needsHuman)g.you++;else g.agents++;\n if(a.updatedAt&&(!g.oldest||a.updatedAt<g.oldest))g.oldest=a.updatedAt;\n}\nconst rows=Object.values(by).sort((x,y)=>y.you-x.you||y.agents-x.agents);\nif(!rows.length){$('principals').innerHTML='';return;}\n$('principals').innerHTML='<section class=\"bf-pr\" aria-label=\"Who the work is for\">'\n +rows.map(g=>{\n const p=policy[g.id]||{name:g.id,kind:'internal',declared:false};\n const on=clientFilter===g.id;\n return '<button class=\"bf-pr__c'+(on?' is-on':'')+(p.kind==='client'?' bf-pr__c--client':'')+'\"'\n +' data-client=\"'+esc(g.id)+'\" aria-pressed=\"'+(on?'true':'false')+'\">'\n +'<span class=\"bf-pr__n\">'+esc(p.name||g.id)+'</span>'\n +'<span class=\"bf-pr__k\">'+esc(KIND_LABEL[p.kind]||'internal')+(p.respondWithinMinutes?' &middot; '+p.respondWithinMinutes+'m clock':'')+'</span>'\n +'<span class=\"bf-pr__s\"><b class=\"'+(g.you?'bf-pr__you':'')+'\">'+g.you+'</b> on you'\n +'<i>'+g.agents+' on agents</i></span>'\n +(g.oldest?'<span class=\"bf-pr__o\">oldest '+relTime(g.oldest)+'</span>':'')\n +'</button>';\n }).join('')+'</section>';\n$('principals').querySelectorAll('[data-client]').forEach(b=>b.onclick=()=>{\n clientFilter=clientFilter===b.dataset.client?'':b.dataset.client;shown={you:PAGE,agents:PAGE};renderPrincipals();renderActions();});\n}\n\nfunction renderActions(){\nconst merged=new Map();\nfor(const n of nodes.filter(n=>n.ok))for(const entry of n.data.actions?.items||[]){\nconst a=entry.action,saved=entry.state;const key=entry.key||JSON.stringify([n.url,entry.sessionId,a.text.toLowerCase().replace(/\\s+/g,' ').trim()]);const source={node:n.url,reviewId:entry.reviewId,index:entry.actionIndex};\nif(merged.has(key)){const m=merged.get(key);m.sources.push(source);if(!m.nodes.includes(n.name))m.nodes.push(n.name);if(saved==='open')m.snoozed=false;if(entry.updatedAt>m.updatedAt){m.updatedAt=entry.updatedAt;m.text=a.text;m.evidence=a.evidence;}}\nelse merged.set(key,{...a,snoozed:saved==='later',sources:[source],nodes:[n.name],agent:entry.agent,sessionId:entry.sessionId,updatedAt:entry.updatedAt,clientId:entry.clientId});\n}\nconst unloaded=nodes.filter(n=>n.ok).reduce((s,n)=>s+Math.max(0,(n.data.actions?.total||0)-(n.data.actions?.items.length||0)),0);\n$('more-actions').hidden=!unloaded;$('more-actions').textContent='Load '+unloaded+' older';\nactions=[...merged.values()];\n/* Each client's stated respondWithin is what makes a wait cost something.\n Work for a client with no clock is flat and can never outrank someone who\n is actually waiting. */\nconst clocks={};\nfor(const n of nodes.filter(n=>n.ok))for(const c of n.data.clients||[])clocks[c.id]=c.respondWithinMinutes;\nconst list=clientFilter?actions.filter(a=>(a.clientId||'unmapped')===clientFilter):actions;\nconst order=rankByDecay(list,clocks)\n .sort((x,y)=>(list[x].snoozed?1:0)-(list[y].snoozed?1:0));\nconst you=[],agents=[];\nfor(const i of order)(list[i].needsHuman?you:agents).push(list[i]);\nconst page=(items,key)=>items.slice(0,shown[key]).map(a=>card(a,actions.indexOf(a),clocks)).join('')\n +(items.length>shown[key]\n ?'<button class=\"ax-btn ax-btn--sm bf-more\" data-more=\"'+key+'\">Show '+Math.min(PAGE,items.length-shown[key])+' more of '+(items.length-shown[key])+'</button>'\n :'');\n$('you').innerHTML=page(you,'you')||'<div class=\"bf-empty\">'+ic('check',22)+'<h4>Nothing needs you</h4><p>Everything open can be finished without your authority.</p></div>';\n$('you-count').textContent=you.length?String(you.length):'';\n$('agents').innerHTML=page(agents,'agents')||'<div class=\"bf-empty\">'+ic('check',22)+'<h4>Nothing queued for agents</h4><p>No open action was marked as needing no human.</p></div>';\n$('agents-count').innerHTML=String(agents.length)+(unloaded?'<span class=\"ax-pill ax-pill--off\">'+unloaded+' not loaded</span>':'');\nconst done=nodes.filter(n=>n.ok).reduce((t,n)=>t+(n.data.doneCount||0),0);\n$('handled').innerHTML=done?'<div class=\"bf-done\">'+ic('check',17)+'<b>'+done+'</b> handled<span>cleared by you or by an agent &mdash; no decision in here</span></div>':'';\n}\n\n/* --- Session briefings + filters ---------------------------------------- */\nconst GROUPS=[['warnings','Warnings','alert','warn'],['decisions','Decisions on your behalf','gavel','info'],['friction','Avoidable round trips','loop',''],['context','Context maintenance','broom','']];\nfunction chip(n,icon,kind,one,many){return '<span class=\"bf-find '+(n?'bf-find--'+kind:'bf-find--zero')+'\">'+ic(icon,12)+(n?n+' '+(n===1?one:many):'no '+many)+'</span>';}\nfunction rows(){return nodes.filter(n=>n.ok).flatMap(n=>(n.data.reviews||[]).map(r=>({n:n,r:r})));}\nfunction passes(e,ignoreKind){\nconst r=e.r,v=r.result;\nif(filter.agent&&r.agent!==filter.agent)return false;\nif(filter.node&&e.n.name!==filter.node)return false;\nif(!ignoreKind){\n if(filter.kind==='failed'&&r.status!=='failed')return false;\n if(filter.kind==='warn'&&!(v&&v.warnings.length))return false;\n if(filter.kind==='needs'&&!(v&&v.actions.some(a=>a.needsHuman)))return false;\n}\nconst q=$('search').value.toLowerCase();\nreturn !q||JSON.stringify(r).toLowerCase().includes(q);\n}\nfunction renderFilters(){\nconst base=rows().filter(e=>passes(e,true));\nconst counts={all:base.length,\n needs:base.filter(e=>e.r.result&&e.r.result.actions.some(a=>a.needsHuman)).length,\n warn:base.filter(e=>e.r.result&&e.r.result.warnings.length).length,\n failed:base.filter(e=>e.r.status==='failed').length};\nconst defs=[['all','file','All'],['needs','alert','Needs you'],['warn','alert','Warnings'],['failed','cross','Failed']];\nlet html=defs.map(([k,icon,label])=>'<button type=\"button\" class=\"bf-filter'+(filter.kind===k?' is-on':'')+'\" data-fkind=\"'+k+'\">'+ic(icon,12)+esc(label)+'<span class=\"bf-filter__n\">'+counts[k]+'</span></button>').join('');\nif(filter.agent)html+='<button type=\"button\" class=\"bf-filter is-on\" data-fagent=\"'+esc(filter.agent)+'\">'+ic('hash',12)+esc(filter.agent)+ic('x',11)+'</button>';\nif(filter.node)html+='<button type=\"button\" class=\"bf-filter is-on\" data-fnode=\"'+esc(filter.node)+'\">'+ic('server',12)+esc(filter.node)+ic('x',11)+'</button>';\nif(filter.kind!=='all'||filter.agent||filter.node)html+='<button type=\"button\" class=\"bf-filter bf-filter--clear\" data-fclear=\"1\">'+ic('x',12)+'Clear</button>';\n$('filters').innerHTML=html;\n}\nfunction renderReviews(){\nrenderFilters();\nconst entries=rows().filter(e=>passes(e,false)).sort((a,b)=>b.r.updated_at-a.r.updated_at).map(e=>{\nconst n=e.n,r=e.r,v=r.result;\nconst head=idBlock(r.agent,n.name,n.url,r.session_id,r.model,relTime(r.updated_at),absTime(r.updated_at));\nif(r.status==='failed')return '<article class=\"bf-brief bf-brief--failed\">'+head\n +'<div class=\"bf-brief__fail\"><span class=\"ax-badge ax-badge--err\">Review failed</span>'\n +'<p class=\"bf-brief__sum\">'+esc(r.error||'Review failed')+'</p>'\n +'<button class=\"ax-btn ax-btn--sm\" data-retry=\"'+esc(r.id)+'\" data-node=\"'+esc(n.url)+'\">Retry</button></div></article>';\nlet html='<article class=\"bf-brief\">'+head+'<p class=\"bf-brief__sum\">'+esc(v?v.summary:'Review '+r.status+'…')+'</p>';\n// Quick access: every link the review produced, plus the trace it came from.\nlet links=v?v.links.filter(l=>/^https?:\\/\\//i.test(l.url)||/^\\/(?!\\/)/.test(l.url))\n .map(l=>lnk(l.url.startsWith('/')?n.url+l.url:l.url,'ext',l.label)).join(''):'';\nif(v)links+=v.relatedTaskIds.map(id=>lnk(taskHref(n.url,id,ownerOf(n,id),true),'route','Related '+String(id).slice(-6))).join('');\n\nif(links)html+='<div class=\"bf-links\">'+links+'</div>';\nif(r.source==='trace')html+=tracePanel(n.url,r.id);\nif(v){\nhtml+='<div class=\"bf-finds\">'+chip(v.warnings.length,'alert','warn','warning','warnings')\n +chip(v.decisions.length,'gavel','info','decision','decisions')\n +chip(v.friction.length,'loop','warn','round trip','round trips')\n +chip(v.context.length,'broom','info','context note','context notes')+'</div>';\nconst detail=GROUPS.filter(g=>v[g[0]].length).map(g=>'<div class=\"bf-fgroup'+(g[3]?' bf-fgroup--'+g[3]:'')+'\"><h4>'+ic(g[2],13)+esc(g[1])+'</h4>'\n +v[g[0]].map(x=>'<div class=\"bf-item\"><p>'+esc(x.text)+'</p><p class=\"bf-ev\">'+esc(x.evidence)+'</p></div>').join('')+'</div>').join('');\nif(detail)html+='<details><summary>'+ic('chev',12)+'Findings &amp; evidence</summary><div class=\"bf-detail\">'+detail+'</div></details>';\n}\nreturn html+'</article>';});\n$('brief-count').textContent=entries.length?String(entries.length):'';\n$('reviews').innerHTML=(entries.slice(0,4).join('')+(entries.length>4?'<details><summary class=\"bf-defer\">'+ic('chevr',14)+'<span class=\"bf-defer__t\">'+(entries.length-4)+' earlier</span></summary><div class=\"bf-col\" style=\"margin-top:10px\">'+entries.slice(4).join('')+'</div></details>':''))\n ||'<div class=\"bf-empty\">'+ic('file',22)+'<h4>Nothing here</h4><p>'+(filter.kind!=='all'||filter.agent||filter.node||$('search').value?'No briefing matches the current filter.':'Runs that finish from now on are reviewed and land here.')+'</p></div>';\n}\nfunction registrations(){const n=nodes.find(n=>n.url===$('node').value);$('registered').innerHTML=(n?.data?.registrations||[]).map(r=>'<option value=\"'+esc(r.id)+'\">'+esc(r.label)+' · '+esc(r.runtime)+'</option>').join('');}\nfunction renderRunning(){\nconst list=nodes.filter(n=>n.ok).flatMap(n=>(n.data.running||[]).map(t=>'<div class=\"bf-run\">'\n +idBlock(t.agentId,n.name,n.url,t.sessionId||'','','')\n +'<p class=\"bf-run__msg\">'+esc(t.messagePreview||'Running task')+'</p>'\n +tracePanel(n.url,t.taskId)+'</div>'));\n$('run-count').textContent=list.length?String(list.length):'';\n$('running').innerHTML=list.join('')||'<div class=\"bf-empty\">'+ic('pulse',22)+'<h4>Nothing running</h4><p>No active AgentX runs reported.</p></div>';\n}\nasync function refresh(){\nif(busy)return;busy=true;$('refresh').disabled=true;$('refresh').setAttribute('aria-busy','true');\ntry{\nconst r=await fetch('/api/monitor');if(!r.ok)throw Error('Briefing unavailable: HTTP '+r.status);\nconst data=await r.json();nodes=data.nodes;expandedActions=false;\nrenderCoverage();\nconst selected=$('node').value;\n$('node').innerHTML=nodes.map(n=>'<option value=\"'+esc(n.url)+'\">'+esc(n.name)+(n.ok?'':' — unavailable')+'</option>').join('');\nif(nodes.some(n=>n.url===selected))$('node').value=selected;\nregistrations();renderActions();renderPrincipals();renderReviews();renderRunning();\n$('synced').textContent='Synced '+new Date().toLocaleTimeString();\nmessage('');\n}catch(e){$('synced').textContent='Refresh failed';message(e.message);}\nfinally{busy=false;$('refresh').disabled=false;$('refresh').removeAttribute('aria-busy');}\n}\n$('more-actions').onclick=async()=>{try{for(const n of nodes.filter(n=>n.ok&&n.data.actions.items.length<n.data.actions.total)){const r=await fetch('/api/monitor/actions?node='+encodeURIComponent(n.url)+'&offset='+n.data.actions.items.length);if(!r.ok)throw Error('Could not load older actions');const d=await r.json();n.data.actions.items.push(...d.items);n.data.actions.total=d.total;}expandedActions=true;renderActions();message('Older actions loaded. Auto-refresh paused until you press Refresh.');}catch(e){message(e.message);}};\ndocument.addEventListener('click',e=>{const b=e.target.closest('[data-more]');if(!b)return;\nshown[b.dataset.more]+=PAGE;renderActions();});\n/* What this page is showing, for the ask-an-agent drawer. Kept small and\n factual: the filter in force, what the buckets hold, and the handful of\n items actually on screen — not the whole backlog. */\nwindow.axPageContext=()=>{\nconst vis=actions.filter(a=>!clientFilter||(a.clientId||'unmapped')===clientFilter);\nreturn {\n page:'monitor',\n client:clientFilter||'all',\n onlyYou:vis.filter(a=>a.needsHuman).length,\n forAgents:vis.filter(a=>!a.needsHuman).length,\n topItems:vis.filter(a=>a.needsHuman).slice(0,8).map(a=>({\n text:String(a.text||'').slice(0,140),client:a.clientId||'unmapped',agent:a.agent,\n where:a.sessionId,seen:a.sources?a.sources.length:1})),\n};};\n\n$('clear-actions').onclick=async()=>{\nconst open=actions.length;\nif(!open){message('Nothing open to clear.');return;}\nif(!confirm('Mark all '+open+' open actions done on every node? The reviews and their findings stay; only the open flags move.'))return;\ntry{let n=0;\nfor(const nd of nodes.filter(n=>n.ok)){const r=await api('clear',nd.url,{});n+=r.cleared||0;}\nmessage('Cleared '+n+'. New actions keep arriving as sessions end.');await refresh();\n}catch(e){message(e.message);}};\n$('search').oninput=renderReviews;$('refresh').onclick=refresh;$('node').onchange=registrations;\ndocument.addEventListener('click',async e=>{\nconst tr=e.target.closest('.bf-trace>summary');\nif(tr){\n const el=tr.parentElement;\n if(!el.dataset.loaded){\n el.dataset.loaded='1';\n const body=el.querySelector('.bf-trace__body');\n fetch('/api/mesh/run?node='+encodeURIComponent(el.dataset.node)+'&task='+encodeURIComponent(el.dataset.task))\n .then(r=>r.json()).then(d=>{body.innerHTML=renderTrace(el,d);})\n .catch(err=>{body.innerHTML='<span class=\"ax-muted\">Could not load the trace: '+esc(err.message)+'</span>';el.dataset.loaded='';});\n }\n return;\n}\nconst t=e.target.closest('[data-fkind],[data-fagent],[data-fnode],[data-fclear]');\nif(t){\n if(t.dataset.fclear)filter={kind:'all',agent:null,node:null};\n else if(t.dataset.fkind)filter.kind=t.dataset.fkind;\n else if(t.dataset.fagent)filter.agent=filter.agent===t.dataset.fagent?null:t.dataset.fagent;\n else if(t.dataset.fnode)filter.node=filter.node===t.dataset.fnode?null:t.dataset.fnode;\n renderReviews();\n if(t.dataset.fagent||t.dataset.fnode)$('reviews').scrollIntoView({behavior:'smooth',block:'start'});\n return;\n}\nconst b=e.target.closest('button');if(!b)return;\ntry{\n if(b.dataset.action){b.disabled=true;const a=actions[Number(b.dataset.index)];await Promise.all(a.sources.map(s=>api('action',s.node,{reviewId:s.reviewId,index:s.index,state:b.dataset.action})));await refresh();}\n if(b.dataset.retry){b.disabled=true;await api('retry',b.dataset.node,{id:b.dataset.retry});await refresh();}\n}catch(err){message(err.message);}finally{b.disabled=false;}});\nfunction cliCard(glyph,name,meta,badge,cta,onclick){\nconst el=document.createElement('div');el.className='bf-cli-card';\nel.innerHTML='<div class=\"bf-cli-card__top\"><div class=\"ax-avatar ax-avatar--'+avatarClass(name)+'\" style=\"width:30px;height:30px;font-size:11px\">'+esc(glyph)+'</div>'\n +'<div style=\"min-width:0\"><div class=\"bf-cli-card__n\">'+esc(name)+'</div><div class=\"bf-cli-card__m\">'+esc(meta)+'</div></div></div>'\n +badge+'<button class=\"ax-btn ax-btn--sm ax-btn--primary\">'+esc(cta)+'</button>';\nel.querySelector('button').onclick=onclick;\nreturn el;\n}\nconst NATIVE='<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>Reports turns natively</span>';\nconst PROC='<span class=\"ax-pill ax-pill--off\">Process only</span>';\n$('discover').onclick=async()=>{\ntry{\nconst node=$('node').value;const data=await api('discover',node);$('discovered').replaceChildren();\nfor(const s of data.sessions||[])\n $('discovered').append(cliCard('cl','claude',s.label||s.id,NATIVE,'Monitor session',async()=>{\n try{await api('register',node,{...s,label:s.label||s.id});message('Registered. Attach stop hooks will review future turns.');await refresh();}catch(e){message(e.message);}}));\nfor(const p of data.processes)\n $('discovered').append(cliCard(p.runtime.slice(0,2),p.runtime,'PID '+p.pid+' · '+p.started,PROC,'Register',async()=>{\n try{await api('register',node,{id:p.runtime+':'+p.pid+':'+p.started,label:p.runtime+' '+p.pid,runtime:p.runtime,pid:p.pid,started:p.started});message('Registered. Connect a stop hook or submit a transcript.');await refresh();}catch(e){message(e.message);}}));\nif(!data.processes.length&&!(data.sessions||[]).length)\n $('discovered').innerHTML='<div class=\"bf-empty\" style=\"grid-column:1/-1\">'+ic('plug',22)+'<h4>No CLI found</h4><p>Only processes owned by the daemon account are visible. Register others manually.</p></div>';\n}catch(e){message(e.message);}};\nfor(const op of ['register','ended'])$(op).onsubmit=async e=>{e.preventDefault();try{const body=Object.fromEntries(new FormData(e.target));await api(op,$('node').value,body);message(op==='ended'?'Run queued for review.':'Session registered.');if(op==='ended')e.target.reset();await refresh();}catch(err){message(err.message);}};\nskeleton();refresh();\nsetInterval(()=>{\nif(document.hidden||expandedActions)return;\nif(document.querySelector('form :focus'))return;\nif(document.querySelector('.bf-trace[open]'))return; // don't yank an open trace\nrefresh();\n},15000);\n})();`\n","// --- Laying out a night of fleet activity ---------------------------------\n//\n// Two problems make a naive timeline useless here.\n//\n// The axis. Nine hours of wall clock hold maybe forty minutes of work in two\n// or three bursts; drawn linearly, 90% of the width is empty and every burst\n// is an unreadable smear. So the axis BREAKS: runs are clustered, each cluster\n// gets its own scale weighted by how much happened in it, and the quiet gaps\n// between clusters are labelled rather than drawn.\n//\n// Overlap. A lane is one agent, one client, one project — whatever the chosen\n// perspective keys on — and those routinely run concurrently. Bars are packed\n// into sub-tracks so a lane grows downward instead of drawing runs on top of\n// each other. That packing is what makes duplicate dispatches visible at all.\n\nexport interface TimelineRun {\n id: string\n laneKey: string\n laneLabel: string\n laneSub?: string\n startedAt: number\n /** Missing or zero for runs that were canceled before doing anything. */\n durationMs?: number\n status: string\n label?: string\n}\n\nexport interface TimelineMark {\n runId: string\n kind: \"decision\" | \"warning\" | \"recommendation\" | \"later\"\n text: string\n}\n\nexport interface Band {\n from: number\n to: number\n /** Fraction of the drawable width this band gets, 0..1. */\n weight: number\n runs: number\n}\n\nexport interface PlacedRun extends TimelineRun {\n band: number\n track: number\n /** Percentage offset and width WITHIN the band. */\n left: number\n width: number\n marks: TimelineMark[]\n}\n\nexport interface Lane {\n key: string\n label: string\n sub?: string\n tracks: number\n runs: PlacedRun[]\n}\n\nexport interface Timeline {\n bands: Band[]\n /** Silence between band i and i+1, in ms. gaps.length === bands.length - 1 */\n gaps: number[]\n lanes: Lane[]\n from: number\n to: number\n}\n\n/** Split runs into activity clusters. A gap only breaks the axis when it is\n * both long in absolute terms and long relative to the work around it —\n * otherwise a slow afternoon shatters into dozens of one-run bands. */\nexport function computeBands(runs: TimelineRun[], minGapMs = 20 * 60_000, maxBands = 6): { bands: Band[]; gaps: number[] } {\n const sorted = [...runs].sort((a, b) => a.startedAt - b.startedAt)\n if (!sorted.length) return { bands: [], gaps: [] }\n const clusters: Array<{ from: number; to: number; runs: number }> = []\n for (const r of sorted) {\n const end = r.startedAt + Math.max(r.durationMs || 0, 0)\n const last = clusters[clusters.length - 1]\n if (last && r.startedAt - last.to <= minGapMs) {\n last.to = Math.max(last.to, end)\n last.runs++\n } else {\n clusters.push({ from: r.startedAt, to: end, runs: 1 })\n }\n }\n // Too many clusters: repeatedly merge across the smallest gap.\n while (clusters.length > maxBands) {\n let at = 0, best = Infinity\n for (let i = 1; i < clusters.length; i++) {\n const gap = clusters[i].from - clusters[i - 1].to\n if (gap < best) { best = gap; at = i }\n }\n clusters[at - 1].to = clusters[at].to\n clusters[at - 1].runs += clusters[at].runs\n clusters.splice(at, 1)\n }\n // Width follows density, not duration: a burst of nine runs in four minutes\n // deserves more room than a single run spread over two hours.\n const total = clusters.reduce((s, c) => s + c.runs, 0) || 1\n const bands = clusters.map(c => ({ from: c.from, to: Math.max(c.to, c.from + 1), weight: c.runs / total, runs: c.runs }))\n const gaps = clusters.slice(1).map((c, i) => c.from - clusters[i].to)\n return { bands, gaps }\n}\n\n/** First-fit packing into sub-tracks. Concurrent runs in one lane stack.\n * Exported because buildTimeline calls it and both travel to the browser. */\nexport function packTracks(runs: PlacedRun[]): number {\n const ends: number[] = []\n for (const r of runs.sort((a, b) => a.startedAt - b.startedAt)) {\n const end = r.startedAt + Math.max(r.durationMs || 0, 0)\n let t = ends.findIndex(e => e <= r.startedAt)\n if (t === -1) { t = ends.length; ends.push(end) } else { ends[t] = end }\n r.track = t\n }\n return Math.max(ends.length, 1)\n}\n\nexport function buildTimeline(runs: TimelineRun[], marks: TimelineMark[] = [], minGapMs?: number): Timeline {\n const { bands, gaps } = computeBands(runs, minGapMs)\n if (!bands.length) return { bands: [], gaps: [], lanes: [], from: 0, to: 0 }\n const marksByRun = new Map<string, TimelineMark[]>()\n for (const m of marks) {\n const list = marksByRun.get(m.runId)\n if (list) list.push(m); else marksByRun.set(m.runId, [m])\n }\n const byLane = new Map<string, Lane>()\n for (const r of runs) {\n const band = bands.findIndex(b => r.startedAt >= b.from && r.startedAt <= b.to)\n if (band === -1) continue\n const b = bands[band]\n const span = b.to - b.from || 1\n const left = ((r.startedAt - b.from) / span) * 100\n // 1.2% floor, inlined rather than hoisted: this function is stringified and\n // shipped to the browser, where a module-scope constant would be unbound.\n const width = Math.min(Math.max(((r.durationMs || 0) / span) * 100, 1.2), 100 - left)\n const lane = byLane.get(r.laneKey) ?? { key: r.laneKey, label: r.laneLabel, sub: r.laneSub, tracks: 1, runs: [] }\n lane.runs.push({ ...r, band, track: 0, left, width, marks: marksByRun.get(r.id) ?? [] })\n byLane.set(r.laneKey, lane)\n }\n const lanes = [...byLane.values()]\n for (const lane of lanes) {\n // Pack per band so a lane idle in one burst does not inherit its tracks.\n lane.tracks = Math.max(...bands.map((_, i) => packTracks(lane.runs.filter(r => r.band === i))), 1)\n }\n lanes.sort((a, b) => b.runs.length - a.runs.length || a.label.localeCompare(b.label))\n return { bands, gaps, lanes, from: bands[0].from, to: bands[bands.length - 1].to }\n}\n","// --- Cache-busted URLs for the browser bundles ----------------------------\n//\n// /assets/* is served with `max-age=60`, which is right for the file but\n// wrong for a deploy: a browser that fetched the bundle a minute ago is\n// entitled to keep it, and in practice people sat on a stale editor across\n// several deploys without knowing it. Appending a content hash gives the\n// bundle a new URL whenever its bytes change, so a deploy invalidates itself\n// and an unchanged bundle still caches.\n//\n// The hash is read once per process and cached: this runs on every page\n// render, and the file cannot change under a running daemon without a\n// restart bringing a fresh process anyway.\n\nimport { createHash } from \"crypto\"\nimport { readFileSync, statSync } from \"fs\"\nimport { resolve, dirname } from \"path\"\nimport { fileURLToPath } from \"url\"\n\nconst cache = new Map<string, string>()\n\n/** `/assets/<name>?v=<hash>` — falls back to the bare path when the bundle\n * is missing, so a page still renders (and 404s the script) rather than\n * throwing during SSR. */\nexport function assetUrl(name: string): string {\n const hit = cache.get(name)\n if (hit) return hit\n let url = `/assets/${name}`\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n // dist layout is flat: this module and web/ share a parent.\n const file = resolve(here, \"web\", name)\n const stat = statSync(file)\n // Hash the bytes for files small enough to read cheaply; fall back to\n // size+mtime for anything unexpectedly large.\n const tag = stat.size < 8_000_000\n ? createHash(\"sha1\").update(readFileSync(file)).digest(\"hex\").slice(0, 10)\n : `${stat.size}-${Math.floor(stat.mtimeMs)}`\n url = `/assets/${name}?v=${tag}`\n } catch { /* bundle not built here — serve the bare path */ }\n cache.set(name, url)\n return url\n}\n","// --- /activity — what happened, as opposed to what to do -------------------\n//\n// The monitor answers \"what needs me\". This answers \"what ran, where, and\n// what did it decide\". They are different questions and they get different\n// pages: crowding both onto one screen wins neither.\n//\n// One substrate, several lane keys. Nothing is recomputed when the\n// perspective changes — the runs are already in the browser, only the lane\n// they hang from changes, so switching is instant and always consistent.\n//\n// Two views of the same window: Timeline (runs on lanes, with review\n// findings) and Map (the live fleet map — projects as lines, agents as\n// stations, work as trains). The map is a React bundle fed by\n// /api/admin/activity-graph and its SSE stream; it loads only when the Map\n// view is first opened. /admin/activity-graph redirects to ?view=map.\n\nimport { renderShell, type TopbarPeer } from \"..\"\nimport { injectFns } from \"../inject\"\nimport { buildTimeline, computeBands, packTracks } from \"../../activity-timeline\"\nimport { assetUrl } from \"../asset-url\"\n\nconst ICON: Record<string, string> = {\n refresh: '<path d=\"M21 2v6h-6\"/><path d=\"M3 12a9 9 0 0 1 15-6.7L21 8\"/><path d=\"M3 22v-6h6\"/><path d=\"M21 12a9 9 0 0 1-15 6.7L3 16\"/>',\n bot: '<rect x=\"4\" y=\"8\" width=\"16\" height=\"12\" rx=\"2\"/><path d=\"M12 8V5M9 14h.01M15 14h.01\"/>',\n user: '<circle cx=\"12\" cy=\"8\" r=\"3.5\"/><path d=\"M5 21a7 7 0 0 1 14 0\"/>',\n folder: '<path d=\"M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/>',\n map: '<path d=\"M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2z\"/><path d=\"M9 4v14M15 6v14\"/>',\n timeline: '<path d=\"M3 6h10M7 12h14M3 18h8\"/>',\n mesh: '<circle cx=\"5\" cy=\"19\" r=\"2.5\"/><circle cx=\"19\" cy=\"19\" r=\"2.5\"/><circle cx=\"12\" cy=\"5\" r=\"2.5\"/><path d=\"M7 17.5l4-10M17 17.5l-4-10\"/>',\n route: '<circle cx=\"6\" cy=\"6\" r=\"3\"/><circle cx=\"18\" cy=\"18\" r=\"3\"/><path d=\"M9 6h6a3 3 0 0 1 0 6H9a3 3 0 0 0 0 6h6\"/>',\n clock: '<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/>',\n x: '<path d=\"M18 6L6 18M6 6l12 12\"/>',\n alert: '<path d=\"M12 9v4M12 17h.01\"/><path d=\"M10.3 3.9L1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z\"/>',\n}\nconst svg = (d: string, s = 14) =>\n '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" width=\"' + s + '\" height=\"' + s + '\" aria-hidden=\"true\">' + d + \"</svg>\"\n\nconst PERSPECTIVES: Array<[string, string, string]> = [\n [\"agent\", \"Agent\", ICON.bot],\n [\"client\", \"Client\", ICON.user],\n [\"project\", \"Project\", ICON.folder],\n [\"channel\", \"Channel\", ICON.route],\n [\"node\", \"Node\", ICON.mesh],\n]\n\nexport type ActivityView = \"timeline\" | \"map\"\n\nconst SUB: Record<ActivityView, string> = {\n timeline: \"Every run on the mesh, from task traces. The axis breaks over silence &mdash; quiet stretches are labelled, not drawn.\",\n map: \"The fleet as a transit map: projects are lines, agents are stations, work in flight is a train. Live.\",\n}\n\nexport function renderActivityPage(opts: { peers?: TopbarPeer[]; view?: string | null } = {}): string {\n const view: ActivityView = opts.view === \"map\" ? \"map\" : \"timeline\"\n const isMap = view === \"map\"\n return renderShell({\n title: \"AgentX · Activity\", activeTab: \"activity\", subtitle: \"Activity\", peers: opts.peers,\n body: `<div class=\"ac\">\n\n<div class=\"ac-head\">\n <div>\n <div class=\"ac-kicker\">Operations</div>\n <h1>What ran, where, and what it decided</h1>\n <p class=\"ac-sub\" id=\"sub\">${SUB[view]}</p>\n </div>\n <div class=\"ac-tools\">\n <div class=\"ac-seg\" id=\"view\" role=\"radiogroup\" aria-label=\"View\">\n ${([[\"timeline\", \"Timeline\", ICON.timeline], [\"map\", \"Map\", ICON.map]] as const).map(([v, t, ic]) =>\n `<button type=\"button\" role=\"radio\" data-value=\"${v}\" aria-checked=\"${v === view}\"${v === view ? ' class=\"is-on\"' : \"\"}>${svg(ic, 13)}${t}</button>`).join(\"\")}\n </div>\n <div class=\"ac-seg\" id=\"hours\" role=\"radiogroup\" aria-label=\"Window\">\n ${[[\"6\", \"6h\"], [\"24\", \"24h\"], [\"72\", \"3d\"], [\"168\", \"7d\"]].map(([v, t], i) =>\n `<button type=\"button\" role=\"radio\" data-value=\"${v}\" aria-checked=\"${i === 1}\"${i === 1 ? ' class=\"is-on\"' : \"\"}>${t}</button>`).join(\"\")}\n </div>\n <button class=\"ax-btn ax-btn--sm\" id=\"refresh\"${isMap ? \" hidden\" : \"\"}>${svg(ICON.refresh, 13)} Refresh</button>\n </div>\n</div>\n\n<div id=\"view-timeline\"${isMap ? \" hidden\" : \"\"}>\n<div class=\"ac-bar\">\n <span class=\"ac-lbl\">Lanes by</span>\n <div class=\"ac-seg\" id=\"persp\" role=\"radiogroup\" aria-label=\"Perspective\">\n ${PERSPECTIVES.map(([v, t, ic], i) =>\n `<button type=\"button\" role=\"radio\" data-value=\"${v}\" aria-checked=\"${i === 0}\"${i === 0 ? ' class=\"is-on\"' : \"\"}>${svg(ic, 13)}${t}</button>`).join(\"\")}\n </div>\n <div class=\"ac-key\">\n <span class=\"ac-k\"><i class=\"ac-d ac-d--decision\"></i>decision</span>\n <span class=\"ac-k\"><i class=\"ac-d ac-d--warning\"></i>warning</span>\n <span class=\"ac-k\"><i class=\"ac-d ac-d--recommendation\"></i>friction</span>\n <span class=\"ac-k\"><i class=\"ac-d ac-d--later\"></i>do later</span>\n <span class=\"ac-k\">${svg(ICON.x, 11)}canceled</span>\n </div>\n</div>\n\n<p id=\"notice\" role=\"status\" aria-live=\"polite\"></p>\n<div id=\"tl\" class=\"ac-tl\"></div>\n<div id=\"detail\"></div>\n</div>\n\n<div id=\"view-map\" class=\"ac-map\"${isMap ? \"\" : \" hidden\"}>\n <div id=\"ax-fleet-root\" data-hours=\"24\" data-src=\"${assetUrl(\"activity-graph.global.js\")}\"></div>\n</div>\n\n</div>`,\n css: ACTIVITY_CSS,\n scripts: `<script>${injectFns({ computeBands, packTracks, buildTimeline })}` +\n `const ICON = ${JSON.stringify(ICON)};const SUB = ${JSON.stringify(SUB)};${ACTIVITY_SCRIPT}</script>`,\n })\n}\n\nexport const ACTIVITY_CSS = `\n.ac{max-width:1440px;margin:0 auto;padding:22px 24px 48px}\n.ac :focus-visible{outline:2px solid var(--ax-accent);outline-offset:2px;border-radius:4px}\n.ac-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:14px}\n.ac-kicker{font-family:var(--ax-mono);font-size:10px;letter-spacing:0.1em;text-transform:uppercase;\n color:var(--ax-text-2);font-weight:600}\n.ac h1{font-size:22px;font-weight:600;letter-spacing:-0.015em;margin:5px 0 0}\n.ac-sub{font-size:13px;color:var(--ax-text-2);margin:5px 0 0;max-width:66ch;line-height:1.5}\n.ac-tools{display:flex;align-items:center;gap:10px;flex:none;padding-top:6px}\n.ac-bar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;background:var(--ax-surface);\n border:var(--ax-border-w) solid var(--ax-border);border-radius:var(--ax-radius-lg);\n padding:11px 16px;box-shadow:var(--ax-shadow);margin-bottom:14px}\n.ac-lbl{font-family:var(--ax-mono);font-size:10px;letter-spacing:0.08em;text-transform:uppercase;\n color:var(--ax-text-2);font-weight:600}\n.ac-seg{display:flex;border:var(--ax-border-w) solid var(--ax-border-2);border-radius:var(--ax-radius);\n overflow:hidden;background:var(--ax-bg)}\n.ac-seg button{background:transparent;border:none;border-right:1px solid var(--ax-border);\n color:var(--ax-text-2);font:inherit;font-size:12.5px;font-weight:600;padding:6px 13px;cursor:pointer;\n white-space:nowrap;display:inline-flex;align-items:center;gap:6px}\n.ac-seg button:last-child{border-right:none}\n.ac-seg button:hover{color:var(--ax-text);background:var(--ax-surface-2)}\n.ac-seg button.is-on{background:var(--ax-blue-t);color:var(--ax-blue-d)}\n.ac-key{display:flex;align-items:center;gap:13px;margin-left:auto;flex-wrap:wrap}\n.ac-k{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--ax-text-2)}\n.ac-d{width:9px;height:9px;flex:none}\n.ac-d--decision{background:var(--ax-blue-d);transform:rotate(45deg)}\n.ac-d--warning{background:var(--ax-amber-ink);clip-path:polygon(50% 0,100% 100%,0 100%)}\n.ac-d--recommendation{background:var(--ax-green-d);border-radius:50%}\n.ac-d--later{border:2px solid var(--ax-text-2);border-radius:2px}\n\n.ac-tl{background:var(--ax-surface);border:var(--ax-border-w) solid var(--ax-border);\n border-radius:var(--ax-radius-lg);box-shadow:var(--ax-shadow);overflow:hidden}\n.ac-row{display:flex;align-items:stretch;border-bottom:1px solid var(--ax-border);position:relative}\n.ac-row:last-child{border-bottom:none}\n.ac-row--hd{background:var(--ax-surface-2);border-bottom:var(--ax-border-w) solid var(--ax-border)}\n.ac-gut{width:186px;flex:none;display:flex;align-items:center;gap:9px;padding:8px 12px;\n border-right:var(--ax-border-w) solid var(--ax-border);min-width:0}\n.ac-gut b{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block}\n.ac-gut i{font-style:normal;font-family:var(--ax-mono);font-size:10px;color:var(--ax-text-2);\n overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block}\n.ac-bands{flex:1;display:flex;min-width:0}\n.ac-band{position:relative;min-width:0}\n.ac-band--hd{display:flex;align-items:center;justify-content:center;font-family:var(--ax-mono);\n font-size:10px;color:var(--ax-text-2);font-weight:600;padding:7px 0}\n.ac-gap{width:44px;flex:none;\n background-image:repeating-linear-gradient(45deg,var(--ax-surface-2) 0 3px,transparent 3px 7px)}\n.ac-gap--hd{display:flex;align-items:center;justify-content:center;background-image:none;\n border-left:1px dashed var(--ax-border-2);border-right:1px dashed var(--ax-border-2);\n font-family:var(--ax-mono);font-size:9px;color:var(--ax-text-2);font-weight:600}\n.ac-run{position:absolute;height:20px;border-radius:5px;background:var(--ax-blue);\n border:1px solid var(--ax-blue-d);cursor:pointer;padding:0}\n.ac-run:hover{filter:brightness(1.08)}\n.ac-run.is-sel{outline:2px solid var(--ax-text);outline-offset:1px}\n.ac-run--failed{background:var(--ax-red);border-color:var(--ax-red-ink)}\n.ac-run--canceled{background:transparent;border:1.5px dashed var(--ax-red-ink);height:14px;margin-top:3px}\n.ac-run--running{background:var(--ax-green);border-color:var(--ax-green-d)}\n.ac-mk{position:absolute;top:1px;width:9px;height:9px;transform:translateX(-50%);pointer-events:none}\n.ac-empty{padding:40px;text-align:center;color:var(--ax-text-2);font-size:13px}\n\n.ac-det{margin-top:14px;background:var(--ax-surface);border:var(--ax-border-w) solid var(--ax-border);\n border-radius:var(--ax-radius-lg);box-shadow:var(--ax-shadow);padding:15px 17px}\n.ac-det__h{display:flex;align-items:center;gap:9px;margin-bottom:11px;flex-wrap:wrap}\n.ac-det__t{font-size:14px;font-weight:600}\n.ac-kv{display:grid;grid-template-columns:80px minmax(0,1fr);gap:5px 12px;font-size:12.5px}\n.ac-kv dt{font-family:var(--ax-mono);font-size:10px;text-transform:uppercase;letter-spacing:0.05em;\n color:var(--ax-text-2);font-weight:600;padding-top:2px}\n.ac-kv dd{margin:0;color:var(--ax-text);min-width:0;overflow-wrap:anywhere}\n.ac-find{display:flex;gap:8px;align-items:flex-start;padding:7px 0;border-top:1px solid var(--ax-border);\n font-size:12.5px;line-height:1.45}\n.ac-chip{display:inline-flex;align-items:center;gap:5px;border-radius:var(--ax-radius-pill);padding:2px 9px;\n font-size:11.5px;font-weight:600;border:1px solid var(--ax-border-2);background:var(--ax-surface-2);\n color:var(--ax-text-2)}\n.ac-map{height:calc(100vh - 190px);min-height:480px;background:var(--ax-surface);\n border:var(--ax-border-w) solid var(--ax-border);border-radius:var(--ax-radius-lg);\n box-shadow:var(--ax-shadow);overflow:hidden}\n#ax-fleet-root{height:100%}\n#ax-fleet-root:empty::before{content:\"Loading fleet map…\";display:block;padding:40px 24px;\n color:var(--ax-text-2);font-size:13px;text-align:center}\n[hidden]{display:none!important}\n#notice{margin:0 0 12px;font-size:12.5px;color:var(--ax-text-2)}\n#notice:not(:empty){padding:9px 13px;border-radius:var(--ax-radius-sm);background:var(--ax-surface-2);\n border:var(--ax-border-w) solid var(--ax-border)}\n@media (max-width:900px){.ac-gut{width:120px}.ac-key{display:none}}\n@media (max-width:640px){.ac{padding:16px 16px 32px}.ac-head{flex-direction:column;gap:10px}\n .ac-tools{flex-wrap:wrap;padding-top:0}.ac-map{height:calc(100vh - 250px);margin:0 -16px;\n border-left:none;border-right:none;border-radius:0}}\n`\n\nexport const ACTIVITY_SCRIPT = String.raw`\nconst $=id=>document.getElementById(id);\nconst esc=s=>String(s==null?'':s).replace(/[&<>\"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst ic=(n,s)=>'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"'+(s||14)+'\" height=\"'+(s||14)+'\" aria-hidden=\"true\">'+ICON[n]+'</svg>';\nlet runs=[],marks=[],persp='agent',hours='24',selected=null,busy=false,loaded=false,\n view=$('view').querySelector('[aria-checked=\"true\"]').dataset.value;\n\nfunction fmtDur(ms){if(!ms)return '0s';if(ms<1000)return ms+'ms';const s=Math.round(ms/1000);\nif(s<60)return s+'s';const m=Math.floor(s/60);return m+'m '+(s%60)+'s';}\nfunction fmtGap(ms){const m=Math.round(ms/60000);if(m<60)return m+'m';const h=Math.floor(m/60);\nreturn h+'h'+(m%60?' '+(m%60)+'m':'');}\nfunction clock(ms){return new Date(ms).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});}\n\n/* The lane a run hangs from. Only this changes between perspectives — the\n runs themselves are already loaded, so switching never refetches. */\nfunction lane(r){\nif(persp==='agent')return {key:r.agentId+'|'+(r.channel||''),label:r.agentId,sub:r.channel||''};\nif(persp==='client')return {key:r.clientId||'unmapped',label:r.clientId||'unmapped',sub:''};\nif(persp==='project')return {key:r.project||('('+(r.channel||'none')+')'),label:r.project||('no project'),sub:r.channel||''};\nif(persp==='channel')return {key:r.channel||'none',label:r.channel||'none',sub:''};\nreturn {key:r.node||'this node',label:r.node||'this node',sub:''};\n}\n\nfunction render(){\nif(!runs.length){$('tl').innerHTML='<div class=\"ac-empty\">Nothing ran in this window.</div>';$('detail').innerHTML='';return;}\nconst t=buildTimeline(runs.map(r=>{const l=lane(r);\n return {id:r.id,laneKey:l.key,laneLabel:l.label,laneSub:l.sub,startedAt:r.startedAt,\n durationMs:r.durationMs||0,status:r.status,label:r.preview||''};}),marks);\nconst cells=(inner)=>t.bands.map((b,i)=>\n (i?'<div class=\"ac-gap'+inner.gapCls+'\">'+(inner.gap?fmtGap(t.gaps[i-1]):'')+'</div>':'')\n +'<div class=\"ac-band'+inner.cls+'\" style=\"flex:'+b.weight.toFixed(4)+'\">'+inner.body(b,i)+'</div>').join('');\n\nlet html='<div class=\"ac-row ac-row--hd\"><div class=\"ac-gut\"><i>'+t.lanes.length+' lanes &middot; '+runs.length+' runs</i></div>'\n +'<div class=\"ac-bands\">'+cells({cls:' ac-band--hd',gapCls:' ac-gap--hd',gap:true,\n body:b=>clock(b.from)+' &ndash; '+clock(b.to)})+'</div></div>';\n\nfor(const ln of t.lanes){\n const h=10+ln.tracks*24;\n html+='<div class=\"ac-row\" style=\"height:'+h+'px\"><div class=\"ac-gut\"><span style=\"min-width:0\"><b>'+esc(ln.label)+'</b>'\n +(ln.sub?'<i>'+esc(ln.sub)+'</i>':'')+'</span></div><div class=\"ac-bands\">'\n +cells({cls:'',gapCls:'',gap:false,body:(b,bi)=>ln.runs.filter(r=>r.band===bi).map(r=>{\n const top=5+r.track*24;\n const cls=r.status==='failed'?' ac-run--failed':r.status==='canceled'?' ac-run--canceled'\n :(r.status==='running'||r.status==='in-flight')?' ac-run--running':'';\n const mk=r.marks.map(m=>'<span class=\"ac-mk ac-d ac-d--'+m.kind+'\" style=\"left:'+r.left.toFixed(3)+'%;top:'+(top-11)+'px\"></span>').join('');\n return mk+'<button class=\"ac-run'+cls+(selected===r.id?' is-sel':'')+'\" data-run=\"'+esc(r.id)+'\"'\n +' style=\"left:'+r.left.toFixed(3)+'%;width:'+r.width.toFixed(3)+'%;top:'+top+'px\"'\n +' title=\"'+esc(r.laneLabel+' · '+clock(r.startedAt)+' · '+fmtDur(r.durationMs))+'\"></button>';\n }).join('')})\n +'</div></div>';\n}\n$('tl').innerHTML=html;\n$('tl').querySelectorAll('[data-run]').forEach(b=>b.onclick=()=>{selected=b.dataset.run;render();detail();});\ndetail();\n}\n\nconst KIND={decision:'Decision taken for you',warning:'Warning',recommendation:'Avoidable round trip',later:'For later'};\nfunction detail(){\nconst r=runs.find(x=>x.id===selected);\nif(!r){$('detail').innerHTML='';return;}\nconst found=marks.filter(m=>m.runId===r.id);\n$('detail').innerHTML='<div class=\"ac-det\"><div class=\"ac-det__h\">'\n +'<span class=\"ac-det__t\">'+esc(r.preview||'(no prompt recorded)')+'</span>'\n +'<span class=\"ac-chip\">'+esc(r.status)+'</span></div>'\n +'<dl class=\"ac-kv\">'\n +'<dt>When</dt><dd>'+clock(r.startedAt)+' &middot; took '+fmtDur(r.durationMs||0)+'</dd>'\n +'<dt>Who</dt><dd>'+esc(r.agentId)+(r.node?' on '+esc(r.node):'')+'</dd>'\n +'<dt>Where</dt><dd>'+esc(r.channel||'&mdash;')+(r.chatId?' &middot; '+esc(r.chatId):'')+'</dd>'\n +'<dt>For</dt><dd>'+esc(r.clientId||'unmapped')+'</dd></dl>'\n +(found.length?found.map(m=>'<div class=\"ac-find\"><i class=\"ac-d ac-d--'+m.kind+'\" style=\"margin-top:4px\"></i>'\n +'<span><b>'+esc(KIND[m.kind]||m.kind)+'.</b> '+esc(m.text)+'</span></div>').join('')\n :'<div class=\"ac-find\" style=\"color:var(--ax-text-2)\">No review findings for this run.</div>')\n +'</div>';\n}\n\nasync function load(){\nif(busy)return;busy=true;$('refresh').disabled=true;\ntry{\n const r=await fetch('/api/monitor');if(!r.ok)throw Error('HTTP '+r.status);\n const nodes=(await r.json()).nodes.filter(n=>n.ok);\n runs=[];marks=[];\n for(const n of nodes){\n try{\n const a=await fetch('/api/monitor/activity?node='+encodeURIComponent(n.url)+'&hours='+hours);\n if(!a.ok)continue;\n const d=await a.json();\n for(const run of d.runs||[])runs.push({...run,node:d.node||n.name});\n for(const m of d.marks||[])marks.push(m);\n }catch{}\n }\n $('notice').textContent='';\n render();\n}catch(e){$('notice').textContent=e.message;}\nfinally{busy=false;$('refresh').disabled=false;}\n}\n\nfunction paint(id,v){for(const b of $(id).children){const on=b.dataset.value===v;\nb.classList.toggle('is-on',on);b.setAttribute('aria-checked',String(on));}}\n$('persp').addEventListener('click',e=>{const b=e.target.closest('button[data-value]');if(!b)return;\npersp=b.dataset.value;paint('persp',persp);selected=null;render();});\n$('hours').addEventListener('click',e=>{const b=e.target.closest('button[data-value]');if(!b)return;\nhours=b.dataset.value;paint('hours',hours);$('ax-fleet-root').dataset.hours=hours;\nwindow.dispatchEvent(new CustomEvent('ax:activity-hours',{detail:Number(hours)}));\nloaded=false;if(view==='timeline'){loaded=true;load();}});\n$('refresh').onclick=load;\n\n/* The map bundle is heavy (React + xyflow); fetch it only when asked for. */\nfunction ensureMap(){const root=$('ax-fleet-root');if(root.dataset.loaded)return;root.dataset.loaded='1';\nconst sc=document.createElement('script');sc.src=root.dataset.src;document.body.appendChild(sc);}\nfunction show(v){view=v;paint('view',v);\n$('view-timeline').hidden=v!=='timeline';$('view-map').hidden=v!=='map';$('refresh').hidden=v!=='timeline';\n$('sub').innerHTML=SUB[v];\nconst u=new URL(location.href);if(v==='map')u.searchParams.set('view','map');else u.searchParams.delete('view');\nhistory.replaceState(null,'',u);\nif(v==='map')ensureMap();else if(!loaded){loaded=true;load();}}\n$('view').addEventListener('click',e=>{const b=e.target.closest('button[data-value]');if(!b||b.dataset.value===view)return;show(b.dataset.value);});\nshow(view);\n`\n","// --- Workflow visual editor — host page ---\n//\n// Thin shell that loads the React Flow-based editor bundle served from\n// /assets/workflow-editor.js. All interaction logic lives in\n// src/web/workflow-editor/ (see main.tsx). This page is deliberately\n// minimal — just a root div the bundle mounts into, plus a reset stylesheet\n// so the bundle's CSS-in-TS can take over without inheriting dashboard\n// component styles we don't want on the canvas.\n\nimport { renderShell, type TopbarPeer } from \"..\"\nimport { assetUrl } from \"../asset-url\"\n\nexport interface WorkflowEditorPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderWorkflowEditorPage(opts: WorkflowEditorPageOpts = {}): string {\n const body = `<div id=\"wfe-root\" style=\"height:100%\"></div>`\n\n return renderShell({\n title: \"AgentX · Workflow editor\",\n activeTab: \"workflows\",\n subtitle: \"Workflow editor\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: HOST_CSS,\n // Bundle is served as an IIFE from /assets/workflow-editor.js — built\n // by `tsup --config tsup.web.config.ts` to dist/web/workflow-editor.js.\n // `defer` keeps the script out of the head-blocking path; the bundle\n // itself waits for DOMContentLoaded before mounting.\n scripts: `<script src=\"${assetUrl(\"workflow-editor.global.js\")}\" defer></script>`,\n noMain: true,\n })\n}\n\nconst HOST_CSS = `\n/* Preload the fonts the redesign's CSS references. Placed in the shell so\n * they start fetching while the React bundle is still parsing. */\n@import url(\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Instrument+Serif&display=swap\");\n\nhtml, body { height: 100%; }\nbody { margin: 0; overflow: hidden; }\n/* The editor takes over the whole viewport except the dashboard topbar. The\n * topbar still renders via the shell so the Workflows tab highlights and\n * global nav works. */\n.ax-topbar { position: relative; z-index: 50; }\n#wfe-root { width: 100%; height: calc(100vh - 48px); }\n/* Tiny fallback while the bundle loads — prevents an empty flash. */\n#wfe-root:empty::before {\n content: \"Loading editor…\";\n display: block;\n padding: 40px 24px;\n color: #94a3b8;\n font: 13px/1 -apple-system, BlinkMacSystemFont, sans-serif;\n text-align: center;\n}\n`\n","import { readMonitorBody, monitorTargets } from \"./session-monitor\"\nimport { renderMonitorPage } from \"./ui/pages/monitor\"\nimport { renderActivityPage } from \"./ui/pages/activity\"\nimport { renderWorkflowEditorPage } from \"./ui/pages/workflow-editor\"\nimport { createServer, type IncomingMessage, type ServerResponse } from \"http\"\nimport { appendFileSync, existsSync, mkdirSync } from \"fs\"\nimport { readFile } from \"fs/promises\"\nimport { fileURLToPath } from \"url\"\nimport { dirname, resolve } from \"path\"\nimport type { DaemonConfig } from \"./config\"\nimport type { BoardConfig, BoardColumn } from \"@/boards/config\"\nimport { deriveStage, transitionDiff } from \"@/boards/config\"\nimport type { WorkSource, WorkItem } from \"@/business/work-pool\"\nimport { GitLabWorkSource } from \"@/business/work-pool\"\nimport { SORTABLE_JS } from \"./vendor/sortable\"\nimport { handleWizardGet, handleWizardPost, handleStartDaemonPost, wizardState } from \"./setup-wizard\"\nimport { handleAdminGet, handleAdminApi, handleAdminConfigGet } from \"./admin-panel\"\nimport { handleGraphGet, handleGraphApi } from \"./graph-panel\"\nimport { handleObservabilityGet, handleObservabilityApi } from \"./observability-panel\"\nimport { handleLedgerApi, renderLedgerPage } from \"./ledger-panel\"\nimport { renderCostPage } from \"./ui/pages/cost\"\nimport { renderProjectsPage } from \"./ui/pages/projects\"\nimport { createWikiHandler } from \"@/wiki/serve\"\nimport { recordSurfaceUse } from \"@/observability/surface-usage\"\nimport { handleActivityGraphApi, handleActivityGraphStream, handleActivityGraphDetail, setDaemonConfigForActivityGraph, buildLocalActivityGraphSnapshot, mergeFleetSnapshots, withForgeStatus, type FleetSnapshot } from \"./activity-graph-panel\"\nimport { handleAgentPageGet, handleAgentApi } from \"./agent-panel\"\nimport { renderLivePage } from \"./ui/pages/live\"\nimport { renderMeshPage } from \"./ui/pages/mesh\"\nimport { fetchMeshAnalytics, fetchMeshDay, proxyNodeAnalytics, type NodeTarget } from \"./mesh-analytics-api\"\nimport { renderBoardsPage } from \"./ui/pages/boards\"\nimport { renderGlossaryPage } from \"./ui/pages/glossary\"\nimport { renderWorkflowsPage } from \"./ui/pages/workflows\"\nimport { renderProceduresPage } from \"./ui/pages/procedures\"\nimport { renderProcessesPage } from \"./ui/pages/processes\"\nimport { renderTaskPage } from \"./ui/pages/task\"\nimport { renderHistoryPage } from \"./ui/pages/history\"\nimport { handleWorkflowsApi } from \"./workflows-api\"\nimport { LayoutStore, RunStore, WorkflowStore, type WorkflowRun } from \"@/workflows\"\nimport { TokenStore, recordHasScope, extractToken, type TokenRecord } from \"./token-store\"\nimport { setTopbarFeatures, type TopbarPeer } from \"./topbar\"\n\n// --- Kanban Board Dashboard ---\n//\n// Zero-build web UI, served on its own port (default 4202, bound 127.0.0.1).\n// Inline HTML/CSS/JS, manual routing, no framework.\n//\n// Column kinds (see src/boards/config.ts):\n// - open-backlog: opened issues with no Status::* scoped label\n// - scoped-label: opened issues with a specific Status::X label\n// - closed: closed issues (bounded by closedWindowDays)\n// - label: opened issues with a flat mapsToLabel (legacy)\n//\n// Drag-drop produces a ColumnTransition that may: add/remove labels, close,\n// or reopen an issue (via GitLab `state_event`).\n\nexport function startBoardDashboard(config: DaemonConfig): void {\n const dashboard = config.dashboard\n const boards = config.boards\n\n const port = dashboard.port\n const bind = dashboard.bind\n const token = dashboard.token\n\n const sources = new Map<string, WorkSource>()\n for (const board of boards) {\n try {\n sources.set(board.id, buildWorkSource(board, config))\n } catch (e: any) {\n console.error(` Board \"${board.id}\" failed to initialize: ${e.message}`)\n }\n }\n\n // Workflow stores are lightweight (filesystem-backed, no network). Created\n // once per dashboard so the /workflows page + editor API don't re-scan\n // directories on every request.\n //\n // Home-node id comes from the mesh config when available; falls back to a\n // stable \"local\" so single-node runs work out of the box. Until the\n // integration seam commits, the dispatcher itself isn't booted in the\n // daemon — the editor's view over these stores is read/write, but the\n // engine's run-time dispatch + mesh forwarding aren't wired yet.\n const wfNodeId = config.node?.id || \"local\"\n const wfDir = config.workflows?.dir ? resolve(process.cwd(), config.workflows.dir) : undefined\n const workflowStore = new WorkflowStore(wfDir ? { baseDir: wfDir } : undefined)\n const workflowRuns = new RunStore(wfDir ? { baseDir: wfDir, nodeId: wfNodeId } : { nodeId: wfNodeId })\n const workflowLayouts = new LayoutStore(wfDir ? { baseDir: wfDir } : undefined)\n\n // Activity-graph snapshot needs agent metadata (tier, model, name) from\n // agentx.json — set the module-level reference so the snapshot builder can\n // read it when serving each request.\n setDaemonConfigForActivityGraph(config)\n\n // Compact primary nav. Every other surface stays routable by URL.\n setTopbarFeatures({\n business: config.business?.enabled === true,\n })\n\n const server = createServer(async (req, res) => {\n try {\n await handleRequest(req, res, { boards, sources, token, config, workflowStore, workflowRuns, workflowLayouts })\n } catch (e: any) {\n sendJson(res, 500, { error: e.message || \"internal error\" })\n }\n })\n\n server.listen(port, bind, () => {\n const displayHost = bind === \"0.0.0.0\" ? \"localhost\" : bind\n if (boards.length === 0) {\n console.log(`\\n Live dashboard: http://${displayHost}:${port}\\n`)\n console.log(\" (no boards configured — '/' serves the live view; add a board with 'agentx board add')\")\n } else {\n console.log(`\\n Kanban dashboard: http://${displayHost}:${port}\\n`)\n console.log(` Boards: ${boards.map((b) => b.id).join(\", \")}`)\n }\n if (bind === \"127.0.0.1\" && !token) {\n console.log(\" (bound to localhost; no auth token — safe default)\")\n } else if (bind !== \"127.0.0.1\" && !token) {\n console.log(\" ⚠ bound to a non-loopback address without a token — writes are UNAUTHENTICATED\")\n }\n })\n}\n\n/** Every page a human can land on. Kept explicit rather than inferred from\n * \"has no file extension\" so assets, redirects and API routes can never\n * drift into the numbers the reduction decisions are made from. */\nconst DASHBOARD_PAGES = new Set([\n \"/\",\n \"/live\",\n \"/mesh\",\n \"/monitor\",\n \"/activity\",\n \"/boards\",\n \"/glossary\",\n \"/workflows\",\n \"/workflows/editor\",\n \"/procedures\",\n \"/processes\",\n \"/graph\",\n \"/setup\",\n \"/admin\",\n \"/admin/graph\",\n \"/admin/health\",\n \"/admin/observability\",\n \"/admin/ledger\",\n \"/admin/cost\",\n \"/admin/projects\",\n \"/admin/wiki\",\n])\n\ninterface Ctx {\n boards: BoardConfig[]\n sources: Map<string, WorkSource>\n token?: string\n config: DaemonConfig\n workflowStore: WorkflowStore\n workflowRuns: RunStore\n workflowLayouts: LayoutStore\n}\n\nasync function handleRequest(req: IncomingMessage, res: ServerResponse, ctx: Ctx): Promise<void> {\n const url = new URL(req.url || \"/\", \"http://localhost\")\n const path = url.pathname\n const method = (req.method || \"GET\").toUpperCase()\n\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\")\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PATCH, OPTIONS\")\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization, X-Requested-With\")\n if (method === \"OPTIONS\") { res.writeHead(204); res.end(); return }\n\n // Count which dashboard pages operators actually open. Page paths only —\n // no query strings, no ids, and nothing under /api (those are XHR from a\n // page we already counted, so they'd inflate every number).\n // See docs/architecture/surface-reduction.md.\n if (method === \"GET\" && DASHBOARD_PAGES.has(path)) {\n recordSurfaceUse(\"page\", path)\n }\n\n if (method === \"GET\" && path === \"/\") {\n // Home is Live, always. It answers the question an operator actually\n // opens the dashboard for — who is alive and what are they doing.\n // Boards used to take the root whenever any were configured, which meant\n // the landing page silently changed identity based on config; now they\n // live at /boards like every other surface.\n //\n // The one exception is a machine with no agents yet, where there is\n // nothing live to show and the wizard is the only useful destination.\n const wz = wizardState()\n if (!wz.configExists || wz.agentCount === 0) {\n res.writeHead(302, { Location: \"/setup\" })\n res.end()\n return\n }\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderLivePage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/boards\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderBoardsPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/live\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderLivePage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n // n8n posts here. It is forwarded to the local node rather than served\n // from the dashboard so the operator has ONE address to paste into n8n —\n // the one they are already looking at — instead of learning which port the\n // daemon happens to be on.\n const n8nIn = method === \"POST\" && path.match(/^\\/webhook\\/n8n\\/[A-Za-z0-9._:-]{1,64}$/)\n if (n8nIn) {\n try {\n const [node] = await resolveNodeTargets(ctx.config)\n if (!node) { sendJson(res, 503, { error: \"no node available\" }); return }\n const body = await new Promise<string>(resolve => {\n let d = \"\"; req.on(\"data\", c => (d += c)); req.on(\"end\", () => resolve(d)); req.on(\"error\", () => resolve(\"\"))\n })\n const r = await fetch(node.url + path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...(node.token ? { Authorization: `Bearer ${node.token}` } : {}) },\n body: body || \"{}\",\n })\n res.writeHead(r.status, { \"Content-Type\": \"application/json\" })\n res.end(await r.text())\n } catch (e: any) {\n sendJson(res, 502, { error: \"could not reach the node\", message: String(e?.message || e) })\n }\n return\n }\n\n // Workflow-builder chat (proxies to main daemon where the dispatcher\n // + AgentRegistry live). The board-dashboard serves /workflows/editor\n // but runs its own workflow stores; the chat endpoint needs the\n // running agent registry, which only the main daemon has.\n // Ask-an-agent drawer. Every verb proxies to one node: `node` picks which,\n // and a conversation belongs to the node that holds it.\n if (path === \"/api/assistant\" || path.startsWith(\"/api/assistant/\")) {\n try {\n const targets = await resolveNodeTargets(ctx.config)\n const wanted = method === \"POST\" ? undefined : url.searchParams.get(\"node\")\n const body: any = method === \"POST\" ? await readJson(req) : undefined\n const pick = body?.node ?? wanted\n const node = pick ? targets.find(n => n.url === pick) : targets[0]\n if (!node) { sendJson(res, 400, { error: \"unknown node\" }); return }\n const qs = url.search ? url.search.replace(/([?&])node=[^&]*/, \"$1\").replace(/[?&]$/, \"\") : \"\"\n const r = await fetch(node.url + path + qs, {\n method,\n headers: { \"Content-Type\": \"application/json\", ...(node.token ? { Authorization: `Bearer ${node.token}` } : {}) },\n ...(method === \"POST\" ? { body: JSON.stringify({ ...body, node: undefined }) } : {}),\n })\n sendJson(res, r.status, await r.json().catch(() => ({ error: `HTTP ${r.status}` })))\n } catch (e: any) {\n sendJson(res, 502, { error: \"could not reach the node\", message: e?.message || String(e) })\n }\n return\n }\n\n if (method === \"POST\" && path === \"/api/workflows/editor/chat\") {\n try {\n const body = await readJson(req)\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" }\n if (ctx.config.dashboard.token) headers[\"Authorization\"] = `Bearer ${ctx.config.dashboard.token}`\n const daemonUrl = ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\")\n const r = await fetch(`${daemonUrl}/api/workflows/editor/chat`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body ?? {}),\n })\n const data = await r.json().catch(() => ({ error: `HTTP ${r.status}` }))\n sendJson(res, r.status, data)\n } catch (e: any) {\n sendJson(res, 502, { error: \"daemon unreachable\", message: e.message || String(e) })\n }\n return\n }\n\n if (method === \"GET\" && path === \"/api/n8n/workflows\") {\n try {\n const [node] = await resolveNodeTargets(ctx.config)\n if (!node) { sendJson(res, 200, { configured: false, workflows: [] }); return }\n const r = await fetch(node.url + \"/api/n8n/workflows\", {\n headers: node.token ? { Authorization: `Bearer ${node.token}` } : {},\n })\n sendJson(res, r.status, await r.json().catch(() => ({ configured: false, workflows: [] })))\n } catch {\n sendJson(res, 200, { configured: false, workflows: [] })\n }\n return\n }\n\n if (method === \"GET\" && path === \"/workflows/editor\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderWorkflowEditorPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/activity\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderActivityPage({ peers: buildTopbarPeers(ctx.config), view: url.searchParams.get(\"view\") }))\n return\n }\n if (method === \"GET\" && path === \"/monitor\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderMonitorPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (path === \"/api/monitor\" || path.startsWith(\"/api/monitor/\")) {\n const ac = new AbortController()\n const timeout = setTimeout(() => ac.abort(), 8000)\n try {\n const targets = monitorTargets(await resolveNodeTargets(ctx.config, ac.signal), ctx.config.mesh.peers)\n if (method === \"GET\" && path === \"/api/monitor\") {\n const nodes = await Promise.all(targets.map(async node => {\n try {\n const r = await fetch(node.url + \"/monitor\", { headers: node.token ? { Authorization: `Bearer ${node.token}` } : {}, signal: ac.signal })\n if (!r.ok) throw new Error(`HTTP ${r.status}`)\n return { name: node.name, url: node.url, ok: true, data: await r.json() }\n } catch (e: any) { return { name: node.name, url: node.url, ok: false, error: e.message } }\n }))\n sendJson(res, 200, { nodes }); return\n }\n const node = targets.find(n => n.url === url.searchParams.get(\"node\"))\n if (!node) { sendJson(res, 400, { error: \"Select a known node\" }); return }\n const op = path.slice(\"/api/monitor/\".length)\n if (!((method === \"GET\" && [\"discover\", \"actions\", \"activity\"].includes(op)) || (method === \"POST\" && [\"register\", \"ended\", \"action\", \"retry\", \"clear\"].includes(op)))) {\n sendJson(res, 405, { error: \"Unknown monitor operation\" }); return\n }\n const body = method === \"POST\" ? JSON.stringify(await readMonitorBody(req)) : undefined\n const r = await fetch(node.url + \"/monitor/\" + op + (op === \"actions\" ? \"?offset=\" + encodeURIComponent(url.searchParams.get(\"offset\") || \"0\")\n : op === \"activity\" ? \"?hours=\" + encodeURIComponent(url.searchParams.get(\"hours\") || \"24\") : \"\"), { method, body, signal: ac.signal, headers: { \"Content-Type\": \"application/json\", ...(node.token ? { Authorization: `Bearer ${node.token}` } : {}) } })\n sendJson(res, r.status, await r.json())\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n finally { clearTimeout(timeout) }\n return\n }\n if (method === \"GET\" && path === \"/mesh\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderMeshPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/glossary\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderGlossaryPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/workflows\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderWorkflowsPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/procedures\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderProceduresPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/processes\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderProcessesPage({}))\n return\n }\n // /tasks/:id — watching an agent work is a place, not a popup. Replaces the\n // full-screen modal on Live and the right-hand drawer on the agent page,\n // both of which streamed this same task into something you couldn't link\n // to, reload, or keep open beside anything else.\n const taskPage = method === \"GET\" && path.match(/^\\/tasks\\/([^/]+)$/)\n if (taskPage) {\n const agentId = url.searchParams.get(\"agent\") || \"\"\n const nodeUrl = url.searchParams.get(\"node\") || ctx.config.dashboard.daemonUrl\n if (!agentId) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(\"<p>Missing <code>?agent=</code>. Open this task from the Live page.</p>\")\n return\n }\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderTaskPage({\n taskId: decodeURIComponent(taskPage[1]),\n agentId,\n agentName: url.searchParams.get(\"name\") || undefined,\n channel: url.searchParams.get(\"channel\") || undefined,\n archived: url.searchParams.get(\"archived\") === \"1\",\n ask: url.searchParams.get(\"ask\") || undefined,\n askAt: url.searchParams.get(\"at\") || undefined,\n nodeUrl,\n peers: buildTopbarPeers(ctx.config),\n }))\n return\n }\n // /agents/:id/history — was a 360px drawer sliding over Live. The list is\n // what you came to read, it wants width, and every row leads somewhere, so\n // it needs to be linkable and back-navigable.\n const histPage = method === \"GET\" && path.match(/^\\/agents\\/([^/]+)\\/history$/)\n if (histPage) {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderHistoryPage({\n agentId: decodeURIComponent(histPage[1]),\n agentName: url.searchParams.get(\"name\") || undefined,\n nodeUrl: url.searchParams.get(\"node\") || ctx.config.dashboard.daemonUrl,\n peers: buildTopbarPeers(ctx.config),\n }))\n return\n }\n // /graph is the canonical doc path; the page itself lives at /admin/graph.\n if (method === \"GET\" && path === \"/graph\") {\n res.writeHead(302, { Location: \"/admin/graph\" })\n res.end()\n return\n }\n\n // Setup wizard — form-driven first-run experience for non-technical operators.\n if (method === \"GET\" && path === \"/setup\") {\n handleWizardGet(req, res)\n return\n }\n if (method === \"POST\" && path === \"/api/setup\") {\n await handleWizardPost(req, res)\n return\n }\n if (method === \"POST\" && path === \"/api/setup/start-daemon\") {\n await handleStartDaemonPost(req, res)\n return\n }\n\n // Admin panel — ongoing management without editing JSON.\n if (method === \"GET\" && path === \"/admin\") {\n handleAdminGet(req, res, buildTopbarPeers(ctx.config), ctx.token)\n return\n }\n // /admin/graph — Intent Knowledge Graph: pending approvals + taxonomy editor.\n if (method === \"GET\" && path === \"/admin/graph\") {\n handleGraphGet(req, res, buildTopbarPeers(ctx.config), ctx.token)\n return\n }\n // /admin/health — platform-health view over route_traces + rotations + errors.\n // Renamed from /admin/observability — that name suggested business flow,\n // but the page only ever covered platform health. Old URL redirects so\n // existing bookmarks keep working.\n if (method === \"GET\" && (path === \"/admin/health\" || path === \"/admin/observability\")) {\n if (path === \"/admin/observability\") {\n res.writeHead(302, { Location: \"/admin/health\" })\n res.end()\n return\n }\n handleObservabilityGet(req, res, buildTopbarPeers(ctx.config))\n return\n }\n // ── Cross-mesh API proxy (must precede every /api/admin/* handler) ──\n // When the topbar peer selector is set to a non-primary peer, the\n // client-side fetch wrapper attaches X-Agentx-Peer / ?peer=<url> to the\n // request. Forward the whole call to that peer's dashboard before any\n // local handler picks it up, so the activity-graph + observability +\n // every other admin surface follow the selector.\n //\n // Special peerId=\"fleet\" → fan-out + merge across local + every\n // dashboard.daemons[] entry. Only meaningful for the activity-graph\n // snapshot endpoint right now; other endpoints fall through to local\n // until they grow merge semantics.\n // /api/workflows + /api/agents live outside /api/admin/ for legacy\n // reasons but are part of the admin surface and follow the same\n // peer-selector contract. Mirror of the shouldProxy() allowlist on\n // the client side (topbar.ts). Treat them the same as /api/admin/*\n // so admin pages proxy to the chosen peer when the selector is set.\n if (path.startsWith(\"/api/admin/\") || path.startsWith(\"/api/workflows\") || path.startsWith(\"/api/agents\")) {\n // An explicit ?peer=fleet (the /activity map) beats the topbar's\n // per-node header: that view is mesh-wide by definition.\n const peerId = url.searchParams.get(\"peer\") === \"fleet\" ? \"fleet\"\n : String(req.headers[\"x-agentx-peer\"] || \"\").trim() || url.searchParams.get(\"peer\") || \"\"\n if (peerId === \"fleet\") {\n if (path === \"/api/admin/activity-graph\") {\n await handleActivityGraphFleet(req, res, ctx)\n return\n }\n if (path === \"/api/admin/activity-graph/stream\") {\n await handleActivityGraphStream(req, res, path, (hours) => buildFleetActivitySnapshot(hours, ctx.config))\n return\n }\n // Dispatch detail in fleet mode: the id was rewritten to\n // \"<nodeName>::<originalId>\" by the merger so React keys stay\n // unique. Route the lookup back to the node that owns the row.\n const detailMatch = path.match(/^\\/api\\/admin\\/activity-graph\\/dispatch\\/(.+)$/)\n if (detailMatch) {\n const decoded = decodeURIComponent(detailMatch[1])\n const sep = decoded.indexOf(\"::\")\n if (sep > 0) {\n const nodeName = decoded.slice(0, sep)\n const realId = decoded.slice(sep + 2)\n const localNodeName = ctx.config.node?.id || ctx.config.node?.name || \"local\"\n if (nodeName === localNodeName) {\n // Local row — strip the prefix and let the local handler answer.\n req.url = `/api/admin/activity-graph/dispatch/${encodeURIComponent(realId)}`\n // Fall through to the regular handler below.\n } else {\n const peerByName = (ctx.config.dashboard?.daemons || []).find((d) => (d.name || d.url) === nodeName)\n if (peerByName) {\n const peer = findPeer(peerByName.url.replace(/\\/+$/, \"\"), ctx.config)\n if (peer) {\n await proxyAdminToPeer(\n req, res, peer,\n `/api/admin/activity-graph/dispatch/${encodeURIComponent(realId)}`,\n )\n return\n }\n }\n sendJson(res, 404, { error: `unknown node \"${nodeName}\" in fleet dispatch id` })\n return\n }\n }\n }\n // Anything else in fleet mode → answer locally.\n } else if (peerId && peerId !== \"primary\") {\n const peer = findPeer(peerId, ctx.config)\n if (!peer) { sendJson(res, 404, { error: `unknown peer: ${peerId}` }); return }\n // Strip ?peer= so the destination doesn't re-proxy.\n const stripped = (url.search || \"\").replace(/[?&]peer=[^&]*/g, (m) => m.startsWith(\"?\") ? \"?\" : \"\")\n const cleanQuery = stripped === \"?\" ? \"\" : stripped\n await proxyAdminToPeer(req, res, peer, path + cleanQuery)\n return\n }\n }\n\n if (method === \"GET\" && path.startsWith(\"/api/admin/observability/\")) {\n if (await handleObservabilityApi(req, res, path)) return\n }\n // /api/admin/logs/stream — SSE tail of the daemon's stdout. Mirrors\n // `agentx daemon logs -f`. On systemd-managed nodes this calls\n // journalctl; on others it falls back to /tmp/agentx-daemon.log.\n if (method === \"GET\" && path === \"/api/admin/logs/stream\") {\n await streamDaemonLogs(req, res)\n return\n }\n // /api/admin/doctor — preflight check JSON. Same shape as\n // `agentx doctor --json`, served in-process.\n if (method === \"GET\" && path === \"/api/admin/doctor\") {\n try {\n const { runDoctorChecks } = await import(\"@/commands/doctor\")\n const url = new URL(req.url || \"/\", \"http://_\")\n const running = url.searchParams.get(\"running\") !== \"false\"\n const result = await runDoctorChecks({ running })\n sendJson(res, 200, result)\n } catch (e: any) {\n sendJson(res, 500, { error: e?.message || String(e) })\n }\n return\n }\n // /admin/ledger — read-only window over .agentx/intent/ledger.sqlite.\n // Mirrors `agentx ledger stats/events/divergences/active`. Replay and\n // lineage stay CLI-only.\n if (method === \"GET\" && path === \"/admin/ledger\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderLedgerPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path.startsWith(\"/api/admin/ledger/\")) {\n if (await handleLedgerApi(req, res, path)) return\n }\n // /admin/cost — token-spend page. Replaces the standalone\n // `agentx usage serve` (port 4201) and the Cost tab inside Health.\n if (method === \"GET\" && path === \"/admin/cost\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderCostPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n // /admin/projects — aggregated per-project view. Data via\n // GET /api/admin/projects on the daemon, proxied by the existing\n // /api/admin/* path-prefix proxy so the mesh selector works.\n if (method === \"GET\" && path === \"/admin/projects\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderProjectsPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n // /admin/wiki/* — wiki UI mounted natively into the dashboard. Same\n // renderer as `agentx wiki serve`, no second port required. The wiki\n // hub instance is created lazily on first request and reused for the\n // lifetime of the dashboard process; updates from `wiki absorb` are\n // picked up because each render rebuilds its index from disk.\n if (method === \"GET\" && (path === \"/admin/wiki\" || path.startsWith(\"/admin/wiki/\"))) {\n if (path === \"/admin/wiki\") {\n // Trailing slash so internal links stay relative to the mount.\n res.writeHead(302, { Location: \"/admin/wiki/\" })\n res.end()\n return\n }\n await getOrCreateWikiHandler(ctx)(req, res)\n return\n }\n // /admin/activity-graph retired: its map lives on as the Map view of\n // /activity. The APIs below still feed that view.\n if (method === \"GET\" && path === \"/admin/activity-graph\") {\n res.writeHead(302, { Location: \"/activity?view=map\" })\n res.end()\n return\n }\n if (method === \"GET\" && path === \"/api/admin/activity-graph\") {\n if (await handleActivityGraphApi(req, res, path)) return\n }\n if (method === \"GET\" && path.startsWith(\"/api/admin/activity-graph/stream\")) {\n if (await handleActivityGraphStream(req, res, path)) return\n }\n if (method === \"GET\" && path.startsWith(\"/api/admin/activity-graph/dispatch/\")) {\n if (await handleActivityGraphDetail(req, res, path)) return\n }\n // /admin/agents/<id> — dedicated per-agent page with md editor + skill mgr.\n const agentPageMatch = method === \"GET\" && path.match(/^\\/admin\\/agents\\/([a-z0-9][a-z0-9_-]*)$/)\n if (agentPageMatch) {\n handleAgentPageGet(req, res, agentPageMatch[1], buildTopbarPeers(ctx.config), ctx.token)\n return\n }\n // (Cross-mesh proxy is now handled at the top of /api/admin/*. The\n // local-token enforcement below still runs for any admin call that\n // didn't match a specific handler above.)\n if (path.startsWith(\"/api/admin/\")) {\n // Local admin calls: enforce dashboard.token (if configured) BEFORE the\n // dispatcher runs. The legacy ctx.token check further down only catches\n // paths nothing else handled — admin routes were silently bypassing it.\n if (ctx.token) {\n const authHeader = req.headers.authorization || \"\"\n const got = authHeader.startsWith(\"Bearer \") ? authHeader.slice(7) : \"\"\n if (got !== ctx.token) {\n const tokStore = new TokenStore()\n const rec = got ? tokStore.verify(got) : null\n if (!rec || !recordHasScope(rec, \"dashboard:write\")) {\n sendJson(res, 401, { error: \"unauthorized\" })\n return\n }\n }\n }\n }\n if (method === \"GET\" && path === \"/api/admin/config\") {\n await handleAdminConfigGet(req, res)\n return\n }\n if (path.startsWith(\"/api/admin/graph/\")) {\n await handleGraphApi(req, res, path)\n return\n }\n if (path.startsWith(\"/api/admin/agent/\")) {\n await handleAgentApi(req, res, path)\n return\n }\n if (path.startsWith(\"/api/admin/\")) {\n await handleAdminApi(req, res, path)\n return\n }\n if (method === \"GET\" && path === \"/sortable.min.js\") {\n res.writeHead(200, { \"Content-Type\": \"application/javascript; charset=utf-8\", \"Cache-Control\": \"public, max-age=86400\" })\n res.end(SORTABLE_JS)\n return\n }\n\n // Serve the web-bundled editor artifact. Built by `tsup --config tsup.web.config.ts`\n // to dist/web/<name>.js. Any `/assets/<name>` request is mapped\n // 1:1 into dist/web so future bundles (graph editor, ...) can live there\n // without another route registration.\n if (method === \"GET\" && path.startsWith(\"/assets/\")) {\n const rel = path.slice(\"/assets/\".length)\n // Allow dotted stems (e.g. \"activity-graph.global.js\") but keep the\n // whitelist narrow to script/map/style files.\n if (!/^[a-zA-Z0-9._-]+\\.(js|map|css)$/.test(rel)) {\n sendJson(res, 400, { error: \"invalid asset name\" }); return\n }\n const assetPath = resolveFromHere(\"web/\" + rel)\n try {\n const buf = await readAsset(assetPath)\n const ct = rel.endsWith(\".map\") ? \"application/json; charset=utf-8\"\n : rel.endsWith(\".css\") ? \"text/css; charset=utf-8\"\n : \"application/javascript; charset=utf-8\"\n // A hashed URL names exactly one build, so it can be cached hard; a bare\n // one must keep revalidating because its bytes change under it.\n const immutable = url.searchParams.has(\"v\")\n res.writeHead(200, {\n \"Content-Type\": ct,\n \"Cache-Control\": immutable ? \"public, max-age=31536000, immutable\" : \"public, max-age=60\",\n })\n res.end(buf)\n } catch {\n sendJson(res, 404, { error: \"asset not found\", hint: \"run: npm run build:web\" })\n }\n return\n }\n\n // Live mesh view endpoints (JSON snapshot + SSE push every 2s)\n if (method === \"GET\" && path === \"/api/live\") {\n try {\n const snap = await buildLiveSnapshot(ctx.config)\n sendJson(res, 200, snap)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n if (method === \"GET\" && path === \"/api/mesh\") {\n try {\n const date = url.searchParams.get(\"date\") || new Date().toISOString().slice(0, 10)\n const timezone = url.searchParams.get(\"timezone\") || \"UTC\"\n const snap = await buildLiveSnapshot(ctx.config, { date, timezone })\n sendJson(res, 200, snap)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // Mesh analytics — fleet-wide activity, failure causes, zombie jobs and\n // thread lifetimes. Fans out to every node's /analytics/mesh and merges.\n // GET /api/mesh/analytics?days=30&tzOffset=<minutes>&limit=60\n if (method === \"GET\" && path === \"/api/mesh/analytics\") {\n const num = (k: string, d: number, lo: number, hi: number): number => {\n const v = parseInt(url.searchParams.get(k) || \"\", 10)\n return Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : d\n }\n try {\n const targets = await resolveNodeTargets(ctx.config)\n sendJson(res, 200, await fetchMeshAnalytics(targets, {\n days: num(\"days\", 30, 0, 180),\n tzOffsetMinutes: num(\"tzOffset\", 0, -840, 840),\n limit: num(\"limit\", 60, 1, 200),\n }))\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // One activity column, across the fleet.\n // GET /api/mesh/day?day=YYYY-MM-DD&tzOffset=<minutes>&limit=40\n if (method === \"GET\" && path === \"/api/mesh/day\") {\n const day = url.searchParams.get(\"day\") || \"\"\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(day)) {\n sendJson(res, 400, { error: \"day=YYYY-MM-DD query param required\" }); return\n }\n const num = (k: string, d: number, lo: number, hi: number): number => {\n const v = parseInt(url.searchParams.get(k) || \"\", 10)\n return Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : d\n }\n try {\n const targets = await resolveNodeTargets(ctx.config)\n sendJson(res, 200, await fetchMeshDay(targets, {\n day,\n tzOffsetMinutes: num(\"tzOffset\", 0, -840, 840),\n limit: num(\"limit\", 40, 1, 200),\n }))\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // Drill-down proxies. The node URL comes from the merged analytics\n // payload and is re-checked against the allowlist before forwarding.\n // GET /api/mesh/thread?node=&agent=&channel=&chat=&limit=\n // GET /api/mesh/job?node=&kind=cron|workflow&key=&limit=\n // GET /api/mesh/run?node=&task=\n // GET /api/mesh/conversation?node=&agent=&channel=&chat=&tzOffset=\n if (method === \"GET\" && (path === \"/api/mesh/thread\" || path === \"/api/mesh/job\"\n || path === \"/api/mesh/run\" || path === \"/api/mesh/conversation\")) {\n const nodeUrl = url.searchParams.get(\"node\")\n if (!nodeUrl) { sendJson(res, 400, { error: \"node query param required\" }); return }\n const q = url.searchParams\n let upstream: string\n if (path === \"/api/mesh/thread\") {\n const [agent, channel, chat] = [q.get(\"agent\"), q.get(\"channel\"), q.get(\"chat\")]\n if (!agent || !channel || !chat) { sendJson(res, 400, { error: \"agent, channel, chat required\" }); return }\n upstream = `/analytics/thread?agent=${encodeURIComponent(agent)}&channel=${encodeURIComponent(channel)}`\n + `&chat=${encodeURIComponent(chat)}&limit=${encodeURIComponent(q.get(\"limit\") || \"120\")}`\n } else if (path === \"/api/mesh/job\") {\n const [kind, key] = [q.get(\"kind\"), q.get(\"key\")]\n if (!kind || !key) { sendJson(res, 400, { error: \"kind and key required\" }); return }\n upstream = `/analytics/job?kind=${encodeURIComponent(kind)}&key=${encodeURIComponent(key)}`\n + `&limit=${encodeURIComponent(q.get(\"limit\") || \"120\")}`\n } else if (path === \"/api/mesh/conversation\") {\n const [agent, channel, chat] = [q.get(\"agent\"), q.get(\"channel\"), q.get(\"chat\")]\n if (!agent || !channel || !chat) { sendJson(res, 400, { error: \"agent, channel, chat required\" }); return }\n upstream = `/analytics/conversation?agent=${encodeURIComponent(agent)}&channel=${encodeURIComponent(channel)}`\n + `&chat=${encodeURIComponent(chat)}&tzOffset=${encodeURIComponent(q.get(\"tzOffset\") || \"0\")}`\n } else {\n const task = q.get(\"task\")\n if (!task) { sendJson(res, 400, { error: \"task required\" }); return }\n upstream = `/analytics/run/${encodeURIComponent(task)}`\n }\n try {\n await proxyNodeAnalytics(res, await resolveNodeTargets(ctx.config), nodeUrl, upstream)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n if (method === \"GET\" && path === \"/api/live/stream\") {\n startLiveStream(req, res, ctx.config)\n return\n }\n\n // Cross-node SSE proxy for a single running task. Browser stays on\n // the dashboard origin; we connect back to the originating daemon URL.\n // GET /api/task/stream?node=<daemonUrl>&agent=<id>&task=<tid>\n if (method === \"GET\" && path === \"/api/task/stream\") {\n const u = new URL(req.url || \"/\", \"http://localhost\")\n const nodeUrl = u.searchParams.get(\"node\")\n const agentId = u.searchParams.get(\"agent\")\n const taskId = u.searchParams.get(\"task\")\n if (!nodeUrl || !agentId || !taskId) {\n sendJson(res, 400, { error: \"node, agent, task query params required\" })\n return\n }\n await proxyTaskStream(req, res, ctx, nodeUrl, agentId, taskId)\n return\n }\n\n // Cross-node JSON proxy for the persisted task history (Recent Activities panel).\n // GET /api/task/history?node=<url>&agent=<id>&limit=N → list\n // GET /api/task/history?node=<url>&agent=<id>&task=<tid> → single record\n if (method === \"GET\" && path === \"/api/task/history\") {\n const u = new URL(req.url || \"/\", \"http://localhost\")\n const nodeUrl = u.searchParams.get(\"node\")\n const agentId = u.searchParams.get(\"agent\")\n const taskId = u.searchParams.get(\"task\")\n const limit = u.searchParams.get(\"limit\") || \"50\"\n if (!nodeUrl || !agentId) {\n sendJson(res, 400, { error: \"node and agent query params required\" })\n return\n }\n await proxyTaskHistory(res, ctx, nodeUrl, agentId, taskId, limit)\n return\n }\n\n // Cross-node action proxy: stop / follow-up a running task on any daemon\n // in the dashboard's allowlist. The browser talks to the dashboard origin\n // (same as /api/task/stream), and the dashboard forwards to the daemon's\n // /api/tasks/:taskId/{cancel|followup} endpoint with the operator token.\n // POST /api/task/action?node=<url>&task=<id>&kind=cancel|followup\n // body: passthrough JSON (cancel: { reason? }; followup: { message, replace?, sender? })\n if (method === \"POST\" && path === \"/api/task/action\") {\n const u = new URL(req.url || \"/\", \"http://localhost\")\n const nodeUrl = u.searchParams.get(\"node\")\n const taskId = u.searchParams.get(\"task\")\n const kind = u.searchParams.get(\"kind\")\n if (!nodeUrl || !taskId || (kind !== \"cancel\" && kind !== \"followup\")) {\n sendJson(res, 400, { error: \"node, task, kind=cancel|followup query params required\" })\n return\n }\n await proxyTaskAction(req, res, ctx, nodeUrl, taskId, kind)\n return\n }\n\n // Agent roster (proxy to configured daemon) — used by draft-agent picker.\n if (method === \"GET\" && path === \"/api/agents\") {\n try {\n const headers: Record<string, string> = {}\n if (ctx.config.dashboard.token) headers[\"Authorization\"] = `Bearer ${ctx.config.dashboard.token}`\n const r = await fetch(ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\") + \"/agents\", { headers })\n if (!r.ok) throw new Error(`HTTP ${r.status}`)\n const list = await r.json()\n sendJson(res, 200, list)\n } catch (e: any) { sendJson(res, 502, { error: e.message || \"agents fetch failed\" }) }\n return\n }\n\n // AI-assisted issue draft — proxies to daemon /task with convention-aware prompt.\n if (method === \"POST\" && path === \"/api/draft\") {\n const body = await readJson(req)\n try {\n const drafted = await draftIssue(ctx.config, body)\n sendJson(res, 200, drafted)\n } catch (e: any) { sendJson(res, 502, { error: e.message || \"draft failed\" }) }\n return\n }\n\n // --- Public agent endpoint (always token-gated, regardless of dashboard.token) ---\n const publicAgentMatch = path.match(/^\\/api\\/public\\/agents\\/([^/]+)\\/messages$/)\n if (publicAgentMatch && method === \"POST\") {\n const agentId = publicAgentMatch[1]\n const tokenRec = requireScopedToken(req, res, [`agent:${agentId}`])\n if (!tokenRec) return\n await proxyPublicAgentMessage(req, res, ctx.config, agentId)\n return\n }\n\n if (path.startsWith(\"/api/\") && ctx.token) {\n // Legacy: if dashboard.token is configured, every /api/* request must\n // carry it (or a scoped token with dashboard:write).\n const authHeader = req.headers.authorization || \"\"\n const got = authHeader.startsWith(\"Bearer \") ? authHeader.slice(7) : \"\"\n if (got === ctx.token) { /* fall through */ }\n else {\n const tokStore = new TokenStore()\n const rec = got ? tokStore.verify(got) : null\n if (!rec || !recordHasScope(rec, \"dashboard:write\")) {\n sendJson(res, 401, { error: \"unauthorized\" })\n return\n }\n }\n }\n\n const isWrite = method === \"POST\" || method === \"PATCH\" || method === \"DELETE\" || method === \"PUT\"\n if (isWrite && path.startsWith(\"/api/\")) {\n const xr = req.headers[\"x-requested-with\"]\n if (xr !== \"agentx-board\") { sendJson(res, 400, { error: \"missing X-Requested-With: agentx-board\" }); return }\n }\n\n // Read-through proxies for runs. The dashboard's local RunStore only\n // sees runs home-noded on THIS machine, but the user typically wants\n // the cross-fleet view: runs fired on peer-server when GitLab events\n // land there, runs fired on Mac when local channels fire. We merge\n // the local list with the main-daemon's list and return the union.\n // Fan-out SSE proxy for /events — multiplexes every configured peer\n // daemon's event stream into a single stream. Lets the board-dashboard\n // show live activity across the whole fleet.\n if (method === \"GET\" && path === \"/events\") {\n const url = new URL(req.url || \"/\", \"http://localhost\")\n const qs = url.searchParams.toString()\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n \"Connection\": \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n })\n const targets: Array<{ name: string; url: string; token?: string }> = [\n { name: ctx.config.node.name || \"local\", url: ctx.config.dashboard.daemonUrl, token: ctx.config.dashboard.token },\n ...(ctx.config.dashboard.daemons || []).map((d) => ({ name: d.name, url: d.url, token: d.token })),\n ]\n const closed = { flag: false }\n const controllers: AbortController[] = []\n req.on(\"close\", () => { closed.flag = true; for (const c of controllers) try { c.abort() } catch { /* */ } })\n // Heartbeat — some proxies drop idle SSE at 30s.\n const heartbeat = setInterval(() => { if (!closed.flag) { try { res.write(\": ping\\n\\n\") } catch { /* */ } } }, 15000)\n req.on(\"close\", () => clearInterval(heartbeat))\n for (const t of targets) {\n void (async () => {\n const headers: Record<string, string> = { Accept: \"text/event-stream\" }\n if (t.token) headers[\"Authorization\"] = `Bearer ${t.token}`\n const ctrl = new AbortController()\n controllers.push(ctrl)\n try {\n const r = await fetch(`${t.url.replace(/\\/+$/, \"\")}/events${qs ? `?${qs}` : \"\"}`, { headers, signal: ctrl.signal })\n if (!r.ok || !r.body) return\n const reader = r.body.getReader()\n const dec = new TextDecoder()\n let buf = \"\"\n while (!closed.flag) {\n const { done, value } = await reader.read()\n if (done) break\n buf += dec.decode(value, { stream: true })\n let idx: number\n while ((idx = buf.indexOf(\"\\n\\n\")) >= 0) {\n const frame = buf.slice(0, idx)\n buf = buf.slice(idx + 2)\n if (closed.flag) break\n try { res.write(frame + \"\\n\\n\") } catch { closed.flag = true }\n }\n }\n } catch { /* ignore — peer disconnected or aborted */ }\n })()\n }\n return\n }\n\n if (method === \"GET\" && path === \"/api/workflows/runs\") {\n try {\n const url = new URL(req.url || \"/\", \"http://localhost\")\n const limit = Math.max(1, Math.min(500, Number(url.searchParams.get(\"limit\") || 50)))\n const workflowId = url.searchParams.get(\"workflowId\") || undefined\n // summary=1 strips the per-run context blob (full webhook payloads,\n // per-node outputs — routinely 10-50KB each) and trims history to\n // the last 5 entries. Cuts a 100-run mesh-aggregated response from\n // multi-MB / 30s+ down to ~100KB / 1s. Run-detail (GET /runs/:id)\n // and SSE stream still return the full shape unchanged.\n const summary = url.searchParams.get(\"summary\") === \"1\" || url.searchParams.get(\"summary\") === \"true\"\n const local = ctx.workflowRuns.list({ limit, workflowId })\n const qs = `?limit=${limit}${workflowId ? `&workflowId=${encodeURIComponent(workflowId)}` : \"\"}${summary ? \"&summary=1\" : \"\"}`\n // Fan out to every configured peer daemon in parallel. Primary\n // (dashboard.daemonUrl) gets the dashboard.token; entries in\n // dashboard.daemons use their own per-peer token. The summary flag\n // is forwarded so peer daemons also slim their responses (the daemon\n // handler in src/daemon/index.ts honors the same query param).\n const targets: Array<{ url: string; token?: string }> = [\n { url: ctx.config.dashboard.daemonUrl, token: ctx.config.dashboard.token },\n ...(ctx.config.dashboard.daemons || []).map((d) => ({ url: d.url, token: d.token })),\n ]\n const remoteLists = await Promise.all(targets.map(async (t) => {\n const headers: Record<string, string> = {}\n if (t.token) headers[\"Authorization\"] = `Bearer ${t.token}`\n try {\n // 5s timeout per peer — a slow/unhealthy mesh node must not pin\n // the dashboard's first paint forever. AbortController keeps the\n // existing fetch ergonomics; on timeout we just skip this peer's\n // contribution and merge whatever we have.\n const ctrl = new AbortController()\n const timer = setTimeout(() => ctrl.abort(), 5000)\n try {\n const r = await fetch(`${t.url.replace(/\\/+$/, \"\")}/api/workflows/runs${qs}`, { headers, signal: ctrl.signal })\n if (!r.ok) return []\n const data = await r.json() as { runs?: WorkflowRun[] }\n return Array.isArray(data.runs) ? data.runs : []\n } finally {\n clearTimeout(timer)\n }\n } catch { return [] }\n }))\n const byId = new Map<string, WorkflowRun>()\n for (const r of local) byId.set(r.id, r)\n for (const list of remoteLists) for (const r of list) byId.set(r.id, r)\n const merged = Array.from(byId.values()).sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1)).slice(0, limit)\n // Ensure the local runs the listing returns are also slimmed; the\n // handleWorkflowsApi path further down does this for its branch but\n // we never reach it because this handler returns first.\n const out = summary ? merged.map((r) => slimRun(r)) : merged\n sendJson(res, 200, { runs: out })\n } catch (e: any) {\n sendJson(res, 502, { error: \"runs fetch failed\", message: e.message || String(e) })\n }\n return\n }\n const runDetailMatch = method === \"GET\" && path.match(/^\\/api\\/workflows\\/runs\\/([^\\/]+)$/)\n if (runDetailMatch) {\n const runId = decodeURIComponent(runDetailMatch[1])\n const local = ctx.workflowRuns.get(runId)\n if (local) { sendJson(res, 200, { run: local }); return }\n // Try every peer until one returns the run. First hit wins.\n const targets: Array<{ url: string; token?: string }> = [\n { url: ctx.config.dashboard.daemonUrl, token: ctx.config.dashboard.token },\n ...(ctx.config.dashboard.daemons || []).map((d) => ({ url: d.url, token: d.token })),\n ]\n for (const t of targets) {\n try {\n const headers: Record<string, string> = {}\n if (t.token) headers[\"Authorization\"] = `Bearer ${t.token}`\n const r = await fetch(`${t.url.replace(/\\/+$/, \"\")}/api/workflows/runs/${encodeURIComponent(runId)}`, { headers })\n if (r.ok) {\n sendJson(res, 200, await r.json())\n return\n }\n } catch { /* try next */ }\n }\n sendJson(res, 404, { error: \"run not found on any configured peer\" })\n return\n }\n\n // /api/actions/builtin — proxy GET to the daemon. The /procedures page\n // reads the live built-in catalog from this endpoint; daemon owns the\n // typed registry, dashboard runs in a separate process.\n if (method === \"GET\" && path === \"/api/actions/builtin\") {\n try {\n const headers: Record<string, string> = {}\n if (ctx.config.dashboard.token) headers[\"Authorization\"] = `Bearer ${ctx.config.dashboard.token}`\n const daemonUrl = ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\")\n const r = await fetch(`${daemonUrl}/api/actions/builtin`, { headers })\n const data = await r.json().catch(() => ({ error: `HTTP ${r.status}` }))\n sendJson(res, r.status, data)\n } catch (e: any) {\n sendJson(res, 502, { error: \"daemon unreachable\", message: e.message || String(e) })\n }\n return\n }\n\n // Active-workflow manual run — proxies the daemon's POST /workflows/:id/run\n // endpoint. The /workflows page on the dashboard fires this for the \"▶ Run\"\n // button. Body shape passed through unchanged: { payload?: object, force?: bool }.\n // The daemon already enforces the trigger.manual constraint and returns\n // 409 when force is needed, which the page surfaces as a toast.\n const runMatch = method === \"POST\" && path.match(/^\\/workflows\\/([^/]+)\\/run$/)\n if (runMatch) {\n try {\n const body = await readJson(req).catch(() => ({}))\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" }\n if (ctx.config.dashboard.token) headers[\"Authorization\"] = `Bearer ${ctx.config.dashboard.token}`\n const daemonUrl = ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\")\n const r = await fetch(`${daemonUrl}/workflows/${encodeURIComponent(runMatch[1])}/run`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body ?? {}),\n })\n const data = await r.json().catch(() => ({ error: `HTTP ${r.status}` }))\n sendJson(res, r.status, data)\n } catch (e: any) {\n sendJson(res, 502, { error: \"daemon unreachable\", message: e.message || String(e) })\n }\n return\n }\n\n // Draft replay — fires an ad-hoc workflow run on the daemon. Lives on\n // the daemon (not in workflows-api.ts) because it needs the dispatcher\n\n // Workflow API — read (dashboard:read) / write (dashboard:write). Fine-\n // grained scope checks let read-only scoped tokens still populate the\n // /workflows observability page when dashboard.token isn't configured.\n if (path.startsWith(\"/api/workflows\")) {\n const handled = handleWorkflowsApi(req, res, {\n store: ctx.workflowStore,\n runs: ctx.workflowRuns,\n layouts: ctx.workflowLayouts,\n requireScope: (r, s, scopes) => {\n // When ctx.token is set the legacy check above already verified\n // write scope on any /api call, so we don't re-gate here.\n // When it's not set (localhost default), enforce scopes ourselves\n // only if the caller *presented* a token — otherwise allow through\n // to preserve the loopback \"safe default\" UX.\n if (ctx.token) return true\n const presented = extractToken(r as any)\n if (!presented) return true\n const rec = new TokenStore().verify(presented)\n if (!rec) { sendJson(s, 401, { error: \"invalid or revoked token\" }); return false }\n for (const scope of scopes) {\n if (!recordHasScope(rec, scope)) { sendJson(s, 403, { error: `token missing scope: ${scope}`, scopes: rec.scopes }); return false }\n }\n return true\n },\n })\n if (handled) return\n }\n\n if (method === \"GET\" && path === \"/api/boards\") {\n sendJson(res, 200, ctx.boards.map((b) => ({\n id: b.id, name: b.name, source: b.source,\n primaryToolLabel: b.primaryToolLabel, labels: b.labels,\n columns: b.columns, timeRangeDays: b.timeRangeDays,\n })))\n return\n }\n\n // /api/boards/:id/members — project members (for assignee picker)\n const membersMatch = path.match(/^\\/api\\/boards\\/([a-z0-9_-]+)\\/members$/)\n if (membersMatch && method === \"GET\") {\n const source = ctx.sources.get(membersMatch[1]) as GitLabWorkSource | undefined\n if (!source) { sendJson(res, 404, { error: \"unknown board\" }); return }\n try {\n const members = typeof source.listMembers === \"function\" ? await source.listMembers() : []\n sendJson(res, 200, members)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // /api/boards/:id/milestones — project milestones (for milestone picker)\n const milestonesMatch = path.match(/^\\/api\\/boards\\/([a-z0-9_-]+)\\/milestones$/)\n if (milestonesMatch && method === \"GET\") {\n const source = ctx.sources.get(milestonesMatch[1]) as GitLabWorkSource | undefined\n if (!source) { sendJson(res, 404, { error: \"unknown board\" }); return }\n try {\n const ms = typeof source.listMilestones === \"function\" ? await source.listMilestones() : []\n sendJson(res, 200, ms)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // /api/boards/:id/items[/...]\n // Legacy shape: /items/:itemId/move (move-only). New shape: /items/:itemId (GET/PATCH), /items (POST, GET).\n const moveMatch = path.match(/^\\/api\\/boards\\/([a-z0-9_-]+)\\/items\\/(.+?)\\/move$/)\n const itemOpMatch = path.match(/^\\/api\\/boards\\/([a-z0-9_-]+)\\/items(?:\\/(.+))?$/)\n\n if (moveMatch && method === \"PATCH\") {\n const [, boardId, rawItemId] = moveMatch\n const itemId = decodeURIComponent(rawItemId)\n const board = ctx.boards.find((b) => b.id === boardId)\n if (!board) { sendJson(res, 404, { error: `unknown board: ${boardId}` }); return }\n const source = ctx.sources.get(boardId)\n if (!source) { sendJson(res, 503, { error: `board \"${boardId}\" source unavailable` }); return }\n const body = await readJson(req)\n const toCol = board.columns.find((c) => c.id === body.to)\n const fromCol = board.columns.find((c) => c.id === body.from)\n if (!toCol) { sendJson(res, 400, { error: `unknown 'to' column: ${body.to}` }); return }\n try {\n await applyTransition(source, itemId, fromCol, toCol)\n audit({ actor: auditActor(req), action: \"move\", boardId, itemId, payload: { from: body.from, to: body.to } })\n sendJson(res, 200, { ok: true })\n } catch (e: any) { sendJson(res, 502, { error: e.message || \"transition failed\" }) }\n return\n }\n\n if (itemOpMatch) {\n const [, boardId, rawItemId] = itemOpMatch\n const itemId = rawItemId ? decodeURIComponent(rawItemId) : undefined\n const board = ctx.boards.find((b) => b.id === boardId)\n if (!board) { sendJson(res, 404, { error: `unknown board: ${boardId}` }); return }\n const source = ctx.sources.get(boardId) as GitLabWorkSource | undefined\n if (!source) { sendJson(res, 503, { error: `board \"${boardId}\" source unavailable` }); return }\n\n // GET /items — column dump\n if (method === \"GET\" && !itemId) {\n const { columns, totals } = await loadBoardColumns(board, source, url.searchParams)\n sendJson(res, 200, { columns, totals })\n return\n }\n\n // GET /items/:itemId — full detail\n if (method === \"GET\" && itemId) {\n try {\n const detail = typeof source.getItem === \"function\" ? await source.getItem(itemId) : null\n if (!detail) { sendJson(res, 404, { error: \"not found\" }); return }\n sendJson(res, 200, detail)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // PATCH /items/:itemId — update\n if (method === \"PATCH\" && itemId) {\n const body = await readJson(req)\n try {\n const updated = typeof source.updateItem === \"function\"\n ? await source.updateItem(itemId, body)\n : null\n if (!updated) { sendJson(res, 501, { error: \"update unsupported\" }); return }\n audit({ actor: auditActor(req), action: \"update\", boardId, itemId, payload: body })\n sendJson(res, 200, updated)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // POST /items — create new issue\n if (method === \"POST\" && !itemId) {\n const body = await readJson(req)\n if (!body.title) { sendJson(res, 400, { error: \"title required\" }); return }\n if (!source.capabilities.create || !source.create) {\n sendJson(res, 400, { error: `source ${source.type} does not support create` }); return\n }\n try {\n const created = await source.create({\n title: body.title,\n description: body.description,\n labels: body.labels,\n assigneeUsernames: body.assigneeUsernames,\n milestoneTitle: body.milestoneTitle,\n projectHint: body.projectHint,\n })\n audit({ actor: auditActor(req), action: \"create\", boardId, itemId: created.id, payload: { title: body.title } })\n sendJson(res, 201, created)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n }\n\n // Friendly aliases for naked paths people copy-paste from terminal output,\n // Slack messages, or muscle memory (\"just open /health\"). The dashboard\n // proper lives under /admin/<page> — unknown top-level paths that match a\n // known admin section get a 302 so the URL works instead of dumping\n // {\"error\":\"not found\"} JSON. Only slugs that resolve to a real admin\n // page are mapped — phantom redirects (/admin/<slug> 404) would be worse\n // than the original 404.\n if (method === \"GET\" && /^\\/[a-z][a-z-]*\\/?$/.test(path)) {\n const slug = path.replace(/^\\/+|\\/+$/g, \"\")\n const SLUG_TO_ADMIN: Record<string, string> = {\n health: \"/admin/health\",\n ledger: \"/admin/ledger\",\n cost: \"/admin/cost\",\n projects: \"/admin/projects\",\n graph: \"/admin/graph\",\n wiki: \"/admin/wiki/\",\n observability: \"/admin/health\",\n }\n const target = SLUG_TO_ADMIN[slug]\n if (target) {\n res.writeHead(302, { Location: target })\n res.end()\n return\n }\n }\n\n sendJson(res, 404, { error: \"not found\", path })\n}\n\n// --- Column loading ---\n\nasync function loadBoardColumns(\n board: BoardConfig,\n source: WorkSource,\n query: URLSearchParams,\n): Promise<{ columns: Record<string, WorkItem[]>; totals: Record<string, number> }> {\n if (!source.capabilities.listAll || !source.listAll) {\n const empty: Record<string, WorkItem[]> = {}\n for (const c of board.columns) empty[c.id] = []\n return { columns: empty, totals: Object.fromEntries(board.columns.map((c) => [c.id, 0])) }\n }\n\n const baseLabels: string[] = []\n if (board.primaryToolLabel) baseLabels.push(board.primaryToolLabel)\n const extraLabel = query.get(\"label\") || undefined\n if (extraLabel) baseLabels.push(extraLabel)\n const search = query.get(\"q\") || undefined\n const milestone = query.get(\"milestone\") || undefined\n const assignee = query.get(\"assignee\") || undefined\n const openDays = parseInt(query.get(\"days\") || String(board.timeRangeDays), 10)\n const closedDays = parseInt(query.get(\"closedDays\") || String(board.closedWindowDays ?? 30), 10)\n\n // One query per column kind type. We batch all scoped-label/label/open-backlog\n // columns into a single \"opened\" query (client-side bucket them), and the\n // closed column (if any) into a second \"closed\" query.\n const hasOpen = board.columns.some((c) => c.kind !== \"closed\")\n const hasClosed = board.columns.some((c) => c.kind === \"closed\")\n\n const open: WorkItem[] = hasOpen\n ? await source.listAll({\n sinceDays: openDays, labels: baseLabels.length ? baseLabels : undefined,\n search, milestone, state: \"opened\",\n })\n : []\n const closed: WorkItem[] = hasClosed\n ? await source.listAll({\n sinceDays: closedDays, labels: baseLabels.length ? baseLabels : undefined,\n search, milestone, state: \"closed\",\n })\n : []\n\n const columns: Record<string, WorkItem[]> = {}\n for (const c of board.columns) columns[c.id] = []\n\n // Closed items → the closed column (if any).\n const closedCol = board.columns.find((c) => c.kind === \"closed\")\n for (const item of closed) {\n if (closedCol) columns[closedCol.id].push({ ...item, stage: closedCol.id })\n }\n\n // Open items → bucket by deriveStage over the open columns only.\n // Items with a scoped Status::* label that no column claims are dropped\n // (they're off-board workflow states like Status::Done when no Done column exists).\n const openColumns = board.columns.filter((c) => c.kind !== \"closed\")\n for (const item of open) {\n const stage = deriveStage(item, openColumns)\n if (!stage) continue\n const list = columns[stage]\n if (list) list.push({ ...item, stage })\n }\n\n // Assignee filter (post-query).\n if (assignee) {\n for (const k of Object.keys(columns)) {\n columns[k] = columns[k].filter((i) => i.assignee === assignee)\n }\n }\n\n // Deterministic in-column ordering: priority asc, updatedAt desc.\n for (const k of Object.keys(columns)) {\n columns[k].sort((a, b) => {\n const p = (a.priority ?? 99) - (b.priority ?? 99)\n if (p !== 0) return p\n return (b.updatedAt || \"\").localeCompare(a.updatedAt || \"\")\n })\n }\n\n const totals = Object.fromEntries(Object.entries(columns).map(([k, v]) => [k, v.length]))\n return { columns, totals }\n}\n\nasync function applyTransition(\n source: WorkSource,\n itemId: string,\n fromCol: BoardColumn | undefined,\n toCol: BoardColumn,\n): Promise<void> {\n const diff = transitionDiff(fromCol, toCol)\n const gl = source as GitLabWorkSource\n // Close/reopen first (so the issue exists in the right state before label edits).\n if (diff.closeIssue && typeof gl.setState === \"function\") {\n await gl.setState(itemId, \"close\")\n } else if (diff.reopen && typeof gl.setState === \"function\") {\n await gl.setState(itemId, \"reopen\")\n }\n if ((diff.addLabels?.length || diff.removeLabels?.length) && typeof gl.transitionMany === \"function\") {\n await gl.transitionMany(itemId, diff.addLabels || [], diff.removeLabels || [])\n } else if (diff.addLabels?.length && source.transition) {\n await source.transition(itemId, diff.addLabels[0], diff.removeLabels?.[0])\n }\n}\n\n// --- WorkSource construction ---\n\n/** Trim a WorkflowRun to just the fields the listing UI needs. Drops the\n * per-run `context` (full webhook payload + per-node outputs, often 10-50KB\n * each) and clips `history` to the last 5 entries. Both the dashboard's\n * mesh-aggregation handler and workflows-api.ts call this for `summary=1`\n * responses; the shape MUST match between the two so the page doesn't see\n * divergent fields per code path. */\nfunction slimRun(r: WorkflowRun): Partial<WorkflowRun> {\n return {\n id: r.id,\n workflowId: r.workflowId,\n workflowVersion: r.workflowVersion,\n homeNode: r.homeNode,\n status: r.status,\n pending: r.pending,\n entityRef: r.entityRef,\n history: (r.history || []).slice(-5),\n parentRunId: r.parentRunId,\n parentNodeId: r.parentNodeId,\n rootRunId: r.rootRunId,\n depth: r.depth,\n createdAt: r.createdAt,\n updatedAt: r.updatedAt,\n }\n}\n\nfunction buildWorkSource(board: BoardConfig, daemon: DaemonConfig): WorkSource {\n if (board.source.type === \"gitlab\") {\n const gl = daemon.channels.gitlab\n if (!gl?.token) throw new Error(\"channels.gitlab.token is required for gitlab boards\")\n const agentUsernames: Record<string, string[]> = {}\n for (const mapping of gl.agentMappings) {\n if (mapping.gitlabUsernames.length) {\n agentUsernames[mapping.agentId] = mapping.gitlabUsernames\n }\n }\n return new GitLabWorkSource(gl.host, gl.token, board.source.projects, agentUsernames, (...args) => console.log(\"[board]\", ...args))\n }\n throw new Error(`unsupported board source: ${(board.source as any).type}`)\n}\n\n// --- Audit log ---\n\nfunction audit(entry: { actor: string; action: string; boardId: string; itemId?: string; payload?: unknown }): void {\n try {\n const dir = resolve(process.cwd(), \".agentx\")\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true })\n const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + \"\\n\"\n appendFileSync(resolve(dir, \"board-audit.jsonl\"), line)\n } catch { /* non-fatal */ }\n}\n\nfunction auditActor(req: IncomingMessage): string {\n const fwd = req.headers[\"x-forwarded-for\"]\n return (Array.isArray(fwd) ? fwd[0] : fwd?.split(\",\")[0]) || req.socket.remoteAddress || \"unknown\"\n}\n\n// --- Live mesh snapshot ---\n//\n// Aggregates /agents + /mesh from the configured primary daemon, then fans out\n// to each mesh peer to collect their /agents (running tasks included) so the\n// dashboard can show a single cross-node view of what every agent is doing.\n\ninterface NodeLive {\n id: string\n name: string\n url: string\n reachable: boolean\n error?: string\n uptimeSec?: number\n agents: Array<{\n id: string\n name: string\n tier: string\n model?: string\n skillCount?: number\n active: number\n total: number\n errors: number\n lastActive?: string\n lastSummary?: { text: string; at: string; ok: boolean }\n hourlyTasks?: number[]\n runningTasks?: Array<{\n id: string\n messagePreview: string\n channel: string\n chatId?: string\n sender?: string\n startedAt: string\n }>\n }>\n /** Today's per-agent usage rollup pulled from /health. */\n usage?: {\n date: string\n agents: Record<string, {\n tasks: number\n totalDuration: number\n errors: number\n inputTokens: number\n outputTokens: number\n cacheReadTokens: number\n cacheCreateTokens: number\n byChannel?: Record<string, { tasks: number }>\n }>\n }\n crons?: Array<{\n id: string\n enabled: boolean\n schedule: string\n timezone?: string\n agent: string\n model?: string\n nextRun?: string\n retryPending?: boolean\n consecutiveErrors: number\n }>\n cronRuns?: Array<{\n jobId: string\n startedAt: string\n completedAt: string\n duration: number\n status: \"success\" | \"failed\" | \"timeout\"\n responseSummary?: string\n errorSummary?: string\n isRetry: boolean\n retryAttempt: number\n taskId?: string\n rootTaskId?: string\n traceId?: string\n sessionId?: string\n }>\n}\ninterface LiveSnapshot {\n ts: string\n nodes: NodeLive[]\n}\n\nasync function fetchDaemonAgents(\n url: string,\n token?: string,\n signal?: AbortSignal,\n day?: { date: string; timezone: string },\n): Promise<NodeLive> {\n const headers: Record<string, string> = {}\n if (token) headers[\"Authorization\"] = `Bearer ${token}`\n const base: NodeLive = { id: url, name: url, url, reachable: false, agents: [] }\n try {\n const cronQuery = day ? `?date=${encodeURIComponent(day.date)}&timezone=${encodeURIComponent(day.timezone)}` : \"\"\n const [healthRes, agentsRes, meshRes, cronsRes, cronRunsRes] = await Promise.all([\n fetch(url + \"/health\", { headers, signal }).catch(() => null),\n fetch(url + \"/agents\", { headers, signal }).catch(() => null),\n fetch(url + \"/mesh\", { headers, signal }).catch(() => null),\n fetch(url + \"/crons\", { headers, signal }).catch(() => null),\n fetch(url + \"/crons/runs\" + cronQuery, { headers, signal }).catch(() => null),\n ])\n if (!agentsRes || !agentsRes.ok) {\n base.error = agentsRes ? `HTTP ${agentsRes.status}` : \"unreachable\"\n return base\n }\n const agents: any[] = await agentsRes.json()\n base.agents = agents.map((a) => ({\n id: a.id, name: a.name, tier: a.tier, model: a.model,\n skillCount: Number.isFinite(a.skillCount) ? a.skillCount : undefined,\n active: a.active || 0, total: a.total || 0, errors: a.errors || 0,\n lastActive: a.lastActive,\n lastSummary: a.lastSummary,\n hourlyTasks: Array.isArray(a.hourlyTasks) ? a.hourlyTasks : undefined,\n runningTasks: Array.isArray(a.runningTasks) ? a.runningTasks : [],\n }))\n if (healthRes && healthRes.ok) {\n const h: any = await healthRes.json()\n base.uptimeSec = h.uptime\n base.name = h.node?.name || h.node?.id || url\n base.id = h.node?.id || url\n // /health already embeds today's usage rollup — reuse it so the\n // dashboard doesn't need a separate /usage call per node.\n if (h.usage) base.usage = h.usage\n }\n if (cronsRes && cronsRes.ok) {\n const jobs: any[] = await cronsRes.json()\n base.crons = jobs.map((job) => ({\n id: job.id,\n enabled: job.enabled === true,\n schedule: String(job.schedule || \"\"),\n timezone: job.timezone,\n agent: String(job.agent || \"\"),\n model: job.model,\n nextRun: job.nextRun,\n retryPending: job.retryPending === true,\n consecutiveErrors: Number(job.consecutiveErrors) || 0,\n }))\n }\n if (cronRunsRes && cronRunsRes.ok) {\n const history: any = await cronRunsRes.json()\n base.cronRuns = Array.isArray(history.runs) ? history.runs : []\n }\n base.reachable = true\n // Expose mesh peer info for discovery, but the caller does fan-out separately.\n ;(base as any)._peers = Array.isArray(meshRes && await meshResSafe(meshRes)) ? (base as any)._peers : undefined\n } catch (e: any) {\n base.error = e.message || String(e)\n }\n return base\n}\n\n// Lazy /admin/wiki/* handler — built once per dashboard process. The\n// WikiHub it wraps reads articles from disk on every request (no\n// in-memory cache), so absorbs and edits from elsewhere show up\n// without a restart.\nlet _wikiHandler: ((req: IncomingMessage, res: ServerResponse) => Promise<void>) | null = null\nfunction getOrCreateWikiHandler(ctx: { config: DaemonConfig; token?: string }): (req: IncomingMessage, res: ServerResponse) => Promise<void> {\n if (_wikiHandler) return _wikiHandler\n const wikiDir = resolve(process.cwd(), \".agentx/wiki\")\n // Match the CLI defaults exactly so the embedded view renders the\n // same hub/sidebar/article markup as `agentx wiki serve` does — they\n // share the handler, but mode changes which `hubHome`/`hubSidebar`\n // branches fire (graph mode adds the article-list + type-label).\n _wikiHandler = createWikiHandler({\n wikiDir,\n pathPrefix: \"/admin/wiki\",\n mode: \"graph\",\n // No remote-peer browsing in the embedded view — keeps the dashboard\n // independent of mesh state. Operators who want cross-mesh wiki nav\n // can still run `agentx wiki serve --peer <url>`.\n })\n // ctx is passed for future use (auth etc.) but unused right now.\n void ctx\n return _wikiHandler\n}\n\n// Tiny helper to double-json a Response only once.\nlet _meshJsonCache = new WeakMap<Response, any>()\nasync function meshResSafe(r: Response): Promise<any> {\n if (_meshJsonCache.has(r)) return _meshJsonCache.get(r)\n try { const j = r.ok ? await r.json() : null; _meshJsonCache.set(r, j); return j } catch { return null }\n}\n\nasync function fetchMeshPeers(primaryUrl: string, token?: string, signal?: AbortSignal): Promise<Array<{ url: string; name: string; token?: string }>> {\n try {\n const headers: Record<string, string> = {}\n if (token) headers[\"Authorization\"] = `Bearer ${token}`\n const r = await fetch(primaryUrl + \"/mesh\", { headers, signal })\n if (!r.ok) return []\n const peers: any[] = await r.json()\n return peers\n .filter((p) => p && p.peerUrl)\n .map((p) => ({ url: String(p.peerUrl).replace(/\\/+$/, \"\"), name: p.peer || p.peerUrl }))\n } catch { return [] }\n}\n\n/** Every daemon this dashboard is allowed to read: the primary, the\n * explicitly configured extras, and whatever the primary's /mesh reports.\n * Shared by the live snapshot and the analytics fan-out so both agree on\n * what \"the fleet\" means, and so the analytics drill-down proxy has the\n * same allowlist as the task proxies. */\nasync function resolveNodeTargets(daemon: DaemonConfig, signal?: AbortSignal): Promise<NodeTarget[]> {\n const dash = daemon.dashboard\n const primaryUrl = dash.daemonUrl.replace(/\\/+$/, \"\")\n const seen = new Map<string, NodeTarget>()\n seen.set(primaryUrl, { name: \"primary\", url: primaryUrl, token: dash.token })\n for (const d of dash.daemons) {\n const key = d.url.replace(/\\/+$/, \"\")\n if (!seen.has(key)) seen.set(key, { name: d.name, url: key, token: d.token })\n }\n const meshPeers = await fetchMeshPeers(primaryUrl, dash.token, signal)\n for (const p of meshPeers) if (!seen.has(p.url)) seen.set(p.url, p)\n return [...seen.values()]\n}\n\nasync function buildLiveSnapshot(\n daemon: DaemonConfig,\n day?: { date: string; timezone: string },\n): Promise<LiveSnapshot> {\n const ac = new AbortController()\n const timeout = setTimeout(() => ac.abort(), 3000)\n try {\n const targets = await resolveNodeTargets(daemon, ac.signal)\n const nodes = await Promise.all(targets.map((d) => fetchDaemonAgents(d.url, d.token, ac.signal, day)))\n return { ts: new Date().toISOString(), nodes }\n } finally { clearTimeout(timeout) }\n}\n\nfunction startLiveStream(req: IncomingMessage, res: ServerResponse, daemon: DaemonConfig): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n \"Connection\": \"keep-alive\",\n \"X-Accel-Buffering\": \"no\",\n })\n let closed = false\n const send = (ev: string, data: unknown) => {\n if (closed) return\n res.write(`event: ${ev}\\ndata: ${JSON.stringify(data)}\\n\\n`)\n }\n const tick = async () => {\n try {\n const snap = await buildLiveSnapshot(daemon)\n send(\"snapshot\", snap)\n } catch (e: any) {\n send(\"error\", { message: e.message })\n }\n }\n // Initial tick immediately, then every 2s.\n tick()\n const timer = setInterval(tick, 2000)\n req.on(\"close\", () => { closed = true; clearInterval(timer); res.end() })\n}\n\n/**\n * Proxy SSE from a daemon's task-stream endpoint back to the dashboard\n * client. Whitelists the target URL against the dashboard's known nodes\n * to avoid being turned into an open SSE proxy.\n */\nasync function proxyTaskStream(\n req: IncomingMessage,\n res: ServerResponse,\n ctx: { config: DaemonConfig },\n nodeUrl: string,\n agentId: string,\n taskId: string,\n): Promise<void> {\n const target = nodeUrl.replace(/\\/+$/, \"\")\n const allowed = new Set<string>()\n allowed.add(ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\"))\n for (const d of ctx.config.dashboard.daemons) allowed.add(d.url.replace(/\\/+$/, \"\"))\n // Also accept any known mesh peer URL discovered at snapshot time.\n try {\n const peers = await fetchMeshPeers(ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\"), ctx.config.dashboard.token)\n for (const p of peers) allowed.add(p.url.replace(/\\/+$/, \"\"))\n } catch { /* best effort */ }\n if (!allowed.has(target)) {\n sendJson(res, 403, { error: \"node not in dashboard allowlist\", target })\n return\n }\n const tokenForNode =\n target === ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\")\n ? ctx.config.dashboard.token\n : ctx.config.dashboard.daemons.find((d) => d.url.replace(/\\/+$/, \"\") === target)?.token\n const headers: Record<string, string> = { Accept: \"text/event-stream\" }\n if (tokenForNode) headers[\"Authorization\"] = `Bearer ${tokenForNode}`\n const upstreamCtl = new AbortController()\n let upstreamRes: Response\n try {\n upstreamRes = await fetch(`${target}/agents/${encodeURIComponent(agentId)}/tasks/${encodeURIComponent(taskId)}/stream`, {\n headers,\n signal: upstreamCtl.signal,\n })\n } catch (e: any) {\n sendJson(res, 502, { error: e.message || \"upstream connect failed\" })\n return\n }\n if (!upstreamRes.ok || !upstreamRes.body) {\n sendJson(res, upstreamRes.status || 502, { error: `upstream HTTP ${upstreamRes.status}` })\n return\n }\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n \"Connection\": \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n \"X-Accel-Buffering\": \"no\",\n })\n const reader = upstreamRes.body.getReader()\n const pump = async () => {\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n if (value) res.write(value)\n }\n } catch { /* upstream closed */ } finally { try { res.end() } catch { /* */ } }\n }\n pump()\n req.on(\"close\", () => { upstreamCtl.abort(); try { res.end() } catch { /* */ } })\n}\n\n/**\n * Reject the request with 401 unless the caller presents a scoped token that\n * covers *all* of the required scopes. Returns the matching record on success,\n * or null after writing the 401 (so callers can early-return).\n */\nfunction requireScopedToken(\n req: IncomingMessage,\n res: ServerResponse,\n required: string[],\n): TokenRecord | null {\n const token = extractToken(req as any)\n if (!token) {\n sendJson(res, 401, { error: \"missing token\", hint: \"pass Authorization: Bearer <token> (agentx token create)\" })\n return null\n }\n const rec = new TokenStore().verify(token)\n if (!rec) {\n sendJson(res, 401, { error: \"invalid or revoked token\" })\n return null\n }\n for (const s of required) {\n if (!recordHasScope(rec, s)) {\n sendJson(res, 403, { error: `token missing scope: ${s}`, scopes: rec.scopes })\n return null\n }\n }\n return rec\n}\n\n/**\n * Public agent endpoint — external apps POST a message, we forward to the\n * daemon's /task and return the final response. Only agents whose config\n * says `access: \"public\"` are reachable.\n */\nasync function proxyPublicAgentMessage(\n req: IncomingMessage,\n res: ServerResponse,\n config: DaemonConfig,\n agentId: string,\n): Promise<void> {\n const agentDef = (config.agents as any)?.[agentId]\n if (!agentDef) { sendJson(res, 404, { error: \"unknown agent\" }); return }\n if (agentDef.access !== \"public\") {\n sendJson(res, 403, { error: \"agent is private\", hint: `set agents.${agentId}.access = \"public\" to expose it` })\n return\n }\n let body: any\n try { body = await readJson(req) } catch { sendJson(res, 400, { error: \"invalid JSON body\" }); return }\n const message = body?.message\n if (!message || typeof message !== \"string\") {\n sendJson(res, 400, { error: \"required: { message: string, context?: {...} }\" })\n return\n }\n const daemonUrl = config.dashboard.daemonUrl.replace(/\\/+$/, \"\")\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" }\n if (config.dashboard.token) headers[\"Authorization\"] = `Bearer ${config.dashboard.token}`\n try {\n const upstream = await fetch(`${daemonUrl}/task`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n agent: agentId,\n message,\n context: {\n channel: \"public-api\",\n sender: body.context?.sender || \"api\",\n ...(body.context || {}),\n },\n }),\n })\n const text = await upstream.text()\n res.writeHead(upstream.status, {\n \"Content-Type\": upstream.headers.get(\"content-type\") || \"application/json; charset=utf-8\",\n \"Access-Control-Allow-Origin\": \"*\",\n })\n res.end(text)\n } catch (e: any) {\n sendJson(res, 502, { error: e.message || \"upstream call failed\" })\n }\n}\n\n/**\n * One-shot JSON proxy for /agents/:id/tasks (list) or /agents/:id/tasks/:tid\n * (single record). Same allowlist semantics as proxyTaskStream.\n */\nasync function proxyTaskHistory(\n res: ServerResponse,\n ctx: { config: DaemonConfig },\n nodeUrl: string,\n agentId: string,\n taskId: string | null,\n limit: string,\n): Promise<void> {\n const target = nodeUrl.replace(/\\/+$/, \"\")\n const allowed = new Set<string>()\n allowed.add(ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\"))\n for (const d of ctx.config.dashboard.daemons) allowed.add(d.url.replace(/\\/+$/, \"\"))\n try {\n const peers = await fetchMeshPeers(ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\"), ctx.config.dashboard.token)\n for (const p of peers) allowed.add(p.url.replace(/\\/+$/, \"\"))\n } catch { /* */ }\n if (!allowed.has(target)) {\n sendJson(res, 403, { error: \"node not in dashboard allowlist\", target })\n return\n }\n const tokenForNode =\n target === ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\")\n ? ctx.config.dashboard.token\n : ctx.config.dashboard.daemons.find((d) => d.url.replace(/\\/+$/, \"\") === target)?.token\n const headers: Record<string, string> = { Accept: \"application/json\" }\n if (tokenForNode) headers[\"Authorization\"] = `Bearer ${tokenForNode}`\n const upstreamPath = taskId\n ? `/agents/${encodeURIComponent(agentId)}/tasks/${encodeURIComponent(taskId)}`\n : `/agents/${encodeURIComponent(agentId)}/tasks?limit=${encodeURIComponent(limit)}`\n try {\n const r = await fetch(`${target}${upstreamPath}`, { headers })\n const body = await r.text()\n res.writeHead(r.status, {\n \"Content-Type\": r.headers.get(\"content-type\") || \"application/json; charset=utf-8\",\n \"Access-Control-Allow-Origin\": \"*\",\n })\n res.end(body)\n } catch (e: any) {\n sendJson(res, 502, { error: e.message || \"upstream fetch failed\" })\n }\n}\n\n/**\n * Cross-node POST proxy for the live page's Stop / Update controls.\n * Forwards the operator's request body verbatim to the originating daemon's\n * /api/tasks/:taskId/{cancel|followup} endpoint. Same allowlist + token\n * resolution as proxyTaskStream / proxyTaskHistory.\n */\nasync function proxyTaskAction(\n req: IncomingMessage,\n res: ServerResponse,\n ctx: { config: DaemonConfig },\n nodeUrl: string,\n taskId: string,\n kind: \"cancel\" | \"followup\",\n): Promise<void> {\n const target = nodeUrl.replace(/\\/+$/, \"\")\n const allowed = new Set<string>()\n allowed.add(ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\"))\n for (const d of ctx.config.dashboard.daemons) allowed.add(d.url.replace(/\\/+$/, \"\"))\n try {\n const peers = await fetchMeshPeers(ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\"), ctx.config.dashboard.token)\n for (const p of peers) allowed.add(p.url.replace(/\\/+$/, \"\"))\n } catch { /* */ }\n if (!allowed.has(target)) {\n sendJson(res, 403, { error: \"node not in dashboard allowlist\", target })\n return\n }\n const tokenForNode =\n target === ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\")\n ? ctx.config.dashboard.token\n : ctx.config.dashboard.daemons.find((d) => d.url.replace(/\\/+$/, \"\") === target)?.token\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\", Accept: \"application/json\" }\n if (tokenForNode) headers[\"Authorization\"] = `Bearer ${tokenForNode}`\n let body = \"{}\"\n try {\n const parsed = await readJson(req)\n body = parsed ? JSON.stringify(parsed) : \"{}\"\n } catch { /* leave default */ }\n try {\n const r = await fetch(`${target}/api/tasks/${encodeURIComponent(taskId)}/${kind}`, {\n method: \"POST\",\n headers,\n body,\n })\n const text = await r.text()\n res.writeHead(r.status, {\n \"Content-Type\": r.headers.get(\"content-type\") || \"application/json; charset=utf-8\",\n \"Access-Control-Allow-Origin\": \"*\",\n })\n res.end(text)\n } catch (e: any) {\n sendJson(res, 502, { error: e.message || \"upstream fetch failed\" })\n }\n}\n\n// --- A2A issue drafting ---\n//\n// Builds a convention-aware prompt, calls the configured agent on the daemon,\n// and parses a strict JSON reply. The agent returns title + refined description\n// + suggested labels + (optional) suggested assignee username.\n\nconst DRAFT_CONVENTIONS = `\nYou are helping draft a new GitLab issue. Follow these conventions strictly.\n\nTitle pattern:\n <Kind>: <page|component> – <short symptom>\nKinds: Bug, UX, Copy, Perf, A11y, Security, UI, Incident (defects)\n Feat, Enh, Deprecate (features)\n Epic, Story, Spike, Req (planning)\n Data, Env, Test, Refactor, Chore, Task, Mgmt (technical)\n\nTitle guidance:\n - Be specific about the location (\"Contact page\", not \"frontend\").\n - Use lowercase for the symptom.\n - Keep the symptom 4-7 words max, present tense.\n - Do NOT include issue IDs.\n\nDescription structure (Markdown):\n For bugs: Problem / Steps to Reproduce / Expected / Actual / Environment / Impact.\n For features/stories: User Story (As a … I want … So that …) + Acceptance Criteria bullets.\n Otherwise: concise problem statement + any relevant context.\n\nLabels:\n - Pick ONE Role label from: Dev, Design, QA, Ops, PM, mgmt.\n - Pick ONE Kind label matching the title prefix (bug, task, ux, ui, perf, security, copy, enhancement, …).\n - Pick ONE Priority: critical, high, medium, low.\n - Pick ONE Difficulty: diff-XS, diff-S, diff-M, diff-L, diff-XL.\n - Optional Area/feature labels if clearly applicable from the known label list.\n\nReturn ONLY a single JSON object on one line — no prose, no code fences, no explanation.\nShape:\n { \"title\": string, \"description\": string, \"labels\": string[], \"assigneeUsername\": string | null, \"kind\": string }\nIf you don't have enough information to pick a label confidently, omit it rather than guessing.\n`\n\nasync function draftIssue(daemon: DaemonConfig, body: any): Promise<{ title: string; description: string; labels: string[]; assigneeUsername?: string; kind?: string; raw?: string }> {\n const rough: string = (body?.rough || \"\").toString().trim()\n if (!rough) throw new Error(\"rough description required\")\n const agentId: string | undefined = body?.agent || daemon.dashboard.draftAgent\n if (!agentId) throw new Error(\"no draft agent configured (set dashboard.draftAgent or pass agent in body)\")\n\n const boardCtx: {\n boardName?: string; primaryLabel?: string; project?: string;\n knownLabels?: string[]; members?: string[]; columnLabel?: string\n } = body?.context || {}\n\n const userPrompt = [\n `Rough description from the user:`,\n `\"\"\"`,\n rough,\n `\"\"\"`,\n ``,\n `Board: ${boardCtx.boardName || \"(unspecified)\"}`,\n boardCtx.primaryLabel ? `Primary board label (include in labels): ${boardCtx.primaryLabel}` : \"\",\n boardCtx.columnLabel ? `Target column label (include in labels): ${boardCtx.columnLabel}` : \"\",\n boardCtx.project ? `GitLab project: ${boardCtx.project}` : \"\",\n boardCtx.knownLabels?.length ? `Known labels in this project (prefer picking from these when they fit):\\n- ${boardCtx.knownLabels.slice(0, 60).join(\"\\n- \")}` : \"\",\n boardCtx.members?.length ? `Possible assignees (GitLab usernames): ${boardCtx.members.slice(0, 20).join(\", \")}` : \"\",\n ``,\n `Produce the JSON draft now.`,\n ].filter(Boolean).join(\"\\n\")\n\n const url = daemon.dashboard.daemonUrl.replace(/\\/+$/, \"\") + \"/task\"\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" }\n if (daemon.dashboard.token) headers[\"Authorization\"] = `Bearer ${daemon.dashboard.token}`\n\n const r = await fetch(url, {\n method: \"POST\", headers,\n body: JSON.stringify({\n agent: agentId,\n message: userPrompt,\n context: { channel: \"a2a\", sender: \"board-dashboard\", systemPrompt: DRAFT_CONVENTIONS },\n }),\n })\n if (!r.ok) throw new Error(`daemon /task HTTP ${r.status}: ${await r.text().catch(() => \"\")}`)\n const data: any = await r.json()\n if (data.error) throw new Error(String(data.error))\n const content: string = (data.content || \"\").toString()\n\n // Agent may wrap JSON in ```json fences or prose; extract the first balanced {...}.\n const parsed = extractJson(content)\n if (!parsed || typeof parsed.title !== \"string\") {\n throw new Error(\"agent did not return a valid JSON draft. Raw: \" + content.slice(0, 400))\n }\n const labels: string[] = Array.isArray(parsed.labels) ? parsed.labels.filter((x: any) => typeof x === \"string\") : []\n // Inject primary/column labels if the agent missed them.\n if (boardCtx.primaryLabel && !labels.includes(boardCtx.primaryLabel)) labels.unshift(boardCtx.primaryLabel)\n if (boardCtx.columnLabel && !labels.includes(boardCtx.columnLabel)) labels.unshift(boardCtx.columnLabel)\n return {\n title: parsed.title.trim(),\n description: (parsed.description || \"\").toString(),\n labels,\n assigneeUsername: typeof parsed.assigneeUsername === \"string\" ? parsed.assigneeUsername : undefined,\n kind: typeof parsed.kind === \"string\" ? parsed.kind : undefined,\n raw: content,\n }\n}\n\nfunction extractJson(text: string): any {\n if (!text) return null\n // Try direct parse\n try { return JSON.parse(text) } catch { /* fallthrough */ }\n // Strip code fences\n const fenced = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/)\n if (fenced) { try { return JSON.parse(fenced[1]) } catch { /* fallthrough */ } }\n // Find first balanced {...}\n const start = text.indexOf(\"{\")\n if (start === -1) return null\n let depth = 0\n for (let i = start; i < text.length; i++) {\n const c = text[i]\n if (c === \"{\") depth++\n else if (c === \"}\") { depth--; if (depth === 0) { try { return JSON.parse(text.slice(start, i + 1)) } catch { return null } } }\n }\n return null\n}\n\n// --- HTTP helpers ---\n\n/** Resolve a path relative to THIS module's directory at runtime. After\n * tsup bundles dist/board-dashboard-*.js is placed in dist/, so\n * `../../dist/web/foo.js` is relative to wherever the bundled module\n * actually runs. `fileURLToPath(import.meta.url)` is the ESM equivalent\n * of __filename. */\nfunction resolveFromHere(rel: string): string {\n const here = dirname(fileURLToPath(import.meta.url))\n return resolve(here, rel)\n}\n\nasync function readAsset(path: string): Promise<Buffer> {\n return readFile(path)\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json; charset=utf-8\" })\n res.end(JSON.stringify(body))\n}\n\nasync function readJson(req: IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n req.on(\"data\", (c) => chunks.push(c))\n req.on(\"end\", () => {\n try {\n const raw = Buffer.concat(chunks).toString(\"utf-8\")\n resolve(raw ? JSON.parse(raw) : {})\n } catch (e) { reject(e) }\n })\n req.on(\"error\", reject)\n })\n}\n\n// --- HTML ---\n\n\n// --- Live full-screen page (/live) ---\n\n\n\n/**\n * Derive a peer's board-dashboard URL from a dashboard.daemons[] entry.\n * Uses the explicit dashboardUrl override if set; otherwise swaps the\n * daemon's port (18800 / 19900 / 18810) for :4202, matching our convention\n * that the dashboard runs alongside the daemon.\n */\nfunction resolvePeerDashboardUrl(d: { url: string; dashboardUrl?: string }): string {\n if (d.dashboardUrl) return d.dashboardUrl.replace(/\\/+$/, \"\")\n return d.url.replace(/\\/+$/, \"\").replace(/:(?:1[89][89]00|18810)$/, \":4202\")\n}\n\n/**\n * Build the peers list shown in the topbar mesh selector. Combines the\n * primary daemon + any `dashboard.daemons[]` entries.\n */\nfunction buildTopbarPeers(config: DaemonConfig, ctxConfig?: DaemonConfig): TopbarPeer[] {\n const cfg = ctxConfig || config\n const primary: TopbarPeer = {\n id: \"primary\",\n name: cfg.node?.name || \"this daemon\",\n dashboardUrl: \"\", // empty = stay on current origin\n primary: true,\n }\n const extras: TopbarPeer[] = (cfg.dashboard?.daemons || []).map((d) => ({\n id: d.url.replace(/\\/+$/, \"\"),\n name: d.name,\n dashboardUrl: resolvePeerDashboardUrl(d),\n tokenScope: (d.dashboardToken || d.token) ? \"proxy-ready\" : undefined,\n }))\n // Fleet pseudo-peer: only meaningful when there's at least one\n // remote peer to merge with. Placed at the bottom so users tend to\n // notice it after they've used the per-peer entries first.\n const fleet: TopbarPeer[] = extras.length > 0\n ? [{\n id: \"fleet\",\n name: \"Fleet (all peers)\",\n dashboardUrl: \"\",\n tokenScope: \"merged across nodes\",\n }]\n : []\n return [primary, ...extras, ...fleet]\n}\n\n/**\n * Forward the entire incoming admin request to the selected peer's\n * dashboard. Copies method, query string, body, and Content-Type. Auth\n * replaces anything the browser sent with the peer's configured token so\n * the local dashboard.token (if any) doesn't leak across nodes.\n */\n/** Fleet-mode activity-graph snapshot: build the local snapshot in-process,\n * fetch every configured peer's snapshot via its dashboard API, merge.\n * Per-peer failures don't fail the whole call — they're just dropped from\n * the merge and logged, so a single dead peer doesn't black out the view. */\nasync function handleActivityGraphFleet(\n req: IncomingMessage,\n res: ServerResponse,\n ctx: { config: DaemonConfig; token?: string },\n): Promise<void> {\n const url = new URL(req.url || \"/\", \"http://_\")\n const hours = parseInt(url.searchParams.get(\"hours\") || \"6\", 10) || 6\n sendJson(res, 200, await withForgeStatus(await buildFleetActivitySnapshot(hours, ctx.config)))\n}\n\nasync function buildFleetActivitySnapshot(hours: number, config: DaemonConfig): Promise<FleetSnapshot> {\n const localNodeId = config.node?.id || config.node?.name || \"local\"\n\n const parts: Array<{ nodeId: string; snap: FleetSnapshot }> = []\n\n // Local node: in-process call, no HTTP round-trip.\n const localSnap = buildLocalActivityGraphSnapshot(hours)\n if (localSnap) parts.push({ nodeId: localNodeId, snap: localSnap })\n\n // Each configured peer: HTTP fetch via the existing per-peer proxy logic.\n // Fire in parallel; whichever peers respond on time get merged in.\n const peerEntries = (config.dashboard?.daemons || []).filter((d) => d.url)\n const peerParts: Array<{ nodeId: string; snap: FleetSnapshot } | null> = await Promise.all(peerEntries.map(async (d) => {\n const peer = findPeer(d.url.replace(/\\/+$/, \"\"), config)\n if (!peer) return null\n try {\n const ac = new AbortController()\n const timer = setTimeout(() => ac.abort(), 8000)\n try {\n const r = await fetch(`${peer.url}/api/admin/activity-graph?hours=${hours}`, {\n headers: {\n \"Accept\": \"application/json\",\n \"X-Agentx-Peer\": \"primary\",\n ...(peer.token ? { Authorization: `Bearer ${peer.token}` } : {}),\n },\n signal: ac.signal,\n })\n if (!r.ok) return null\n return { nodeId: d.name || d.url, snap: await r.json() as FleetSnapshot }\n } finally {\n clearTimeout(timer)\n }\n } catch {\n // Drop this peer from the merge; the rest of the snapshot remains usable.\n return null\n }\n }))\n // Keep local first: the merger reports parts[0] as the serving node.\n for (const p of peerParts) if (p) parts.push(p)\n\n return mergeFleetSnapshots(parts)\n}\n\n/** SSE-stream the daemon's stdout to the dashboard /admin/health Logs tab.\n * Picks the best available source: prefers `journalctl -u agentx -f`\n * (systemd-managed nodes like peer-server), falls back to `tail -F` on\n * /tmp/agentx-daemon.log (Mac dev launches), or the tail of the\n * workspace pid log if neither exists. Mirrors the source the\n * `agentx daemon logs -f` CLI hits, so both surfaces show the same\n * data.\n *\n * The spawned process inherits the dashboard's permissions; on\n * peer-server `peer` already has read access to its agentx unit's\n * journal via the systemd-journal group (no sudo needed). On Mac the\n * log file is world-readable. */\nasync function streamDaemonLogs(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const { spawn, execSync } = await import(\"child_process\")\n\n // Prefer journalctl when available — it captures stdout from systemd,\n // which is where the daemon's logs actually land on prod. Fall back to\n // the legacy /tmp file the CLI uses.\n let cmd: string\n let args: string[]\n try {\n execSync(\"which journalctl\", { stdio: \"pipe\" })\n cmd = \"journalctl\"\n args = [\"-u\", \"agentx\", \"--no-pager\", \"-n\", \"200\", \"-f\", \"-o\", \"cat\"]\n } catch {\n // Mac (launchd) logs to ~/.agentx/logs/daemon-stdout.log; the legacy\n // /tmp/agentx-daemon.log is the foreground-launch path. Probe both.\n const home = process.env.HOME || \"\"\n const candidates = [\n home && `${home}/.agentx/logs/daemon-stdout.log`,\n \"/tmp/agentx-daemon.log\",\n ].filter(Boolean) as string[]\n const file = candidates.find((f) => existsSync(f))\n if (!file) {\n res.writeHead(503, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify({\n error: \"no log source — install journalctl, run the daemon under launchd, or run it foreground writing to /tmp/agentx-daemon.log\",\n searched: candidates,\n }))\n return\n }\n cmd = \"tail\"\n args = [\"-F\", \"-n\", \"200\", file]\n }\n\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n \"Connection\": \"keep-alive\",\n \"X-Accel-Buffering\": \"no\",\n })\n res.write(`: source=${cmd}\\n\\n`)\n\n const child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] })\n let buffer = \"\"\n const onData = (chunk: Buffer) => {\n buffer += chunk.toString(\"utf8\")\n let idx\n while ((idx = buffer.indexOf(\"\\n\")) >= 0) {\n const line = buffer.slice(0, idx).replace(/\\r$/, \"\")\n buffer = buffer.slice(idx + 1)\n // SSE: each line gets its own `event: line` so the client can\n // listen by name. Escape newlines defensively (lines should not\n // contain any since we split on them, but be safe).\n try { res.write(`event: line\\ndata: ${line.replace(/\\n/g, \" \")}\\n\\n`) } catch { /* */ }\n }\n }\n child.stdout?.on(\"data\", onData)\n // Mirror stderr too so the operator sees crash output.\n child.stderr?.on(\"data\", onData)\n\n // Heartbeat keeps middleboxes from idling the connection out.\n const hb = setInterval(() => {\n try { res.write(`: hb\\n\\n`) } catch { /* */ }\n }, 15_000)\n\n const cleanup = () => {\n clearInterval(hb)\n try { child.kill() } catch { /* */ }\n try { res.end() } catch { /* */ }\n }\n req.on(\"close\", cleanup)\n child.on(\"exit\", cleanup)\n child.on(\"error\", cleanup)\n}\n\nasync function proxyAdminToPeer(\n req: IncomingMessage,\n res: ServerResponse,\n peer: { url: string; token?: string },\n pathWithQuery: string,\n): Promise<void> {\n const target = peer.url + pathWithQuery\n // Read the incoming body (if any). For GET/HEAD there's no body to drain.\n const method = (req.method || \"GET\").toUpperCase()\n const hasBody = method !== \"GET\" && method !== \"HEAD\"\n const body: Buffer | undefined = hasBody\n ? await new Promise<Buffer>((resolve, reject) => {\n const chunks: Buffer[] = []\n req.on(\"data\", (c: Buffer) => chunks.push(c))\n req.on(\"end\", () => resolve(Buffer.concat(chunks)))\n req.on(\"error\", reject)\n })\n : undefined\n\n const headers: Record<string, string> = {}\n const ct = req.headers[\"content-type\"]\n if (ct) headers[\"Content-Type\"] = Array.isArray(ct) ? ct[0] : ct\n const xrw = req.headers[\"x-requested-with\"]\n if (xrw) headers[\"X-Requested-With\"] = Array.isArray(xrw) ? xrw[0] : xrw\n if (peer.token) headers[\"Authorization\"] = `Bearer ${peer.token}`\n // Tell the peer not to recurse — belt-and-braces guard.\n headers[\"X-Agentx-Peer\"] = \"primary\"\n\n try {\n const upstream = await fetch(target, {\n method,\n headers,\n body: body && body.length > 0 ? body : undefined,\n })\n const respBody = Buffer.from(await upstream.arrayBuffer())\n res.writeHead(upstream.status, {\n \"Content-Type\": upstream.headers.get(\"content-type\") || \"application/json\",\n \"Cache-Control\": \"no-store\",\n \"Access-Control-Allow-Origin\": \"*\",\n })\n res.end(respBody)\n } catch (e: any) {\n sendJson(res, 502, { error: `proxy to peer failed: ${e?.message || e}`, target })\n }\n}\n\n/**\n * Look up a peer by id (matches `dashboard.daemons[].url` normalised) so\n * the proxy middleware knows where to forward + what token to use.\n */\nfunction findPeer(id: string, config: DaemonConfig): { url: string; token?: string } | null {\n const peerId = id.replace(/\\/+$/, \"\")\n const match = (config.dashboard?.daemons || []).find((d) => d.url.replace(/\\/+$/, \"\") === peerId)\n if (!match) return null\n return {\n url: resolvePeerDashboardUrl(match),\n token: match.dashboardToken || match.token,\n }\n}\n\n/**\n * Cross-surface theme switcher. Reads persisted theme from localStorage BEFORE\n * first paint (dropped into <head> to avoid FOUC), then wires any segmented\n * control with [data-theme-opt=\"...\"] buttons once the DOM is ready.\n */\n\n\n","// Vendored SortableJS 1.15.2 — MIT, https://github.com/SortableJS/Sortable\n// Served by board-dashboard at /sortable.min.js. Do not edit by hand.\nexport const SORTABLE_JS = \"/*! Sortable 1.15.2 - MIT | git://github.com/SortableJS/Sortable.git */\\n!function(t,e){\\\"object\\\"==typeof exports&&\\\"undefined\\\"!=typeof module?module.exports=e():\\\"function\\\"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){\\\"use strict\\\";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?e(Object(i),!0).forEach(function(t){var e,n;e=o,t=i[n=t],n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach(function(t){Object.defineProperty(o,t,Object.getOwnPropertyDescriptor(i,t))})}return o}function o(t){return(o=\\\"function\\\"==typeof Symbol&&\\\"symbol\\\"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&\\\"function\\\"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?\\\"symbol\\\":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)Object.prototype.hasOwnProperty.call(o,n)&&(t[n]=o[n])}return t}).apply(this,arguments)}function i(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};for(var n,o={},i=Object.keys(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function r(t){return function(t){if(Array.isArray(t))return l(t)}(t)||function(t){if(\\\"undefined\\\"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t[\\\"@@iterator\\\"])return Array.from(t)}(t)||function(t,e){if(t){if(\\\"string\\\"==typeof t)return l(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return\\\"Map\\\"===(n=\\\"Object\\\"===n&&t.constructor?t.constructor.name:n)||\\\"Set\\\"===n?Array.from(t):\\\"Arguments\\\"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(t,e):void 0}}(t)||function(){throw new TypeError(\\\"Invalid attempt to spread non-iterable instance.\\\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\\\")}()}function l(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n<e;n++)o[n]=t[n];return o}function t(t){if(\\\"undefined\\\"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\\\\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),s=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),n=t(/iP(ad|od|hone)/i),c=t(/chrome/i)&&t(/android/i),d={capture:!1,passive:!1};function h(t,e,n){t.addEventListener(e,n,!y&&d)}function f(t,e,n){t.removeEventListener(e,n,!y&&d)}function p(t,e){if(e&&(\\\">\\\"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(\\\">\\\"!==e[0]||t.parentNode===n)&&p(t,e)||o&&t===n)return t}while(t!==n&&(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode))}var i;return null}var g,m=/\\\\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?\\\"add\\\":\\\"remove\\\"](e):(o=(\\\" \\\"+t.className+\\\" \\\").replace(m,\\\" \\\").replace(\\\" \\\"+e+\\\" \\\",\\\" \\\"),t.className=(o+(n?\\\" \\\"+e:\\\"\\\")).replace(m,\\\" \\\")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,\\\"\\\"):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf(\\\"webkit\\\"))?\\\"-webkit-\\\"+e:e]=n+(\\\"string\\\"==typeof n?\\\"\\\":\\\"px\\\")}}function v(t,e){var n=\\\"\\\";if(\\\"string\\\"==typeof t)n=t;else do{var o=R(t,\\\"transform\\\")}while(o&&\\\"none\\\"!==o&&(n=o+\\\" \\\"+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function b(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&(\\\"none\\\"!==R(i,\\\"transform\\\")||n&&\\\"static\\\"!==R(i,\\\"position\\\"))){var h=i.getBoundingClientRect();a-=h.top+parseInt(R(i,\\\"border-top-width\\\")),l-=h.left+parseInt(R(i,\\\"border-left-width\\\")),s=a+r.height,c=l+r.width;break}}while(i=i.parentNode);return o&&t!==window&&(o=(e=v(i||t))&&e.a,t=e&&e.d,e&&(s=(a/=t)+(u/=t),c=(l/=o)+(d/=o))),{top:a,left:l,bottom:s,right:c,width:d,height:u}}}function Y(t,e,n){for(var o=M(t,!0),i=X(t)[e];o;){var r=X(o)[n];if(!(\\\"top\\\"===n||\\\"left\\\"===n?r<=i:i<=r))return o;if(o===O())break;o=M(o,!1)}return!1}function B(t,e,n,o){for(var i=0,r=0,a=t.children;r<a.length;){if(\\\"none\\\"!==a[r].style.display&&a[r]!==Ft.ghost&&(o||a[r]!==Ft.dragged)&&P(a[r],n.draggable,t,!1)){if(i===e)return a[r];i++}r++}return null}function F(t,e){for(var n=t.lastElementChild;n&&(n===Ft.ghost||\\\"none\\\"===R(n,\\\"display\\\")||e&&!p(n,e));)n=n.previousElementSibling;return n||null}function j(t,e){var n=0;if(!t||!t.parentNode)return-1;for(;t=t.previousElementSibling;)\\\"TEMPLATE\\\"===t.nodeName.toUpperCase()||t===Ft.clone||e&&!p(t,e)||n++;return n}function E(t){var e=0,n=0,o=O();if(t)do{var i=v(t),r=i.a,i=i.d}while(e+=t.scrollLeft*r,n+=t.scrollTop*i,t!==o&&(t=t.parentNode));return[e,n]}function M(t,e){if(!t||!t.getBoundingClientRect)return O();var n=t,o=!1;do{if(n.clientWidth<n.scrollWidth||n.clientHeight<n.scrollHeight){var i=R(n);if(n.clientWidth<n.scrollWidth&&(\\\"auto\\\"==i.overflowX||\\\"scroll\\\"==i.overflowX)||n.clientHeight<n.scrollHeight&&(\\\"auto\\\"==i.overflowY||\\\"scroll\\\"==i.overflowY)){if(!n.getBoundingClientRect||n===document.body)return O();if(o||e)return n;o=!0}}}while(n=n.parentNode);return O()}function D(t,e){return Math.round(t.top)===Math.round(e.top)&&Math.round(t.left)===Math.round(e.left)&&Math.round(t.height)===Math.round(e.height)&&Math.round(t.width)===Math.round(e.width)}function S(e,n){return function(){var t;g||(1===(t=arguments).length?e.call(this,t[0]):e.apply(this,t),g=setTimeout(function(){g=void 0},n))}}function H(t,e,n){t.scrollLeft+=e,t.scrollTop+=n}function _(t){var e=window.Polymer,n=window.jQuery||window.Zepto;return e&&e.dom?e.dom(t).cloneNode(!0):n?n(t).clone(!0)[0]:t.cloneNode(!0)}function C(t,e){R(t,\\\"position\\\",\\\"absolute\\\"),R(t,\\\"top\\\",e.top),R(t,\\\"left\\\",e.left),R(t,\\\"width\\\",e.width),R(t,\\\"height\\\",e.height)}function T(t){R(t,\\\"position\\\",\\\"\\\"),R(t,\\\"top\\\",\\\"\\\"),R(t,\\\"left\\\",\\\"\\\"),R(t,\\\"width\\\",\\\"\\\"),R(t,\\\"height\\\",\\\"\\\")}function L(n,o,i){var r={};return Array.from(n.children).forEach(function(t){var e;P(t,o.draggable,n,!1)&&!t.animated&&t!==i&&(e=X(t),r.left=Math.min(null!==(t=r.left)&&void 0!==t?t:1/0,e.left),r.top=Math.min(null!==(t=r.top)&&void 0!==t?t:1/0,e.top),r.right=Math.max(null!==(t=r.right)&&void 0!==t?t:-1/0,e.right),r.bottom=Math.max(null!==(t=r.bottom)&&void 0!==t?t:-1/0,e.bottom))}),r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}var K=\\\"Sortable\\\"+(new Date).getTime();function x(){var e,o=[];return{captureAnimationState:function(){o=[],this.options.animation&&[].slice.call(this.el.children).forEach(function(t){var e,n;\\\"none\\\"!==R(t,\\\"display\\\")&&t!==Ft.ghost&&(o.push({target:t,rect:X(t)}),e=I({},o[o.length-1].rect),!t.thisAnimationDuration||(n=v(t,!0))&&(e.top-=n.f,e.left-=n.e),t.fromRect=e)})},addAnimationState:function(t){o.push(t)},removeAnimationState:function(t){o.splice(function(t,e){for(var n in t)if(t.hasOwnProperty(n))for(var o in e)if(e.hasOwnProperty(o)&&e[o]===t[n][o])return Number(n);return-1}(o,{target:t}),1)},animateAll:function(t){var c=this;if(!this.options.animation)return clearTimeout(e),void(\\\"function\\\"==typeof t&&t());var u=!1,d=0;o.forEach(function(t){var e=0,n=t.target,o=n.fromRect,i=X(n),r=n.prevFromRect,a=n.prevToRect,l=t.rect,s=v(n,!0);s&&(i.top-=s.f,i.left-=s.e),n.toRect=i,n.thisAnimationDuration&&D(r,i)&&!D(o,i)&&(l.top-i.top)/(l.left-i.left)==(o.top-i.top)/(o.left-i.left)&&(t=l,s=r,r=a,a=c.options,e=Math.sqrt(Math.pow(s.top-t.top,2)+Math.pow(s.left-t.left,2))/Math.sqrt(Math.pow(s.top-r.top,2)+Math.pow(s.left-r.left,2))*a.animation),D(i,o)||(n.prevFromRect=o,n.prevToRect=i,e=e||c.options.animation,c.animate(n,l,i,e)),e&&(u=!0,d=Math.max(d,e),clearTimeout(n.animationResetTimer),n.animationResetTimer=setTimeout(function(){n.animationTime=0,n.prevFromRect=null,n.fromRect=null,n.prevToRect=null,n.thisAnimationDuration=null},e),n.thisAnimationDuration=e)}),clearTimeout(e),u?e=setTimeout(function(){\\\"function\\\"==typeof t&&t()},d):\\\"function\\\"==typeof t&&t(),o=[]},animate:function(t,e,n,o){var i,r;o&&(R(t,\\\"transition\\\",\\\"\\\"),R(t,\\\"transform\\\",\\\"\\\"),i=(r=v(this.el))&&r.a,r=r&&r.d,i=(e.left-n.left)/(i||1),r=(e.top-n.top)/(r||1),t.animatingX=!!i,t.animatingY=!!r,R(t,\\\"transform\\\",\\\"translate3d(\\\"+i+\\\"px,\\\"+r+\\\"px,0)\\\"),this.forRepaintDummy=t.offsetWidth,R(t,\\\"transition\\\",\\\"transform \\\"+o+\\\"ms\\\"+(this.options.easing?\\\" \\\"+this.options.easing:\\\"\\\")),R(t,\\\"transform\\\",\\\"translate3d(0,0,0)\\\"),\\\"number\\\"==typeof t.animated&&clearTimeout(t.animated),t.animated=setTimeout(function(){R(t,\\\"transition\\\",\\\"\\\"),R(t,\\\"transform\\\",\\\"\\\"),t.animated=!1,t.animatingX=!1,t.animatingY=!1},o))}}}var A=[],N={initializeByDefault:!0},W={mount:function(e){for(var t in N)!N.hasOwnProperty(t)||t in e||(e[t]=N[t]);A.forEach(function(t){if(t.pluginName===e.pluginName)throw\\\"Sortable: Cannot mount plugin \\\".concat(e.pluginName,\\\" more than once\\\")}),A.push(e)},pluginEvent:function(e,n,o){var t=this;this.eventCanceled=!1,o.cancel=function(){t.eventCanceled=!0};var i=e+\\\"Global\\\";A.forEach(function(t){n[t.pluginName]&&(n[t.pluginName][i]&&n[t.pluginName][i](I({sortable:n},o)),n.options[t.pluginName]&&n[t.pluginName][e]&&n[t.pluginName][e](I({sortable:n},o)))})},initializePlugins:function(n,o,i,t){for(var e in A.forEach(function(t){var e=t.pluginName;(n.options[e]||t.initializeByDefault)&&((t=new t(n,o,n.options)).sortable=n,t.options=n.options,n[e]=t,a(i,t.defaults))}),n.options){var r;n.options.hasOwnProperty(e)&&(void 0!==(r=this.modifyOption(n,e,n.options[e]))&&(n.options[e]=r))}},getEventProperties:function(e,n){var o={};return A.forEach(function(t){\\\"function\\\"==typeof t.eventProperties&&a(o,t.eventProperties.call(n[t.pluginName],e))}),o},modifyOption:function(e,n,o){var i;return A.forEach(function(t){e[t.pluginName]&&t.optionListeners&&\\\"function\\\"==typeof t.optionListeners[n]&&(i=t.optionListeners[n].call(e[t.pluginName],o))}),i}};function z(t){var e=t.sortable,n=t.rootEl,o=t.name,i=t.targetEl,r=t.cloneEl,a=t.toEl,l=t.fromEl,s=t.oldIndex,c=t.newIndex,u=t.oldDraggableIndex,d=t.newDraggableIndex,h=t.originalEvent,f=t.putSortable,p=t.extraEventProperties;if(e=e||n&&n[K]){var g,m=e.options,t=\\\"on\\\"+o.charAt(0).toUpperCase()+o.substr(1);!window.CustomEvent||y||w?(g=document.createEvent(\\\"Event\\\")).initEvent(o,!0,!0):g=new CustomEvent(o,{bubbles:!0,cancelable:!0}),g.to=a||n,g.from=l||n,g.item=i||n,g.clone=r,g.oldIndex=s,g.newIndex=c,g.oldDraggableIndex=u,g.newDraggableIndex=d,g.originalEvent=h,g.pullMode=f?f.lastPutMode:void 0;var v,b=I(I({},p),W.getEventProperties(o,e));for(v in b)g[v]=b[v];n&&n.dispatchEvent(g),m[t]&&m[t].call(e,g)}}function G(t,e){var n=(o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{}).evt,o=i(o,U);W.pluginEvent.bind(Ft)(t,e,I({dragEl:V,parentEl:Z,ghostEl:$,rootEl:Q,nextEl:J,lastDownEl:tt,cloneEl:et,cloneHidden:nt,dragStarted:gt,putSortable:st,activeSortable:Ft.active,originalEvent:n,oldIndex:ot,oldDraggableIndex:rt,newIndex:it,newDraggableIndex:at,hideGhostForTarget:Rt,unhideGhostForTarget:Xt,cloneNowHidden:function(){nt=!0},cloneNowShown:function(){nt=!1},dispatchSortableEvent:function(t){q({sortable:e,name:t,originalEvent:n})}},o))}var U=[\\\"evt\\\"];function q(t){z(I({putSortable:st,cloneEl:et,targetEl:V,rootEl:Q,oldIndex:ot,oldDraggableIndex:rt,newIndex:it,newDraggableIndex:at},t))}var V,Z,$,Q,J,tt,et,nt,ot,it,rt,at,lt,st,ct,ut,dt,ht,ft,pt,gt,mt,vt,bt,yt,wt=!1,Et=!1,Dt=[],St=!1,_t=!1,Ct=[],Tt=!1,xt=[],Ot=\\\"undefined\\\"!=typeof document,Mt=n,At=w||y?\\\"cssFloat\\\":\\\"float\\\",Nt=Ot&&!c&&!n&&\\\"draggable\\\"in document.createElement(\\\"div\\\"),It=function(){if(Ot){if(y)return!1;var t=document.createElement(\\\"x\\\");return t.style.cssText=\\\"pointer-events:auto\\\",\\\"auto\\\"===t.style.pointerEvents}}(),Pt=function(t,e){var n=R(t),o=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),i=B(t,0,e),r=B(t,1,e),a=i&&R(i),l=r&&R(r),s=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+X(i).width,t=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+X(r).width;if(\\\"flex\\\"===n.display)return\\\"column\\\"===n.flexDirection||\\\"column-reverse\\\"===n.flexDirection?\\\"vertical\\\":\\\"horizontal\\\";if(\\\"grid\\\"===n.display)return n.gridTemplateColumns.split(\\\" \\\").length<=1?\\\"vertical\\\":\\\"horizontal\\\";if(i&&a.float&&\\\"none\\\"!==a.float){e=\\\"left\\\"===a.float?\\\"left\\\":\\\"right\\\";return!r||\\\"both\\\"!==l.clear&&l.clear!==e?\\\"horizontal\\\":\\\"vertical\\\"}return i&&(\\\"block\\\"===a.display||\\\"flex\\\"===a.display||\\\"table\\\"===a.display||\\\"grid\\\"===a.display||o<=s&&\\\"none\\\"===n[At]||r&&\\\"none\\\"===n[At]&&o<s+t)?\\\"vertical\\\":\\\"horizontal\\\"},kt=function(t){function l(r,a){return function(t,e,n,o){var i=t.options.group.name&&e.options.group.name&&t.options.group.name===e.options.group.name;if(null==r&&(a||i))return!0;if(null==r||!1===r)return!1;if(a&&\\\"clone\\\"===r)return r;if(\\\"function\\\"==typeof r)return l(r(t,e,n,o),a)(t,e,n,o);e=(a?t:e).options.group.name;return!0===r||\\\"string\\\"==typeof r&&r===e||r.join&&-1<r.indexOf(e)}}var e={},n=t.group;n&&\\\"object\\\"==o(n)||(n={name:n}),e.name=n.name,e.checkPull=l(n.pull,!0),e.checkPut=l(n.put),e.revertClone=n.revertClone,t.group=e},Rt=function(){!It&&$&&R($,\\\"display\\\",\\\"none\\\")},Xt=function(){!It&&$&&R($,\\\"display\\\",\\\"\\\")};Ot&&!c&&document.addEventListener(\\\"click\\\",function(t){if(Et)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),Et=!1},!0);function Yt(t){if(V){t=t.touches?t.touches[0]:t;var e=(i=t.clientX,r=t.clientY,Dt.some(function(t){var e=t[K].options.emptyInsertThreshold;if(e&&!F(t)){var n=X(t),o=i>=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Bt(t){V&&V.parentNode[K]._isOutsideThisEl(t.target)}function Ft(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw\\\"Sortable: `el` must be an HTMLElement, not \\\".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?\\\">li\\\":\\\">*\\\",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Pt(t,this.options)},ghostClass:\\\"sortable-ghost\\\",chosenClass:\\\"sortable-chosen\\\",dragClass:\\\"sortable-drag\\\",ignore:\\\"a, img\\\",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData(\\\"Text\\\",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:\\\"data-id\\\",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:\\\"sortable-fallback\\\",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Ft.supportPointer&&\\\"PointerEvent\\\"in window&&!u,emptyInsertThreshold:5};for(n in W.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in kt(e),this)\\\"_\\\"===o.charAt(0)&&\\\"function\\\"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Nt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,\\\"pointerdown\\\",this._onTapStart):(h(t,\\\"mousedown\\\",this._onTapStart),h(t,\\\"touchstart\\\",this._onTapStart)),this.nativeDraggable&&(h(t,\\\"dragover\\\",this),h(t,\\\"dragenter\\\",this)),Dt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,x())}function jt(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent(\\\"Event\\\")).initEvent(\\\"move\\\",!0,!0):s=new CustomEvent(\\\"move\\\",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Ht(t){t.draggable=!1}function Lt(){Tt=!1}function Kt(t){return setTimeout(t,0)}function Wt(t){return clearTimeout(t)}Ft.prototype={constructor:Ft,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(mt=null)},_getDirection:function(t,e){return\\\"function\\\"==typeof this.options.direction?this.options.direction.call(this,t,e,V):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&\\\"touch\\\"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){xt.length=0;var e=t.getElementsByTagName(\\\"input\\\"),n=e.length;for(;n--;){var o=e[n];o.checked&&xt.push(o)}}(o),!V&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||\\\"SELECT\\\"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||tt===l)){if(ot=j(l),rt=j(l,t.draggable),\\\"function\\\"==typeof c){if(c.call(this,e,l,this))return q({sortable:n,rootEl:s,name:\\\"filter\\\",targetEl:l,toEl:o,fromEl:o}),G(\\\"filter\\\",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c=c&&c.split(\\\",\\\").some(function(t){if(t=P(s,t.trim(),o,!1))return q({sortable:n,rootEl:t,name:\\\"filter\\\",targetEl:l,fromEl:o,toEl:o}),G(\\\"filter\\\",n,{evt:e}),!0}))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!V&&n.parentNode===r&&(o=X(n),Q=r,Z=(V=n).parentNode,J=V.nextSibling,tt=n,lt=a.group,ct={target:Ft.dragged=V,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ct.clientX-o.left,pt=ct.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,V.style[\\\"will-change\\\"]=\\\"all\\\",o=function(){G(\\\"delayEnded\\\",i,{evt:t}),Ft.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(V.draggable=!0),i._triggerDragStart(t,e),q({sortable:i,name:\\\"choose\\\",originalEvent:t}),k(V,a.chosenClass,!0))},a.ignore.split(\\\",\\\").forEach(function(t){b(V,t.trim(),Ht)}),h(l,\\\"dragover\\\",Yt),h(l,\\\"mousemove\\\",Yt),h(l,\\\"touchmove\\\",Yt),h(l,\\\"mouseup\\\",i._onDrop),h(l,\\\"touchend\\\",i._onDrop),h(l,\\\"touchcancel\\\",i._onDrop),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,V.draggable=!0),G(\\\"delayStart\\\",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():Ft.eventCanceled?this._onDrop():(h(l,\\\"mouseup\\\",i._disableDelayedDrag),h(l,\\\"touchend\\\",i._disableDelayedDrag),h(l,\\\"touchcancel\\\",i._disableDelayedDrag),h(l,\\\"mousemove\\\",i._delayedDragTouchMoveHandler),h(l,\\\"touchmove\\\",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,\\\"pointermove\\\",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){V&&Ht(V),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;f(t,\\\"mouseup\\\",this._disableDelayedDrag),f(t,\\\"touchend\\\",this._disableDelayedDrag),f(t,\\\"touchcancel\\\",this._disableDelayedDrag),f(t,\\\"mousemove\\\",this._delayedDragTouchMoveHandler),f(t,\\\"touchmove\\\",this._delayedDragTouchMoveHandler),f(t,\\\"pointermove\\\",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||\\\"touch\\\"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,\\\"pointermove\\\",this._onTouchMove):h(document,e?\\\"touchmove\\\":\\\"mousemove\\\",this._onTouchMove):(h(V,\\\"dragend\\\",this),h(Q,\\\"dragstart\\\",this._onDragStart));try{document.selection?Kt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;wt=!1,Q&&V?(G(\\\"dragStarted\\\",this,{evt:e}),this.nativeDraggable&&h(document,\\\"dragover\\\",Bt),n=this.options,t||k(V,n.dragClass,!1),k(V,n.ghostClass,!0),Ft.active=this,t&&this._appendGhost(),q({sortable:this,name:\\\"start\\\",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(ut){this._lastX=ut.clientX,this._lastY=ut.clientY,Rt();for(var t=document.elementFromPoint(ut.clientX,ut.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(ut.clientX,ut.clientY))!==e;)e=t;if(V.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:ut.clientX,clientY:ut.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=(t=e).parentNode);Xt()}},_onTouchMove:function(t){if(ct){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=$&&v($,!0),a=$&&r&&r.a,l=$&&r&&r.d,e=Mt&&yt&&E(yt),a=(i.clientX-ct.clientX+o.x)/(a||1)+(e?e[0]-Ct[0]:0)/(a||1),l=(i.clientY-ct.clientY+o.y)/(l||1)+(e?e[1]-Ct[1]:0)/(l||1);if(!Ft.active&&!wt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))<n)return;this._onDragStart(t,!0)}$&&(r?(r.e+=a-(dt||0),r.f+=l-(ht||0)):r={a:1,b:0,c:0,d:1,e:a,f:l},r=\\\"matrix(\\\".concat(r.a,\\\",\\\").concat(r.b,\\\",\\\").concat(r.c,\\\",\\\").concat(r.d,\\\",\\\").concat(r.e,\\\",\\\").concat(r.f,\\\")\\\"),R($,\\\"webkitTransform\\\",r),R($,\\\"mozTransform\\\",r),R($,\\\"msTransform\\\",r),R($,\\\"transform\\\",r),dt=a,ht=l,ut=i),t.cancelable&&t.preventDefault()}},_appendGhost:function(){if(!$){var t=this.options.fallbackOnBody?document.body:Q,e=X(V,!0,Mt,!0,t),n=this.options;if(Mt){for(yt=t;\\\"static\\\"===R(yt,\\\"position\\\")&&\\\"none\\\"===R(yt,\\\"transform\\\")&&yt!==document;)yt=yt.parentNode;yt!==document.body&&yt!==document.documentElement?(yt===document&&(yt=O()),e.top+=yt.scrollTop,e.left+=yt.scrollLeft):yt=O(),Ct=E(yt)}k($=V.cloneNode(!0),n.ghostClass,!1),k($,n.fallbackClass,!0),k($,n.dragClass,!0),R($,\\\"transition\\\",\\\"\\\"),R($,\\\"transform\\\",\\\"\\\"),R($,\\\"box-sizing\\\",\\\"border-box\\\"),R($,\\\"margin\\\",0),R($,\\\"top\\\",e.top),R($,\\\"left\\\",e.left),R($,\\\"width\\\",e.width),R($,\\\"height\\\",e.height),R($,\\\"opacity\\\",\\\"0.8\\\"),R($,\\\"position\\\",Mt?\\\"absolute\\\":\\\"fixed\\\"),R($,\\\"zIndex\\\",\\\"100000\\\"),R($,\\\"pointerEvents\\\",\\\"none\\\"),Ft.ghost=$,t.appendChild($),R($,\\\"transform-origin\\\",ft/parseInt($.style.width)*100+\\\"% \\\"+pt/parseInt($.style.height)*100+\\\"%\\\")}},_onDragStart:function(t,e){var n=this,o=t.dataTransfer,i=n.options;G(\\\"dragStart\\\",this,{evt:t}),Ft.eventCanceled?this._onDrop():(G(\\\"setupClone\\\",this),Ft.eventCanceled||((et=_(V)).removeAttribute(\\\"id\\\"),et.draggable=!1,et.style[\\\"will-change\\\"]=\\\"\\\",this._hideClone(),k(et,this.options.chosenClass,!1),Ft.clone=et),n.cloneId=Kt(function(){G(\\\"clone\\\",n),Ft.eventCanceled||(n.options.removeCloneOnHide||Q.insertBefore(et,V),n._hideClone(),q({sortable:n,name:\\\"clone\\\"}))}),e||k(V,i.dragClass,!0),e?(Et=!0,n._loopId=setInterval(n._emulateDragOver,50)):(f(document,\\\"mouseup\\\",n._onDrop),f(document,\\\"touchend\\\",n._onDrop),f(document,\\\"touchcancel\\\",n._onDrop),o&&(o.effectAllowed=\\\"move\\\",i.setData&&i.setData.call(n,o,V)),h(document,\\\"drop\\\",n),R(V,\\\"transform\\\",\\\"translateZ(0)\\\")),wt=!0,n._dragStartId=Kt(n._dragStarted.bind(n,e,t)),h(document,\\\"selectstart\\\",n),gt=!0,u&&R(document.body,\\\"user-select\\\",\\\"none\\\"))},_onDragOver:function(n){var o,i,r,t,e,a=this.el,l=n.target,s=this.options,c=s.group,u=Ft.active,d=lt===c,h=s.sort,f=st||u,p=this,g=!1;if(!Tt){if(void 0!==n.preventDefault&&n.cancelable&&n.preventDefault(),l=P(l,s.draggable,a,!0),O(\\\"dragOver\\\"),Ft.eventCanceled)return g;if(V.contains(n.target)||l.animated&&l.animatingX&&l.animatingY||p._ignoreWhileAnimating===l)return A(!1);if(Et=!1,u&&!s.disabled&&(d?h||(i=Z!==Q):st===this||(this.lastPutMode=lt.checkPull(this,u,V,n))&&c.checkPut(this,u,V,n))){if(r=\\\"vertical\\\"===this._getDirection(n,l),o=X(V),O(\\\"dragOverValid\\\"),Ft.eventCanceled)return g;if(i)return Z=Q,M(),this._hideClone(),O(\\\"revert\\\"),Ft.eventCanceled||(J?Q.insertBefore(V,J):Q.appendChild(V)),A(!0);var m=F(a,s.draggable);if(m&&(S=n,c=r,x=X(F((D=this).el,D.options.draggable)),D=L(D.el,D.options,$),!(c?S.clientX>D.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>D.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,$),e?t.clientX<_.left-10||t.clientY<C.top&&t.clientX<C.right:t.clientY<_.top-10||t.clientY<C.bottom&&t.clientX<C.left)){var v=B(a,0,s,!0);if(v===V)return A(!1);if(E=X(l=v),!1!==jt(Q,a,V,o,l,E,n,!1))return M(),a.insertBefore(V,v),Z=a,N(),A(!0)}else if(l.parentNode===a){var b,y,w,E=X(l),D=V.parentNode!==a,S=(S=V.animated&&V.toRect||o,x=l.animated&&l.toRect||E,_=(e=r)?S.left:S.top,t=e?S.right:S.bottom,C=e?S.width:S.height,v=e?x.left:x.top,S=e?x.right:x.bottom,x=e?x.width:x.height,!(_===v||t===S||_+C/2===v+x/2)),_=r?\\\"top\\\":\\\"left\\\",C=Y(l,\\\"top\\\",\\\"top\\\")||Y(V,\\\"top\\\",\\\"top\\\"),v=C?C.scrollTop:void 0;if(mt!==l&&(y=E[_],St=!1,_t=!S&&s.invertSwap||D),0!==(b=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,t=o?n.top:n.left,o=o?n.bottom:n.right,n=!1;if(!a)if(l&&bt<c*i){if(St=!St&&(1===vt?t+c*r/2<s:s<o-c*r/2)?!0:St)n=!0;else if(1===vt?s<t+bt:o-bt<s)return-vt}else if(t+c*(1-i)/2<s&&s<o-c*(1-i)/2)return function(t){return j(V)<j(t)?1:-1}(e);if((n=n||a)&&(s<t+c*r/2||o-c*r/2<s))return t+c/2<s?1:-1;return 0}(n,l,E,r,S?1:s.swapThreshold,null==s.invertedSwapThreshold?s.swapThreshold:s.invertedSwapThreshold,_t,mt===l)))for(var T=j(V);(w=Z.children[T-=b])&&(\\\"none\\\"===R(w,\\\"display\\\")||w===$););if(0===b||w===l)return A(!1);vt=b;var x=(mt=l).nextElementSibling,D=!1,S=jt(Q,a,V,o,l,E,n,D=1===b);if(!1!==S)return 1!==S&&-1!==S||(D=1===S),Tt=!0,setTimeout(Lt,30),M(),D&&!x?a.appendChild(V):l.parentNode.insertBefore(V,D?x:l),C&&H(C,0,v-C.scrollTop),Z=V.parentNode,void 0===y||_t||(bt=Math.abs(y-X(l)[_])),N(),A(!0)}}else{if(m===V)return A(!1);if((l=m&&a===n.target?m:l)&&(E=X(l)),!1!==jt(Q,a,V,o,l,E,n,!!l))return M(),m&&m.nextSibling?a.insertBefore(V,m.nextSibling):a.appendChild(V),Z=a,N(),A(!0)}if(a.contains(V))return A(!1)}return!1}function O(t,e){G(t,p,I({evt:n,isOwner:d,axis:r?\\\"vertical\\\":\\\"horizontal\\\",revert:i,dragRect:o,targetRect:E,canSort:h,fromSortable:f,target:l,completed:A,onMove:function(t,e){return jt(Q,a,V,o,t,X(t),n,e)},changed:N},e))}function M(){O(\\\"dragOverAnimationCapture\\\"),p.captureAnimationState(),p!==f&&f.captureAnimationState()}function A(t){return O(\\\"dragOverCompleted\\\",{insertion:t}),t&&(d?u._hideClone():u._showClone(p),p!==f&&(k(V,(st||u).options.ghostClass,!1),k(V,s.ghostClass,!0)),st!==p&&p!==Ft.active?st=p:p===Ft.active&&st&&(st=null),f===p&&(p._ignoreWhileAnimating=l),p.animateAll(function(){O(\\\"dragOverAnimationComplete\\\"),p._ignoreWhileAnimating=null}),p!==f&&(f.animateAll(),f._ignoreWhileAnimating=null)),(l===V&&!V.animated||l===a&&!l.animated)&&(mt=null),s.dragoverBubble||n.rootEl||l===document||(V.parentNode[K]._isOutsideThisEl(n.target),t||Yt(n)),!s.dragoverBubble&&n.stopPropagation&&n.stopPropagation(),g=!0}function N(){it=j(V),at=j(V,s.draggable),q({sortable:p,name:\\\"change\\\",toEl:a,newIndex:it,newDraggableIndex:at,originalEvent:n})}},_ignoreWhileAnimating:null,_offMoveEvents:function(){f(document,\\\"mousemove\\\",this._onTouchMove),f(document,\\\"touchmove\\\",this._onTouchMove),f(document,\\\"pointermove\\\",this._onTouchMove),f(document,\\\"dragover\\\",Yt),f(document,\\\"mousemove\\\",Yt),f(document,\\\"touchmove\\\",Yt)},_offUpEvents:function(){var t=this.el.ownerDocument;f(t,\\\"mouseup\\\",this._onDrop),f(t,\\\"touchend\\\",this._onDrop),f(t,\\\"pointerup\\\",this._onDrop),f(t,\\\"touchcancel\\\",this._onDrop),f(document,\\\"selectstart\\\",this)},_onDrop:function(t){var e=this.el,n=this.options;it=j(V),at=j(V,n.draggable),G(\\\"drop\\\",this,{evt:t}),Z=V&&V.parentNode,it=j(V),at=j(V,n.draggable),Ft.eventCanceled||(St=_t=wt=!1,clearInterval(this._loopId),clearTimeout(this._dragStartTimer),Wt(this.cloneId),Wt(this._dragStartId),this.nativeDraggable&&(f(document,\\\"drop\\\",this),f(e,\\\"dragstart\\\",this._onDragStart)),this._offMoveEvents(),this._offUpEvents(),u&&R(document.body,\\\"user-select\\\",\\\"\\\"),R(V,\\\"transform\\\",\\\"\\\"),t&&(gt&&(t.cancelable&&t.preventDefault(),n.dropBubble||t.stopPropagation()),$&&$.parentNode&&$.parentNode.removeChild($),(Q===Z||st&&\\\"clone\\\"!==st.lastPutMode)&&et&&et.parentNode&&et.parentNode.removeChild(et),V&&(this.nativeDraggable&&f(V,\\\"dragend\\\",this),Ht(V),V.style[\\\"will-change\\\"]=\\\"\\\",gt&&!wt&&k(V,(st||this).options.ghostClass,!1),k(V,this.options.chosenClass,!1),q({sortable:this,name:\\\"unchoose\\\",toEl:Z,newIndex:null,newDraggableIndex:null,originalEvent:t}),Q!==Z?(0<=it&&(q({rootEl:Z,name:\\\"add\\\",toEl:Z,fromEl:Q,originalEvent:t}),q({sortable:this,name:\\\"remove\\\",toEl:Z,originalEvent:t}),q({rootEl:Z,name:\\\"sort\\\",toEl:Z,fromEl:Q,originalEvent:t}),q({sortable:this,name:\\\"sort\\\",toEl:Z,originalEvent:t})),st&&st.save()):it!==ot&&0<=it&&(q({sortable:this,name:\\\"update\\\",toEl:Z,originalEvent:t}),q({sortable:this,name:\\\"sort\\\",toEl:Z,originalEvent:t})),Ft.active&&(null!=it&&-1!==it||(it=ot,at=rt),q({sortable:this,name:\\\"end\\\",toEl:Z,originalEvent:t}),this.save())))),this._nulling()},_nulling:function(){G(\\\"nulling\\\",this),Q=V=Z=$=J=et=tt=nt=ct=ut=gt=it=at=ot=rt=mt=vt=st=lt=Ft.dragged=Ft.ghost=Ft.clone=Ft.active=null,xt.forEach(function(t){t.checked=!0}),xt.length=dt=ht=0},handleEvent:function(t){switch(t.type){case\\\"drop\\\":case\\\"dragend\\\":this._onDrop(t);break;case\\\"dragenter\\\":case\\\"dragover\\\":V&&(this._onDragOver(t),function(t){t.dataTransfer&&(t.dataTransfer.dropEffect=\\\"move\\\");t.cancelable&&t.preventDefault()}(t));break;case\\\"selectstart\\\":t.preventDefault()}},toArray:function(){for(var t,e=[],n=this.el.children,o=0,i=n.length,r=this.options;o<i;o++)P(t=n[o],r.draggable,this.el,!1)&&e.push(t.getAttribute(r.dataIdAttr)||function(t){var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;for(;n--;)o+=e.charCodeAt(n);return o.toString(36)}(t));return e},sort:function(t,e){var n={},o=this.el;this.toArray().forEach(function(t,e){e=o.children[e];P(e,this.options.draggable,o,!1)&&(n[t]=e)},this),e&&this.captureAnimationState(),t.forEach(function(t){n[t]&&(o.removeChild(n[t]),o.appendChild(n[t]))}),e&&this.animateAll()},save:function(){var t=this.options.store;t&&t.set&&t.set(this)},closest:function(t,e){return P(t,e||this.options.draggable,this.el,!1)},option:function(t,e){var n=this.options;if(void 0===e)return n[t];var o=W.modifyOption(this,t,e);n[t]=void 0!==o?o:e,\\\"group\\\"===t&&kt(n)},destroy:function(){G(\\\"destroy\\\",this);var t=this.el;t[K]=null,f(t,\\\"mousedown\\\",this._onTapStart),f(t,\\\"touchstart\\\",this._onTapStart),f(t,\\\"pointerdown\\\",this._onTapStart),this.nativeDraggable&&(f(t,\\\"dragover\\\",this),f(t,\\\"dragenter\\\",this)),Array.prototype.forEach.call(t.querySelectorAll(\\\"[draggable]\\\"),function(t){t.removeAttribute(\\\"draggable\\\")}),this._onDrop(),this._disableDelayedDragEvents(),Dt.splice(Dt.indexOf(this.el),1),this.el=t=null},_hideClone:function(){nt||(G(\\\"hideClone\\\",this),Ft.eventCanceled||(R(et,\\\"display\\\",\\\"none\\\"),this.options.removeCloneOnHide&&et.parentNode&&et.parentNode.removeChild(et),nt=!0))},_showClone:function(t){\\\"clone\\\"===t.lastPutMode?nt&&(G(\\\"showClone\\\",this),Ft.eventCanceled||(V.parentNode!=Q||this.options.group.revertClone?J?Q.insertBefore(et,J):Q.appendChild(et):Q.insertBefore(et,V),this.options.group.revertClone&&this.animate(V,et),R(et,\\\"display\\\",\\\"\\\"),nt=!1)):this._hideClone()}},Ot&&h(document,\\\"touchmove\\\",function(t){(Ft.active||wt)&&t.cancelable&&t.preventDefault()}),Ft.utils={on:h,off:f,css:R,find:b,is:function(t,e){return!!P(t,e,t,!1)},extend:function(t,e){if(t&&e)for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t},throttle:S,closest:P,toggleClass:k,clone:_,index:j,nextTick:Kt,cancelNextTick:Wt,detectDirection:Pt,getChild:B},Ft.get=function(t){return t[K]},Ft.mount=function(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];(e=e[0].constructor===Array?e[0]:e).forEach(function(t){if(!t.prototype||!t.prototype.constructor)throw\\\"Sortable: Mounted plugin must be a constructor function, not \\\".concat({}.toString.call(t));t.utils&&(Ft.utils=I(I({},Ft.utils),t.utils)),W.mount(t)})},Ft.create=function(t,e){return new Ft(t,e)};var zt,Gt,Ut,qt,Vt,Zt,$t=[],Qt=!(Ft.version=\\\"1.15.2\\\");function Jt(){$t.forEach(function(t){clearInterval(t.pid)}),$t=[]}function te(){clearInterval(Zt)}var ee,ne=S(function(n,t,e,o){if(t.scroll){var i,r=(n.touches?n.touches[0]:n).clientX,a=(n.touches?n.touches[0]:n).clientY,l=t.scrollSensitivity,s=t.scrollSpeed,c=O(),u=!1;Gt!==e&&(Gt=e,Jt(),zt=t.scroll,i=t.scrollFn,!0===zt&&(zt=M(e,!0)));var d=0,h=zt;do{var f=h,p=X(f),g=p.top,m=p.bottom,v=p.left,b=p.right,y=p.width,w=p.height,E=void 0,D=void 0,S=f.scrollWidth,_=f.scrollHeight,C=R(f),T=f.scrollLeft,p=f.scrollTop,D=f===c?(E=y<S&&(\\\"auto\\\"===C.overflowX||\\\"scroll\\\"===C.overflowX||\\\"visible\\\"===C.overflowX),w<_&&(\\\"auto\\\"===C.overflowY||\\\"scroll\\\"===C.overflowY||\\\"visible\\\"===C.overflowY)):(E=y<S&&(\\\"auto\\\"===C.overflowX||\\\"scroll\\\"===C.overflowX),w<_&&(\\\"auto\\\"===C.overflowY||\\\"scroll\\\"===C.overflowY)),T=E&&(Math.abs(b-r)<=l&&T+y<S)-(Math.abs(v-r)<=l&&!!T),p=D&&(Math.abs(m-a)<=l&&p+w<_)-(Math.abs(g-a)<=l&&!!p);if(!$t[d])for(var x=0;x<=d;x++)$t[x]||($t[x]={});$t[d].vx==T&&$t[d].vy==p&&$t[d].el===f||($t[d].el=f,$t[d].vx=T,$t[d].vy=p,clearInterval($t[d].pid),0==T&&0==p||(u=!0,$t[d].pid=setInterval(function(){o&&0===this.layer&&Ft.active._onTouchMove(Vt);var t=$t[this.layer].vy?$t[this.layer].vy*s:0,e=$t[this.layer].vx?$t[this.layer].vx*s:0;\\\"function\\\"==typeof i&&\\\"continue\\\"!==i.call(Ft.dragged.parentNode[K],e,t,n,Vt,$t[this.layer].el)||H($t[this.layer].el,e,t)}.bind({layer:d}),24))),d++}while(t.bubbleScroll&&h!==c&&(h=M(h,!1)));Qt=u}},30),c=function(t){var e=t.originalEvent,n=t.putSortable,o=t.dragEl,i=t.activeSortable,r=t.dispatchSortableEvent,a=t.hideGhostForTarget,t=t.unhideGhostForTarget;e&&(i=n||i,a(),e=e.changedTouches&&e.changedTouches.length?e.changedTouches[0]:e,e=document.elementFromPoint(e.clientX,e.clientY),t(),i&&!i.el.contains(e)&&(r(\\\"spill\\\"),this.onSpill({dragEl:o,putSortable:n})))};function oe(){}function ie(){}oe.prototype={startIndex:null,dragStart:function(t){t=t.oldDraggableIndex;this.startIndex=t},onSpill:function(t){var e=t.dragEl,n=t.putSortable;this.sortable.captureAnimationState(),n&&n.captureAnimationState();t=B(this.sortable.el,this.startIndex,this.options);t?this.sortable.el.insertBefore(e,t):this.sortable.el.appendChild(e),this.sortable.animateAll(),n&&n.animateAll()},drop:c},a(oe,{pluginName:\\\"revertOnSpill\\\"}),ie.prototype={onSpill:function(t){var e=t.dragEl,t=t.putSortable||this.sortable;t.captureAnimationState(),e.parentNode&&e.parentNode.removeChild(e),t.animateAll()},drop:c},a(ie,{pluginName:\\\"removeOnSpill\\\"});var re,ae,le,se,ce,ue=[],de=[],he=!1,fe=!1,pe=!1;function ge(n,o){de.forEach(function(t,e){e=o.children[t.sortableIndex+(n?Number(e):0)];e?o.insertBefore(t,e):o.appendChild(t)})}function me(){ue.forEach(function(t){t!==le&&t.parentNode&&t.parentNode.removeChild(t)})}return Ft.mount(new function(){function t(){for(var t in this.defaults={scroll:!0,forceAutoScrollFallback:!1,scrollSensitivity:30,scrollSpeed:10,bubbleScroll:!0},this)\\\"_\\\"===t.charAt(0)&&\\\"function\\\"==typeof this[t]&&(this[t]=this[t].bind(this))}return t.prototype={dragStarted:function(t){t=t.originalEvent;this.sortable.nativeDraggable?h(document,\\\"dragover\\\",this._handleAutoScroll):this.options.supportPointer?h(document,\\\"pointermove\\\",this._handleFallbackAutoScroll):t.touches?h(document,\\\"touchmove\\\",this._handleFallbackAutoScroll):h(document,\\\"mousemove\\\",this._handleFallbackAutoScroll)},dragOverCompleted:function(t){t=t.originalEvent;this.options.dragOverBubble||t.rootEl||this._handleAutoScroll(t)},drop:function(){this.sortable.nativeDraggable?f(document,\\\"dragover\\\",this._handleAutoScroll):(f(document,\\\"pointermove\\\",this._handleFallbackAutoScroll),f(document,\\\"touchmove\\\",this._handleFallbackAutoScroll),f(document,\\\"mousemove\\\",this._handleFallbackAutoScroll)),te(),Jt(),clearTimeout(g),g=void 0},nulling:function(){Vt=Gt=zt=Qt=Zt=Ut=qt=null,$t.length=0},_handleFallbackAutoScroll:function(t){this._handleAutoScroll(t,!0)},_handleAutoScroll:function(e,n){var o,i=this,r=(e.touches?e.touches[0]:e).clientX,a=(e.touches?e.touches[0]:e).clientY,t=document.elementFromPoint(r,a);Vt=e,n||this.options.forceAutoScrollFallback||w||y||u?(ne(e,this.options,t,n),o=M(t,!0),!Qt||Zt&&r===Ut&&a===qt||(Zt&&te(),Zt=setInterval(function(){var t=M(document.elementFromPoint(r,a),!0);t!==o&&(o=t,Jt()),ne(e,i.options,t,n)},10),Ut=r,qt=a)):this.options.bubbleScroll&&M(t,!0)!==O()?ne(e,this.options,M(t,!1),!1):Jt()}},a(t,{pluginName:\\\"scroll\\\",initializeByDefault:!0})}),Ft.mount(ie,oe),Ft.mount(new function(){function t(){this.defaults={swapClass:\\\"sortable-swap-highlight\\\"}}return t.prototype={dragStart:function(t){t=t.dragEl;ee=t},dragOverValid:function(t){var e=t.completed,n=t.target,o=t.onMove,i=t.activeSortable,r=t.changed,a=t.cancel;i.options.swap&&(t=this.sortable.el,i=this.options,n&&n!==t&&(t=ee,ee=!1!==o(n)?(k(n,i.swapClass,!0),n):null,t&&t!==ee&&k(t,i.swapClass,!1)),r(),e(!0),a())},drop:function(t){var e,n,o=t.activeSortable,i=t.putSortable,r=t.dragEl,a=i||this.sortable,l=this.options;ee&&k(ee,l.swapClass,!1),ee&&(l.swap||i&&i.options.swap)&&r!==ee&&(a.captureAnimationState(),a!==o&&o.captureAnimationState(),n=ee,t=(e=r).parentNode,l=n.parentNode,t&&l&&!t.isEqualNode(n)&&!l.isEqualNode(e)&&(i=j(e),r=j(n),t.isEqualNode(l)&&i<r&&r++,t.insertBefore(n,t.children[i]),l.insertBefore(e,l.children[r])),a.animateAll(),a!==o&&o.animateAll())},nulling:function(){ee=null}},a(t,{pluginName:\\\"swap\\\",eventProperties:function(){return{swapItem:ee}}})}),Ft.mount(new function(){function t(o){for(var t in this)\\\"_\\\"===t.charAt(0)&&\\\"function\\\"==typeof this[t]&&(this[t]=this[t].bind(this));o.options.avoidImplicitDeselect||(o.options.supportPointer?h(document,\\\"pointerup\\\",this._deselectMultiDrag):(h(document,\\\"mouseup\\\",this._deselectMultiDrag),h(document,\\\"touchend\\\",this._deselectMultiDrag))),h(document,\\\"keydown\\\",this._checkKeyDown),h(document,\\\"keyup\\\",this._checkKeyUp),this.defaults={selectedClass:\\\"sortable-selected\\\",multiDragKey:null,avoidImplicitDeselect:!1,setData:function(t,e){var n=\\\"\\\";ue.length&&ae===o?ue.forEach(function(t,e){n+=(e?\\\", \\\":\\\"\\\")+t.textContent}):n=e.textContent,t.setData(\\\"Text\\\",n)}}}return t.prototype={multiDragKeyDown:!1,isMultiDrag:!1,delayStartGlobal:function(t){t=t.dragEl;le=t},delayEnded:function(){this.isMultiDrag=~ue.indexOf(le)},setupClone:function(t){var e=t.sortable,t=t.cancel;if(this.isMultiDrag){for(var n=0;n<ue.length;n++)de.push(_(ue[n])),de[n].sortableIndex=ue[n].sortableIndex,de[n].draggable=!1,de[n].style[\\\"will-change\\\"]=\\\"\\\",k(de[n],this.options.selectedClass,!1),ue[n]===le&&k(de[n],this.options.chosenClass,!1);e._hideClone(),t()}},clone:function(t){var e=t.sortable,n=t.rootEl,o=t.dispatchSortableEvent,t=t.cancel;this.isMultiDrag&&(this.options.removeCloneOnHide||ue.length&&ae===e&&(ge(!0,n),o(\\\"clone\\\"),t()))},showClone:function(t){var e=t.cloneNowShown,n=t.rootEl,t=t.cancel;this.isMultiDrag&&(ge(!1,n),de.forEach(function(t){R(t,\\\"display\\\",\\\"\\\")}),e(),ce=!1,t())},hideClone:function(t){var e=this,n=(t.sortable,t.cloneNowHidden),t=t.cancel;this.isMultiDrag&&(de.forEach(function(t){R(t,\\\"display\\\",\\\"none\\\"),e.options.removeCloneOnHide&&t.parentNode&&t.parentNode.removeChild(t)}),n(),ce=!0,t())},dragStartGlobal:function(t){t.sortable;!this.isMultiDrag&&ae&&ae.multiDrag._deselectMultiDrag(),ue.forEach(function(t){t.sortableIndex=j(t)}),ue=ue.sort(function(t,e){return t.sortableIndex-e.sortableIndex}),pe=!0},dragStarted:function(t){var e,n=this,t=t.sortable;this.isMultiDrag&&(this.options.sort&&(t.captureAnimationState(),this.options.animation&&(ue.forEach(function(t){t!==le&&R(t,\\\"position\\\",\\\"absolute\\\")}),e=X(le,!1,!0,!0),ue.forEach(function(t){t!==le&&C(t,e)}),he=fe=!0)),t.animateAll(function(){he=fe=!1,n.options.animation&&ue.forEach(function(t){T(t)}),n.options.sort&&me()}))},dragOver:function(t){var e=t.target,n=t.completed,t=t.cancel;fe&&~ue.indexOf(e)&&(n(!1),t())},revert:function(t){var n,o,e=t.fromSortable,i=t.rootEl,r=t.sortable,a=t.dragRect;1<ue.length&&(ue.forEach(function(t){r.addAnimationState({target:t,rect:fe?X(t):a}),T(t),t.fromRect=a,e.removeAnimationState(t)}),fe=!1,n=!this.options.removeCloneOnHide,o=i,ue.forEach(function(t,e){e=o.children[t.sortableIndex+(n?Number(e):0)];e?o.insertBefore(t,e):o.appendChild(t)}))},dragOverCompleted:function(t){var e,n=t.sortable,o=t.isOwner,i=t.insertion,r=t.activeSortable,a=t.parentEl,l=t.putSortable,t=this.options;i&&(o&&r._hideClone(),he=!1,t.animation&&1<ue.length&&(fe||!o&&!r.options.sort&&!l)&&(e=X(le,!1,!0,!0),ue.forEach(function(t){t!==le&&(C(t,e),a.appendChild(t))}),fe=!0),o||(fe||me(),1<ue.length?(o=ce,r._showClone(n),r.options.animation&&!ce&&o&&de.forEach(function(t){r.addAnimationState({target:t,rect:se}),t.fromRect=se,t.thisAnimationDuration=null})):r._showClone(n)))},dragOverAnimationCapture:function(t){var e=t.dragRect,n=t.isOwner,t=t.activeSortable;ue.forEach(function(t){t.thisAnimationDuration=null}),t.options.animation&&!n&&t.multiDrag.isMultiDrag&&(se=a({},e),e=v(le,!0),se.top-=e.f,se.left-=e.e)},dragOverAnimationComplete:function(){fe&&(fe=!1,me())},drop:function(t){var e=t.originalEvent,n=t.rootEl,o=t.parentEl,i=t.sortable,r=t.dispatchSortableEvent,a=t.oldIndex,l=t.putSortable,s=l||this.sortable;if(e){var c,u,d,h=this.options,f=o.children;if(!pe)if(h.multiDragKey&&!this.multiDragKeyDown&&this._deselectMultiDrag(),k(le,h.selectedClass,!~ue.indexOf(le)),~ue.indexOf(le))ue.splice(ue.indexOf(le),1),re=null,z({sortable:i,rootEl:n,name:\\\"deselect\\\",targetEl:le,originalEvent:e});else{if(ue.push(le),z({sortable:i,rootEl:n,name:\\\"select\\\",targetEl:le,originalEvent:e}),e.shiftKey&&re&&i.el.contains(re)){var p=j(re),t=j(le);if(~p&&~t&&p!==t)for(var g,m=p<t?(g=p,t):(g=t,p+1);g<m;g++)~ue.indexOf(f[g])||(k(f[g],h.selectedClass,!0),ue.push(f[g]),z({sortable:i,rootEl:n,name:\\\"select\\\",targetEl:f[g],originalEvent:e}))}else re=le;ae=s}pe&&this.isMultiDrag&&(fe=!1,(o[K].options.sort||o!==n)&&1<ue.length&&(c=X(le),u=j(le,\\\":not(.\\\"+this.options.selectedClass+\\\")\\\"),!he&&h.animation&&(le.thisAnimationDuration=null),s.captureAnimationState(),he||(h.animation&&(le.fromRect=c,ue.forEach(function(t){var e;t.thisAnimationDuration=null,t!==le&&(e=fe?X(t):c,t.fromRect=e,s.addAnimationState({target:t,rect:e}))})),me(),ue.forEach(function(t){f[u]?o.insertBefore(t,f[u]):o.appendChild(t),u++}),a===j(le)&&(d=!1,ue.forEach(function(t){t.sortableIndex!==j(t)&&(d=!0)}),d&&(r(\\\"update\\\"),r(\\\"sort\\\")))),ue.forEach(function(t){T(t)}),s.animateAll()),ae=s),(n===o||l&&\\\"clone\\\"!==l.lastPutMode)&&de.forEach(function(t){t.parentNode&&t.parentNode.removeChild(t)})}},nullingGlobal:function(){this.isMultiDrag=pe=!1,de.length=0},destroyGlobal:function(){this._deselectMultiDrag(),f(document,\\\"pointerup\\\",this._deselectMultiDrag),f(document,\\\"mouseup\\\",this._deselectMultiDrag),f(document,\\\"touchend\\\",this._deselectMultiDrag),f(document,\\\"keydown\\\",this._checkKeyDown),f(document,\\\"keyup\\\",this._checkKeyUp)},_deselectMultiDrag:function(t){if(!(void 0!==pe&&pe||ae!==this.sortable||t&&P(t.target,this.options.draggable,this.sortable.el,!1)||t&&0!==t.button))for(;ue.length;){var e=ue[0];k(e,this.options.selectedClass,!1),ue.shift(),z({sortable:this.sortable,rootEl:this.sortable.el,name:\\\"deselect\\\",targetEl:e,originalEvent:t})}},_checkKeyDown:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!0)},_checkKeyUp:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!1)}},a(t,{pluginName:\\\"multiDrag\\\",utils:{select:function(t){var e=t.parentNode[K];e&&e.options.multiDrag&&!~ue.indexOf(t)&&(ae&&ae!==e&&(ae.multiDrag._deselectMultiDrag(),ae=e),k(t,e.options.selectedClass,!0),ue.push(t))},deselect:function(t){var e=t.parentNode[K],n=ue.indexOf(t);e&&e.options.multiDrag&&~n&&(k(t,e.options.selectedClass,!1),ue.splice(n,1))}},eventProperties:function(){var n=this,o=[],i=[];return ue.forEach(function(t){var e;o.push({multiDragElement:t,index:t.sortableIndex}),e=fe&&t!==le?-1:fe?j(t,\\\":not(.\\\"+n.options.selectedClass+\\\")\\\"):j(t),i.push({multiDragElement:t,index:e})}),{items:r(ue),clones:[].concat(de),oldIndicies:o,newIndicies:i}},optionListeners:{multiDragKey:function(t){return\\\"ctrl\\\"===(t=t.toLowerCase())?t=\\\"Control\\\":1<t.length&&(t=t.charAt(0).toUpperCase()+t.substr(1)),t}}})}),Ft});\"\n","import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, copyFileSync, readdirSync } from \"fs\"\nimport { resolve } from \"path\"\nimport type { IncomingMessage, ServerResponse } from \"http\"\nimport { mutateAgentxConfig } from \"./config-mutate\"\nimport { TokenStore } from \"./token-store\"\nimport { loadDaemonConfig } from \"./config\"\nimport { listAgentFiles, readAgentFile, writeAgentFile, createAgentSkill, deleteAgentSkill } from \"./file-ops\"\nimport { getWhatsAppState } from \"./whatsapp-state\"\nimport type { TopbarPeer } from \"./topbar\"\nimport { renderAdminPage } from \"./ui/pages/admin\"\n\n// --- /admin panel: form-driven management for agents, channels, crons ---\n//\n// Built for the business operator who opened the dashboard and wants to\n// change something without editing JSON. Shares the same safe-write machinery\n// as `agentx board add` and the setup wizard.\n//\n// Tabs:\n// - Agents: list, add, delete\n// - Channels: Telegram accounts (add, toggle, delete)\n// - Crons: schedules (add with plain-English timing, delete)\n// - Advanced: raw agentx.json editor with validate + backup-on-save\n\n// ========================================================================\n// HTTP entry points\n// ========================================================================\n\nexport function handleAdminGet(_req: IncomingMessage, res: ServerResponse, peers: TopbarPeer[] = [], localToken?: string): void {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderAdminPage({ peers, localToken }))\n}\n\nexport async function handleAdminConfigGet(_req: IncomingMessage, res: ServerResponse): Promise<void> {\n const cfg = readConfigRaw()\n res.writeHead(200, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(cfg))\n}\n\nexport async function handleAdminApi(req: IncomingMessage, res: ServerResponse, path: string): Promise<void> {\n try {\n // Non-JSON carve-out: the WhatsApp QR rendered as SVG. Proxies to the\n // daemon process (different memory space) where the adapter actually\n // emits the QR.\n if (req.method === \"GET\" && path === \"/api/admin/channels/whatsapp/qr.svg\") {\n await proxyDaemonSvg(res, \"/whatsapp/qr.svg\")\n return\n }\n const body = req.method === \"GET\" ? undefined : await readJsonBody(req)\n const dispatch: Record<string, () => unknown> = {\n \"GET /api/admin/state\": () => getAdminState(),\n \"POST /api/admin/agents\": () => addAgent(body),\n \"PATCH /api/admin/agents\": () => editAgent(body),\n \"DELETE /api/admin/agents\": () => deleteAgent(body),\n \"POST /api/admin/agents/access\": () => setAgentAccess(body),\n \"POST /api/admin/channels/telegram\": () => addTelegramAccount(body),\n \"DELETE /api/admin/channels/telegram\": () => deleteTelegramAccount(body),\n \"POST /api/admin/channels/telegram/toggle\": () => toggleTelegram(body),\n \"POST /api/admin/crons\": () => addCron(body),\n \"DELETE /api/admin/crons\": () => deleteCron(body),\n \"POST /api/admin/config/raw\": () => replaceConfigRaw(body),\n \"GET /api/admin/tokens\": () => listTokens(),\n \"POST /api/admin/tokens\": () => createToken(body),\n \"DELETE /api/admin/tokens\": () => revokeToken(body),\n \"POST /api/admin/agents/test\": () => testDriveAgent(body),\n \"POST /api/admin/agents/integrations\": () => addIntegration(body),\n \"DELETE /api/admin/agents/integrations\": () => removeIntegration(body),\n \"PATCH /api/admin/agents/integrations\": () => editIntegration(body),\n \"PATCH /api/admin/channels/telegram\": () => editTelegramAccount(body),\n \"POST /api/admin/channels/slack\": () => configureSlack(body),\n \"POST /api/admin/channels/slack/toggle\": () => toggleSlack(body),\n \"POST /api/admin/channels/discord\": () => configureDiscord(body),\n \"POST /api/admin/channels/discord/toggle\": () => toggleDiscord(body),\n \"POST /api/admin/channels/gitlab\": () => configureGitLab(body),\n \"POST /api/admin/channels/gitlab/toggle\": () => toggleGitLab(body),\n \"GET /api/admin/channels/whatsapp/state\": () => proxyDaemonJson(\"/whatsapp/state\"),\n // WebRTC bot history — active calls + ring buffer of recently-completed\n // sessions. Lives on the daemon (BotManager owns it); the dashboard\n // proxies the read-only view here.\n \"GET /api/admin/channels/webrtc/history\": () => proxyDaemonJson(\"/webrtc/history\"),\n // Action registry — list/upsert/delete + on-demand run.\n \"POST /api/admin/actions\": () => upsertAction(body),\n \"DELETE /api/admin/actions\": () => deleteAction(body),\n \"POST /api/admin/actions/run\": () => runActionFromAdmin(body),\n \"GET /api/admin/channels/whatsapp/chats\": () => proxyDaemonJson(\"/whatsapp/chats\"),\n \"GET /api/admin/channels/whatsapp/contacts\": () => proxyDaemonJson(\"/whatsapp/contacts\"),\n \"POST /api/admin/channels/whatsapp/ingest\": () => proxyDaemonPostJson(\"/whatsapp/ingest\", body),\n \"POST /api/admin/crons/preview\": () => previewCron(body),\n \"POST /api/admin/webhooks\": () => addWebhook(body),\n \"PATCH /api/admin/webhooks\": () => editWebhook(body),\n \"DELETE /api/admin/webhooks\": () => deleteWebhook(body),\n \"POST /api/admin/mesh/peers\": () => addMeshPeer(body),\n \"DELETE /api/admin/mesh/peers\": () => deleteMeshPeer(body),\n \"POST /api/admin/mesh/toggle\": () => toggleMesh(body),\n \"GET /api/admin/files\": () => listFilesForAgent(req),\n \"GET /api/admin/files/read\": () => readFileForAgent(req),\n \"PUT /api/admin/files\": () => writeFileForAgent(body),\n \"POST /api/admin/files/skill\": () => addSkillForAgent(body),\n \"DELETE /api/admin/files/skill\": () => removeSkillForAgent(body),\n // Business layer — orgChart / projects / contactMap (mirrors `agentx business`).\n \"POST /api/admin/business/orgchart\": () => upsertOrgEntry(body),\n \"DELETE /api/admin/business/orgchart\": () => deleteOrgEntry(body),\n \"POST /api/admin/business/project\": () => upsertProject(body),\n \"DELETE /api/admin/business/project\": () => deleteProject(body),\n \"POST /api/admin/business/client\": () => upsertClient(body),\n \"DELETE /api/admin/business/client\": () => deleteClient(body),\n \"POST /api/admin/business/contact\": () => upsertContact(body),\n \"DELETE /api/admin/business/contact\": () => deleteContact(body),\n // Boards — mirrors `agentx board` + `agentx board column` CLI.\n \"POST /api/admin/boards\": () => upsertBoard(body),\n \"DELETE /api/admin/boards\": () => deleteBoard(body),\n \"POST /api/admin/boards/columns\": () => upsertBoardColumn(body),\n \"DELETE /api/admin/boards/columns\":() => deleteBoardColumn(body),\n // Notifications — single mutation endpoint that takes a partial body\n // (destination?, on?, longTaskThreshold?). Mirrors `agentx notifications`.\n \"POST /api/admin/notifications\": () => updateNotifications(body),\n // Webhook triggers + defaultWorkflow editor (in addition to existing\n // /api/admin/webhooks add/edit/delete).\n \"POST /api/admin/webhooks/triggers\": () => updateWebhookTriggers(body),\n // Mesh health-check cadence — mirrors `agentx mesh health`.\n \"POST /api/admin/mesh/health\": () => updateMeshHealth(body),\n // Projects aggregator — read-side join over project rules,\n // workflows, channel routes, contacts. Implementation lives on\n // the daemon (needs ProjectRulesStore + workflowStore); the\n // dashboard simply forwards.\n \"GET /api/admin/projects\": () => proxyDaemonJson(\"/api/admin/projects\"),\n // Projects mutations — link/unlink a workflow to/from a project\n // by toggling its top-level `project:` field. Same forwarder\n // pattern; daemon owns the on-disk write.\n \"POST /api/admin/projects/workflows/link\": () => proxyDaemonPostJson(\"/api/admin/projects/workflows/link\", body),\n \"POST /api/admin/projects/workflows/unlink\": () => proxyDaemonPostJson(\"/api/admin/projects/workflows/unlink\", body),\n // Header (kind/displayName/homeUrl/runbook/agent), contact list,\n // create, delete — all daemon-side mutations on the rule YAML.\n \"POST /api/admin/projects/header\": () => proxyDaemonPostJson(\"/api/admin/projects/header\", body),\n \"POST /api/admin/projects/clauses\": () => proxyDaemonPostJson(\"/api/admin/projects/clauses\", body),\n \"POST /api/admin/projects/contacts/link\": () => proxyDaemonPostJson(\"/api/admin/projects/contacts/link\", body),\n \"POST /api/admin/projects/contacts/unlink\": () => proxyDaemonPostJson(\"/api/admin/projects/contacts/unlink\", body),\n \"POST /api/admin/projects/create\": () => proxyDaemonPostJson(\"/api/admin/projects/create\", body),\n \"POST /api/admin/projects/delete\": () => proxyDaemonPostJson(\"/api/admin/projects/delete\", body),\n }\n const key = `${req.method} ${path}`\n const handler = dispatch[key]\n if (!handler) { sendJson(res, 404, { error: `unknown admin endpoint: ${key}` }); return }\n const result = await handler()\n sendJson(res, 200, result)\n } catch (e: any) {\n sendJson(res, 400, { error: e.message || \"admin op failed\" })\n }\n}\n\nfunction sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(data))\n}\n\nfunction readJsonBody(req: IncomingMessage): Promise<any> {\n return new Promise((ok, err) => {\n let raw = \"\"\n req.on(\"data\", (c) => { raw += c })\n req.on(\"end\", () => { try { ok(raw ? JSON.parse(raw) : {}) } catch (e) { err(e) } })\n req.on(\"error\", err)\n })\n}\n\n// ========================================================================\n// Read helpers — /api/admin/state returns everything the panel needs in one call\n// ========================================================================\n\nfunction readJsonDir(relPath: string): any[] {\n const dir = resolve(process.cwd(), relPath)\n if (!existsSync(dir)) return []\n const out: any[] = []\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isFile() || !entry.name.endsWith(\".json\")) continue\n try {\n out.push(JSON.parse(readFileSync(resolve(dir, entry.name), \"utf-8\")))\n } catch {\n // skip malformed\n }\n }\n return out\n}\n\nfunction readConfigRaw(): any {\n const file = resolve(process.cwd(), \"agentx.json\")\n if (!existsSync(file)) return null\n return JSON.parse(readFileSync(file, \"utf-8\"))\n}\n\nfunction getAdminState() {\n const cfg = readConfigRaw()\n if (!cfg) return { exists: false }\n const agents = Object.entries(cfg.agents || {}).map(([id, a]: [string, any]) => ({\n id,\n name: a.name,\n tier: a.tier,\n model: a.model,\n mentions: a.mentions || [],\n workspace: a.workspace,\n access: a.access || \"private\",\n // Edit-form fields — cheap to include, saves a second round-trip.\n systemPrompt: a.systemPrompt || \"\",\n maxConcurrent: a.maxConcurrent ?? 1,\n maxExecutionMinutes: a.maxExecutionMinutes ?? 20,\n permissionMode: a.permissionMode || \"default\",\n // Per-agent integrations registry. Each entry: { kind, label, credentials, metadata, enabled }.\n // Credentials carry env-var REFS (uppercase identifier) — not values.\n // The integration list endpoint also resolves env-set status server-side\n // so the UI can render ✓/✗ without leaking secrets.\n integrations: (Array.isArray(a.integrations) ? a.integrations : []).map((i: any) => {\n const creds = i.credentials || {}\n const credStatus: Record<string, \"set\" | \"unset\" | \"literal\"> = {}\n for (const [k, v] of Object.entries(creds)) {\n if (typeof v === \"string\" && /^[A-Z][A-Z0-9_]*$/.test(v)) {\n credStatus[k] = process.env[v] ? \"set\" : \"unset\"\n } else if (typeof v === \"string\") {\n credStatus[k] = \"literal\"\n }\n }\n return {\n kind: i.kind,\n label: i.label,\n enabled: i.enabled !== false,\n credentials: creds,\n credStatus,\n metadata: i.metadata || {},\n }\n }),\n }))\n const telegramAccounts = cfg.channels?.telegram?.accounts || {}\n const telegram = {\n enabled: !!cfg.channels?.telegram?.enabled,\n accounts: Object.entries(telegramAccounts).map(([id, acc]: [string, any]) => ({\n id,\n botUsername: acc.botUsername || \"\",\n agentBinding: acc.agentBinding || \"\",\n botTokenRef: acc.token || \"\",\n })),\n }\n const slack = {\n enabled: !!cfg.channels?.slack?.enabled,\n botTokenRef: cfg.channels?.slack?.botToken || \"\",\n appTokenRef: cfg.channels?.slack?.appToken || \"\",\n agentBinding: cfg.channels?.slack?.agentBinding || \"\",\n }\n const discord = {\n enabled: !!cfg.channels?.discord?.enabled,\n tokenRef: cfg.channels?.discord?.token || \"\",\n agentBinding: cfg.channels?.discord?.agentBinding || \"\",\n }\n const gitlab = {\n enabled: !!cfg.channels?.gitlab?.enabled,\n host: cfg.channels?.gitlab?.host || \"\",\n webhookPort: cfg.channels?.gitlab?.webhookPort || 18810,\n tokenRef: cfg.channels?.gitlab?.token || \"\",\n routeCount: Array.isArray(cfg.channels?.gitlab?.routes) ? cfg.channels.gitlab.routes.length : 0,\n agentMappingCount: Array.isArray(cfg.channels?.gitlab?.agentMappings) ? cfg.channels.gitlab.agentMappings.length : 0,\n }\n const whatsapp = {\n enabled: !!cfg.channels?.whatsapp?.enabled,\n sessionDir: cfg.channels?.whatsapp?.sessionDir || \".agentx/whatsapp-sessions\",\n routeCount: Array.isArray(cfg.channels?.whatsapp?.routes) ? cfg.channels.whatsapp.routes.length : 0,\n }\n const crons = Object.entries(cfg.crons || {}).map(([id, c]: [string, any]) => ({\n id,\n schedule: c.schedule,\n agent: c.agent,\n prompt: (c.prompt || \"\").slice(0, 200),\n enabled: c.enabled !== false,\n }))\n const webhooks = Array.isArray(cfg.webhooks) ? cfg.webhooks.map((w: any) => ({\n id: w.id,\n source: w.source,\n agentId: w.agentId,\n secretEnv: w.secretEnv || \"\",\n description: w.description || \"\",\n enabled: w.enabled !== false,\n // Trigger / defaultWorkflow routing — surfaced so the Webhooks tab\n // can render them inline without a second round-trip.\n triggers: (w.triggers && typeof w.triggers === \"object\") ? w.triggers : {},\n defaultWorkflow: w.defaultWorkflow || \"\",\n })) : []\n const mesh = {\n enabled: !!cfg.mesh?.enabled,\n discovery: cfg.mesh?.discovery || \"static\",\n peers: (cfg.mesh?.peers || []).map((p: any) => ({\n url: p.url,\n name: p.name,\n hasToken: !!p.token,\n })),\n healthCheck: {\n interval: cfg.mesh?.healthCheck?.interval ?? 60,\n timeout: cfg.mesh?.healthCheck?.timeout ?? 10,\n },\n }\n // Daemon URL — used by the admin UI to compose full webhook URLs for copy.\n const daemonUrl = cfg.dashboard?.daemonUrl || \"http://localhost:18800\"\n // Business layer — read straight off the config so the panel stays in\n // sync with `agentx business` CLI mutations.\n const businessCfg = (cfg.business || {}) as any\n const business = {\n enabled: !!businessCfg.enabled,\n timezone: businessCfg.timezone || \"UTC\",\n orgChart: businessCfg.orgChart || {},\n projects: Array.isArray(businessCfg.projects) ? businessCfg.projects : [],\n contactMap: Array.isArray(businessCfg.contactMap) ? businessCfg.contactMap : [],\n clients: businessCfg.clients && typeof businessCfg.clients === \"object\" ? businessCfg.clients : {},\n }\n // Notifications — destination + event toggles + long-task threshold.\n const notificationsCfg = (cfg.notifications || {}) as any\n const notifications = {\n longTaskThreshold: notificationsCfg.longTaskThreshold ?? 30,\n destination: notificationsCfg.destination || null,\n on: {\n taskComplete: notificationsCfg.on?.taskComplete !== false,\n taskError: notificationsCfg.on?.taskError !== false,\n taskQueued: !!notificationsCfg.on?.taskQueued,\n },\n }\n // Actions — load all registered actions for the Actions tab.\n let actions: Array<any> = []\n try {\n const items = readJsonDir(\".agentx/actions\")\n actions = items.filter((a: any) => a && typeof a === \"object\" && a.id).map((a: any) => ({\n id: a.id,\n title: a.title,\n kind: a.kind,\n description: a.description || \"\",\n inputs: Array.isArray(a.inputs) ? a.inputs : [],\n timeoutMs: a.timeoutMs ?? 30_000,\n // Surface the kind-specific summary fields the tab renders.\n command: a.kind === \"shell\" ? a.command : undefined,\n url: a.kind === \"http\" ? a.url : undefined,\n method: a.kind === \"http\" ? a.method : undefined,\n })).sort((a: any, b: any) => a.id.localeCompare(b.id))\n } catch { /* empty registry is fine */ }\n // Boards — list with column metadata so the admin tab can edit them.\n const boards = (Array.isArray(cfg.boards) ? cfg.boards : []).map((b: any) => ({\n id: b.id,\n name: b.name,\n source: b.source || { type: \"gitlab\", projects: [] },\n primaryToolLabel: b.primaryToolLabel || \"\",\n timeRangeDays: b.timeRangeDays ?? 30,\n closedWindowDays: b.closedWindowDays ?? 30,\n columns: Array.isArray(b.columns) ? b.columns : [],\n }))\n return { exists: true, agents, telegram, slack, discord, gitlab, whatsapp, crons, webhooks, mesh, daemonUrl, nodeName: cfg.node?.name, business, boards, notifications, actions }\n}\n\n// ========================================================================\n// Mutations\n// ========================================================================\n\nfunction setAgentAccess(body: any) {\n const id = String(body?.id || \"\").trim()\n const access = body?.access === \"public\" ? \"public\" : \"private\"\n if (!id) throw new Error(\"Agent id is required.\")\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.agents?.[id]) throw new Error(`Agent \"${id}\" not found.`)\n cfg.agents[id].access = access\n return `agent \"${id}\" is now ${access}`\n })\n return { summary }\n}\n\nfunction listTokens() {\n return new TokenStore().list().map((r) => ({\n id: r.id,\n name: r.name,\n prefix: r.prefix,\n scopes: r.scopes,\n createdAt: r.createdAt,\n expiresAt: r.expiresAt,\n revokedAt: r.revokedAt,\n lastUsedAt: r.lastUsedAt,\n }))\n}\n\nfunction createToken(body: any) {\n const name = String(body?.name || \"\").trim()\n const scopes = Array.isArray(body?.scopes) ? body.scopes.map(String) : []\n const expiresInDays = body?.expiresInDays ? Number(body.expiresInDays) : undefined\n const { token, record } = new TokenStore().create({ name, scopes, expiresInDays })\n // Return the secret in the body exactly once — the UI must surface it\n // immediately and then forget it (no re-display after refresh).\n return { summary: `created token \"${name}\"`, token, record }\n}\n\nfunction revokeToken(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"Token id is required.\")\n const rec = new TokenStore().revoke(id)\n if (!rec) throw new Error(`Token \"${id}\" not found.`)\n return { summary: `revoked token ${id} (${rec.name})` }\n}\n\n/**\n * Admin-only shortcut that sends a message to an agent and returns the full\n * reply. Used by the dashboard's \"Test drive\" chat modal so operators can\n * sanity-check a freshly configured agent before wiring a real channel.\n *\n * Routes through the daemon's /task endpoint (unchanged runtime semantics)\n * and labels the run with channel=\"test-drive\" so it's obvious in the task\n * history / dashboard. A stable chatId lets the agent's session carry\n * context across follow-up messages within one modal sitting.\n */\nasync function testDriveAgent(body: any) {\n const id = String(body?.agent || body?.id || \"\").trim()\n const message = String(body?.message || \"\").trim()\n const chatId = String(body?.chatId || \"\").trim() || `admin-${Date.now()}`\n if (!id) throw new Error(\"Agent id is required.\")\n if (!message) throw new Error(\"Message is required.\")\n\n // Resolve the primary daemon URL from config. Fall back to the local default.\n let daemonUrl = \"http://127.0.0.1:18800\"\n let daemonToken: string | undefined\n try {\n const cfg = loadDaemonConfig()\n daemonUrl = cfg.dashboard.daemonUrl.replace(/\\/+$/, \"\") || daemonUrl\n daemonToken = cfg.dashboard.token\n } catch { /* missing config shouldn't block test-drive */ }\n\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" }\n if (daemonToken) headers[\"Authorization\"] = `Bearer ${daemonToken}`\n\n const r = await fetch(`${daemonUrl}/task`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n agent: id,\n message,\n context: { channel: \"test-drive\", sender: \"admin\", chatId },\n }),\n })\n const text = await r.text()\n let parsed: any\n try { parsed = JSON.parse(text) } catch { parsed = { content: text } }\n if (!r.ok) {\n throw new Error(parsed?.error || `daemon HTTP ${r.status}`)\n }\n return { chatId, response: parsed }\n}\n\nfunction addAgent(body: any) {\n const id = String(body?.id || \"\").trim()\n const name = String(body?.name || \"\").trim()\n const tier = [\"claude-code\", \"codex-cli\", \"opencode\", \"sdk\", \"orchestrator\"].includes(body?.tier) ? body.tier : \"claude-code\"\n const mentions = normaliseMentions(body?.triggerWords)\n const model = body?.model ? String(body.model).trim() : undefined\n const personality = body?.personality ? String(body.personality).trim() : undefined\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(id)) throw new Error(\"Agent id must be lowercase (letters, digits, -, _).\")\n if (!name) throw new Error(\"Agent name is required.\")\n if (mentions.length === 0) throw new Error(\"At least one trigger word is required.\")\n\n const workspace = resolve(process.cwd(), \"agents\", id)\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.agents = cfg.agents || {}\n if (cfg.agents[id]) throw new Error(`Agent \"${id}\" already exists.`)\n const block: any = {\n name, workspace, tier, mentions, maxConcurrent: 2, permissionMode: \"default\",\n }\n if (model) block.model = model\n if (personality) block.systemPrompt = personality.slice(0, 400)\n cfg.agents[id] = block\n return `added agent \"${id}\"`\n })\n // Scaffold the workspace if it doesn't exist.\n const claudeMd = resolve(workspace, \"CLAUDE.md\")\n if (!existsSync(claudeMd)) {\n try {\n mkdirSync(workspace, { recursive: true })\n writeFileSync(claudeMd, `# ${name}\\n\\n${personality || \"You are \" + name + \".\"}\\n`)\n } catch { /* best-effort */ }\n }\n return { summary }\n}\n\nfunction editAgent(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"Agent id is required.\")\n const patch = body?.patch || {}\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.agents?.[id]) throw new Error(`Agent \"${id}\" not found.`)\n const a = cfg.agents[id]\n // Whitelist every field the admin UI is allowed to change. Anything outside\n // this list requires the raw JSON editor — keeps the simple form from\n // silently nuking nested config shapes.\n if (typeof patch.name === \"string\" && patch.name.trim()) a.name = patch.name.trim()\n if (typeof patch.model === \"string\") a.model = patch.model.trim() || undefined\n if (typeof patch.tier === \"string\" && [\"claude-code\", \"codex-cli\", \"opencode\", \"sdk\", \"orchestrator\"].includes(patch.tier)) a.tier = patch.tier\n if (typeof patch.systemPrompt === \"string\") a.systemPrompt = patch.systemPrompt.trim() || undefined\n if (Array.isArray(patch.mentions)) {\n a.mentions = patch.mentions.map((m: any) => String(m).trim()).filter(Boolean)\n } else if (typeof patch.triggerWords === \"string\") {\n a.mentions = normaliseMentions(patch.triggerWords)\n }\n if (typeof patch.maxConcurrent === \"number\" && patch.maxConcurrent >= 1) a.maxConcurrent = patch.maxConcurrent\n if (typeof patch.maxExecutionMinutes === \"number\" && patch.maxExecutionMinutes >= 1 && patch.maxExecutionMinutes <= 240) {\n a.maxExecutionMinutes = patch.maxExecutionMinutes\n }\n if (typeof patch.permissionMode === \"string\") a.permissionMode = patch.permissionMode\n if (patch.access === \"public\" || patch.access === \"private\") a.access = patch.access\n return `updated agent \"${id}\"`\n })\n return { summary }\n}\n\nfunction deleteAgent(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"Agent id is required.\")\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.agents || !cfg.agents[id]) throw new Error(`Agent \"${id}\" not found.`)\n delete cfg.agents[id]\n // Also clear any channel bindings pointing at this agent.\n if (cfg.channels?.telegram?.accounts) {\n for (const k of Object.keys(cfg.channels.telegram.accounts)) {\n if (cfg.channels.telegram.accounts[k]?.agentBinding === id) {\n cfg.channels.telegram.accounts[k].agentBinding = \"\"\n }\n }\n }\n return `removed agent \"${id}\"`\n })\n return { summary }\n}\n\nfunction addTelegramAccount(body: any) {\n const id = String(body?.id || \"\").trim()\n const agentBinding = String(body?.agentBinding || \"\").trim()\n const botUsername = String(body?.botUsername || \"\").trim()\n const botTokenEnv = String(body?.botTokenEnv || \"\").trim()\n if (!id) throw new Error(\"Account id is required.\")\n if (!agentBinding) throw new Error(\"Choose an agent to bind this account to.\")\n if (!botTokenEnv) throw new Error(\"Bot token env-var name is required (e.g. TG_SUPPORT_BOT_TOKEN).\")\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.channels = cfg.channels || {}\n cfg.channels.telegram = cfg.channels.telegram || { enabled: true, accounts: {}, policy: { dm: \"pair\", group: \"mention-required\" } }\n cfg.channels.telegram.enabled = true\n cfg.channels.telegram.accounts = cfg.channels.telegram.accounts || {}\n if (cfg.channels.telegram.accounts[id]) throw new Error(`Telegram account \"${id}\" already exists.`)\n if (!cfg.agents?.[agentBinding]) throw new Error(`Unknown agent \"${agentBinding}\".`)\n // Schema (telegramAccountSchema) keys are `token` + `agentBinding`. Zod\n // silently strips unknown fields, so writing `botToken` produced configs\n // that failed to validate with `token: Required` even after the operator\n // added the env var. `botUsername` is not in the schema; passing it does\n // nothing (kept here only because the form collects it — drop once the\n // form is updated to omit it).\n void botUsername\n cfg.channels.telegram.accounts[id] = {\n token: \"${\" + botTokenEnv + \"}\",\n agentBinding,\n }\n return `added telegram account \"${id}\"`\n })\n return { summary, hint: `Add ${botTokenEnv}=<bot-token> to your .env file.` }\n}\n\nfunction editTelegramAccount(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"Account id is required.\")\n const patch = body?.patch || {}\n const { summary } = mutateAgentxConfig((cfg) => {\n const acc = cfg.channels?.telegram?.accounts?.[id]\n if (!acc) throw new Error(`Account \"${id}\" not found.`)\n if (typeof patch.agentBinding === \"string\" && patch.agentBinding.trim()) {\n if (!cfg.agents?.[patch.agentBinding]) throw new Error(`Unknown agent \"${patch.agentBinding}\".`)\n acc.agentBinding = patch.agentBinding.trim()\n }\n if (typeof patch.botUsername === \"string\") acc.botUsername = patch.botUsername.trim()\n if (typeof patch.botTokenEnv === \"string\" && patch.botTokenEnv.trim()) {\n acc.token = \"${\" + patch.botTokenEnv.trim() + \"}\"\n }\n return `updated telegram account \"${id}\"`\n })\n return { summary }\n}\n\n/**\n * Forward a GET to the daemon and return its JSON body. Used for state\n * that lives in the daemon process (WhatsApp QR/connection, etc.) since the\n * board server is a separate process with separate memory.\n */\nasync function proxyDaemonJson(pathOnDaemon: string): Promise<any> {\n const { url, headers } = daemonTarget()\n const r = await fetch(url + pathOnDaemon, { headers })\n const text = await r.text()\n if (!r.ok) throw new Error(`daemon ${r.status}: ${text.slice(0, 200)}`)\n try { return JSON.parse(text) } catch { return { raw: text } }\n}\n\n/** POST variant — forwards a JSON body to a daemon endpoint and returns\n * the parsed JSON. Used to reach daemon-process-owned operations like\n * WhatsApp ingest from the dashboard process. */\nasync function proxyDaemonPostJson(pathOnDaemon: string, body: any): Promise<any> {\n const { url, headers } = daemonTarget()\n const r = await fetch(url + pathOnDaemon, {\n method: \"POST\",\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body || {}),\n })\n const text = await r.text()\n if (!r.ok) throw new Error(`daemon ${r.status}: ${text.slice(0, 200)}`)\n try { return JSON.parse(text) } catch { return { raw: text } }\n}\n\n/**\n * Proxy a non-JSON response (SVG, binary) from the daemon straight through.\n */\nasync function proxyDaemonSvg(res: ServerResponse, pathOnDaemon: string): Promise<void> {\n try {\n const { url, headers } = daemonTarget()\n const r = await fetch(url + pathOnDaemon, { headers })\n const body = await r.text()\n res.writeHead(r.status, {\n \"Content-Type\": r.headers.get(\"content-type\") || \"image/svg+xml\",\n \"Cache-Control\": \"no-store\",\n })\n res.end(body)\n } catch (e: any) {\n res.writeHead(502, { \"Content-Type\": \"text/plain; charset=utf-8\" })\n res.end(\"daemon unreachable: \" + (e?.message || \"unknown\"))\n }\n}\n\nfunction daemonTarget(): { url: string; headers: Record<string, string> } {\n let url = \"http://127.0.0.1:18800\"\n const headers: Record<string, string> = {}\n try {\n const cfg = loadDaemonConfig()\n url = cfg.dashboard.daemonUrl?.replace(/\\/+$/, \"\") || url\n if (cfg.dashboard.token) headers[\"Authorization\"] = `Bearer ${cfg.dashboard.token}`\n } catch { /* */ }\n return { url, headers }\n}\n\n// --- per-agent file operations ---------------------------------------------\n\nfunction workspaceFor(agentId: string): string {\n const cfg = readConfigRaw()\n const def = cfg?.agents?.[agentId]\n if (!def) throw new Error(`Unknown agent \"${agentId}\".`)\n if (!def.workspace) throw new Error(`Agent \"${agentId}\" has no workspace configured.`)\n return def.workspace\n}\n\nfunction queryParam(req: IncomingMessage, name: string): string {\n try {\n const url = new URL(req.url || \"/\", \"http://localhost\")\n return url.searchParams.get(name) || \"\"\n } catch { return \"\" }\n}\n\nfunction listFilesForAgent(req: IncomingMessage) {\n const agentId = queryParam(req, \"agent\").trim()\n if (!agentId) throw new Error(\"agent= query param is required.\")\n const overview = listAgentFiles(workspaceFor(agentId))\n return { agentId, ...overview }\n}\n\nfunction readFileForAgent(req: IncomingMessage) {\n const agentId = queryParam(req, \"agent\").trim()\n const path = queryParam(req, \"path\").trim()\n if (!agentId || !path) throw new Error(\"agent= and path= query params are required.\")\n return readAgentFile(workspaceFor(agentId), path)\n}\n\nfunction writeFileForAgent(body: any) {\n const agentId = String(body?.agent || \"\").trim()\n const path = String(body?.path || \"\").trim()\n const content = typeof body?.content === \"string\" ? body.content : \"\"\n if (!agentId || !path) throw new Error(\"agent and path are required.\")\n const r = writeAgentFile(workspaceFor(agentId), path, content)\n return { summary: `saved ${path} (${r.bytes} bytes)`, ...r }\n}\n\nfunction addSkillForAgent(body: any) {\n const agentId = String(body?.agent || \"\").trim()\n const slug = String(body?.slug || \"\").trim()\n const title = body?.title ? String(body.title) : undefined\n const content = body?.content ? String(body.content) : undefined\n if (!agentId) throw new Error(\"agent is required.\")\n const r = createAgentSkill(workspaceFor(agentId), slug, { title, content })\n return { summary: `added skill \"${slug}\"`, ...r }\n}\n\nfunction removeSkillForAgent(body: any) {\n const agentId = String(body?.agent || \"\").trim()\n const slug = String(body?.slug || \"\").trim()\n if (!agentId || !slug) throw new Error(\"agent and slug are required.\")\n const r = deleteAgentSkill(workspaceFor(agentId), slug)\n return { summary: `removed skill \"${slug}\"`, ...r }\n}\n\n// --- Per-agent integration registry (agents[].integrations) ---\n//\n// Mirrors the dashboard's Integrations tab. Keeps secrets out of the JSON —\n// only env-var refs and non-secret metadata are persisted. The CLI\n// (`agentx agent integrations`) and this admin panel converge on the same\n// mutator path through mutateAgentxConfig.\n\nfunction addIntegration(body: any) {\n const agentId = String(body?.agentId || \"\").trim()\n const kind = String(body?.kind || \"\").trim()\n const label = String(body?.label || \"\").trim()\n if (!agentId) throw new Error(\"agentId is required\")\n if (!kind) throw new Error(\"kind is required\")\n if (!label) throw new Error(\"label is required\")\n const credentials: Record<string, string> = {}\n const c = (body?.credentials || {}) as Record<string, unknown>\n for (const k of [\"tokenEnv\", \"privateAppTokenEnv\", \"apiKeyEnv\", \"refreshTokenEnv\", \"clientIdEnv\", \"clientSecretEnv\"]) {\n const v = c[k]\n if (typeof v === \"string\" && v.trim()) {\n if (!/^[A-Z][A-Z0-9_]*$/.test(v.trim())) {\n throw new Error(`${k} must be uppercase identifier (got \"${v}\")`)\n }\n credentials[k] = v.trim()\n }\n }\n if (c.auth === \"keyring\") credentials.auth = \"keyring\"\n if (typeof c.sessionDir === \"string\" && c.sessionDir.trim()) credentials.sessionDir = c.sessionDir.trim()\n if (Object.keys(credentials).length === 0) throw new Error(\"at least one credential field required\")\n const metadata: Record<string, string | number | boolean> = {}\n for (const [k, v] of Object.entries((body?.metadata || {}) as Record<string, unknown>)) {\n if (typeof v === \"string\" || typeof v === \"number\" || typeof v === \"boolean\") metadata[k] = v\n }\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.agents?.[agentId]) throw new Error(`Unknown agent \"${agentId}\"`)\n cfg.agents[agentId].integrations = Array.isArray(cfg.agents[agentId].integrations)\n ? cfg.agents[agentId].integrations\n : []\n if (cfg.agents[agentId].integrations.find((i: any) => i.kind === kind && i.label === label)) {\n throw new Error(`integration (${kind}, ${label}) already exists on ${agentId}`)\n }\n cfg.agents[agentId].integrations.push({ kind, label, credentials, metadata, enabled: true })\n return `added ${kind} integration \"${label}\" to ${agentId}`\n })\n return { summary }\n}\n\nfunction removeIntegration(body: any) {\n const agentId = String(body?.agentId || \"\").trim()\n const kind = String(body?.kind || \"\").trim()\n const label = String(body?.label || \"\").trim()\n if (!agentId) throw new Error(\"agentId is required\")\n if (!kind || !label) throw new Error(\"kind and label are required\")\n const { summary } = mutateAgentxConfig((cfg) => {\n const a = cfg.agents?.[agentId]\n if (!a) throw new Error(`Unknown agent \"${agentId}\"`)\n const list = (a.integrations as any[] | undefined) || []\n const idx = list.findIndex((i: any) => i.kind === kind && i.label === label)\n if (idx === -1) throw new Error(`integration (${kind}, ${label}) not found on ${agentId}`)\n list.splice(idx, 1)\n return `removed ${kind} integration \"${label}\" from ${agentId}`\n })\n return { summary }\n}\n\nfunction editIntegration(body: any) {\n const agentId = String(body?.agentId || \"\").trim()\n const kind = String(body?.kind || \"\").trim()\n const label = String(body?.label || \"\").trim()\n if (!agentId) throw new Error(\"agentId is required\")\n if (!kind || !label) throw new Error(\"kind and label are required\")\n const patch = body?.patch || {}\n const { summary } = mutateAgentxConfig((cfg) => {\n const a = cfg.agents?.[agentId]\n if (!a) throw new Error(`Unknown agent \"${agentId}\"`)\n const list = (a.integrations as any[] | undefined) || []\n const i = list.find((x: any) => x.kind === kind && x.label === label)\n if (!i) throw new Error(`integration (${kind}, ${label}) not found on ${agentId}`)\n if (typeof patch.enabled === \"boolean\") i.enabled = patch.enabled\n if (patch.metadata && typeof patch.metadata === \"object\") {\n i.metadata = { ...(i.metadata || {}), ...patch.metadata }\n }\n return `updated integration (${kind}, ${label}) on ${agentId}`\n })\n return { summary }\n}\n\nconst WEBHOOK_SOURCES = [\"gitlab\", \"github\", \"sentry\", \"stripe\", \"vercel\", \"odoo\", \"hubspot\", \"discord\", \"slack\", \"custom\"] as const\n\nfunction addWebhook(body: any) {\n const id = String(body?.id || \"\").trim()\n const source = String(body?.source || \"\").trim()\n const agentId = String(body?.agentId || \"\").trim()\n const secretEnv = String(body?.secretEnv || \"\").trim()\n const description = String(body?.description || \"\").trim()\n const node = String(body?.node || \"\").trim()\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(id)) throw new Error(\"Webhook id must be lowercase (letters, digits, -, _).\")\n if (!WEBHOOK_SOURCES.includes(source as any)) throw new Error(`Unknown source: ${source}`)\n if (!agentId) throw new Error(\"Pick an agent to bind the webhook to.\")\n const { summary } = mutateAgentxConfig((cfg) => {\n // Mesh-routed entries point at an agent on a remote peer; the local\n // daemon doesn't need to know about that agent. We still validate the\n // peer exists in mesh.peers so a typo doesn't silently 404 forever.\n if (node) {\n const peers = cfg.mesh?.peers || []\n if (!peers.find((p: any) => p.name === node)) {\n throw new Error(`Mesh peer \"${node}\" not found in mesh.peers. Add the peer first or remove the node field.`)\n }\n } else if (!cfg.agents?.[agentId]) {\n throw new Error(`Unknown agent \"${agentId}\". Add it locally, or set --node <peer> to forward to a mesh peer.`)\n }\n cfg.webhooks = Array.isArray(cfg.webhooks) ? cfg.webhooks : []\n if (cfg.webhooks.find((w: any) => w.id === id)) throw new Error(`Webhook \"${id}\" already exists.`)\n const entry: any = { id, source, agentId, enabled: true }\n if (secretEnv) entry.secretEnv = secretEnv\n if (description) entry.description = description\n if (node) entry.node = node\n cfg.webhooks.push(entry)\n return `added webhook \"${id}\" (${source} → ${agentId}${node ? ` @ ${node}` : \"\"})`\n })\n return { summary }\n}\n\nfunction editWebhook(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"Webhook id is required.\")\n const patch = body?.patch || {}\n const { summary } = mutateAgentxConfig((cfg) => {\n const w = (cfg.webhooks || []).find((x: any) => x.id === id)\n if (!w) throw new Error(`Webhook \"${id}\" not found.`)\n if (typeof patch.source === \"string\" && WEBHOOK_SOURCES.includes(patch.source as any)) w.source = patch.source\n if (typeof patch.agentId === \"string\" && patch.agentId.trim()) {\n if (!cfg.agents?.[patch.agentId]) throw new Error(`Unknown agent \"${patch.agentId}\".`)\n w.agentId = patch.agentId.trim()\n }\n if (typeof patch.secretEnv === \"string\") w.secretEnv = patch.secretEnv.trim() || undefined\n if (typeof patch.description === \"string\") w.description = patch.description.trim() || undefined\n if (typeof patch.enabled === \"boolean\") w.enabled = patch.enabled\n return `updated webhook \"${id}\"`\n })\n return { summary }\n}\n\nfunction addMeshPeer(body: any) {\n const url = String(body?.url || \"\").trim().replace(/\\/+$/, \"\")\n const name = String(body?.name || \"\").trim()\n const token = String(body?.token || \"\").trim() || undefined\n if (!url || !/^https?:\\/\\//.test(url)) throw new Error(\"Peer URL must start with http:// or https://\")\n if (!name) throw new Error(\"Peer name is required.\")\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.mesh = cfg.mesh || { enabled: true, peers: [], discovery: \"static\", healthCheck: { interval: 60, timeout: 10 } }\n cfg.mesh.enabled = true\n cfg.mesh.peers = Array.isArray(cfg.mesh.peers) ? cfg.mesh.peers : []\n if (cfg.mesh.peers.find((p: any) => p.url === url)) throw new Error(`Peer at ${url} already registered.`)\n const entry: any = { url, name }\n if (token) entry.token = token\n cfg.mesh.peers.push(entry)\n return `added mesh peer \"${name}\"`\n })\n return { summary }\n}\n\nfunction deleteMeshPeer(body: any) {\n const url = String(body?.url || \"\").trim().replace(/\\/+$/, \"\")\n if (!url) throw new Error(\"Peer URL is required.\")\n const { summary } = mutateAgentxConfig((cfg) => {\n const before = (cfg.mesh?.peers || []).length\n cfg.mesh = cfg.mesh || { enabled: false, peers: [], discovery: \"static\", healthCheck: { interval: 60, timeout: 10 } }\n cfg.mesh.peers = (cfg.mesh.peers || []).filter((p: any) => p.url.replace(/\\/+$/, \"\") !== url)\n if (cfg.mesh.peers.length === before) throw new Error(`Peer at ${url} not found.`)\n return `removed mesh peer ${url}`\n })\n return { summary }\n}\n\nfunction toggleMesh(body: any) {\n const enabled = !!body?.enabled\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.mesh = cfg.mesh || { enabled, peers: [], discovery: \"static\", healthCheck: { interval: 60, timeout: 10 } }\n cfg.mesh.enabled = enabled\n return `mesh ${enabled ? \"enabled\" : \"disabled\"}`\n })\n return { summary }\n}\n\nfunction deleteWebhook(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"Webhook id is required.\")\n const { summary } = mutateAgentxConfig((cfg) => {\n const before = (cfg.webhooks || []).length\n cfg.webhooks = (cfg.webhooks || []).filter((w: any) => w.id !== id)\n if (cfg.webhooks.length === before) throw new Error(`Webhook \"${id}\" not found.`)\n return `removed webhook \"${id}\"`\n })\n return { summary }\n}\n\nfunction configureSlack(body: any) {\n const botTokenEnv = String(body?.botTokenEnv || \"\").trim()\n const appTokenEnv = String(body?.appTokenEnv || \"\").trim()\n const agentBinding = String(body?.agentBinding || \"\").trim()\n if (!botTokenEnv) throw new Error(\"Bot token env-var name is required (e.g. SLACK_BOT_TOKEN).\")\n if (!appTokenEnv) throw new Error(\"App-level token env-var name is required (e.g. SLACK_APP_TOKEN).\")\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.channels = cfg.channels || {}\n cfg.channels.slack = cfg.channels.slack || { enabled: false }\n cfg.channels.slack.enabled = true\n cfg.channels.slack.botToken = \"${\" + botTokenEnv + \"}\"\n cfg.channels.slack.appToken = \"${\" + appTokenEnv + \"}\"\n if (agentBinding) {\n if (!cfg.agents?.[agentBinding]) throw new Error(`Unknown agent \"${agentBinding}\".`)\n cfg.channels.slack.agentBinding = agentBinding\n }\n return \"configured slack connector\"\n })\n return { summary, hint: `Add ${botTokenEnv}=xoxb-… and ${appTokenEnv}=xapp-… to your .env.` }\n}\n\nfunction toggleSlack(body: any) {\n const enabled = !!body?.enabled\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.channels = cfg.channels || {}\n cfg.channels.slack = cfg.channels.slack || { enabled: false }\n cfg.channels.slack.enabled = enabled\n return `slack ${enabled ? \"enabled\" : \"disabled\"}`\n })\n return { summary }\n}\n\nfunction configureDiscord(body: any) {\n const tokenEnv = String(body?.tokenEnv || \"\").trim()\n const agentBinding = String(body?.agentBinding || \"\").trim()\n if (!tokenEnv) throw new Error(\"Bot token env-var name is required (e.g. DISCORD_BOT_TOKEN).\")\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.channels = cfg.channels || {}\n cfg.channels.discord = cfg.channels.discord || { enabled: false }\n cfg.channels.discord.enabled = true\n cfg.channels.discord.token = \"${\" + tokenEnv + \"}\"\n if (agentBinding) {\n if (!cfg.agents?.[agentBinding]) throw new Error(`Unknown agent \"${agentBinding}\".`)\n cfg.channels.discord.agentBinding = agentBinding\n }\n return \"configured discord connector\"\n })\n return { summary, hint: `Add ${tokenEnv}=<bot-token> to your .env.` }\n}\n\nfunction toggleDiscord(body: any) {\n const enabled = !!body?.enabled\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.channels = cfg.channels || {}\n cfg.channels.discord = cfg.channels.discord || { enabled: false }\n cfg.channels.discord.enabled = enabled\n return `discord ${enabled ? \"enabled\" : \"disabled\"}`\n })\n return { summary }\n}\n\nfunction configureGitLab(body: any) {\n const host = String(body?.host || \"\").trim().replace(/\\/+$/, \"\")\n const tokenEnv = String(body?.tokenEnv || \"\").trim()\n const webhookPort = parseInt(String(body?.webhookPort || \"18810\"), 10)\n if (!host || !/^https?:\\/\\//.test(host)) throw new Error(\"Host must start with http:// or https://\")\n if (!tokenEnv) throw new Error(\"Admin token env-var name is required (e.g. GITLAB_TOKEN).\")\n if (!Number.isFinite(webhookPort) || webhookPort < 1 || webhookPort > 65535) throw new Error(\"Webhook port must be between 1 and 65535.\")\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.channels = cfg.channels || {}\n cfg.channels.gitlab = cfg.channels.gitlab || { enabled: false, routes: [], agentMappings: [] }\n cfg.channels.gitlab.enabled = true\n cfg.channels.gitlab.host = host\n cfg.channels.gitlab.token = \"${\" + tokenEnv + \"}\"\n cfg.channels.gitlab.webhookPort = webhookPort\n return \"configured gitlab connector\"\n })\n return { summary, hint: `Add ${tokenEnv}=<personal-access-token> to your .env. Per-project routes + per-agent tokens stay in the Advanced JSON editor.` }\n}\n\nfunction toggleGitLab(body: any) {\n const enabled = !!body?.enabled\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.channels = cfg.channels || {}\n cfg.channels.gitlab = cfg.channels.gitlab || { enabled: false, routes: [], agentMappings: [] }\n cfg.channels.gitlab.enabled = enabled\n return `gitlab ${enabled ? \"enabled\" : \"disabled\"}`\n })\n return { summary }\n}\n\nasync function previewCron(body: any) {\n const expr = String(body?.schedule || \"\").trim()\n if (!expr) throw new Error(\"schedule is required\")\n const { getNextCronDate } = await import(\"@/crons/scheduler\")\n let human: string | undefined\n try {\n const cronstrue = (await import(\"cronstrue\")).default\n human = cronstrue.toString(expr, { use24HourTimeFormat: false })\n } catch (e: any) {\n throw new Error(`Invalid cron: ${e.message || \"unknown\"}`)\n }\n const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || \"UTC\"\n const next: string[] = []\n let cursor = new Date()\n try {\n for (let i = 0; i < 3; i++) {\n cursor = getNextCronDate(expr, cursor, tz)\n next.push(cursor.toISOString())\n }\n } catch (e: any) {\n throw new Error(`Cron preview failed: ${e.message}`)\n }\n return { human, next, timezone: tz }\n}\n\nfunction deleteTelegramAccount(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"Account id is required.\")\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.channels?.telegram?.accounts?.[id]) throw new Error(`Account \"${id}\" not found.`)\n delete cfg.channels.telegram.accounts[id]\n return `removed telegram account \"${id}\"`\n })\n return { summary }\n}\n\nfunction toggleTelegram(body: any) {\n const enabled = !!body?.enabled\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.channels = cfg.channels || {}\n cfg.channels.telegram = cfg.channels.telegram || { enabled: false, accounts: {}, policy: { dm: \"pair\", group: \"mention-required\" } }\n cfg.channels.telegram.enabled = enabled\n return `telegram ${enabled ? \"enabled\" : \"disabled\"}`\n })\n return { summary }\n}\n\nfunction addCron(body: any) {\n const id = String(body?.id || \"\").trim()\n const schedule = String(body?.schedule || \"\").trim()\n const agent = String(body?.agent || \"\").trim()\n const prompt = String(body?.prompt || \"\").trim()\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(id)) throw new Error(\"Cron id must be lowercase (letters, digits, -, _).\")\n if (!schedule) throw new Error(\"Cron expression is required (e.g. '0 9 * * 1' for Mondays at 9am).\")\n if (!agent) throw new Error(\"Pick an agent to run this schedule.\")\n if (!prompt) throw new Error(\"A prompt is required (what should the agent do?).\")\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.crons = cfg.crons || {}\n if (cfg.crons[id]) throw new Error(`Cron \"${id}\" already exists.`)\n if (!cfg.agents?.[agent]) throw new Error(`Unknown agent \"${agent}\".`)\n cfg.crons[id] = { schedule, agent, prompt, enabled: true }\n return `added cron \"${id}\"`\n })\n return { summary }\n}\n\nfunction deleteCron(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"Cron id is required.\")\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.crons?.[id]) throw new Error(`Cron \"${id}\" not found.`)\n delete cfg.crons[id]\n return `removed cron \"${id}\"`\n })\n return { summary }\n}\n\nfunction replaceConfigRaw(body: any): { summary: string; backupPath?: string } {\n const raw = String(body?.raw || \"\")\n if (!raw.trim()) throw new Error(\"Config body is empty.\")\n let parsed: any\n try { parsed = JSON.parse(raw) }\n catch (e: any) { throw new Error(`Invalid JSON: ${e.message}`) }\n const file = resolve(process.cwd(), \"agentx.json\")\n const backupPath = `${file}.bak.${Date.now()}`\n if (existsSync(file)) {\n try { copyFileSync(file, backupPath) } catch { /* best-effort */ }\n }\n writeFileSync(file, JSON.stringify(parsed, null, 2) + \"\\n\", \"utf-8\")\n // Best-effort /reload; caller can also restart the daemon.\n try {\n const cfg = loadDaemonConfig()\n const url = cfg.dashboard.daemonUrl?.replace(/\\/+$/, \"\") || \"http://127.0.0.1:18800\"\n fetch(`${url}/reload`, { method: \"POST\" }).catch(() => null)\n } catch { /* daemon may not be running */ }\n return { summary: \"agentx.json replaced\", backupPath }\n}\n\nfunction normaliseMentions(s: any): string[] {\n return String(s || \"\")\n .split(/[,\\s]+/)\n .map((m) => m.trim())\n .filter(Boolean)\n .filter((m, i, a) => a.indexOf(m) === i)\n}\n\n// ========================================================================\n// Business layer — admin write handlers (mirrors `agentx business`)\n// ========================================================================\n\nasync function upsertOrgEntry(body: any) {\n const agentId = String(body?.agentId || \"\").trim()\n const role = String(body?.role || \"\").trim()\n if (!agentId) throw new Error(\"agentId required\")\n if (!role) throw new Error(\"role required\")\n const days = Array.isArray(body?.days) && body.days.length > 0\n ? body.days\n : [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\"]\n const start = String(body?.start || \"09:00\")\n const end = String(body?.end || \"17:00\")\n const reportsTo = body?.reportsTo ? String(body.reportsTo).trim() : \"\"\n const utilization = typeof body?.utilizationTarget === \"number\" ? body.utilizationTarget : 0.8\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.business = cfg.business || {}\n cfg.business.orgChart = cfg.business.orgChart || {}\n cfg.business.orgChart[agentId] = {\n role,\n ...(reportsTo ? { reportsTo } : {}),\n schedule: { days, start, end },\n utilizationTarget: utilization,\n }\n return `orgChart \"${agentId}\" upserted`\n })\n return { summary }\n}\n\nasync function deleteOrgEntry(body: any) {\n const agentId = String(body?.agentId || \"\").trim()\n if (!agentId) throw new Error(\"agentId required\")\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.business?.orgChart || !(agentId in cfg.business.orgChart)) {\n throw new Error(`no orgChart entry for \"${agentId}\"`)\n }\n delete cfg.business.orgChart[agentId]\n return `orgChart \"${agentId}\" removed`\n })\n return { summary }\n}\n\nasync function upsertProject(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"project id required\")\n const pm = body?.pm ? String(body.pm).trim() : \"\"\n const client = body?.client ? String(body.client).trim() : \"\"\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.business = cfg.business || {}\n cfg.business.projects = cfg.business.projects || []\n const idx = cfg.business.projects.findIndex((p: any) => p.id === id)\n const next: any = { id, ...(pm ? { pm } : {}), ...(client ? { client } : {}) }\n if (idx >= 0) cfg.business.projects[idx] = { ...cfg.business.projects[idx], ...next }\n else cfg.business.projects.push(next)\n return idx >= 0 ? `project \"${id}\" updated` : `project \"${id}\" added`\n })\n return { summary }\n}\n\n/** Clients are derived from projects and contacts, so this only stores\n * overrides: a display name, what kind of relationship it is, how fast a\n * wait costs, and what agents may do for it unattended. Writing an entry\n * never creates a client — it annotates one that already resolves. */\nasync function upsertClient(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"client id required\")\n const kind = String(body?.kind || \"\").trim()\n if (kind && ![\"client\", \"internal\", \"own\"].includes(kind)) throw new Error('kind must be client, internal or own')\n const respondWithin = String(body?.respondWithin || \"\").trim()\n if (respondWithin && !/^\\d+\\s*[mhd]$/.test(respondWithin)) throw new Error('respondWithin must look like \"4h\", \"90m\" or \"2d\"')\n const standing = Array.isArray(body?.standing)\n ? body.standing.map((x: any) => String(x).trim()).filter(Boolean)\n : String(body?.standing || \"\").split(\",\").map(x => x.trim()).filter(Boolean)\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.business = cfg.business || {}\n cfg.business.clients = cfg.business.clients || {}\n const had = Boolean(cfg.business.clients[id])\n cfg.business.clients[id] = {\n ...(cfg.business.clients[id] || {}),\n ...(body?.name ? { name: String(body.name).trim() } : {}),\n ...(kind ? { kind } : {}),\n ...(respondWithin ? { respondWithin } : {}),\n standing,\n }\n return had ? `client \"${id}\" updated` : `client \"${id}\" configured`\n })\n return { summary }\n}\n\nasync function deleteClient(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"client id required\")\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.business?.clients?.[id]) throw new Error(`no client override \"${id}\"`)\n delete cfg.business.clients[id]\n return `client \"${id}\" reset to defaults`\n })\n return { summary }\n}\n\nasync function deleteProject(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"project id required\")\n const { summary } = mutateAgentxConfig((cfg) => {\n const list = cfg.business?.projects || []\n const before = list.length\n cfg.business.projects = list.filter((p: any) => p.id !== id)\n if (cfg.business.projects.length === before) throw new Error(`no project \"${id}\"`)\n return `project \"${id}\" removed`\n })\n return { summary }\n}\n\nasync function upsertContact(body: any) {\n const client = String(body?.client || \"\").trim()\n if (!client) throw new Error(\"client required\")\n if (!body?.chatId && !body?.username && !body?.senderId) {\n throw new Error(\"one of chatId / username / senderId required\")\n }\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.business = cfg.business || {}\n cfg.business.contactMap = cfg.business.contactMap || []\n const entry: any = { client }\n if (body?.channel) entry.channel = String(body.channel)\n if (body?.chatId) entry.chatId = String(body.chatId)\n if (body?.username) entry.username = String(body.username)\n if (body?.senderId) entry.senderId = String(body.senderId)\n if (body?.project) entry.project = String(body.project)\n if (body?.displayName) entry.displayName = String(body.displayName)\n cfg.business.contactMap.push(entry)\n const key = entry.chatId || entry.username || entry.senderId\n return `contact ${entry.channel ? entry.channel + \"/\" : \"\"}${key} → ${client} added`\n })\n return { summary }\n}\n\nasync function deleteContact(body: any) {\n const filters = [\"channel\", \"chatId\", \"username\", \"senderId\"]\n const provided = filters.filter((f) => body?.[f])\n if (provided.length === 0) throw new Error(\"at least one filter (channel/chatId/username/senderId) required\")\n const { summary } = mutateAgentxConfig((cfg) => {\n const list = cfg.business?.contactMap || []\n const before = list.length\n cfg.business.contactMap = list.filter((c: any) => {\n // Drop entries that match every provided filter; keep the rest.\n for (const f of provided) {\n if (c[f] !== body[f]) return true\n }\n return false\n })\n if (cfg.business.contactMap.length === before) throw new Error(\"no matching contact entry\")\n return `${before - cfg.business.contactMap.length} contact entry/entries removed`\n })\n return { summary }\n}\n\n// ========================================================================\n// Boards — admin write handlers (mirrors `agentx board` + `agentx board column`)\n// ========================================================================\n\nasync function upsertBoard(body: any) {\n const id = String(body?.id || \"\").trim()\n const name = String(body?.name || \"\").trim()\n const projectsRaw = String(body?.projects || \"\").split(/[,\\n]/).map((s: string) => s.trim()).filter(Boolean)\n if (!id) throw new Error(\"id required\")\n if (!name) throw new Error(\"name required\")\n if (projectsRaw.length === 0) throw new Error(\"at least one project path required\")\n const primaryToolLabel = body?.primaryToolLabel ? String(body.primaryToolLabel).trim() : \"\"\n const timeRangeDays = Number.isFinite(Number(body?.timeRangeDays)) ? Number(body.timeRangeDays) : 30\n const closedWindowDays = Number.isFinite(Number(body?.closedWindowDays)) ? Number(body.closedWindowDays) : 30\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.boards = Array.isArray(cfg.boards) ? cfg.boards : []\n const idx = cfg.boards.findIndex((b: any) => b.id === id)\n const next: any = {\n id, name,\n source: { type: \"gitlab\", projects: projectsRaw },\n timeRangeDays, closedWindowDays,\n }\n if (primaryToolLabel) next.primaryToolLabel = primaryToolLabel\n if (idx >= 0) {\n // Preserve any existing columns / labels — we're only editing top-level fields here.\n const prev = cfg.boards[idx]\n cfg.boards[idx] = { ...prev, ...next, columns: prev.columns, labels: prev.labels }\n return `board \"${id}\" updated`\n }\n cfg.boards.push(next)\n return `board \"${id}\" added`\n })\n return { summary }\n}\n\nasync function deleteBoard(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"id required\")\n const { summary } = mutateAgentxConfig((cfg) => {\n const list = Array.isArray(cfg.boards) ? cfg.boards : []\n const before = list.length\n cfg.boards = list.filter((b: any) => b.id !== id)\n if (cfg.boards.length === before) throw new Error(`board \"${id}\" not found`)\n return `board \"${id}\" removed`\n })\n return { summary }\n}\n\nasync function upsertBoardColumn(body: any) {\n const boardId = String(body?.boardId || \"\").trim()\n const columnId = String(body?.columnId || \"\").trim().toLowerCase().replace(/[^a-z0-9_-]/g, \"-\")\n const title = String(body?.title || \"\").trim()\n const kind = String(body?.kind || \"scoped-label\")\n if (!boardId) throw new Error(\"boardId required\")\n if (!columnId) throw new Error(\"columnId required\")\n if (!title) throw new Error(\"title required\")\n if (![\"open-backlog\", \"scoped-label\", \"closed\", \"label\"].includes(kind)) {\n throw new Error(\"kind must be open-backlog | scoped-label | closed | label\")\n }\n const { summary } = mutateAgentxConfig((cfg) => {\n const list = Array.isArray(cfg.boards) ? cfg.boards : []\n const b = list.find((x: any) => x.id === boardId)\n if (!b) throw new Error(`board \"${boardId}\" not found`)\n b.columns = Array.isArray(b.columns) ? b.columns : []\n const col: any = { id: columnId, title, kind, scopedPrefix: body?.scopedPrefix || \"Status\" }\n if (kind === \"scoped-label\") {\n if (!body?.scopedLabel) throw new Error(\"scopedLabel required for kind=scoped-label\")\n col.scopedLabel = String(body.scopedLabel)\n }\n if (kind === \"label\") {\n if (!body?.mapsToLabel) throw new Error(\"mapsToLabel required for kind=label\")\n col.mapsToLabel = String(body.mapsToLabel)\n }\n if (body?.accent) col.accent = String(body.accent)\n const idx = b.columns.findIndex((c: any) => c.id === columnId)\n if (idx >= 0) { b.columns[idx] = { ...b.columns[idx], ...col }; return `column \"${columnId}\" updated` }\n b.columns.push(col)\n return `column \"${columnId}\" added to board \"${boardId}\"`\n })\n return { summary }\n}\n\nasync function deleteBoardColumn(body: any) {\n const boardId = String(body?.boardId || \"\").trim()\n const columnId = String(body?.columnId || \"\").trim()\n if (!boardId || !columnId) throw new Error(\"boardId and columnId required\")\n const { summary } = mutateAgentxConfig((cfg) => {\n const list = Array.isArray(cfg.boards) ? cfg.boards : []\n const b = list.find((x: any) => x.id === boardId)\n if (!b) throw new Error(`board \"${boardId}\" not found`)\n const before = (b.columns || []).length\n b.columns = (b.columns || []).filter((c: any) => c.id !== columnId)\n if (b.columns.length === before) throw new Error(`column \"${columnId}\" not found`)\n return `column \"${columnId}\" removed from board \"${boardId}\"`\n })\n return { summary }\n}\n\n// ========================================================================\n// Action registry — admin write + run handlers\n// ========================================================================\n//\n// Mirrors the `agentx actions` CLI: upsert / delete / run actions stored\n// under `.agentx/actions/<id>.json`. The runner returns the same shape\n// the CLI prints, capped at 32KB per stream.\n\nasync function upsertAction(body: any) {\n const { ActionStore } = await import(\"@/actions/store\")\n const { actionSchema } = await import(\"@/actions/types\")\n if (!body || typeof body !== \"object\") throw new Error(\"body required\")\n const parsed = actionSchema.safeParse(body)\n if (!parsed.success) {\n const issue = parsed.error.issues[0]\n throw new Error(`${issue.path.join(\".\") || \"<root>\"}: ${issue.message}`)\n }\n const saved = new ActionStore().save(parsed.data)\n return { summary: `action \"${saved.id}\" saved (${saved.kind})`, action: saved }\n}\n\nasync function deleteAction(body: any) {\n const { ActionStore } = await import(\"@/actions/store\")\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"id required\")\n if (!new ActionStore().delete(id)) throw new Error(`action \"${id}\" not found`)\n return { summary: `action \"${id}\" removed` }\n}\n\nasync function runActionFromAdmin(body: any) {\n const { ActionStore } = await import(\"@/actions/store\")\n const { runAction } = await import(\"@/actions/runner\")\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"id required\")\n const action = new ActionStore().get(id)\n if (!action) throw new Error(`action \"${id}\" not found`)\n const inputs = (body?.inputs && typeof body.inputs === \"object\") ? body.inputs as Record<string, unknown> : {}\n const result = await runAction(action, inputs)\n return { summary: `action \"${id}\" ${result.ok ? \"ok\" : \"failed\"} (${result.durationMs}ms)`, result }\n}\n\n// ========================================================================\n// Mesh health-check cadence — admin write handler\n// ========================================================================\n\nasync function updateMeshHealth(body: any) {\n const interval = body?.interval !== undefined ? Number(body.interval) : undefined\n const timeout = body?.timeout !== undefined ? Number(body.timeout) : undefined\n if (interval !== undefined && (!Number.isFinite(interval) || interval < 5 || interval > 3600)) {\n throw new Error(\"interval must be 5..3600 seconds\")\n }\n if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 1 || timeout > 60)) {\n throw new Error(\"timeout must be 1..60 seconds\")\n }\n if (interval === undefined && timeout === undefined) throw new Error(\"nothing to update\")\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.mesh = cfg.mesh || {}\n cfg.mesh.healthCheck = cfg.mesh.healthCheck || {}\n if (interval !== undefined) cfg.mesh.healthCheck.interval = interval\n if (timeout !== undefined) cfg.mesh.healthCheck.timeout = timeout\n return `mesh.healthCheck updated (interval=${cfg.mesh.healthCheck.interval}s, timeout=${cfg.mesh.healthCheck.timeout}s)`\n })\n return { summary }\n}\n\n// ========================================================================\n// Notifications — admin write handler (mirrors `agentx notifications`)\n// ========================================================================\n\nasync function updateNotifications(body: any) {\n const { summary } = mutateAgentxConfig((cfg) => {\n cfg.notifications = cfg.notifications || {}\n const changes: string[] = []\n if (body && typeof body === \"object\") {\n if (\"destination\" in body) {\n if (!body.destination) {\n delete cfg.notifications.destination\n changes.push(\"destination cleared\")\n } else if (body.destination.channel && body.destination.chatId) {\n cfg.notifications.destination = {\n channel: String(body.destination.channel),\n chatId: String(body.destination.chatId),\n ...(body.destination.accountId ? { accountId: String(body.destination.accountId) } : {}),\n }\n changes.push(`destination=${body.destination.channel}:${body.destination.chatId}`)\n } else {\n throw new Error(\"destination requires channel + chatId\")\n }\n }\n if (\"on\" in body && body.on && typeof body.on === \"object\") {\n cfg.notifications.on = cfg.notifications.on || {}\n for (const k of [\"taskComplete\", \"taskError\", \"taskQueued\"]) {\n if (k in body.on) cfg.notifications.on[k] = !!body.on[k]\n }\n changes.push(\"on=\" + JSON.stringify(cfg.notifications.on))\n }\n if (\"longTaskThreshold\" in body) {\n const n = Number(body.longTaskThreshold)\n if (!Number.isFinite(n) || n < 0) throw new Error(\"longTaskThreshold must be a non-negative number\")\n cfg.notifications.longTaskThreshold = n\n changes.push(`threshold=${n}s`)\n }\n }\n if (changes.length === 0) throw new Error(\"nothing to update\")\n return `notifications updated (${changes.join(\", \")})`\n })\n return { summary }\n}\n\n// ========================================================================\n// Webhook triggers — granular event-type → workflow id mapping\n// ========================================================================\n\nasync function updateWebhookTriggers(body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"webhook id required\")\n const { summary } = mutateAgentxConfig((cfg) => {\n const list = Array.isArray(cfg.webhooks) ? cfg.webhooks : []\n const w = list.find((x: any) => x.id === id)\n if (!w) throw new Error(`webhook \"${id}\" not found`)\n if (\"triggers\" in body) {\n if (body.triggers === null || (typeof body.triggers === \"object\" && Object.keys(body.triggers).length === 0)) {\n delete w.triggers\n } else if (typeof body.triggers === \"object\") {\n // shape check — keys are event names (free-form), values are workflow ids\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(body.triggers as Record<string, unknown>)) {\n if (typeof v === \"string\" && v.trim()) out[String(k)] = v.trim()\n }\n if (Object.keys(out).length === 0) delete w.triggers\n else w.triggers = out\n }\n }\n if (\"defaultWorkflow\" in body) {\n if (!body.defaultWorkflow) delete w.defaultWorkflow\n else w.defaultWorkflow = String(body.defaultWorkflow).trim()\n }\n return `webhook \"${id}\" triggers/defaultWorkflow updated`\n })\n return { summary }\n}\n\n// Keep reference so unused-import linters don't trip. rmSync is wired for a\n// future \"delete workspace too\" feature; for now the admin panel never deletes\n// files on disk, only config entries.\nexport const _reserved = { rmSync }\n\n// ========================================================================\n// HTML\n// ========================================================================\n","// --- Admin page (/admin) ---\n//\n// Single-file settings surface with seven tabs rendered in-place via a\n// client-side data-tab switcher:\n// Agents / Channels / Schedules / Webhooks / Mesh / Tokens / Advanced\n//\n// API is in admin-panel.ts; this module owns the HTML + CSS + client JS only.\n//\n// ### Why a single file (for now)\n//\n// The tabs share: a common data-fetch-and-render pipeline, the peer banner,\n// the files modal, and mutation helpers. Splitting them into seven files\n// would force those cross-tab primitives into a shared helper with a\n// wider interface; the savings aren't worth the ceremony at this size.\n// When the page grows past ~3k lines again, a tab-by-tab split becomes\n// worth the cost — the scaffold is already ready for it under\n// ui/pages/admin/<tab>.ts.\n\nimport {\n renderShell,\n pageHead, healthStrip, witBanner, sectionHead, secLabel,\n TOAST_HTML, TOAST_SCRIPT, ROW_CARD_SCRIPT,\n type TopbarPeer,\n} from \"..\"\nimport { getTopbarFeatures } from \"../../topbar\"\n\n/** Feather-style stroked icons for each section head. Kept together at the\n * top so we can swap the icon set in one place if/when the design calls\n * for a change. */\nconst ICONS = {\n agents: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"10\" rx=\"2\"/><circle cx=\"12\" cy=\"5\" r=\"2\"/><path d=\"M12 7v4M8 16h.01M16 16h.01\"/></svg>`,\n channels: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\"/></svg>`,\n schedules: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>`,\n webhooks: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 12a3 3 0 1 0 3-3m-8 11a3 3 0 1 0 2.6-1.5L10 12m7 3a3 3 0 1 0-2-5.5\"/></svg>`,\n mesh: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"2\"/><circle cx=\"18\" cy=\"6\" r=\"2\"/><circle cx=\"6\" cy=\"18\" r=\"2\"/><circle cx=\"18\" cy=\"18\" r=\"2\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/><path d=\"M7.4 7.4l3.2 3.2M13.4 13.4l3.2 3.2M16.6 7.4l-3.2 3.2M10.6 13.4l-3.2 3.2\"/></svg>`,\n tokens: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 2l-2 2m-8 8a4 4 0 1 1-6 6m6-6l5-5 3 3-5 5m-3-3l3 3\"/></svg>`,\n advanced: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 4h8l4 4v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><path d=\"M9 13l2 2 4-4\"/></svg>`,\n actions: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg>`,\n}\n\nexport interface AdminPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n localToken?: string\n}\n\nexport function renderAdminPage(opts: AdminPageOpts = {}): string {\n const tokenScript = opts.localToken\n ? `<script>window.AX_LOCAL_TOKEN = ${JSON.stringify(opts.localToken)};</script>`\n : \"\"\n\n // New page chrome — sits above the peer banner + tabs. Placeholder values\n // for the health strip live here; ADMIN_HEALTH_SCRIPT populates them from\n // /api/admin/config once the page loads.\n const chrome = pageHead({\n kicker: \"Settings\",\n title: \"Your team's control room\",\n lead: `Manage the agents, the places they answer from, and who's allowed to reach them. Everything here writes to <span class=\"mono muted\">agentx.json</span> on your machine — nothing leaves.`,\n }) + healthStrip([\n { kind: \"off\", num: \"—\", label: \"Agents online\" },\n { kind: \"off\", num: \"—\", label: \"Channels connected\" },\n { kind: \"off\", num: \"0\", label: \"Needs attention\" },\n { kind: \"off\", num: \"—\", label: \"Active tokens\" },\n ]).replace(\n \"<div class=\\\"ax-health-strip\\\">\",\n `<div class=\"ax-health-strip\" id=\"ax-health\">`,\n )\n\n return renderShell({\n title: \"AgentX · Settings\",\n activeTab: \"admin\",\n subtitle: \"Settings\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n noMain: true,\n body: chrome + adminPageBody() + TOAST_HTML,\n css: ADMIN_PAGE_CSS,\n scripts: `${tokenScript}<script>${TOAST_SCRIPT}\\n${ROW_CARD_SCRIPT}\\n${ADMIN_HEALTH_SCRIPT}\\n${ADMIN_PAGE_SCRIPT}</script>`,\n })\n}\n\n// Populates the 4-up health strip + tab count bubbles from the same\n// /api/admin/config payload the main admin script already fetches. Runs\n// after the admin bootstrap attaches state under window.__AX_ADMIN.\nconst ADMIN_HEALTH_SCRIPT = `\n(function(){\n function fmt(n) { return n == null ? '—' : String(n); }\n async function refresh() {\n try {\n const headers = { 'Content-Type': 'application/json' };\n if (window.AX_LOCAL_TOKEN) headers['Authorization'] = 'Bearer ' + window.AX_LOCAL_TOKEN;\n const r = await fetch('/api/admin/config', { headers });\n if (!r.ok) return;\n const cfg = await r.json();\n const agents = Object.keys(cfg.agents || {});\n const channels = cfg.channels || {};\n const channelDefs = ['telegram','whatsapp','gitlab','github'];\n const enabled = channelDefs.filter(c => channels[c] && channels[c].enabled).length;\n const crons = Object.keys(cfg.crons || {}).length;\n const webhooks = Object.keys(cfg.webhooks || {}).length;\n const meshPeers = (cfg.mesh && Array.isArray(cfg.mesh.peers) ? cfg.mesh.peers.length : 0);\n\n // Health strip numbers\n const strip = document.getElementById('ax-health');\n if (strip) {\n const cards = strip.querySelectorAll('.ax-health-card');\n const setCard = (i, num, kind) => {\n const c = cards[i]; if (!c) return;\n const n = c.querySelector('.ax-hc-num'); if (n) n.textContent = fmt(num);\n const d = c.querySelector('.ax-hc-dot');\n if (d) { d.className = 'ax-hc-dot ax-hc-dot--' + kind; }\n };\n setCard(0, agents.length, agents.length > 0 ? 'ok' : 'off');\n setCard(1, enabled + '/' + channelDefs.length, enabled > 0 ? 'ok' : 'off');\n // \"Needs attention\" is a placeholder until we surface real warnings.\n setCard(2, 0, 'off');\n const tokenCount = Array.isArray(cfg.tokens) ? cfg.tokens.length : 0;\n setCard(3, tokenCount, tokenCount > 0 ? 'ok' : 'off');\n }\n\n // Tab counts\n const setCount = (tab, val) => {\n const btn = document.querySelector('nav.tabs button[data-tab=\"' + tab + '\"]');\n if (!btn) return;\n let c = btn.querySelector('.ax-tab-count');\n if (!c) {\n c = document.createElement('span');\n c.className = 'ax-tab-count';\n btn.appendChild(document.createTextNode(' '));\n btn.appendChild(c);\n }\n c.textContent = String(val);\n };\n setCount('agents', agents.length);\n setCount('channels', enabled + '/' + channelDefs.length);\n setCount('crons', crons);\n setCount('webhooks', webhooks);\n setCount('mesh', meshPeers);\n const tokenCount = Array.isArray(cfg.tokens) ? cfg.tokens.length : 0;\n setCount('tokens', tokenCount);\n } catch (e) { /* best-effort */ }\n }\n refresh();\n // Refresh again after the main admin script has had a chance to mutate config.\n document.addEventListener('ax-config-saved', refresh);\n})();`\n\n/** Business is a business-layer surface (experimental) — its Settings\n * sub-tab renders only when config.business.enabled. The section stays\n * in the DOM either way; without a button it's simply unreachable, so\n * no tab-switching JS changes. */\nfunction adminPageBody(): string {\n const business = getTopbarFeatures().business\n ? `<button data-tab=\"business\">Business</button>\n `\n : \"\"\n return ADMIN_PAGE_BODY.replace(\"__BUSINESS_TABS__\", business)\n}\n\nconst ADMIN_PAGE_BODY = `\n<div id=\"peer-banner\" class=\"peer-banner\">\n <span class=\"label\">Managing</span>\n <span class=\"name\" id=\"peer-banner-name\">—</span>\n <span>from this dashboard via scoped token.</span>\n <span class=\"spacer\"></span>\n <button id=\"peer-banner-back\">← Back to primary</button>\n</div>\n<nav class=\"tabs\">\n <button data-tab=\"agents\" class=\"active\">Agents</button>\n <button data-tab=\"channels\">Channels</button>\n <button data-tab=\"crons\">Schedules</button>\n <button data-tab=\"webhooks\">Webhooks</button>\n <button data-tab=\"mesh\">Mesh</button>\n __BUSINESS_TABS__<button data-tab=\"boards-cfg\">Boards</button>\n <button data-tab=\"actions\">Actions</button>\n <button data-tab=\"tokens\">Tokens</button>\n <button data-tab=\"advanced\">Advanced</button>\n</nav>\n<div id=\"files-modal\" class=\"td-modal hidden\" aria-hidden=\"true\">\n <div class=\"td-backdrop\"></div>\n <div class=\"td-card\" role=\"dialog\" aria-modal=\"true\" style=\"width:min(960px,94vw);height:min(720px,90vh)\">\n <header>\n <span class=\"chip-small\" id=\"files-tab-label\">Identity</span>\n <h3 id=\"files-title\">Agent · Files</h3>\n <nav style=\"display:flex;gap:0;margin-right:10px\">\n <button class=\"ghost\" id=\"files-tab-identity\" style=\"padding:6px 12px;font-size:12px;border-top-right-radius:0;border-bottom-right-radius:0\">Identity</button>\n <button class=\"ghost\" id=\"files-tab-skills\" style=\"padding:6px 12px;font-size:12px;border-top-left-radius:0;border-bottom-left-radius:0;border-left:none\">Skills</button>\n </nav>\n <button class=\"td-close\" id=\"files-close\" aria-label=\"Close\">×</button>\n </header>\n <div class=\"files-split\">\n <aside class=\"files-picker\" id=\"files-picker\"></aside>\n <div class=\"files-editor\">\n <div id=\"files-editor-empty\" class=\"files-editor-empty\">Pick a file on the left to open it.</div>\n <div id=\"files-editor-head\" class=\"files-editor-head\" style=\"display:none\">\n <span class=\"path\" id=\"files-current-path\"></span>\n <label class=\"ghost\" style=\"padding:6px 10px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:4px;border:1px solid var(--ax-border-2);border-radius:4px\">\n ↑ Upload<input type=\"file\" id=\"files-upload\" accept=\".md,.markdown,.txt,text/markdown,text/plain\" style=\"display:none\" />\n </label>\n <button class=\"ghost\" id=\"files-revert\" style=\"padding:6px 10px;font-size:12px\">Revert</button>\n <button class=\"primary\" id=\"files-save\" style=\"padding:6px 14px;font-size:12px\">Save</button>\n </div>\n <div class=\"files-editor-body\" style=\"display:none\" id=\"files-editor-wrap\">\n <textarea id=\"files-editor\" spellcheck=\"false\"></textarea>\n </div>\n <div id=\"files-save-msg\" class=\"msg\" style=\"margin:10px 16px\"></div>\n\n <div id=\"files-add-skill\" class=\"files-add\" style=\"display:none\">\n <h4>Add a skill</h4>\n <div class=\"rowf\">\n <div><label>Slug<span class=\"hint\">(lowercase)</span></label><input id=\"skill-slug\" placeholder=\"my-skill\" /></div>\n <div><label>Title<span class=\"hint\">(optional)</span></label><input id=\"skill-title\" placeholder=\"What the skill does\" /></div>\n </div>\n <label>Upload SKILL.md<span class=\"hint\">(optional — otherwise we scaffold a template)</span></label>\n <input id=\"skill-file\" type=\"file\" accept=\".md,.markdown,.txt,text/markdown,text/plain\" />\n <div class=\"actions\"><button class=\"primary\" onclick=\"addSkill()\">Create</button><div id=\"skill-msg\" class=\"msg\"></div></div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<div id=\"tg-edit-modal\" class=\"td-modal hidden\" aria-hidden=\"true\">\n <div class=\"td-backdrop\"></div>\n <div class=\"td-card\" role=\"dialog\" aria-modal=\"true\" style=\"height:auto;max-height:90vh;width:min(520px,94vw)\">\n <header>\n <span class=\"chip-small\">Edit</span>\n <h3 id=\"tg-edit-title\">Telegram account</h3>\n <button class=\"td-close\" id=\"tg-edit-close\" aria-label=\"Close\">×</button>\n </header>\n <div style=\"flex:1;overflow-y:auto;padding:16px 20px\">\n <input type=\"hidden\" id=\"tg-edit-id\" />\n <label>Bind to agent</label>\n <select id=\"tg-edit-agent\"></select>\n <label>Bot username<span class=\"hint\">(optional)</span></label>\n <input id=\"tg-edit-user\" placeholder=\"my_bot\" />\n <label>Bot token env-var<span class=\"hint\">(e.g. <code>TG_SUPPORT_BOT_TOKEN</code>)</span></label>\n <input id=\"tg-edit-env\" placeholder=\"TG_SUPPORT_BOT_TOKEN\" />\n <div class=\"hint-block\">Token value stays in <code>.env</code>; this panel only edits the reference name.</div>\n <div id=\"tg-edit-msg\" class=\"msg\"></div>\n </div>\n <div class=\"td-footer\" style=\"justify-content:flex-end\">\n <button class=\"ghost\" id=\"tg-edit-cancel\" style=\"padding:7px 14px\">Cancel</button>\n <button class=\"primary\" id=\"tg-edit-save\">Save changes</button>\n </div>\n </div>\n</div>\n\n<div id=\"edit-modal\" class=\"td-modal hidden\" aria-hidden=\"true\">\n <div class=\"td-backdrop\"></div>\n <div class=\"td-card\" role=\"dialog\" aria-modal=\"true\" style=\"height:auto;max-height:90vh;width:min(560px,94vw)\">\n <header>\n <span class=\"chip-small\">Edit</span>\n <h3 id=\"edit-title\">Agent</h3>\n <button class=\"td-close\" id=\"edit-close\" aria-label=\"Close\">×</button>\n </header>\n <div style=\"flex:1;overflow-y:auto;padding:16px 20px\">\n <div class=\"rowf\">\n <div><label>Name</label><input id=\"e-name\" /></div>\n <div><label>AI engine</label><select id=\"e-tier\"><option value=\"claude-code\">Claude Code</option><option value=\"codex-cli\">Codex CLI</option><option value=\"opencode\">OpenCode</option><option value=\"sdk\">Anthropic API (SDK)</option><option value=\"orchestrator\">Orchestrator</option></select></div>\n </div>\n <label>Model<span class=\"hint\">(optional)</span></label>\n <input id=\"e-model\" />\n <label>Trigger words<span class=\"hint\">(comma or space separated)</span></label>\n <input id=\"e-triggers\" />\n <label>Personality / system prompt</label>\n <textarea id=\"e-personality\" rows=\"5\"></textarea>\n <div class=\"rowf\">\n <div><label>Max concurrent tasks</label><input id=\"e-max-concurrent\" type=\"number\" min=\"1\" max=\"20\" /></div>\n <div><label>Max execution (minutes)</label><input id=\"e-max-exec\" type=\"number\" min=\"1\" max=\"240\" /></div>\n </div>\n <label>Permission mode<span class=\"hint\">(default / bypassPermissions / plan)</span></label>\n <select id=\"e-perm\"><option value=\"default\">default</option><option value=\"bypassPermissions\">bypassPermissions</option><option value=\"plan\">plan</option></select>\n <label class=\"toggle-switch\" style=\"margin-top:12px\"><input type=\"checkbox\" id=\"e-access\" /> <span>Expose via public API</span></label>\n <div id=\"e-msg\" class=\"msg\"></div>\n </div>\n <div class=\"td-footer\" style=\"justify-content:flex-end\">\n <button class=\"ghost\" id=\"edit-cancel\" style=\"padding:7px 14px\">Cancel</button>\n <button class=\"primary\" id=\"edit-save\">Save changes</button>\n </div>\n </div>\n</div>\n<div id=\"td-modal\" class=\"td-modal hidden\" aria-hidden=\"true\">\n <div class=\"td-backdrop\"></div>\n <div class=\"td-card\" role=\"dialog\" aria-modal=\"true\">\n <header>\n <span class=\"chip-small\">Test drive</span>\n <h3 id=\"td-title\">Agent</h3>\n <button class=\"td-close\" id=\"td-close\" aria-label=\"Close\">×</button>\n </header>\n <div id=\"td-body\" class=\"td-body\"><div class=\"td-empty\">Send a message to sanity-check this agent before wiring it to a real channel.</div></div>\n <div class=\"td-footer\">\n <textarea id=\"td-input\" placeholder=\"Message the agent… (Cmd/Ctrl-Enter to send)\" rows=\"1\"></textarea>\n <button id=\"td-send\">Send</button>\n </div>\n <div class=\"td-hint\" id=\"td-hint\"></div>\n </div>\n</div>\n<main>\n <div id=\"global-msg\" class=\"msg\"></div>\n\n <section id=\"tab-agents\" class=\"tab active\">\n ${sectionHead({\n icon: ICONS.agents,\n title: \"Your agents\",\n lead: \"Each agent is a role with its own trigger words and workspace. Delete removes the config only — the folder on disk stays.\",\n })}\n ${witBanner({\n persistKey: \"agents\",\n bodyHtml: `<b>What's an agent, exactly?</b> Think of it as a specialised teammate. A sales agent handles pricing questions, a support agent answers tickets, a reports agent sends a weekly summary. You decide what they do and what to say to wake them up (their \"trigger words\", like <code>@support</code>).`,\n })}\n <div id=\"agent-list\" class=\"ax-stack\"></div>\n <div class=\"add-form\">\n <h3>Add a new agent</h3>\n <div class=\"rowf\">\n <div><label>Agent id<span class=\"hint\">(lowercase)</span></label><input id=\"a-id\" placeholder=\"sales\" /></div>\n <div><label>Agent name</label><input id=\"a-name\" placeholder=\"Sales Bot\" /></div>\n </div>\n <label>Trigger words<span class=\"hint\">(comma or space separated)</span></label>\n <input id=\"a-triggers\" placeholder=\"@sales, sales\" />\n <div class=\"rowf\">\n <div><label>AI engine</label><select id=\"a-tier\"><option value=\"claude-code\">Claude Code</option><option value=\"codex-cli\">Codex CLI</option><option value=\"opencode\">OpenCode</option><option value=\"sdk\">Anthropic API (SDK)</option><option value=\"orchestrator\">Orchestrator</option></select></div>\n <div><label>Model<span class=\"hint\">(optional)</span></label><input id=\"a-model\" placeholder=\"claude-sonnet-4-6\" /></div>\n </div>\n <label>Personality / instructions<span class=\"hint\">(optional)</span></label>\n <textarea id=\"a-personality\" placeholder=\"You are a sales agent for Acme. Qualify leads, route pricing questions to @support.\"></textarea>\n <label class=\"toggle-switch\" style=\"margin-top:12px\"><input type=\"checkbox\" id=\"a-public\" /> <span>Expose via public API (needs a token with <code>agent:&lt;id&gt;</code> scope)</span></label>\n <div class=\"actions\"><button class=\"primary\" onclick=\"addAgent()\">Add agent</button><div id=\"a-msg\" class=\"msg\"></div></div>\n </div>\n </section>\n\n <section id=\"tab-channels\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.channels,\n title: \"Where your agents show up\",\n lead: \"Connect a chat app, and your agents can answer from there. Set up once — they'll keep working in the background.\",\n })}\n ${witBanner({\n persistKey: \"channels\",\n bodyHtml: `<b>How does this work?</b> Connect Telegram with a bot token or pair WhatsApp with a QR code. For webhook sources (GitHub, GitLab), use the URL shown in setup. Slack and Discord adapters are not supported in this build.`,\n })}\n\n <details class=\"add-form\" style=\"margin-top:14px;margin-bottom:18px\" id=\"notif-section\">\n <summary class=\"primary\">⚙ Notifications routing</summary>\n <div style=\"margin-top:10px\">\n <p style=\"font-size:11px;color:var(--ax-muted);margin:0 0 10px\">Where AgentX pings you when a task finishes, errors, or runs long. Mirrors <code>agentx notifications</code>.</p>\n <div id=\"notif-current\" style=\"font-size:12px;margin-bottom:10px;padding:8px 10px;background:var(--ax-surface);border-radius:4px;color:var(--ax-muted)\">—</div>\n <label>Channel<span class=\"hint\">(telegram | whatsapp — leave blank to clear)</span></label>\n <input id=\"notif-channel\" placeholder=\"telegram\" />\n <label>Chat id</label>\n <input id=\"notif-chat-id\" placeholder=\"-1001234567890\" />\n <label>Account id <span class=\"hint\">(optional — when the channel is multi-account, e.g. multiple Telegram bots)</span></label>\n <input id=\"notif-account-id\" placeholder=\"default\" />\n <label>Long-task threshold (seconds) <span class=\"hint\">(0 disables long-task pings)</span></label>\n <input id=\"notif-threshold\" type=\"number\" min=\"0\" max=\"3600\" />\n <fieldset style=\"margin-top:8px;border:1px solid var(--ax-border);border-radius:4px;padding:8px 10px\">\n <legend style=\"font-size:11px;color:var(--ax-muted);padding:0 4px\">Events to ping on</legend>\n <label class=\"ax-inline\" style=\"display:inline-flex;gap:6px;font-size:12px;margin-right:14px\"><input type=\"checkbox\" id=\"notif-on-complete\" /> task complete</label>\n <label class=\"ax-inline\" style=\"display:inline-flex;gap:6px;font-size:12px;margin-right:14px\"><input type=\"checkbox\" id=\"notif-on-error\" /> task error</label>\n <label class=\"ax-inline\" style=\"display:inline-flex;gap:6px;font-size:12px\"><input type=\"checkbox\" id=\"notif-on-queued\" /> task queued</label>\n </fieldset>\n <div class=\"actions\" style=\"margin-top:10px\">\n <button class=\"primary\" onclick=\"saveNotifications()\">Save notifications</button>\n <button class=\"ghost\" onclick=\"clearNotificationsDestination()\">Clear destination</button>\n <div id=\"notif-msg\" class=\"msg\"></div>\n </div>\n </div>\n </details>\n\n <div id=\"ch-chatapps-label\">${secLabel({ label: \"Chat apps\" })}</div>\n <div class=\"ax-connectors\" id=\"ch-chatapps\"></div>\n\n <div id=\"ch-devtools-label\">${secLabel({ label: \"Developer tools\", rightHtml: `<span class=\"ax-pill\">Send events from GitHub, GitLab, Sentry, etc.</span>` })}</div>\n <div class=\"ax-connectors\" id=\"ch-devtools\"></div>\n\n <!-- Active-channel configuration panes. One shows at a time; clicking\n a connector card above flips the 'hidden' attribute on the\n matching div. Existing render* pipelines still populate these by id. -->\n <div id=\"ch-panes\" style=\"margin-top:22px\">\n <div id=\"ch-telegram\" hidden><div id=\"tg-section\" class=\"section-block\"></div></div>\n <div id=\"ch-whatsapp\" hidden></div>\n <div id=\"ch-slack\" hidden><div id=\"slack-section\" class=\"section-block\"></div></div>\n <div id=\"ch-discord\" hidden></div>\n <div id=\"ch-gitlab\" hidden></div>\n <div id=\"ch-webrtc\" hidden></div>\n <div id=\"ch-github\" hidden></div>\n </div>\n </section>\n\n <section id=\"tab-crons\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.schedules,\n title: \"Schedules\",\n lead: \"Make an agent do something on a timer — no one has to ask. Daily summaries, weekly reports, hourly health checks.\",\n })}\n ${witBanner({\n persistKey: \"crons\",\n bodyHtml: `<b>What's a good schedule?</b> Anything recurring. A <i>Reports agent</i> sending Monday-morning numbers, a <i>Health agent</i> pinging you at 3am if a server's down, a <i>Standup agent</i> reminding the team. Pick a time below.`,\n })}\n <div id=\"cron-list\" class=\"ax-stack\" style=\"margin-bottom:22px\"></div>\n\n <div class=\"ax-builder\">\n <!-- Hidden fields shared between guided + expert modes. addCron()\n reads these canonical ids; the mode-specific inputs mirror\n their values in. -->\n <select id=\"c-agent\" style=\"display:none\"></select>\n <h3>Create a new schedule</h3>\n <p class=\"ax-builder__hint\">Build your timing here. Switch to expert mode if you'd rather type a cron expression.</p>\n <div class=\"ax-mode-switch\" id=\"sched-mode\">\n <button data-mode=\"simple\" class=\"is-active\">Guided</button>\n <button data-mode=\"expert\">Expert (cron)</button>\n </div>\n\n <!-- Guided mode — sentence builder. The pill-picks emit a cron\n string into the hidden #c-schedule input used by addCron(). -->\n <div id=\"sched-simple\">\n <div class=\"ax-sentence\">\n <span>Run</span>\n <select class=\"ax-pill-pick\" id=\"s-agent\"></select>\n <select class=\"ax-pill-pick\" id=\"s-freq\">\n <option value=\"daily\">every day</option>\n <option value=\"weekly\" selected>every week</option>\n <option value=\"weekdays\">every weekday</option>\n <option value=\"hourly\">every hour</option>\n <option value=\"15min\">every 15 minutes</option>\n </select>\n <div id=\"s-days\" class=\"ax-day-picker\">\n <button data-day=\"1\">M</button><button data-day=\"2\">T</button><button data-day=\"3\">W</button><button data-day=\"4\">T</button><button data-day=\"5\">F</button><button data-day=\"6\">S</button><button data-day=\"0\">S</button>\n </div>\n <span id=\"s-at\" class=\"ax-sentence__when\">at</span>\n <select class=\"ax-pill-pick ax-pill-pick--sm\" id=\"s-hour\">\n <option>6:00 am</option><option>7:00 am</option><option>8:00 am</option>\n <option selected>9:00 am</option><option>10:00 am</option><option>11:00 am</option>\n <option>12:00 pm</option><option>1:00 pm</option><option>2:00 pm</option>\n <option>3:00 pm</option><option>4:00 pm</option><option>5:00 pm</option>\n <option>6:00 pm</option><option>7:00 pm</option><option>8:00 pm</option>\n <option>9:00 pm</option><option>10:00 pm</option>\n </select>\n </div>\n\n <label class=\"ax-builder__lbl\">Give it a name <span class=\"opt\">lowercase, no spaces</span></label>\n <input class=\"ax-builder__inp\" id=\"c-id\" placeholder=\"weekly-sales-digest\" />\n\n <label class=\"ax-builder__lbl\">What should the agent do?</label>\n <textarea class=\"ax-builder__inp\" id=\"c-prompt\" placeholder=\"Send me the weekly sales summary, with the top 3 wins and anything that regressed.\"></textarea>\n\n <div class=\"ax-preview-box\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>\n <span id=\"s-preview-text\"></span>\n </div>\n\n <div style=\"display:flex;gap:8px;margin-top:16px;align-items:center\">\n <button class=\"ax-btn ax-btn--primary\" onclick=\"addCron()\">Add schedule</button>\n <div id=\"c-msg\" class=\"msg\"></div>\n <div style=\"flex:1\"></div>\n <span class=\"muted\" style=\"font-size:11.5px\">Cron equivalent: <span class=\"mono\" style=\"color:var(--ax-text-2)\" id=\"s-cron-echo\">0 9 * * 1</span></span>\n </div>\n </div>\n\n <!-- Expert mode — original cron + prompt form. Shares the #c-id /\n #c-prompt / #c-agent fields with guided mode so addCron() is\n mode-agnostic. -->\n <div id=\"sched-expert\" style=\"display:none\">\n <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px\">\n <div>\n <label class=\"ax-builder__lbl\">Schedule id <span class=\"opt\">lowercase, no spaces</span></label>\n <input class=\"ax-builder__inp\" id=\"c-id-expert\" placeholder=\"weekly-report\" />\n </div>\n <div>\n <label class=\"ax-builder__lbl\">Agent</label>\n <select class=\"ax-builder__inp\" id=\"c-agent-expert\"></select>\n </div>\n </div>\n <label class=\"ax-builder__lbl\">Cron expression <span class=\"opt\">standard five-field cron</span></label>\n <input class=\"ax-builder__inp mono\" id=\"c-schedule\" value=\"0 9 * * 1\" />\n <div id=\"c-preview\" class=\"ax-preview-box\" style=\"margin-top:10px\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>\n <span id=\"c-preview-text\">At 9:00 AM, only on Monday</span>\n </div>\n <div class=\"ax-cron-hints\">\n <div><code>0 9 * * 1</code>Mondays at 9am</div>\n <div><code>*/15 * * * *</code>Every 15 minutes</div>\n <div><code>0 0 1 * *</code>First of every month</div>\n <div><code>0 17 * * 5</code>Fridays at 5pm</div>\n </div>\n <label class=\"ax-builder__lbl\" style=\"margin-top:12px\">Prompt</label>\n <textarea class=\"ax-builder__inp\" id=\"c-prompt-expert\" placeholder=\"What should the agent do on every tick?\"></textarea>\n <div style=\"display:flex;gap:8px;margin-top:16px\">\n <button class=\"ax-btn ax-btn--primary\" onclick=\"addCronExpert()\">Add schedule</button>\n </div>\n </div>\n </div>\n </section>\n\n <section id=\"tab-webhooks\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.webhooks,\n title: \"Webhooks\",\n lead: \"A URL you give to another service. When something happens there — a PR is merged, an error fires — they POST to that URL, and we forward it to your agent as a readable message.\",\n })}\n ${witBanner({\n persistKey: \"webhooks\",\n bodyHtml: `<b>Not sure you need this?</b> If a service you use has a \"webhooks\" setting (GitHub, Stripe, Sentry, Typeform…), you probably do. It's how those services tell AgentX when something's worth looking at.`,\n })}\n <div id=\"webhook-list\" class=\"ax-stack\"></div>\n <div class=\"add-form\">\n <h3>Add a webhook</h3>\n <div class=\"rowf\">\n <div><label>Webhook id<span class=\"hint\">(lowercase)</span></label><input id=\"w-id\" placeholder=\"globex-gitlab\" /></div>\n <div><label>Source</label><select id=\"w-source\"><option value=\"gitlab\">GitLab ✓</option><option value=\"github\">GitHub ✓</option><option value=\"sentry\">Sentry ✓</option><option value=\"stripe\">Stripe ✓</option><option value=\"vercel\">Vercel ✓</option><option value=\"odoo\">Odoo ✓</option><option value=\"hubspot\">HubSpot ✓</option><option value=\"discord\">Discord</option><option value=\"slack\">Slack</option><option value=\"custom\">Custom</option></select></div>\n </div>\n <div class=\"rowf\">\n <div><label>Agent</label><select id=\"w-agent\"></select></div>\n <div><label>Signing secret env-var<span class=\"hint\">(optional)</span></label><input id=\"w-secret\" placeholder=\"GITLAB_WEBHOOK_SECRET\" /></div>\n </div>\n <label>Description<span class=\"hint\">(optional)</span></label>\n <input id=\"w-desc\" placeholder=\"Globex main project webhooks\" />\n <div class=\"actions\"><button class=\"primary\" onclick=\"addWebhook()\">Add webhook</button><div id=\"w-msg\" class=\"msg\"></div></div>\n </div>\n </section>\n\n <section id=\"tab-mesh\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.mesh,\n title: \"Mesh\",\n lead: \"Link this AgentX to other machines so they share work. Good for teams running AgentX on multiple laptops or a mix of laptop + server.\",\n })}\n <div id=\"mesh-hero\" class=\"ax-mesh-hero\"></div>\n ${witBanner({\n persistKey: \"mesh\",\n bodyHtml: `<b>Do I need mesh?</b> Only if you're running AgentX in more than one place. Solo users on one laptop can skip this entirely.`,\n })}\n ${secLabel({ label: \"Connected peers\" })}\n <div id=\"mesh-peers\" class=\"ax-stack\"></div>\n <div class=\"add-form\">\n <h3>Add a peer</h3>\n <div class=\"rowf\">\n <div><label>Peer name<span class=\"hint\">(human label)</span></label><input id=\"m-name\" placeholder=\"peer-server\" /></div>\n <div><label>URL</label><input id=\"m-url\" placeholder=\"http://192.168.1.50:18800\" /></div>\n </div>\n <label>Auth token<span class=\"hint\">(optional — use a scoped token with <code>mesh:peer</code>)</span></label>\n <input id=\"m-token\" type=\"password\" />\n <div class=\"actions\"><button class=\"primary\" onclick=\"addMeshPeer()\">Add peer</button><div id=\"m-msg\" class=\"msg\"></div></div>\n </div>\n\n <details class=\"add-form\" style=\"margin-top:16px\">\n <summary class=\"primary\">⏱ Health-check cadence</summary>\n <div style=\"margin-top:10px\">\n <p style=\"font-size:11px;color:var(--ax-muted);margin:0 0 10px\">How often AgentX pings each peer to check if it's alive. Mirrors <code>agentx mesh health</code>. Lower interval = faster peer-down detection (more wake noise on flaky links); higher = quieter on battery-constrained mobile peers.</p>\n <div class=\"rowf\">\n <div><label>Interval (seconds, 5..3600)<span class=\"hint\">(default 60)</span></label><input id=\"mh-interval\" type=\"number\" min=\"5\" max=\"3600\" /></div>\n <div><label>Timeout (seconds, 1..60)<span class=\"hint\">(default 10)</span></label><input id=\"mh-timeout\" type=\"number\" min=\"1\" max=\"60\" /></div>\n </div>\n <div class=\"actions\"><button class=\"primary\" onclick=\"saveMeshHealth()\">Save</button><div id=\"mh-msg\" class=\"msg\"></div></div>\n </div>\n </details>\n </section>\n\n <section id=\"tab-business\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.tokens,\n title: \"Business layer\",\n lead: \"Org chart, projects, and contact map — the data that drives PM gating and activity-graph attribution. Mirrors <code>agentx business</code>.\",\n })}\n ${witBanner({\n persistKey: \"business\",\n bodyHtml: `<b>Three concepts.</b> The <b>org chart</b> is who reports to whom (<code>reportsTo</code>). <b>Projects</b> map an id to a PM and client (drives the PM gate and lets the activity graph attribute work to the right client). The <b>contact map</b> tells the activity graph which client a Telegram/WhatsApp chat belongs to — without it, free-text channels fall into the catch-all \"internal\" bucket. Editing here writes to <code>agentx.json</code>; restart the daemon (or POST /reload) for the change to take effect.`,\n })}\n\n <div class=\"ax-stack\" style=\"margin-top:14px\">\n <h3 style=\"margin:0 0 6px;font-size:13px\">Org chart</h3>\n <div id=\"business-org-list\"></div>\n <details class=\"add-form\" style=\"margin-top:10px\">\n <summary class=\"primary\">+ Add or update org-chart entry</summary>\n <div style=\"margin-top:10px\">\n <label>Agent id<span class=\"hint\">(must match a configured agent — e.g. <code>devops-agent</code>)</span></label>\n <input id=\"bo-agentId\" placeholder=\"devops-agent\" />\n <label>Role title</label>\n <input id=\"bo-role\" placeholder=\"DevOps\" />\n <label>Reports to <span class=\"hint\">(another agent id; leave blank for top-level)</span></label>\n <input id=\"bo-reportsTo\" placeholder=\"coo-agent\" />\n <label>Schedule <span class=\"hint\">(start–end · days, e.g. <code>09:00 17:00 mon,tue,wed,thu,fri</code>)</span></label>\n <div style=\"display:flex;gap:6px\"><input id=\"bo-start\" placeholder=\"09:00\" style=\"width:80px\" /><input id=\"bo-end\" placeholder=\"17:00\" style=\"width:80px\" /><input id=\"bo-days\" placeholder=\"mon,tue,wed,thu,fri\" /></div>\n <div class=\"actions\"><button class=\"primary\" onclick=\"upsertOrgEntry()\">Save entry</button><div id=\"bo-msg\" class=\"msg\"></div></div>\n </div>\n </details>\n </div>\n\n <div class=\"ax-stack\" style=\"margin-top:24px\">\n <h3 style=\"margin:0 0 6px;font-size:13px\">Projects</h3>\n <div id=\"business-project-list\"></div>\n <details class=\"add-form\" style=\"margin-top:10px\">\n <summary class=\"primary\">+ Add or update project</summary>\n <div style=\"margin-top:10px\">\n <label>Project id<span class=\"hint\">(<code>owner/repo</code> for GitLab/GitHub; stable string for internal projects)</span></label>\n <input id=\"bp-id\" placeholder=\"globex/system\" />\n <label>PM <span class=\"hint\">(agentId — drives the PM gate)</span></label>\n <input id=\"bp-pm\" placeholder=\"pm-globex\" />\n <label>Client <span class=\"hint\">(used by the activity graph to attribute traffic)</span></label>\n <input id=\"bp-client\" placeholder=\"globex\" />\n <div class=\"actions\"><button class=\"primary\" onclick=\"upsertProject()\">Save project</button><div id=\"bp-msg\" class=\"msg\"></div></div>\n </div>\n </details>\n </div>\n\n <div class=\"ax-stack\" style=\"margin-top:24px\">\n <h3 style=\"margin:0 0 6px;font-size:13px\">Clients</h3>\n <p class=\"hint\" style=\"margin:0 0 8px\">Clients are derived from projects and the contact map &mdash; one exists as soon as either mentions it. Everything below is optional: set it only when you want the monitor to treat a client differently.</p>\n <div id=\"business-client-list\"></div>\n <details class=\"add-form\" style=\"margin-top:10px\">\n <summary class=\"primary\">+ Configure a client</summary>\n <div style=\"margin-top:10px\">\n <label>Client id<span class=\"hint\">(as it already appears on a project or contact)</span></label>\n <input id=\"bc-id\" placeholder=\"globex\" />\n <label>Display name <span class=\"hint\">(optional)</span></label>\n <input id=\"bc-name\" placeholder=\"Globex\" />\n <label>Kind <span class=\"hint\">(client = someone waiting on you; own = your product; internal = machinery)</span></label>\n <select id=\"bc-kind\"><option value=\"\">(default)</option><option value=\"client\">client</option><option value=\"own\">own</option><option value=\"internal\">internal</option></select>\n <label>Respond within <span class=\"hint\">(<code>4h</code>, <code>90m</code>, <code>2d</code> &mdash; blank means no clock, delay stays flat)</span></label>\n <input id=\"bc-respond\" placeholder=\"4h\" />\n <label>Standing authorizations <span class=\"hint\">(comma-separated; <code>*</code> for all. Ignored for kind <code>client</code> &mdash; agents never act unattended on a client's work)</span></label>\n <input id=\"bc-standing\" placeholder=\"rebase-branch, open-followup-issue\" />\n <div class=\"actions\"><button class=\"primary\" onclick=\"upsertClient()\">Save client</button><div id=\"bc-msg\" class=\"msg\"></div></div>\n </div>\n </details>\n </div>\n\n <div class=\"ax-stack\" style=\"margin-top:24px\">\n <h3 style=\"margin:0 0 6px;font-size:13px\">Contact map</h3>\n <div id=\"business-contact-list\"></div>\n <details class=\"add-form\" style=\"margin-top:10px\">\n <summary class=\"primary\">+ Add contact mapping</summary>\n <div style=\"margin-top:10px\">\n <label>Channel <span class=\"hint\">(telegram | whatsapp — optional)</span></label>\n <input id=\"bc-channel\" placeholder=\"telegram\" />\n <label>Chat id <span class=\"hint\">(e.g. <code>-100…</code>, JID — pick at least one of these three)</span></label>\n <input id=\"bc-chatId\" placeholder=\"-1003861455814\" />\n <label>Username</label>\n <input id=\"bc-username\" placeholder=\"alex\" />\n <label>Sender id</label>\n <input id=\"bc-senderId\" placeholder=\"8500203323\" />\n <label>Client <span class=\"hint\">(required)</span></label>\n <input id=\"bc-client\" placeholder=\"acme\" />\n <label>Project <span class=\"hint\">(optional — defaults to <code>&lt;client&gt;/_chat</code>)</span></label>\n <input id=\"bc-project\" placeholder=\"acme/internal\" />\n <label>Display name <span class=\"hint\">(initiator pill override)</span></label>\n <input id=\"bc-displayName\" placeholder=\"Alex Rivera\" />\n <div class=\"actions\"><button class=\"primary\" onclick=\"upsertContact()\">Save mapping</button><div id=\"bc-msg\" class=\"msg\"></div></div>\n </div>\n </details>\n </div>\n </section>\n\n <section id=\"tab-boards-cfg\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.tokens,\n title: \"Kanban boards\",\n lead: \"Configure the boards rendered on the home page. Mirrors <code>agentx board add/edit/remove</code> + <code>agentx board column</code>. Source is GitLab today; backlog/wiki sources land when the schema unlocks them.\",\n })}\n ${witBanner({\n persistKey: \"boards-cfg\",\n bodyHtml: `<b>Two layers.</b> A board points to one or more GitLab projects (<code>source.projects</code>) and optionally filters by a primary tool label. Inside each board, <b>columns</b> map drag-drop actions to scoped-labels (e.g. dropping a card on \"Doing\" adds <code>Status::Doing</code>). The default flow is Open → To Do → Doing → On Hold → Review → Closed; override per-board with the column controls below.`,\n })}\n\n <div class=\"ax-stack\" style=\"margin-top:14px\">\n <h3 style=\"margin:0 0 6px;font-size:13px\">Boards</h3>\n <div id=\"boards-cfg-list\"></div>\n <details class=\"add-form\" style=\"margin-top:10px\">\n <summary class=\"primary\">+ Add or update board</summary>\n <div style=\"margin-top:10px\">\n <label>Board id<span class=\"hint\">(unique slug, e.g. <code>globex</code>)</span></label>\n <input id=\"bd-id\" placeholder=\"globex\" />\n <label>Display name</label>\n <input id=\"bd-name\" placeholder=\"Globex System\" />\n <label>GitLab project paths<span class=\"hint\">(comma-separated, e.g. <code>globex/system,globex/website</code>)</span></label>\n <input id=\"bd-projects\" placeholder=\"globex/system\" />\n <label>Primary tool label <span class=\"hint\">(optional — ANDed into every query, e.g. <code>Tool::Claude</code>)</span></label>\n <input id=\"bd-label\" placeholder=\"Tool::Claude\" />\n <div style=\"display:flex;gap:6px\"><label style=\"flex:1\">Open-window days<input id=\"bd-days\" type=\"number\" min=\"1\" max=\"365\" value=\"30\" /></label><label style=\"flex:1\">Closed-window days<input id=\"bd-closed-days\" type=\"number\" min=\"1\" max=\"365\" value=\"30\" /></label></div>\n <div class=\"actions\"><button class=\"primary\" onclick=\"upsertBoardCfg()\">Save board</button><div id=\"bd-msg\" class=\"msg\"></div></div>\n </div>\n </details>\n </div>\n </section>\n\n <section id=\"tab-actions\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.actions,\n title: \"Action registry\",\n lead: \"Reusable shell commands and HTTP calls. Workflows invoke them by id; the dashboard runs them on demand for smoke-tests. Mirrors <code>agentx actions</code>.\",\n })}\n ${witBanner({\n persistKey: \"actions\",\n bodyHtml: `<b>Two flavors.</b> <code>shell</code> runs a templated command on the daemon host (cwd + extra env vars supported). <code>http</code> calls a URL with templated method/headers/body. Inputs are typed (string / number / boolean) and fill <code>{{name}}</code> markers; <code>$VAR</code> resolves against the daemon's process env. Output capped at 32KB.`,\n })}\n\n <div class=\"ax-stack\" style=\"margin-top:14px\">\n <h3 style=\"margin:0 0 6px;font-size:13px\">Registered actions</h3>\n <div id=\"actions-list\"></div>\n <details class=\"add-form\" style=\"margin-top:10px\">\n <summary class=\"primary\">+ Add or update action</summary>\n <div style=\"margin-top:10px\">\n <label>Id<span class=\"hint\">(unique slug — e.g. <code>deploy-staging</code>)</span></label>\n <input id=\"ac-id\" placeholder=\"deploy-staging\" />\n <label>Title</label>\n <input id=\"ac-title\" placeholder=\"Deploy staging\" />\n <label>Description<span class=\"hint\">(optional)</span></label>\n <input id=\"ac-desc\" placeholder=\"Build, push, restart staging\" />\n <label>Kind</label>\n <select id=\"ac-kind\">\n <option value=\"shell\">shell</option>\n <option value=\"http\">http</option>\n </select>\n\n <div id=\"ac-shell-fields\">\n <label>Command<span class=\"hint\">(supports <code>{{input}}</code> + <code>$ENV</code>)</span></label>\n <input id=\"ac-command\" placeholder=\"git pull && pnpm deploy --version={{version}}\" />\n <label>Working directory<span class=\"hint\">(optional)</span></label>\n <input id=\"ac-cwd\" placeholder=\"/srv/staging\" />\n </div>\n\n <div id=\"ac-http-fields\" style=\"display:none\">\n <label>URL</label>\n <input id=\"ac-url\" placeholder=\"https://api.example.com/deploy\" />\n <label>Method</label>\n <select id=\"ac-method\">\n <option value=\"POST\">POST</option>\n <option value=\"GET\">GET</option>\n <option value=\"PUT\">PUT</option>\n <option value=\"PATCH\">PATCH</option>\n <option value=\"DELETE\">DELETE</option>\n </select>\n <label>Headers<span class=\"hint\">(JSON object, optional)</span></label>\n <input id=\"ac-headers\" placeholder='{\"Authorization\":\"Bearer $TOKEN\"}' />\n <label>Body<span class=\"hint\">(template — JSON or text, skipped on GET/DELETE)</span></label>\n <textarea id=\"ac-body\" rows=\"3\" placeholder='{\"version\":\"{{version}}\"}'></textarea>\n </div>\n\n <label>Inputs<span class=\"hint\">(comma-separated <code>name:type[!]</code> — <code>!</code> = required. e.g. <code>version:string!,dryRun:boolean</code>)</span></label>\n <input id=\"ac-inputs\" placeholder=\"version:string!,dryRun:boolean\" />\n <label>Timeout (ms)</label>\n <input id=\"ac-timeout\" type=\"number\" min=\"100\" max=\"600000\" value=\"30000\" />\n <div class=\"actions\"><button class=\"primary\" onclick=\"upsertAction()\">Save action</button><div id=\"ac-msg\" class=\"msg\"></div></div>\n </div>\n </details>\n </div>\n </section>\n\n <section id=\"tab-tokens\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.tokens,\n title: \"Access tokens\",\n lead: \"Passwords, but for software. Anything that wants to talk to AgentX from outside (a Slack bridge, another AgentX machine, a script) uses one.\",\n })}\n ${witBanner({\n persistKey: \"tokens\",\n bodyHtml: `<b>Tokens get scopes.</b> A scope is what the token is allowed to do. Give the narrowest one that works — a Slack bridge only needs to send messages, it doesn't need to edit settings. You can't recover a token after you close this page, so copy it immediately.`,\n })}\n <div id=\"token-list\" class=\"ax-stack\"></div>\n <div class=\"add-form\">\n <h3>Mint a new token</h3>\n <label>Name<span class=\"hint\">(who or what this is for — e.g. \"Slack bridge\", \"Mesh peer: laptop\")</span></label>\n <input id=\"t-name\" placeholder=\"Slack bridge\" />\n <label>Scopes<span class=\"hint\">(pick one or more)</span></label>\n <div id=\"t-scopes\" style=\"display:flex;flex-direction:column;gap:4px;margin-top:4px\"></div>\n <label>Expires after<span class=\"hint\">(in days; blank = never)</span></label>\n <input id=\"t-expires\" type=\"number\" min=\"1\" max=\"3650\" placeholder=\"90\" />\n <div class=\"actions\"><button class=\"primary\" onclick=\"createToken()\">Create token</button><div id=\"t-msg\" class=\"msg\"></div></div>\n <div id=\"t-reveal\" style=\"display:none;margin-top:14px;padding:14px 16px;border-radius:6px;background:rgba(34,197,94,0.08);border:1px solid rgba(34,197,94,0.3)\">\n <div style=\"font-size:12px;color:var(--muted);margin-bottom:6px\">Copy this now — it won't be shown again.</div>\n <code id=\"t-secret\" style=\"display:block;word-break:break-all;color:var(--green);font-family:ui-monospace,monospace;font-size:12px;padding:8px;background:#0e1119;border-radius:4px\"></code>\n </div>\n </div>\n </section>\n\n <section id=\"tab-advanced\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.advanced,\n title: \"Raw configuration\",\n lead: \"Everything on the other tabs writes to this file. Edit here for bulk changes, diffs, or fields we haven't put in the UI yet. Saving creates a timestamped backup.\",\n })}\n ${witBanner({\n persistKey: \"advanced\",\n bodyHtml: `<b>Safe to look, careful editing.</b> We've pretty-printed and annotated each section so you can read it. Click any <span class=\"mono\" style=\"color:var(--ax-muted)\">▸</span> to fold. Switch to <b>Edit</b> to change values — invalid JSON is refused before saving.`,\n })}\n\n <div class=\"ax-jv-toolbar\">\n <input class=\"ax-jv-search\" id=\"jv-search\" placeholder=\"Search keys & values…\" />\n <div class=\"ax-jv-seg\" id=\"jv-mode\">\n <button data-jv-mode=\"tree\" class=\"is-active\">Tree</button>\n <button data-jv-mode=\"raw\">Raw</button>\n <button data-jv-mode=\"edit\">Edit</button>\n </div>\n <div class=\"ax-jv-spacer\"></div>\n <span class=\"muted mono\" id=\"jv-meta\" style=\"font-size:11px\">agentx.json</span>\n <button class=\"ax-btn\" id=\"jv-expand-all\">Expand all</button>\n <button class=\"ax-btn\" id=\"jv-collapse-all\">Collapse all</button>\n <button class=\"ax-btn ax-btn--primary\" id=\"jv-save\">Save</button>\n </div>\n <div class=\"ax-jv-viewer\" id=\"jv-viewer\"></div>\n <textarea id=\"raw-editor\" class=\"raw\" style=\"display:none\" spellcheck=\"false\"></textarea>\n <div id=\"r-msg\" class=\"msg\" style=\"margin-top:10px\"></div>\n <div class=\"actions\">\n <button class=\"primary\" onclick=\"saveRaw()\">Save config</button>\n <button class=\"ghost\" onclick=\"loadRaw()\">Reload from disk</button>\n <div id=\"r-msg\" class=\"msg\"></div>\n </div>\n <div class=\"hint-block\">The schema is documented at <code>docs/reference/config-schema.md</code>. Invalid JSON is refused; schema-level errors only surface after the daemon tries to use the new config.</div>\n </section>\n</main>\n`\nconst ADMIN_PAGE_CSS = `\n\n/* --- Inner tabs (Agents / Channels / Schedules / …) --- */\nnav.tabs{display:flex;gap:4px;padding:14px 24px 0;background:var(--ax-bg-elev);\n border-bottom:1px solid var(--ax-border);overflow-x:auto}\nnav.tabs button{background:transparent;border:none;color:var(--ax-muted);\n padding:8px 14px;font:inherit;font-size:12px;cursor:pointer;\n border-bottom:2px solid transparent;letter-spacing:-0.005em;\n border-top-left-radius:4px;border-top-right-radius:4px;\n transition:color 120ms,border-color 120ms,background 120ms}\nnav.tabs button:hover{color:var(--ax-text);background:color-mix(in oklch,var(--ax-accent) 4%,transparent)}\nnav.tabs button.active{color:var(--ax-text);border-bottom-color:var(--ax-accent);\n background:color-mix(in oklch,var(--ax-accent) 8%,transparent)}\n\nmain{max-width:960px;margin:0 auto;padding:20px 24px 60px}\nsection.tab{display:none}\nsection.tab.active{display:block}\n\nh2{font-size:16px;margin:0 0 4px;font-weight:600;letter-spacing:-0.01em}\n.lead{color:var(--ax-muted);margin:0 0 20px;font-size:12px;line-height:1.55;max-width:720px}\n\n/* --- Row cards --- */\n.list{display:flex;flex-direction:column;gap:6px;margin-bottom:22px}\n.row-card{background:var(--ax-surface);border:1px solid var(--ax-border);\n border-radius:6px;padding:12px 14px;display:flex;align-items:center;gap:12px;\n transition:border-color 120ms}\n.row-card:hover{border-color:var(--ax-border-2)}\n.row-card .info{flex:1;min-width:0}\n.row-card .info h3{margin:0 0 2px;font-size:13px;font-weight:600;letter-spacing:-0.005em}\n.row-card .info .meta{font-size:11px;color:var(--ax-muted);font-family:var(--ax-mono);\n overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.5}\n.row-card .info .meta b{color:var(--ax-text);font-weight:500}\n.row-card .info .meta a{color:var(--ax-accent)}\n\n/* --- Buttons --- */\nbutton.primary,button.ghost,button.danger{font-family:inherit;font-size:12px;cursor:pointer;\n border-radius:4px;transition:background 120ms,border-color 120ms,color 120ms}\nbutton.primary{background:color-mix(in oklch,var(--ax-accent) 12%,var(--ax-surface));\n border:1px solid color-mix(in oklch,var(--ax-accent) 40%,var(--ax-border-2));\n color:var(--ax-accent);padding:6px 14px;font-weight:500}\nbutton.primary:hover{background:color-mix(in oklch,var(--ax-accent) 20%,var(--ax-surface))}\nbutton.primary:disabled{opacity:0.4;cursor:not-allowed}\nbutton.ghost{background:var(--ax-surface);border:1px solid var(--ax-border-2);\n color:var(--ax-text);padding:6px 12px}\nbutton.ghost:hover{background:var(--ax-surface-2);border-color:var(--ax-accent)}\nbutton.danger{background:transparent;color:var(--ax-err);\n border:1px solid color-mix(in oklch,var(--ax-err) 35%,transparent);padding:5px 11px}\nbutton.danger:hover{background:color-mix(in oklch,var(--ax-err) 10%,transparent);\n border-color:color-mix(in oklch,var(--ax-err) 55%,transparent)}\n\n/* --- Forms --- */\n.add-form{background:var(--ax-surface);border:1px solid var(--ax-border);\n border-radius:6px;padding:16px 18px;margin-top:8px}\n.add-form h3{margin:0 0 10px;font-size:12px;font-weight:600;\n text-transform:uppercase;letter-spacing:0.06em;color:var(--ax-muted)}\nlabel{display:block;margin:8px 0 4px;font-size:11px;color:var(--ax-muted);\n text-transform:uppercase;letter-spacing:0.04em}\nlabel .hint{font-weight:400;font-size:11px;margin-left:6px;\n text-transform:none;letter-spacing:normal;color:var(--ax-muted);opacity:0.85}\ninput,textarea,select{width:100%;background:var(--ax-bg);color:var(--ax-text);\n border:1px solid var(--ax-border);border-radius:4px;padding:7px 10px;\n font:inherit;font-size:12px;transition:border-color 120ms}\ninput:focus,textarea:focus,select:focus{outline:none;border-color:var(--ax-accent)}\ntextarea{resize:vertical;min-height:64px;line-height:1.55}\ntextarea.raw{min-height:480px;font-family:var(--ax-mono);font-size:12px;line-height:1.6}\n\n.rowf{display:flex;gap:10px}\n.rowf > *{flex:1}\n.actions{display:flex;gap:8px;margin-top:14px;align-items:center;flex-wrap:wrap}\n\n/* --- Messages --- */\n.msg{margin:10px 0;padding:8px 12px;border-radius:4px;font-size:12px;\n display:none;line-height:1.5}\n.msg.ok{display:block;background:color-mix(in oklch,var(--ax-accent) 10%,transparent);\n color:var(--ax-accent);border:1px solid color-mix(in oklch,var(--ax-accent) 35%,transparent)}\n.msg.err{display:block;background:color-mix(in oklch,var(--ax-err) 10%,transparent);\n color:var(--ax-err);border:1px solid color-mix(in oklch,var(--ax-err) 35%,transparent)}\n.msg.warn{display:block;background:color-mix(in oklch,var(--ax-warn) 10%,transparent);\n color:var(--ax-warn);border:1px solid color-mix(in oklch,var(--ax-warn) 35%,transparent)}\n\n.empty{color:var(--ax-muted);font-style:italic;padding:28px 12px;font-size:12px;\n text-align:center;border:1px dashed var(--ax-border);border-radius:4px}\n\n/* --- Toggles + chips --- */\n.toggle-switch{display:flex;align-items:center;gap:10px;margin-bottom:14px;font-size:12px}\n.toggle-switch input{width:auto;accent-color:var(--ax-accent)}\n.chip{display:inline-block;font-size:10px;padding:1px 7px;line-height:16px;\n border-radius:3px;background:var(--ax-surface);border:1px solid var(--ax-border);\n color:var(--ax-muted);font-family:var(--ax-mono);margin-right:4px;\n text-transform:uppercase;letter-spacing:0.02em}\n.chip.off{background:var(--ax-surface-2);color:var(--ax-muted)}\n.hint-block{font-size:11px;color:var(--ax-muted);margin-top:8px;line-height:1.55}\n.hint-block code{background:var(--ax-bg);padding:1px 6px;border-radius:3px;\n color:var(--ax-text);font-family:var(--ax-mono);font-size:11px;\n border:1px solid var(--ax-border)}\n.section-block{margin-bottom:28px}\n\n/* --- Channels sidemenu --- */\n.ch-split{display:grid;grid-template-columns:220px 1fr;gap:16px;align-items:start}\n@media (max-width:720px){.ch-split{grid-template-columns:1fr}}\n.ch-menu{background:var(--ax-surface);border:1px solid var(--ax-border);\n border-radius:6px;padding:6px;display:flex;flex-direction:column;gap:2px;\n position:sticky;top:120px}\n.ch-menu button{background:transparent;border:1px solid transparent;\n color:var(--ax-text-2);padding:8px 10px;border-radius:4px;cursor:pointer;\n font:inherit;font-size:12px;text-align:left;display:flex;align-items:center;gap:10px;\n transition:background 120ms,border-color 120ms,color 120ms}\n.ch-menu button:hover{background:var(--ax-surface-2);color:var(--ax-text)}\n.ch-menu button.is-active{background:color-mix(in oklch,var(--ax-accent) 12%,var(--ax-surface));\n color:var(--ax-text);border-color:color-mix(in oklch,var(--ax-accent) 35%,var(--ax-border-2))}\n.ch-menu .ch-icon{font-family:var(--ax-mono);font-size:10px;line-height:16px;\n width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;\n background:var(--ax-bg);border:1px solid var(--ax-border);border-radius:3px;\n color:var(--ax-muted);flex-shrink:0}\n.ch-menu .ch-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ch-menu .ch-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;background:var(--ax-border-2)}\n.ch-menu .ch-dot--on{background:var(--ax-accent)}\n.ch-menu .ch-dot--off{background:var(--ax-warn)}\n.ch-view{display:block}\n.ch-view[hidden]{display:none}\n\n/* --- Files modal --- */\n.files-split{display:flex;flex:1;min-height:0}\n.files-picker{width:260px;min-width:220px;border-right:1px solid var(--ax-border);\n overflow-y:auto;padding:10px 8px;background:var(--ax-bg-elev)}\n.files-picker .group{font-size:10px;text-transform:uppercase;letter-spacing:0.06em;\n color:var(--ax-muted);padding:8px 10px 4px;font-weight:600}\n.files-picker .file{display:flex;align-items:center;gap:8px;padding:8px 10px;\n border-radius:4px;cursor:pointer;font-size:12px;color:var(--ax-text);\n border-left:2px solid transparent;transition:background 120ms}\n.files-picker .file:hover{background:color-mix(in oklch,var(--ax-accent) 6%,transparent)}\n.files-picker .file.active{background:color-mix(in oklch,var(--ax-accent) 12%,transparent);\n border-left-color:var(--ax-accent);color:var(--ax-text)}\n.files-picker .file.missing{color:var(--ax-muted);font-style:italic}\n.files-picker .file .info{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.files-picker .file .size{font-size:10px;color:var(--ax-muted);font-family:var(--ax-mono)}\n.files-picker .file .del{background:transparent;border:none;color:var(--ax-muted);\n cursor:pointer;padding:0 4px;font-size:13px}\n.files-picker .file .del:hover{color:var(--ax-err)}\n.files-editor{flex:1;display:flex;flex-direction:column;overflow:hidden}\n.files-editor-empty{flex:1;display:flex;align-items:center;justify-content:center;\n color:var(--ax-muted);font-style:italic;padding:40px;text-align:center;font-size:12px}\n.files-editor-head{padding:10px 16px;border-bottom:1px solid var(--ax-border);\n display:flex;align-items:center;gap:10px;background:var(--ax-bg-elev)}\n.files-editor-head .path{flex:1;font-family:var(--ax-mono);font-size:12px;color:var(--ax-muted)}\n.files-editor-body{flex:1;overflow:hidden;display:flex}\n.files-editor-body textarea{flex:1;background:var(--ax-bg);color:var(--ax-text);\n border:none;padding:14px 16px;font-family:var(--ax-mono);font-size:12px;line-height:1.6;resize:none}\n.files-editor-body textarea:focus{outline:none}\n.files-add{border-top:1px solid var(--ax-border);padding:10px 16px;background:var(--ax-bg-elev)}\n.files-add h4{margin:0 0 8px;font-size:11px;font-weight:600;color:var(--ax-muted);\n text-transform:uppercase;letter-spacing:0.06em}\n\n/* --- Test drive chat modal --- */\n.td-modal{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;\n justify-content:center;padding:24px}\n.td-modal.hidden{display:none}\n.td-backdrop{position:absolute;inset:0;background:color-mix(in oklch,var(--ax-bg) 60%,black)}\n.td-card{position:relative;width:min(640px,94vw);height:min(720px,90vh);\n background:var(--ax-surface);border:1px solid var(--ax-border-2);border-radius:8px;\n display:flex;flex-direction:column;box-shadow:0 18px 48px rgba(0,0,0,0.5);overflow:hidden}\n.td-card > header{display:flex;align-items:center;gap:10px;padding:12px 16px;\n border-bottom:1px solid var(--ax-border);background:var(--ax-bg-elev)}\n.td-card > header h3{margin:0;font-size:14px;font-weight:600;flex:1;letter-spacing:-0.005em}\n.td-card .chip-small{font-size:10px;padding:2px 7px;border-radius:3px;\n background:color-mix(in oklch,var(--ax-accent) 14%,transparent);\n color:var(--ax-accent);letter-spacing:0.04em;text-transform:uppercase;font-family:var(--ax-mono)}\n.td-close{background:transparent;border:none;color:var(--ax-muted);font-size:22px;\n cursor:pointer;padding:0 6px;line-height:1}\n.td-close:hover{color:var(--ax-text)}\n.td-body{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;\n gap:10px;background:var(--ax-bg)}\n.td-msg{max-width:82%;padding:8px 12px;border-radius:10px;font-size:13px;\n line-height:1.5;word-wrap:break-word;white-space:pre-wrap}\n.td-msg.user{align-self:flex-end;\n background:color-mix(in oklch,var(--ax-accent) 75%,var(--ax-bg));\n color:var(--ax-bg);font-weight:500;border-bottom-right-radius:2px}\n.td-msg.agent{align-self:flex-start;background:var(--ax-surface);color:var(--ax-text);\n border:1px solid var(--ax-border);border-bottom-left-radius:2px}\n.td-msg.err{align-self:flex-start;background:color-mix(in oklch,var(--ax-err) 10%,transparent);\n color:var(--ax-err);border:1px solid color-mix(in oklch,var(--ax-err) 35%,transparent);\n border-bottom-left-radius:2px}\n.td-msg.thinking{align-self:flex-start;color:var(--ax-muted);font-style:italic;\n background:transparent;padding:4px 8px}\n.td-empty{color:var(--ax-muted);text-align:center;padding:40px 12px;font-style:italic;font-size:13px}\n.td-footer{border-top:1px solid var(--ax-border);padding:10px 12px;display:flex;\n gap:8px;align-items:flex-end;background:var(--ax-bg-elev)}\n.td-footer textarea{flex:1;background:var(--ax-bg);color:var(--ax-text);\n border:1px solid var(--ax-border);border-radius:4px;padding:8px 10px;\n font:inherit;font-size:13px;min-height:40px;max-height:120px;resize:none}\n.td-footer textarea:focus{outline:none;border-color:var(--ax-accent)}\n.td-footer button{background:color-mix(in oklch,var(--ax-accent) 20%,var(--ax-surface));\n color:var(--ax-accent);border:1px solid color-mix(in oklch,var(--ax-accent) 40%,var(--ax-border-2));\n border-radius:4px;padding:8px 16px;font-weight:500;font-size:13px;cursor:pointer}\n.td-footer button:hover{background:color-mix(in oklch,var(--ax-accent) 28%,var(--ax-surface))}\n.td-footer button:disabled{opacity:0.4;cursor:not-allowed}\n.td-hint{padding:6px 16px;font-size:10px;color:var(--ax-muted);\n border-top:1px solid var(--ax-border);background:var(--ax-bg-elev);font-family:var(--ax-mono)}\n.peer-banner{display:none;padding:10px 20px;background:color-mix(in oklch,var(--ax-accent) 14%,var(--ax-surface));\n border-bottom:1px solid color-mix(in oklch,var(--ax-accent) 40%,var(--ax-border-2));\n color:var(--ax-accent);font-size:12px;align-items:center;gap:12px;font-family:var(--ax-mono)}\n.peer-banner.is-active{display:flex}\n.peer-banner .label{text-transform:uppercase;letter-spacing:0.06em;font-size:10px;opacity:0.8}\n.peer-banner .name{font-weight:600;color:var(--ax-accent)}\n.peer-banner .spacer{flex:1}\n.peer-banner button{background:transparent;border:1px solid currentColor;color:var(--ax-accent);\n padding:4px 10px;border-radius:4px;font:inherit;font-size:11px;cursor:pointer}\n.peer-banner button:hover{background:color-mix(in oklch,var(--ax-accent) 12%,transparent)}\n`\nconst ADMIN_PAGE_SCRIPT = `\nconst $ = (id) => document.getElementById(id);\nlet state = null;\n\nfunction currentPeer() {\n try { return localStorage.getItem('ax-peer') || 'primary'; } catch { return 'primary'; }\n}\nfunction setCurrentPeer(id) {\n try {\n if (id && id !== 'primary') localStorage.setItem('ax-peer', id);\n else localStorage.removeItem('ax-peer');\n } catch { /**/ }\n}\n\nasync function req(method, url, body) {\n const opts = { method, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'agentx-admin' } };\n const peer = currentPeer();\n if (peer !== 'primary') opts.headers['X-Agentx-Peer'] = peer;\n // If this dashboard has a local token configured, inject it on every\n // request so the new /api/admin/* auth gate lets us through. Peer-proxy\n // requests still work because the server-side proxy swaps this header\n // for the peer's own token before forwarding.\n if (window.AX_LOCAL_TOKEN) opts.headers['Authorization'] = 'Bearer ' + window.AX_LOCAL_TOKEN;\n if (body !== undefined) opts.body = JSON.stringify(body);\n const r = await fetch(url, opts);\n const data = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));\n return data;\n}\n\nfunction showMsg(el, kind, text) {\n el.className = 'msg ' + kind;\n el.textContent = text;\n if (kind === 'ok') setTimeout(() => { el.className = 'msg'; el.textContent = ''; }, 4000);\n}\n\nasync function refresh() {\n try {\n state = await req('GET', '/api/admin/state');\n if (!state.exists) {\n showMsg($('global-msg'), 'warn', 'No agentx.json found. Run the setup wizard first.');\n return;\n }\n renderAgents();\n renderChannels();\n renderCrons();\n renderWebhooks();\n renderMesh();\n renderBusiness();\n wireBusinessHandlers();\n renderBoardsCfg();\n wireBoardsCfgHandlers();\n renderNotifications();\n renderActions();\n wireActionsHandlers();\n wireActionKindToggle();\n wireWebhookTriggerHandlers();\n initScheduleBuilder();\n initScheduleExpertAgentSelect();\n } catch (e) {\n showMsg($('global-msg'), 'err', e.message);\n }\n}\n\nfunction renderWebhooks() {\n const list = $('webhook-list');\n const daemonUrl = (state.daemonUrl || '').replace(/\\\\/+$/, '');\n const sources = {\n gitlab: { logoBg: '#FC6D26', label: 'GitLab', init: 'GL', hint: 'In GitLab → Settings → Webhooks; tick Push/Issue/Pipeline events.', wired: true, eventTypes: ['Push Hook', 'Issue Hook', 'Note Hook', 'Merge Request Hook', 'Pipeline Hook', 'Tag Push Hook'] },\n github: { logoBg: '#24292e', label: 'GitHub', init: 'GH', hint: 'In GitHub → Repo Settings → Webhooks; pick individual events.', wired: true, eventTypes: ['issues.opened', 'issues.closed', 'issues.reopened', 'issue_comment.created', 'pull_request.opened', 'pull_request.closed', 'pull_request.reopened', 'pull_request_review.submitted', 'push', 'release.published'] },\n sentry: { logoBg: '#362D59', label: 'Sentry', init: 'SE', hint: 'In Sentry → Project Settings → Alerts → Webhooks.', wired: true, eventTypes: ['issue', 'event_alert', 'metric_alert'] },\n stripe: { logoBg: '#635BFF', label: 'Stripe', init: 'ST', hint: 'In Stripe Dashboard → Developers → Webhooks.', wired: true, eventTypes: ['invoice.paid', 'invoice.payment_failed', 'customer.subscription.created', 'customer.subscription.deleted', 'customer.subscription.updated', 'charge.succeeded', 'charge.refunded', 'checkout.session.completed', 'payment_intent.succeeded'] },\n vercel: { logoBg: '#000000', label: 'Vercel', init: 'VR', hint: 'In Vercel → Project Settings → Webhooks.', wired: true, eventTypes: ['deployment.created', 'deployment.ready', 'deployment.error', 'deployment.canceled'] },\n odoo: { logoBg: '#714B67', label: 'Odoo', init: 'OD', hint: 'In Odoo → Settings → Technical → Automation → Webhooks (or your installed webhook module). Use the URL below as target.', wired: true, eventTypes: ['res.partner.create', 'res.partner.write', 'crm.lead.create', 'crm.lead.write', 'sale.order.create', 'sale.order.action_confirm', 'account.move.posted', 'account.move.paid', 'project.task.create', 'project.task.write', 'stock.picking.done', 'hr.employee.create'] },\n hubspot: { logoBg: '#FF7A59', label: 'HubSpot', init: 'HB', hint: 'In HubSpot → Settings → Integrations → Private Apps → Webhooks. Use the URL below as target.', wired: true, eventTypes: ['contact.creation', 'contact.deletion', 'contact.propertyChange', 'company.creation', 'company.propertyChange', 'deal.creation', 'deal.deletion', 'deal.propertyChange', 'ticket.creation', 'ticket.propertyChange', 'conversation.creation', 'conversation.newMessage'] },\n discord: { logoBg: '#5865F2', label: 'Discord', init: 'DC', hint: 'In Discord → Channel Settings → Integrations → Webhooks.', wired: false, eventTypes: [] },\n slack: { logoBg: '#4A154B', label: 'Slack', init: 'SL', hint: 'Slack Outgoing Webhooks / Events API — point at the URL below.', wired: false, eventTypes: [] },\n custom: { logoBg: 'var(--ax-surface-3)', label: 'Custom', init: '?', hint: 'Any service that can POST JSON — payload is forwarded as-is.', wired: false, eventTypes: [] },\n };\n if (!state.webhooks.length) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:42px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><h3 style=\"margin:0 0 4px;color:var(--ax-text);font-size:15px;font-weight:600\">No webhooks yet</h3><p style=\"margin:0;font-size:13px\">Register one below and paste the URL into the external service.</p></div>';\n } else {\n list.innerHTML = '';\n for (const w of state.webhooks) {\n const meta = sources[w.source] || sources.custom;\n const url = daemonUrl + '/webhook/' + encodeURIComponent(w.agentId) + '/' + encodeURIComponent(w.source);\n const missingSecret = w.source !== 'custom' && !w.secretEnv;\n const statusPill = missingSecret\n ? '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>signing secret missing</span>'\n : (w.enabled\n ? '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>receiving</span>'\n : '<span class=\"ax-pill ax-pill--off\">disabled</span>');\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n if (missingSecret) card.style.borderColor = 'color-mix(in oklch, var(--ax-warn) 35%, var(--ax-border))';\n const logoStyle = 'background:' + meta.logoBg + ';color:white;border-color:' + meta.logoBg;\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar\" style=\"' + logoStyle + '\">' + meta.init + '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(w.id) + ' ' + statusPill + '</div>' +\n '<div class=\"ax-sub\">' +\n escapeHtml(meta.label) + ' · routes to <b>' + escapeHtml(w.agentId) + '</b>' +\n (w.secretEnv ? ' · secret: <code>${' + escapeHtml(w.secretEnv) + '}</code>' : '') +\n (w.description ? ' · ' + escapeHtml(w.description) : '') +\n '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n '<button class=\"ax-btn\" data-toggle-wh=\"' + escapeHtml(w.id) + '\" data-enabled=\"' + (w.enabled ? '1' : '0') + '\">' + (w.enabled ? 'Disable' : 'Enable') + '</button>' +\n '<button class=\"ax-btn ax-btn--danger\" data-rm-wh=\"' + escapeHtml(w.id) + '\">Delete</button>' +\n '</div>' +\n '</div>' +\n '<div style=\"display:flex;align-items:center;gap:8px;background:var(--ax-bg);border:1px solid var(--ax-border);border-radius:5px;padding:8px 10px;font-family:var(--ax-mono);font-size:11.5px;color:var(--ax-text);margin-top:10px\">' +\n '<span class=\"muted\">POST</span>' +\n '<span style=\"flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\">' + escapeHtml(url) + '</span>' +\n '<button class=\"ax-btn ax-btn--ghost\" style=\"padding:3px 9px;font-size:11px\" data-copy=\"' + escapeHtml(url) + '\">Copy</button>' +\n '</div>' +\n '<div style=\"font-size:11px;color:var(--ax-muted);margin-top:6px\">' + escapeHtml(meta.hint) + '</div>';\n\n // Triggers + defaultWorkflow editor — collapsed by default. Maps a\n // platform event-type (e.g. \"issues.opened\") to a workflow id; the\n // defaultWorkflow runs when no specific trigger matches.\n // Autocomplete the event-type input from the source's known events.\n const triggers = (w.triggers && typeof w.triggers === 'object') ? w.triggers : {};\n const triggerRows = Object.keys(triggers).map(function(evt){\n return '<div style=\"display:flex;gap:6px;align-items:center;margin-top:4px\"><code style=\"flex:1;font-size:11px\">' + escapeHtml(evt) + ' → ' + escapeHtml(triggers[evt]) + '</code><a href=\"#\" data-act=\"wh-trig-rm\" data-id=\"' + escapeHtml(w.id) + '\" data-event=\"' + escapeHtml(evt) + '\" style=\"color:var(--ax-muted);font-size:11px\">×</a></div>';\n }).join('');\n const datalistId = 'wh-events-' + escapeHtml(w.id);\n const eventOptions = (meta.eventTypes || []).map(function(e){ return '<option value=\"' + escapeHtml(e) + '\"></option>'; }).join('');\n const wiredBadge = meta.wired\n ? '<span style=\"font-size:10px;color:var(--ax-ok,#27ae60);margin-left:6px\">✓ emits on:' + escapeHtml(w.source) + '-* hook events</span>'\n : '<span style=\"font-size:10px;color:var(--ax-muted);margin-left:6px\">⚠ inbound-only — no on:* hook events emitted</span>';\n const trigBlock = document.createElement('details');\n trigBlock.style.marginTop = '8px';\n trigBlock.innerHTML =\n '<summary style=\"font-size:11px;cursor:pointer;color:var(--ax-accent,#3a7bd5)\">Routing — event-type triggers + default workflow' + wiredBadge + '</summary>' +\n '<div style=\"margin-top:8px;padding:8px 10px;background:var(--ax-surface);border-radius:4px\">' +\n '<div style=\"font-size:11px;color:var(--ax-muted);margin-bottom:4px\">Event-type → workflow id' + (meta.eventTypes && meta.eventTypes.length ? ' (start typing for ' + meta.label + ' events)' : '') + ':</div>' +\n (triggerRows || '<div style=\"font-size:11px;color:var(--ax-muted);font-style:italic\">no triggers — every event uses the default workflow below</div>') +\n '<datalist id=\"' + datalistId + '\">' + eventOptions + '</datalist>' +\n '<div style=\"display:flex;gap:6px;margin-top:6px\">' +\n '<input data-wh-trig-event list=\"' + datalistId + '\" placeholder=\"event-type\" style=\"flex:1;font-size:11px\" />' +\n '<input data-wh-trig-wf placeholder=\"workflow id\" style=\"flex:1;font-size:11px\" />' +\n '<button data-act=\"wh-trig-add\" data-id=\"' + escapeHtml(w.id) + '\" class=\"ax-btn\" style=\"padding:3px 9px;font-size:11px\">Add</button>' +\n '</div>' +\n '<div style=\"margin-top:10px;padding-top:8px;border-top:1px dashed var(--ax-border)\">' +\n '<div style=\"font-size:11px;color:var(--ax-muted);margin-bottom:4px\">Default workflow (fires when no trigger matches):</div>' +\n '<div style=\"display:flex;gap:6px\">' +\n '<input data-wh-default value=\"' + escapeHtml(w.defaultWorkflow || '') + '\" placeholder=\"(none)\" style=\"flex:1;font-size:11px\" />' +\n '<button data-act=\"wh-default-save\" data-id=\"' + escapeHtml(w.id) + '\" class=\"ax-btn\" style=\"padding:3px 9px;font-size:11px\">Save</button>' +\n '</div>' +\n '</div>' +\n '</div>';\n card.appendChild(trigBlock);\n\n card.querySelector('[data-copy]').addEventListener('click', (e) => {\n const u = e.currentTarget.dataset.copy;\n navigator.clipboard.writeText(u).catch(() => {});\n if (window.showToast) window.showToast('URL copied');\n });\n card.querySelector('[data-rm-wh]').addEventListener('click', () => deleteWebhookAction(w.id));\n card.querySelector('[data-toggle-wh]').addEventListener('click', () => toggleWebhook(w.id, !w.enabled));\n list.appendChild(card);\n }\n }\n // Refresh the add-webhook agent picker.\n const sel = $('w-agent');\n const cur = sel.value;\n sel.innerHTML = state.agents.map((a) => '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>').join('');\n if (cur) sel.value = cur;\n}\n\nasync function addWebhook() {\n const body = {\n id: $('w-id').value.trim(),\n source: $('w-source').value,\n agentId: $('w-agent').value,\n secretEnv: $('w-secret').value.trim(),\n description: $('w-desc').value.trim(),\n };\n try {\n const r = await req('POST', '/api/admin/webhooks', body);\n showMsg($('w-msg'), 'ok', r.summary);\n $('w-id').value = ''; $('w-secret').value = ''; $('w-desc').value = '';\n refresh();\n } catch (e) { showMsg($('w-msg'), 'err', e.message); }\n}\n\nasync function deleteWebhookAction(id) {\n if (!confirm('Delete webhook \"' + id + '\"?')) return;\n try { await req('DELETE', '/api/admin/webhooks', { id }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nasync function toggleWebhook(id, enabled) {\n try { await req('PATCH', '/api/admin/webhooks', { id, patch: { enabled } }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nfunction renderMesh() {\n const m = state.mesh || { enabled: false, peers: [], healthCheck: { interval: 60, timeout: 10 } };\n const hc = m.healthCheck || { interval: 60, timeout: 10 };\n if ($('mh-interval')) $('mh-interval').value = hc.interval ?? 60;\n if ($('mh-timeout')) $('mh-timeout').value = hc.timeout ?? 10;\n\n // Hero card: status message + enable toggle + SVG network viz.\n const hero = $('mesh-hero');\n if (hero) {\n const count = m.peers.length;\n const nodeName = (state.node && state.node.name) || 'this machine';\n const viz = renderMeshViz(count);\n const toggleCls = m.enabled ? 'ax-mesh-toggle is-on' : 'ax-mesh-toggle';\n const msg = !m.enabled\n ? 'Mesh is off. Turn it on to share work between AgentX machines.'\n : (count === 0\n ? 'Mesh is on, but no peers yet. Add one below to link another machine.'\n : 'Your machine (<b>' + escapeHtml(nodeName) + '</b>) is connected to ' + count + ' peer' + (count === 1 ? '' : 's') + '. Work from any of them can land here — and vice versa.');\n hero.innerHTML =\n '<div>' +\n '<h3>' + (m.enabled ? (count === 0 ? 'Mesh is on' : 'Mesh is active') : 'Mesh is off') + '</h3>' +\n '<p>' + msg + '</p>' +\n '<div class=\"' + toggleCls + '\" onclick=\"toggleMesh(this)\">' +\n '<div class=\"ax-mesh-switch\"></div>' +\n '<span>Mesh networking ' + (m.enabled ? 'enabled' : 'disabled') + '</span>' +\n '</div>' +\n '</div>' +\n '<div class=\"ax-mesh-viz\">' + viz + '</div>';\n }\n\n const peers = $('mesh-peers');\n if (!m.peers.length) {\n peers.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:32px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><p style=\"margin:0;font-size:13px\">No peers yet. Add one below to get started.</p></div>';\n } else {\n peers.innerHTML = '';\n for (const p of m.peers) {\n const statusPill = p.hasToken\n ? '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>authenticated</span>'\n : '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>no token</span>';\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar ax-avatar--' + avatarVariant(p.url) + '\">' + escapeHtml(initialsOf(p.name, p.name)) + '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(p.name) + ' ' + statusPill + '</div>' +\n '<div class=\"ax-sub\"><span class=\"mono muted\">' + escapeHtml(p.url) + '</span></div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n '<button class=\"ax-btn ax-btn--danger\" data-rm-peer=\"' + escapeHtml(p.url) + '\">Remove</button>' +\n '</div>' +\n '</div>';\n card.querySelector('[data-rm-peer]').addEventListener('click', () => removeMeshPeer(p.url));\n peers.appendChild(card);\n }\n }\n}\n\n/** Tiny SVG showing the mesh topology with live dashed wires — YOU in the\n * middle, up to three peers around. Decorative; scales down on narrow\n * viewports via CSS. */\nfunction renderMeshViz(peerCount) {\n // Fixed layout: YOU centered, three peer slots. Extra peers collapse into \"+\".\n const slots = [\n { x: 18, y: 8, label: peerCount >= 1 ? 'A' : '+' },\n { x: 178, y: 8, label: peerCount >= 2 ? 'B' : '+' },\n { x: 18, y: 88, label: peerCount >= 3 ? '+' + (peerCount - 2) : '+' },\n ];\n let lines = '';\n if (peerCount >= 1) lines += '<line x1=\"120\" y1=\"70\" x2=\"40\" y2=\"30\" class=\"live\"/>';\n if (peerCount >= 2) lines += '<line x1=\"120\" y1=\"70\" x2=\"200\" y2=\"30\" class=\"live\"/>';\n if (peerCount >= 3) lines += '<line x1=\"120\" y1=\"70\" x2=\"40\" y2=\"110\" class=\"live\"/>';\n if (peerCount === 0) lines += '<line x1=\"120\" y1=\"70\" x2=\"40\" y2=\"30\"/><line x1=\"120\" y1=\"70\" x2=\"200\" y2=\"30\"/>';\n let dots = '<div class=\"ax-mesh-dot self\" style=\"left:98px;top:48px\">YOU</div>';\n for (const s of slots) {\n const cls = s.label === '+' || /^\\\\+\\\\d+$/.test(s.label) ? 'ax-mesh-dot empty' : 'ax-mesh-dot';\n dots += '<div class=\"' + cls + '\" style=\"left:' + s.x + 'px;top:' + s.y + 'px\">' + s.label + '</div>';\n }\n return '<svg class=\"ax-mesh-wires\" viewBox=\"0 0 240 140\">' + lines + '</svg>' + dots;\n}\n\nasync function toggleMesh(pill) {\n const want = !pill.classList.contains('is-on');\n try {\n await req('POST', '/api/admin/mesh/toggle', { enabled: want });\n refresh();\n } catch (err) { showMsg($('global-msg'), 'err', err.message); }\n}\n\n// Keep the legacy checkbox listener for the hidden input if any calls reach it\n// (the form below the hero still uses #m-url/#m-name/#m-token).\nconst __meshToggle = $('mesh-toggle');\nif (__meshToggle) {\n __meshToggle.addEventListener('change', async (e) => {\n try { await req('POST', '/api/admin/mesh/toggle', { enabled: e.target.checked }); refresh(); }\n catch (err) { showMsg($('global-msg'), 'err', err.message); e.target.checked = !e.target.checked; }\n });\n}\n\nasync function addMeshPeer() {\n const body = { url: $('m-url').value.trim(), name: $('m-name').value.trim(), token: $('m-token').value.trim() };\n try {\n const r = await req('POST', '/api/admin/mesh/peers', body);\n showMsg($('m-msg'), 'ok', r.summary);\n $('m-url').value = ''; $('m-name').value = ''; $('m-token').value = '';\n refresh();\n } catch (e) { showMsg($('m-msg'), 'err', e.message); }\n}\n\nasync function removeMeshPeer(url) {\n if (!confirm('Remove mesh peer at ' + url + '?')) return;\n try { await req('DELETE', '/api/admin/mesh/peers', { url }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nfunction initialsOf(name, id) {\n const src = (name || id || '?').replace(/[^a-zA-Z ]/g, '').trim();\n const parts = src.split(/\\\\s+/).filter(Boolean);\n if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();\n return (parts[0] || id || '?').slice(0, 2).toUpperCase();\n}\n\n// ---------------- Actors & Roles tab ----------------\n//\n// Inline onclick handlers can't pass quoted strings cleanly here — the\n// whole admin page is generated via a TypeScript template literal, so\n// escaped single-quotes in the JS source collapse and break the JS\n// parser. We attach a single delegated click handler instead and route\n// by data-act / data-id attributes.\n\nfunction renderBusiness() {\n const b = state.business || {};\n const orgChart = b.orgChart || {};\n const projects = b.projects || [];\n const contactMap = b.contactMap || [];\n\n const ol = $('business-org-list');\n if (ol) {\n const entries = Object.entries(orgChart);\n if (entries.length === 0) {\n ol.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:18px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:6px;color:var(--ax-muted);font-size:12px\">no org-chart entries</div>';\n } else {\n ol.innerHTML = entries.map(function(kv){\n const id = kv[0]; const e = kv[1];\n const reports = e.reportsTo ? '<span style=\"font-size:11px;color:var(--ax-muted)\">→ ' + escapeHtml(e.reportsTo) + '</span>' : '';\n const sched = e.schedule ? '<span style=\"font-size:11px;color:var(--ax-muted)\">' + escapeHtml((e.schedule.days || []).join(',')) + ' ' + escapeHtml(e.schedule.start || '') + '–' + escapeHtml(e.schedule.end || '') + '</span>' : '';\n return '<div class=\"ax-row-card\" style=\"display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--ax-border);border-radius:6px;margin-bottom:5px\">' +\n '<div style=\"flex:1;min-width:0;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap\">' +\n '<code style=\"font-size:12px\">' + escapeHtml(id) + '</code>' +\n '<b>' + escapeHtml(e.role || '') + '</b>' + reports + sched +\n '</div>' +\n '<button class=\"ghost danger\" data-act=\"biz-org-rm\" data-id=\"' + escapeHtml(id) + '\">Remove</button>' +\n '</div>';\n }).join('');\n }\n }\n\n const pl = $('business-project-list');\n if (pl) {\n if (projects.length === 0) {\n pl.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:18px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:6px;color:var(--ax-muted);font-size:12px\">no projects</div>';\n } else {\n pl.innerHTML = projects.map(function(p){\n const pm = p.pm ? '<span style=\"font-size:11px;color:var(--ax-muted)\">pm=' + escapeHtml(p.pm) + '</span>' : '';\n const client = p.client ? '<span style=\"font-size:11px;color:var(--ax-muted)\">client=' + escapeHtml(p.client) + '</span>' : '';\n return '<div class=\"ax-row-card\" style=\"display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--ax-border);border-radius:6px;margin-bottom:5px\">' +\n '<div style=\"flex:1;min-width:0;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap\"><code style=\"font-size:12px\">' + escapeHtml(p.id) + '</code>' + pm + client + '</div>' +\n '<button class=\"ghost danger\" data-act=\"biz-proj-rm\" data-id=\"' + escapeHtml(p.id) + '\">Remove</button>' +\n '</div>';\n }).join('');\n }\n }\n\n const clientsCfg = (state.business && state.business.clients) || {};\n // Show every client that resolves, not just the configured ones, so the\n // operator can see what exists before deciding to override anything.\n const derived = {};\n projects.forEach(function(p){ derived[p.client || (p.id.indexOf('/') > 0 ? p.id.slice(0, p.id.indexOf('/')) : p.id)] = true; });\n ((state.business && state.business.contactMap) || []).forEach(function(c){ if (c.client) derived[c.client] = true; });\n Object.keys(clientsCfg).forEach(function(id){ derived[id] = true; });\n const clientIds = Object.keys(derived).sort();\n const kl = $('business-client-list');\n if (kl) {\n if (clientIds.length === 0) {\n kl.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:18px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:6px;color:var(--ax-muted);font-size:12px\">no clients yet &mdash; add a project or a contact mapping</div>';\n } else {\n kl.innerHTML = clientIds.map(function(id){\n const c = clientsCfg[id] || {};\n const tag = function(t){ return '<span style=\"font-size:11px;color:var(--ax-muted)\">' + t + '</span>'; };\n const bits = [\n tag(c.kind ? 'kind=' + escapeHtml(c.kind) : 'kind=client (default)'),\n c.respondWithin ? tag('within=' + escapeHtml(c.respondWithin)) : '',\n (c.standing && c.standing.length) ? tag('standing=' + escapeHtml(c.standing.join(' '))) : '',\n c.name ? tag('\"' + escapeHtml(c.name) + '\"') : '',\n clientsCfg[id] ? '' : tag('derived'),\n ].filter(Boolean).join('');\n return '<div class=\"ax-row-card\" style=\"display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--ax-border);border-radius:6px;margin-bottom:5px\">' +\n '<div style=\"flex:1;min-width:0;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap\"><code style=\"font-size:12px\">' + escapeHtml(id) + '</code>' + bits + '</div>' +\n (clientsCfg[id] ? '<button class=\"ghost danger\" data-act=\"biz-client-rm\" data-id=\"' + escapeHtml(id) + '\">Reset</button>' : '') +\n '</div>';\n }).join('');\n }\n }\n\n const cl = $('business-contact-list');\n if (cl) {\n if (contactMap.length === 0) {\n cl.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:18px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:6px;color:var(--ax-muted);font-size:12px\">no contact mappings</div>';\n } else {\n cl.innerHTML = contactMap.map(function(c, idx){\n const key = c.chatId ? 'chatId=' + c.chatId : c.username ? 'username=' + c.username : c.senderId ? 'senderId=' + c.senderId : '?';\n const dn = c.displayName ? '<span style=\"font-size:11px;color:var(--ax-muted)\">(' + escapeHtml(c.displayName) + ')</span>' : '';\n return '<div class=\"ax-row-card\" style=\"display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--ax-border);border-radius:6px;margin-bottom:5px\">' +\n '<div style=\"flex:1;min-width:0;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap\">' +\n (c.channel ? '<span class=\"ax-pill\" style=\"font-size:10px;padding:2px 6px\">' + escapeHtml(c.channel) + '</span>' : '') +\n '<code style=\"font-size:12px\">' + escapeHtml(key) + '</code>' +\n '<span style=\"font-size:11px;color:var(--ax-muted)\">→ ' + escapeHtml(c.client) + (c.project ? '/' + escapeHtml(c.project) : '') + '</span>' + dn +\n '</div>' +\n '<button class=\"ghost danger\" data-act=\"biz-contact-rm\" data-idx=\"' + idx + '\">Remove</button>' +\n '</div>';\n }).join('');\n }\n }\n}\n\nfunction wireBusinessHandlers() {\n if (window.__bizWired) return;\n window.__bizWired = true;\n document.addEventListener('click', async (ev) => {\n const t = ev.target.closest('[data-act^=\"biz-\"]');\n if (!t) return;\n const act = t.getAttribute('data-act');\n if (act === 'biz-org-rm') {\n const agentId = t.getAttribute('data-id');\n if (!confirm('Remove org-chart entry for ' + agentId + '?')) return;\n try { await req('DELETE', '/api/admin/business/orgchart', { agentId }); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n } else if (act === 'biz-proj-rm') {\n const id = t.getAttribute('data-id');\n if (!confirm('Remove project ' + id + '?')) return;\n try { await req('DELETE', '/api/admin/business/project', { id }); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n } else if (act === 'biz-client-rm') {\n const id = t.getAttribute('data-id');\n if (!confirm('Reset ' + id + ' to derived defaults?')) return;\n try { await req('DELETE', '/api/admin/business/client', { id }); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n } else if (act === 'biz-contact-rm') {\n const idx = parseInt(t.getAttribute('data-idx') || '-1', 10);\n const list = (state.business && state.business.contactMap) || [];\n const c = list[idx];\n if (!c) return;\n const filters = {};\n if (c.channel) filters.channel = c.channel;\n if (c.chatId) filters.chatId = c.chatId;\n else if (c.username) filters.username = c.username;\n else if (c.senderId) filters.senderId = c.senderId;\n try { await req('DELETE', '/api/admin/business/contact', filters); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n }\n });\n}\n\nwindow.upsertOrgEntry = async function() {\n const agentId = $('bo-agentId').value.trim();\n const role = $('bo-role').value.trim();\n const reportsTo = $('bo-reportsTo').value.trim();\n const start = $('bo-start').value.trim() || '09:00';\n const end = $('bo-end').value.trim() || '17:00';\n const days = ($('bo-days').value.trim() || 'mon,tue,wed,thu,fri').split(',').map(s => s.trim()).filter(Boolean);\n try {\n await req('POST', '/api/admin/business/orgchart', { agentId, role, reportsTo, start, end, days });\n $('bo-agentId').value = ''; $('bo-role').value = ''; $('bo-reportsTo').value = '';\n showMsg($('bo-msg'), 'ok', 'Entry saved');\n await load();\n } catch (e) { showMsg($('bo-msg'), 'err', e.message); }\n}\n\nwindow.upsertClient = async function() {\n const id = $('bc-id').value.trim();\n const body = {\n id: id,\n name: $('bc-name').value.trim(),\n kind: $('bc-kind').value,\n respondWithin: $('bc-respond').value.trim(),\n standing: $('bc-standing').value.split(',').map(function(s){ return s.trim(); }).filter(Boolean),\n };\n try {\n await req('POST', '/api/admin/business/client', body);\n $('bc-id').value = ''; $('bc-name').value = ''; $('bc-respond').value = ''; $('bc-standing').value = '';\n showMsg($('bc-msg'), 'ok', 'Client saved');\n await load();\n } catch (e) { showMsg($('bc-msg'), 'err', e.message); }\n}\n\nwindow.upsertProject = async function() {\n const id = $('bp-id').value.trim();\n const pm = $('bp-pm').value.trim();\n const client = $('bp-client').value.trim();\n try {\n await req('POST', '/api/admin/business/project', { id, pm, client });\n $('bp-id').value = ''; $('bp-pm').value = ''; $('bp-client').value = '';\n showMsg($('bp-msg'), 'ok', 'Project saved');\n await load();\n } catch (e) { showMsg($('bp-msg'), 'err', e.message); }\n}\n\n// ---------------- Boards-cfg tab ----------------\n\nfunction renderBoardsCfg() {\n const boards = state.boards || [];\n const list = $('boards-cfg-list');\n if (!list) return;\n if (boards.length === 0) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:24px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:6px;color:var(--ax-muted);font-size:12px\">no boards configured</div>';\n return;\n }\n list.innerHTML = boards.map(function(b){\n const projects = ((b.source && b.source.projects) || []).map(p => '<span class=\"ax-pill\" style=\"font-size:10px;padding:2px 6px\">' + escapeHtml(p) + '</span>').join(' ');\n const cols = (b.columns || []).map(function(c){\n const map = c.kind === 'scoped-label' ? 'scoped=' + (c.scopedLabel || '')\n : c.kind === 'label' ? 'label=' + (c.mapsToLabel || '')\n : c.kind === 'open-backlog' ? 'prefix=' + (c.scopedPrefix || 'Status')\n : c.kind;\n return '<span style=\"display:inline-flex;align-items:center;gap:4px;font-size:10px;padding:2px 6px;border:1px solid var(--ax-border);border-radius:3px;margin-right:4px;margin-bottom:4px\"><b>' + escapeHtml(c.title) + '</b><span style=\"color:var(--ax-muted)\">(' + escapeHtml(map) + ')</span><a href=\"#\" data-act=\"bd-col-rm\" data-board=\"' + escapeHtml(b.id) + '\" data-column=\"' + escapeHtml(c.id) + '\" style=\"color:var(--ax-muted);text-decoration:none\">×</a></span>';\n }).join('');\n return '<div class=\"ax-row-card\" style=\"padding:10px 14px;border:1px solid var(--ax-border);border-radius:6px;margin-bottom:8px\">' +\n '<div style=\"display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;justify-content:space-between\">' +\n '<div><b>' + escapeHtml(b.name) + '</b> <code style=\"font-size:10px;color:var(--ax-muted)\">' + escapeHtml(b.id) + '</code></div>' +\n '<div style=\"display:flex;gap:4px\">' +\n '<button class=\"ghost\" data-act=\"bd-edit\" data-id=\"' + escapeHtml(b.id) + '\">Edit</button>' +\n '<button class=\"ghost danger\" data-act=\"bd-rm\" data-id=\"' + escapeHtml(b.id) + '\">Remove</button>' +\n '</div>' +\n '</div>' +\n '<div style=\"margin-top:6px\">' + projects +\n (b.primaryToolLabel ? '<span class=\"ax-pill\" style=\"font-size:10px;padding:2px 6px;margin-left:4px\">label: ' + escapeHtml(b.primaryToolLabel) + '</span>' : '') +\n '<span class=\"ax-pill\" style=\"font-size:10px;padding:2px 6px;margin-left:4px\">open=' + (b.timeRangeDays || 30) + 'd / closed=' + (b.closedWindowDays || 30) + 'd</span>' +\n '</div>' +\n '<div style=\"margin-top:8px;padding-top:8px;border-top:1px dashed var(--ax-border)\">' +\n '<div style=\"font-size:11px;color:var(--ax-muted);margin-bottom:4px\">columns:</div>' +\n (cols || '<i style=\"font-size:11px;color:var(--ax-muted)\">no custom columns (board uses GitLab default flow)</i>') +\n '<details style=\"margin-top:6px\"><summary style=\"font-size:11px;cursor:pointer;color:var(--ax-accent,#3a7bd5)\">+ add column</summary>' +\n '<div style=\"margin-top:6px;display:grid;grid-template-columns:1fr 1fr 1fr 1fr auto;gap:6px;align-items:end\">' +\n '<label>Id<input data-bd-col=\"id\" data-board=\"' + escapeHtml(b.id) + '\" placeholder=\"doing\" /></label>' +\n '<label>Title<input data-bd-col=\"title\" data-board=\"' + escapeHtml(b.id) + '\" placeholder=\"Doing\" /></label>' +\n '<label>Kind<select data-bd-col=\"kind\" data-board=\"' + escapeHtml(b.id) + '\"><option value=\"scoped-label\">scoped-label</option><option value=\"label\">label</option><option value=\"open-backlog\">open-backlog</option><option value=\"closed\">closed</option></select></label>' +\n '<label>Scoped/label value<input data-bd-col=\"value\" data-board=\"' + escapeHtml(b.id) + '\" placeholder=\"Status::Doing\" /></label>' +\n '<button data-act=\"bd-col-add\" data-board=\"' + escapeHtml(b.id) + '\">Add</button>' +\n '</div>' +\n '</details>' +\n '</div>' +\n '</div>';\n }).join('');\n}\n\nfunction wireBoardsCfgHandlers() {\n if (window.__boardsCfgWired) return;\n window.__boardsCfgWired = true;\n document.addEventListener('click', async (ev) => {\n const t = ev.target.closest('[data-act^=\"bd-\"]');\n if (!t) return;\n const act = t.getAttribute('data-act');\n if (act === 'bd-rm') {\n const id = t.getAttribute('data-id');\n if (!confirm('Remove board ' + id + '?')) return;\n try { await req('DELETE', '/api/admin/boards', { id }); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n } else if (act === 'bd-edit') {\n // Pre-fill the add form so the operator can re-save.\n const id = t.getAttribute('data-id');\n const b = (state.boards || []).find(x => x.id === id);\n if (!b) return;\n $('bd-id').value = b.id;\n $('bd-name').value = b.name || '';\n $('bd-projects').value = ((b.source && b.source.projects) || []).join(',');\n $('bd-label').value = b.primaryToolLabel || '';\n $('bd-days').value = b.timeRangeDays || 30;\n $('bd-closed-days').value = b.closedWindowDays || 30;\n // Open the details and scroll to it\n const details = document.querySelector('#tab-boards-cfg .add-form');\n if (details) { details.open = true; details.scrollIntoView({ behavior: 'smooth' }); }\n } else if (act === 'bd-col-rm') {\n ev.preventDefault();\n const boardId = t.getAttribute('data-board');\n const columnId = t.getAttribute('data-column');\n try { await req('DELETE', '/api/admin/boards/columns', { boardId, columnId }); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n } else if (act === 'bd-col-add') {\n const boardId = t.getAttribute('data-board');\n const card = t.closest('.ax-row-card');\n if (!card) return;\n const id = card.querySelector('[data-bd-col=\"id\"]').value.trim();\n const title = card.querySelector('[data-bd-col=\"title\"]').value.trim();\n const kind = card.querySelector('[data-bd-col=\"kind\"]').value;\n const value = card.querySelector('[data-bd-col=\"value\"]').value.trim();\n if (!id || !title) { showMsg($('global-msg'), 'err', 'column id and title required'); return; }\n const payload = { boardId, columnId: id, title, kind };\n if (kind === 'scoped-label') payload.scopedLabel = value;\n else if (kind === 'label') payload.mapsToLabel = value;\n try { await req('POST', '/api/admin/boards/columns', payload); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n }\n });\n}\n\n// ---------------- Notifications + Webhook triggers ----------------\n\nfunction renderNotifications() {\n const n = state.notifications || {};\n const cur = $('notif-current');\n if (cur) {\n if (n.destination) {\n const acct = n.destination.accountId ? ' (account=' + escapeHtml(n.destination.accountId) + ')' : '';\n cur.innerHTML = '<b>Routing to</b> <code>' + escapeHtml(n.destination.channel) + ':' + escapeHtml(n.destination.chatId) + '</code>' + acct;\n } else {\n cur.innerHTML = '<i>No destination set — notifications go to the daemon log only.</i>';\n }\n }\n if ($('notif-channel') && n.destination) {\n $('notif-channel').value = n.destination.channel || '';\n $('notif-chat-id').value = n.destination.chatId || '';\n $('notif-account-id').value = n.destination.accountId || '';\n }\n if ($('notif-threshold')) $('notif-threshold').value = n.longTaskThreshold ?? 30;\n if ($('notif-on-complete')) $('notif-on-complete').checked = n.on?.taskComplete !== false;\n if ($('notif-on-error')) $('notif-on-error').checked = n.on?.taskError !== false;\n if ($('notif-on-queued')) $('notif-on-queued').checked = !!n.on?.taskQueued;\n}\n\nwindow.saveNotifications = async function() {\n const channel = $('notif-channel').value.trim();\n const chatId = $('notif-chat-id').value.trim();\n const accountId = $('notif-account-id').value.trim();\n const threshold = parseInt($('notif-threshold').value, 10);\n const body = {\n on: {\n taskComplete: $('notif-on-complete').checked,\n taskError: $('notif-on-error').checked,\n taskQueued: $('notif-on-queued').checked,\n },\n longTaskThreshold: Number.isFinite(threshold) ? threshold : 30,\n };\n if (channel && chatId) {\n body.destination = { channel, chatId, ...(accountId ? { accountId } : {}) };\n }\n try {\n await req('POST', '/api/admin/notifications', body);\n showMsg($('notif-msg'), 'ok', 'Saved');\n await load();\n } catch (e) { showMsg($('notif-msg'), 'err', e.message); }\n}\n\nwindow.clearNotificationsDestination = async function() {\n try {\n await req('POST', '/api/admin/notifications', { destination: null });\n showMsg($('notif-msg'), 'ok', 'Destination cleared');\n await load();\n } catch (e) { showMsg($('notif-msg'), 'err', e.message); }\n}\n\n// ---------------- Action registry ----------------\n\nfunction renderActions() {\n const list = $('actions-list');\n if (!list) return;\n const items = state.actions || [];\n if (items.length === 0) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:24px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:6px;color:var(--ax-muted);font-size:12px\">no actions yet — add one below or via <code>agentx actions add</code></div>';\n return;\n }\n list.innerHTML = items.map(function(a) {\n const target = a.kind === 'shell'\n ? '<span class=\"muted\">shell:</span> <code>' + escapeHtml(String(a.command || '').slice(0, 90)) + '</code>'\n : '<span class=\"muted\">' + escapeHtml(a.method || 'POST') + '</span> <code>' + escapeHtml(String(a.url || '').slice(0, 90)) + '</code>';\n const inputPills = (a.inputs || []).map(function(inp) {\n const req = inp.required ? '<span style=\"color:var(--ax-warn)\">*</span>' : '';\n return '<span class=\"ax-pill\" style=\"font-size:10px;padding:2px 6px;margin-right:4px\">' + escapeHtml(inp.name) + req + ':' + escapeHtml(inp.type) + '</span>';\n }).join('');\n const inputForm = (a.inputs || []).map(function(inp) {\n const ph = inp.description ? escapeHtml(inp.description) : escapeHtml(inp.name);\n const t = inp.type === 'number' ? 'number' : inp.type === 'boolean' ? 'checkbox' : 'text';\n const def = inp.defaultValue !== undefined ? String(inp.defaultValue) : '';\n const valAttr = t === 'checkbox'\n ? (def === 'true' ? ' checked' : '')\n : (def ? ' value=\"' + escapeHtml(def) + '\"' : '');\n return '<label style=\"display:block;margin:4px 0;font-size:11px\"><span style=\"display:inline-block;min-width:120px;color:var(--ax-muted)\">' + escapeHtml(inp.name) + '</span>'\n + '<input data-ac-input data-name=\"' + escapeHtml(inp.name) + '\" type=\"' + t + '\" placeholder=\"' + ph + '\"' + valAttr + ' style=\"width:60%\" />'\n + '</label>';\n }).join('');\n return '<div class=\"ax-row-card\" style=\"padding:10px 14px;border:1px solid var(--ax-border);border-radius:6px;margin-bottom:8px\" data-ac-card=\"' + escapeHtml(a.id) + '\">'\n + '<div style=\"display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;justify-content:space-between\">'\n + '<div><b>' + escapeHtml(a.title) + '</b> <code style=\"font-size:10px;color:var(--ax-muted)\">' + escapeHtml(a.id) + '</code> '\n + '<span class=\"ax-pill\" style=\"font-size:10px;padding:2px 6px;margin-left:4px\">' + escapeHtml(a.kind) + '</span></div>'\n + '<div style=\"display:flex;gap:4px\">'\n + '<button class=\"ghost\" data-act=\"ac-edit\" data-id=\"' + escapeHtml(a.id) + '\">Edit</button>'\n + '<button class=\"ghost danger\" data-act=\"ac-rm\" data-id=\"' + escapeHtml(a.id) + '\">Remove</button>'\n + '</div>'\n + '</div>'\n + '<div style=\"margin-top:6px;font-size:11px\">' + target + '</div>'\n + (a.description ? '<div style=\"margin-top:4px;font-size:11px;color:var(--ax-muted)\">' + escapeHtml(a.description) + '</div>' : '')\n + (inputPills ? '<div style=\"margin-top:6px\">' + inputPills + '</div>' : '')\n + '<details style=\"margin-top:8px;padding-top:8px;border-top:1px dashed var(--ax-border)\">'\n + '<summary style=\"font-size:11px;cursor:pointer;color:var(--ax-accent)\">▶ Run</summary>'\n + '<div style=\"margin-top:8px\">'\n + (inputForm || '<div style=\"font-size:11px;color:var(--ax-muted);margin-bottom:6px\">no inputs — invokes verbatim</div>')\n + '<div style=\"margin-top:6px;display:flex;gap:6px;align-items:center\">'\n + '<button class=\"primary\" data-act=\"ac-run\" data-id=\"' + escapeHtml(a.id) + '\">Run now</button>'\n + '<span class=\"msg\" data-ac-msg style=\"margin:0;padding:4px 8px\"></span>'\n + '</div>'\n + '<pre data-ac-output style=\"display:none;margin-top:8px;padding:8px;background:var(--ax-bg);border:1px solid var(--ax-border);border-radius:4px;font-family:var(--ax-mono);font-size:11px;line-height:1.5;white-space:pre-wrap;max-height:280px;overflow:auto\"></pre>'\n + '</div>'\n + '</details>'\n + '</div>';\n }).join('');\n}\n\nfunction wireActionKindToggle() {\n if (window.__acKindWired) return;\n window.__acKindWired = true;\n const sel = $('ac-kind');\n if (!sel) return;\n const apply = () => {\n const k = sel.value;\n const sh = $('ac-shell-fields'); if (sh) sh.style.display = k === 'shell' ? '' : 'none';\n const ht = $('ac-http-fields'); if (ht) ht.style.display = k === 'http' ? '' : 'none';\n };\n sel.addEventListener('change', apply);\n apply();\n}\n\nfunction wireActionsHandlers() {\n if (window.__actionsWired) return;\n window.__actionsWired = true;\n document.addEventListener('click', async (ev) => {\n const t = ev.target.closest('[data-act^=\"ac-\"]');\n if (!t) return;\n const act = t.getAttribute('data-act');\n const id = t.getAttribute('data-id');\n if (act === 'ac-rm') {\n if (!confirm('Remove action ' + id + '?')) return;\n try { await req('DELETE', '/api/admin/actions', { id }); await refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n } else if (act === 'ac-edit') {\n const a = (state.actions || []).find(x => x.id === id);\n if (!a) return;\n $('ac-id').value = a.id;\n $('ac-title').value = a.title || '';\n $('ac-desc').value = a.description || '';\n $('ac-kind').value = a.kind;\n $('ac-kind').dispatchEvent(new Event('change'));\n if (a.kind === 'shell') {\n $('ac-command').value = a.command || '';\n $('ac-cwd').value = a.cwd || '';\n } else {\n $('ac-url').value = a.url || '';\n $('ac-method').value = a.method || 'POST';\n }\n $('ac-inputs').value = (a.inputs || []).map(i => i.name + ':' + i.type + (i.required ? '!' : '')).join(',');\n $('ac-timeout').value = a.timeoutMs ?? 30000;\n const details = document.querySelector('#tab-actions .add-form');\n if (details) { details.open = true; details.scrollIntoView({ behavior: 'smooth' }); }\n } else if (act === 'ac-run') {\n const card = t.closest('[data-ac-card]');\n if (!card) return;\n const inputs = {};\n card.querySelectorAll('[data-ac-input]').forEach(el => {\n const name = el.getAttribute('data-name');\n if (el.type === 'checkbox') inputs[name] = el.checked;\n else if (el.value !== '') inputs[name] = el.value;\n });\n const msgEl = card.querySelector('[data-ac-msg]');\n const outEl = card.querySelector('[data-ac-output]');\n if (msgEl) { msgEl.className = 'msg warn'; msgEl.textContent = 'running…'; msgEl.style.display = 'inline-block'; }\n try {\n const r = await req('POST', '/api/admin/actions/run', { id, inputs });\n const result = r.result || {};\n if (msgEl) {\n msgEl.className = 'msg ' + (result.ok ? 'ok' : 'err');\n msgEl.textContent = (result.ok ? 'ok' : 'failed') + ' · status=' + result.status + ' · ' + result.durationMs + 'ms';\n }\n if (outEl) {\n let text = '';\n if (result.output) text += result.output;\n if (result.errors) text += (text ? '\\\\n--- stderr ---\\\\n' : '') + result.errors;\n outEl.textContent = text || '(no output)';\n outEl.style.display = 'block';\n }\n } catch (e) {\n if (msgEl) { msgEl.className = 'msg err'; msgEl.textContent = e.message; }\n if (outEl) outEl.style.display = 'none';\n }\n }\n });\n}\n\nwindow.upsertAction = async function() {\n const id = $('ac-id').value.trim();\n const title = $('ac-title').value.trim();\n const kind = $('ac-kind').value;\n if (!id) { showMsg($('ac-msg'), 'err', 'id required'); return; }\n if (!title) { showMsg($('ac-msg'), 'err', 'title required'); return; }\n const inputsCsv = $('ac-inputs').value.trim();\n const inputs = inputsCsv ? inputsCsv.split(',').map(s => s.trim()).filter(Boolean).map(part => {\n const required = part.endsWith('!');\n const clean = required ? part.slice(0, -1) : part;\n const colon = clean.indexOf(':');\n const name = colon >= 0 ? clean.slice(0, colon) : clean;\n const type = colon >= 0 ? clean.slice(colon + 1) : 'string';\n return { name, type, required };\n }) : [];\n const body = {\n id, title, kind, inputs,\n timeoutMs: parseInt($('ac-timeout').value, 10) || 30000,\n };\n const desc = $('ac-desc').value.trim();\n if (desc) body.description = desc;\n if (kind === 'shell') {\n const cmd = $('ac-command').value.trim();\n if (!cmd) { showMsg($('ac-msg'), 'err', 'command required for shell'); return; }\n body.command = cmd;\n const cwd = $('ac-cwd').value.trim();\n if (cwd) body.cwd = cwd;\n } else if (kind === 'http') {\n const url = $('ac-url').value.trim();\n if (!url) { showMsg($('ac-msg'), 'err', 'url required for http'); return; }\n body.url = url;\n body.method = $('ac-method').value;\n const headers = $('ac-headers').value.trim();\n if (headers) {\n try { body.headers = JSON.parse(headers); }\n catch { showMsg($('ac-msg'), 'err', 'headers must be valid JSON'); return; }\n }\n const reqBody = $('ac-body').value;\n if (reqBody) body.body = reqBody;\n }\n try {\n await req('POST', '/api/admin/actions', body);\n showMsg($('ac-msg'), 'ok', 'saved');\n await refresh();\n } catch (e) { showMsg($('ac-msg'), 'err', e.message); }\n}\n\nfunction wireWebhookTriggerHandlers() {\n if (window.__whTrigWired) return;\n window.__whTrigWired = true;\n document.addEventListener('click', async (ev) => {\n const t = ev.target.closest('[data-act^=\"wh-\"]');\n if (!t) return;\n const id = t.getAttribute('data-id');\n if (t.getAttribute('data-act') === 'wh-trig-rm') {\n ev.preventDefault();\n const evt = t.getAttribute('data-event');\n const wh = state.webhooks.find(w => w.id === id);\n if (!wh) return;\n const next = { ...(wh.triggers || {}) };\n delete next[evt];\n try { await req('POST', '/api/admin/webhooks/triggers', { id, triggers: next }); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n } else if (t.getAttribute('data-act') === 'wh-trig-add') {\n const card = t.closest('.ax-row-card');\n if (!card) return;\n const evt = card.querySelector('[data-wh-trig-event]').value.trim();\n const wf = card.querySelector('[data-wh-trig-wf]').value.trim();\n if (!evt || !wf) { showMsg($('global-msg'), 'err', 'event-type + workflow id required'); return; }\n const wh = state.webhooks.find(w => w.id === id);\n const next = { ...((wh && wh.triggers) || {}), [evt]: wf };\n try { await req('POST', '/api/admin/webhooks/triggers', { id, triggers: next }); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n } else if (t.getAttribute('data-act') === 'wh-default-save') {\n const card = t.closest('.ax-row-card');\n if (!card) return;\n const defaultWorkflow = card.querySelector('[data-wh-default]').value.trim();\n try { await req('POST', '/api/admin/webhooks/triggers', { id, defaultWorkflow }); await load(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n }\n });\n}\n\nwindow.saveMeshHealth = async function() {\n const interval = parseInt($('mh-interval').value, 10);\n const timeout = parseInt($('mh-timeout').value, 10);\n const body = {};\n if (Number.isFinite(interval)) body.interval = interval;\n if (Number.isFinite(timeout)) body.timeout = timeout;\n if (Object.keys(body).length === 0) {\n showMsg($('mh-msg'), 'err', 'set interval and/or timeout');\n return;\n }\n try {\n await req('POST', '/api/admin/mesh/health', body);\n showMsg($('mh-msg'), 'ok', 'Saved');\n await load();\n } catch (e) { showMsg($('mh-msg'), 'err', e.message); }\n}\n\nwindow.upsertBoardCfg = async function() {\n const id = $('bd-id').value.trim();\n const name = $('bd-name').value.trim();\n const projects = $('bd-projects').value.trim();\n const primaryToolLabel = $('bd-label').value.trim();\n const timeRangeDays = parseInt($('bd-days').value, 10) || 30;\n const closedWindowDays = parseInt($('bd-closed-days').value, 10) || 30;\n try {\n await req('POST', '/api/admin/boards', { id, name, projects, primaryToolLabel, timeRangeDays, closedWindowDays });\n showMsg($('bd-msg'), 'ok', 'Board saved');\n await load();\n } catch (e) { showMsg($('bd-msg'), 'err', e.message); }\n}\n\nwindow.upsertContact = async function() {\n const channel = $('bc-channel').value.trim();\n const chatId = $('bc-chatId').value.trim();\n const username = $('bc-username').value.trim();\n const senderId = $('bc-senderId').value.trim();\n const client = $('bc-client').value.trim();\n const project = $('bc-project').value.trim();\n const displayName = $('bc-displayName').value.trim();\n if (!chatId && !username && !senderId) {\n showMsg($('bc-msg'), 'err', 'one of chat-id / username / sender-id required');\n return;\n }\n try {\n await req('POST', '/api/admin/business/contact', { channel, chatId, username, senderId, client, project, displayName });\n ['bc-channel','bc-chatId','bc-username','bc-senderId','bc-client','bc-project','bc-displayName'].forEach(id => { $(id).value = ''; });\n showMsg($('bc-msg'), 'ok', 'Mapping saved');\n await load();\n } catch (e) { showMsg($('bc-msg'), 'err', e.message); }\n}\n\n/** Pick a stable avatar variant from the string hash. Gives each agent a\n * distinct-looking square without the operator having to configure one. */\nfunction avatarVariant(key) {\n const variants = ['teal', 'amber', 'blue', 'coral'];\n let h = 0;\n for (let i = 0; i < String(key).length; i++) h = (h * 31 + String(key).charCodeAt(i)) >>> 0;\n return variants[h % variants.length];\n}\n\nfunction renderAgents() {\n const list = $('agent-list');\n if (!state.agents.length) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:42px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><h3 style=\"margin:0 0 4px;color:var(--ax-text);font-size:15px;font-weight:600\">No agents yet</h3><p style=\"margin:0 0 16px;font-size:13px\">Click <b>+ New agent</b> below to add your first one.</p></div>';\n // still refresh cron agent picker even when the list is empty\n const selE = $('c-agent'); selE.innerHTML = '';\n return;\n }\n list.innerHTML = '';\n for (const a of state.agents) {\n const accessPill = a.access === 'public'\n ? '<span class=\"ax-pill ax-pill--info\"><span class=\"ax-pill__dot\"></span>public API</span>'\n : '<span class=\"ax-pill ax-pill--off\">private</span>';\n const triggers = (a.mentions || []).map(t =>\n '<span class=\"ax-trigger-pill\">' + escapeHtml(t) + '</span>'\n ).join('') || '<span class=\"muted\">—</span>';\n\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar ax-avatar--' + avatarVariant(a.id) + '\">' + escapeHtml(initialsOf(a.name, a.id)) + '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(a.name) + ' ' + accessPill + '</div>' +\n '<div class=\"ax-sub\">' +\n '<span class=\"mono\">' + escapeHtml(a.id) + '</span>' +\n (a.model ? ' · ' + escapeHtml(a.model) : '') +\n (a.tier ? ' · ' + escapeHtml(a.tier) : '') +\n '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n '<button class=\"ax-btn ax-btn--primary\" data-test=\"' + escapeHtml(a.id) + '\" data-name=\"' + escapeHtml(a.name) + '\">Test drive</button>' +\n '<a class=\"ax-btn\" href=\"/admin/agents/' + encodeURIComponent(a.id) + '\">Manage</a>' +\n '<button class=\"ax-btn ax-btn--ghost\" onclick=\"toggleCard(this)\">Details <svg class=\"ax-chev\" width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\"><path d=\"M2 4l3 3 3-3\"/></svg></button>' +\n '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__details\">' +\n '<dl class=\"ax-detail-grid\">' +\n '<div><dt>AI engine</dt><dd>' + escapeHtml(a.tier || '—') + (a.model ? ' · ' + escapeHtml(a.model) : '') + '</dd></div>' +\n '<div><dt>Max concurrent</dt><dd>' + escapeHtml(String(a.maxConcurrent ?? '—')) + '</dd></div>' +\n '<div style=\"grid-column:1/-1\"><dt>Trigger words</dt><dd><div class=\"ax-triggers\">' + triggers + '</div></dd></div>' +\n '</dl>' +\n (a.systemPrompt ? '<div><dt style=\"font-size:10px;letter-spacing:0.08em;text-transform:uppercase;color:var(--ax-muted);margin-bottom:4px;font-family:var(--ax-mono)\">Personality</dt><dd style=\"margin:0;color:var(--ax-text-2);line-height:1.6;font-size:12.5px;white-space:pre-wrap\">' + escapeHtml(a.systemPrompt) + '</dd></div>' : '') +\n renderIntegrationsBlock(a) +\n '<div style=\"margin-top:14px;display:flex;gap:6px;flex-wrap:wrap\">' +\n '<button class=\"ax-btn\" data-edit=\"' + escapeHtml(a.id) + '\">Edit</button>' +\n '<button class=\"ax-btn\" data-files=\"' + escapeHtml(a.id) + '\" data-name=\"' + escapeHtml(a.name) + '\">Files</button>' +\n '<button class=\"ax-btn\" data-toggle=\"' + escapeHtml(a.id) + '\" data-access=\"' + escapeHtml(a.access) + '\">' + (a.access === 'public' ? 'Make private' : 'Make public') + '</button>' +\n '<div style=\"flex:1\"></div>' +\n '<button class=\"ax-btn ax-btn--danger\" data-delete=\"' + escapeHtml(a.id) + '\">Delete</button>' +\n '</div>' +\n '</div>';\n\n card.querySelector('[data-test]').addEventListener('click', (e) => {\n const btn = e.currentTarget;\n openTestDrive(btn.dataset.test, btn.dataset.name);\n });\n card.querySelector('[data-edit]').addEventListener('click', () => openAgentEdit(a));\n card.querySelector('[data-files]').addEventListener('click', (e) => {\n const btn = e.currentTarget;\n openFilesModal(btn.dataset.files, btn.dataset.name);\n });\n card.querySelector('[data-toggle]').addEventListener('click', (e) => {\n const btn = e.currentTarget;\n const want = btn.dataset.access === 'public' ? 'private' : 'public';\n setAgentAccess(btn.dataset.toggle, want);\n });\n card.querySelector('[data-delete]').addEventListener('click', () => deleteAgent(a.id));\n // Wire integration row actions: delete one, add via inline form.\n card.querySelectorAll('[data-int-rm]').forEach((el) => {\n el.addEventListener('click', () => {\n const ds = el.dataset;\n removeIntegration(a.id, ds.intRm, ds.intLabel);\n });\n });\n const addBtn = card.querySelector('[data-int-add]');\n if (addBtn) addBtn.addEventListener('click', () => addIntegration(a.id, card));\n list.appendChild(card);\n }\n // Refresh the cron agent picker so new agents show up.\n const sel = $('c-agent');\n const current = sel.value;\n sel.innerHTML = state.agents.map((a) => '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>').join('');\n if (current) sel.value = current;\n}\n\nasync function addAgent() {\n const body = {\n id: $('a-id').value.trim(),\n name: $('a-name').value.trim(),\n triggerWords: $('a-triggers').value.trim(),\n tier: $('a-tier').value,\n model: $('a-model').value.trim() || undefined,\n personality: $('a-personality').value.trim() || undefined,\n access: $('a-public').checked ? 'public' : 'private',\n };\n try {\n const r = await req('POST', '/api/admin/agents', body);\n if (body.access === 'public') {\n await req('POST', '/api/admin/agents/access', { id: body.id, access: 'public' }).catch(() => {});\n }\n showMsg($('a-msg'), 'ok', r.summary || 'Added.');\n $('a-id').value = ''; $('a-name').value = ''; $('a-triggers').value = ''; $('a-personality').value = '';\n $('a-public').checked = false;\n refresh();\n } catch (e) { showMsg($('a-msg'), 'err', e.message); }\n}\n\nasync function setAgentAccess(id, access) {\n try {\n await req('POST', '/api/admin/agents/access', { id, access });\n refresh();\n } catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nasync function deleteAgent(id) {\n if (!confirm('Delete agent \"' + id + '\"? The config entry is removed; the workspace folder on disk is kept.')) return;\n try { await req('DELETE', '/api/admin/agents', { id }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\n// Per-agent integrations registry. Each row: { kind, label, credentials, credStatus, metadata, enabled }.\n// credStatus comes from server: { tokenEnv: \"set\"|\"unset\", auth: \"literal\", ... }.\nconst INT_KINDS = [\n 'telegram-bot', 'whatsapp', 'slack-bot', 'discord-bot',\n 'gitlab-user', 'github-user',\n 'gmail', 'hotmail', 'smtp',\n 'hubspot', 'salesforce', 'pipedrive', 'odoo',\n 'trello', 'linear', 'asana', 'jira', 'notion',\n 'stripe', 'sentry', 'vercel', 'twilio',\n 'custom',\n];\n\nfunction renderIntegrationsBlock(a) {\n const list = Array.isArray(a.integrations) ? a.integrations : [];\n const rows = list.map((i) => {\n const statusBits = Object.entries(i.credStatus || {}).map(([k, st]) => {\n if (st === 'set') return '<span class=\"ax-pill ax-pill--ok\" style=\"font-size:10px\"><span class=\"ax-pill__dot\"></span>' + escapeHtml(k) + '=set</span>';\n if (st === 'unset') return '<span class=\"ax-pill ax-pill--warn\" style=\"font-size:10px\"><span class=\"ax-pill__dot\"></span>' + escapeHtml(k) + '=UNSET</span>';\n return '<span class=\"ax-pill ax-pill--off\" style=\"font-size:10px\">' + escapeHtml(k) + '</span>';\n }).join(' ');\n const credText = Object.entries(i.credentials || {}).map(([k, v]) => k + '=' + v).join(', ');\n const metaText = Object.entries(i.metadata || {}).map(([k, v]) => k + '=' + v).join(' · ');\n return '<div style=\"display:flex;align-items:flex-start;gap:8px;padding:8px 10px;background:var(--ax-surface);border:1px solid var(--ax-border);border-radius:5px;margin-top:6px\">' +\n '<div style=\"flex:1\">' +\n '<div style=\"font-size:12px\"><span class=\"mono\" style=\"color:var(--ax-accent,#3a7bd5)\">' + escapeHtml(i.kind) + '</span> · <b>' + escapeHtml(i.label) + '</b>' + (i.enabled === false ? ' <span class=\"ax-pill ax-pill--off\" style=\"font-size:10px\">disabled</span>' : '') + '</div>' +\n (statusBits ? '<div style=\"margin-top:4px\">' + statusBits + '</div>' : '') +\n (credText ? '<div style=\"font-size:10.5px;color:var(--ax-muted);margin-top:3px;font-family:var(--ax-mono)\">' + escapeHtml(credText) + '</div>' : '') +\n (metaText ? '<div style=\"font-size:10.5px;color:var(--ax-muted);margin-top:2px\">' + escapeHtml(metaText) + '</div>' : '') +\n '</div>' +\n '<button class=\"ax-btn ax-btn--ghost\" data-int-rm=\"' + escapeHtml(i.kind) + '\" data-int-label=\"' + escapeHtml(i.label) + '\" style=\"padding:3px 8px;font-size:11px\">Remove</button>' +\n '</div>';\n }).join('');\n const empty = list.length === 0\n ? '<div style=\"font-size:11px;color:var(--ax-muted);font-style:italic;padding:8px 0\">No integrations declared. Add one below to give this agent credentials for HubSpot, GitLab, Telegram, etc.</div>'\n : '';\n const kindOptions = INT_KINDS.map((k) => '<option value=\"' + k + '\">' + k + '</option>').join('');\n return '<details style=\"margin-top:14px\">' +\n '<summary style=\"cursor:pointer;font-size:11px;color:var(--ax-accent,#3a7bd5)\">Integrations <span style=\"color:var(--ax-muted)\">(' + list.length + ')</span></summary>' +\n '<div style=\"margin-top:8px\">' +\n '<div style=\"font-size:11px;color:var(--ax-muted);margin-bottom:6px\">Per-agent third-party credentials. Secrets stay in env vars / keyring; this lists declarations and resolved env-set status.</div>' +\n empty + rows +\n '<div style=\"margin-top:10px;padding:8px 10px;background:var(--ax-bg);border:1px dashed var(--ax-border-2);border-radius:5px\">' +\n '<div style=\"display:grid;grid-template-columns:1fr 1.5fr;gap:6px\">' +\n '<select data-int-kind style=\"font-size:11px\">' + kindOptions + '</select>' +\n '<input data-int-label-add placeholder=\"Label (e.g. Acme CRM, @cx_bot)\" style=\"font-size:11px\" />' +\n '</div>' +\n '<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:6px\">' +\n '<input data-int-tokenenv placeholder=\"Token env var (UPPERCASE)\" style=\"font-size:11px\" />' +\n '<input data-int-meta placeholder=\"Metadata: k=v,k=v (optional)\" style=\"font-size:11px\" />' +\n '</div>' +\n '<button class=\"ax-btn ax-btn--primary\" data-int-add=\"' + escapeHtml(a.id) + '\" style=\"margin-top:6px;padding:4px 10px;font-size:11px\">+ Add integration</button>' +\n '</div>' +\n '</div>' +\n '</details>';\n}\n\nasync function addIntegration(agentId, card) {\n const kind = (card.querySelector('[data-int-kind]') || {}).value || '';\n const label = (card.querySelector('[data-int-label-add]') || {}).value.trim();\n const tokenEnv = (card.querySelector('[data-int-tokenenv]') || {}).value.trim();\n const metaRaw = (card.querySelector('[data-int-meta]') || {}).value.trim();\n if (!kind || !label) {\n showMsg($('global-msg'), 'err', 'kind and label are required');\n return;\n }\n const credentials = {};\n if (tokenEnv) credentials.tokenEnv = tokenEnv;\n if (!Object.keys(credentials).length) {\n showMsg($('global-msg'), 'err', 'token env var is required (or use the CLI for keyring/sessionDir variants)');\n return;\n }\n const metadata = {};\n if (metaRaw) {\n for (const pair of metaRaw.split(',')) {\n const [k, ...rest] = pair.split('=');\n if (k && k.trim()) metadata[k.trim()] = rest.join('=').trim();\n }\n }\n try {\n await req('POST', '/api/admin/agents/integrations', { agentId, kind, label, credentials, metadata });\n refresh();\n } catch (e) {\n showMsg($('global-msg'), 'err', e.message);\n }\n}\n\nasync function removeIntegration(agentId, kind, label) {\n if (!confirm('Remove integration ' + kind + ' \"' + label + '\" from ' + agentId + '?')) return;\n try {\n await req('DELETE', '/api/admin/agents/integrations', { agentId, kind, label });\n refresh();\n } catch (e) {\n showMsg($('global-msg'), 'err', e.message);\n }\n}\n\nfunction renderChannels() {\n renderChannelsMenu();\n renderChannelsGrid();\n const t = state.telegram || { enabled: false, accounts: [] };\n const agentOptions = state.agents.map((a) => '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>').join('');\n const accountRows = t.accounts.length === 0\n ? '<div class=\"empty\">No Telegram accounts yet.</div>'\n : t.accounts.map((acc) =>\n '<div class=\"row-card\"><div class=\"info\"><h3>' + escapeHtml(acc.id) + '</h3>' +\n '<div class=\"meta\">' +\n '<span class=\"chip ' + (t.enabled ? '' : 'off') + '\">' + (t.enabled ? 'enabled' : 'disabled') + '</span>' +\n 'bot: <b>' + escapeHtml(acc.botUsername || '—') + '</b> · agent: <b>' + escapeHtml(acc.agentBinding || '—') + '</b> · token ref: <b>' + escapeHtml(acc.botTokenRef || '—') + '</b>' +\n '</div></div>' +\n '<button class=\"ghost\" data-tg-edit=\"' + escapeHtml(acc.id) + '\" style=\"margin-right:6px\">Edit</button>' +\n '<button class=\"danger\" data-id=\"' + escapeHtml(acc.id) + '\">Delete</button></div>').join('');\n $('tg-section').innerHTML =\n '<div class=\"toggle-switch\"><input type=\"checkbox\" id=\"tg-toggle\"' + (t.enabled ? ' checked' : '') + ' /><label for=\"tg-toggle\" style=\"color:var(--text);margin:0\">Telegram connector enabled</label></div>' +\n '<div class=\"list\">' + accountRows + '</div>' +\n '<div class=\"add-form\"><h3>Add a Telegram account</h3>' +\n '<div class=\"rowf\"><div><label>Account id<span class=\"hint\">(e.g. support)</span></label><input id=\"tg-id\" /></div>' +\n '<div><label>Bind to agent</label><select id=\"tg-agent\">' + agentOptions + '</select></div></div>' +\n '<label>Bot username<span class=\"hint\">(optional)</span></label><input id=\"tg-user\" placeholder=\"my_bot\" />' +\n '<label>Bot token env-var<span class=\"hint\">(name of the <code>.env</code> variable holding the token)</span></label><input id=\"tg-env\" placeholder=\"TG_SUPPORT_BOT_TOKEN\" />' +\n '<div class=\"actions\"><button class=\"primary\" onclick=\"addTelegram()\">Add account</button><div id=\"tg-msg\" class=\"msg\"></div></div>' +\n '<div class=\"hint-block\">We reference the token as <code>\\${TG_..._BOT_TOKEN}</code> in <code>agentx.json</code>. The actual value lives in <code>.env</code> — this panel never reads or transmits it.</div>' +\n '</div>';\n $('tg-toggle').addEventListener('change', async (e) => {\n try { await req('POST', '/api/admin/channels/telegram/toggle', { enabled: e.target.checked }); refresh(); }\n catch (err) { showMsg($('global-msg'), 'err', err.message); }\n });\n for (const btn of document.querySelectorAll('#tg-section button.danger')) {\n btn.addEventListener('click', () => deleteTelegram(btn.dataset.id));\n }\n for (const btn of document.querySelectorAll('#tg-section button[data-tg-edit]')) {\n btn.addEventListener('click', () => {\n const id = btn.dataset.tgEdit;\n const acc = (state.telegram.accounts || []).find((a) => a.id === id);\n if (acc) openTelegramEdit(acc);\n });\n }\n renderSlackSection();\n}\n\nconst CHANNEL_DEFS = [\n { id: 'telegram', icon: 'TG', label: 'Telegram' },\n { id: 'whatsapp', icon: 'WA', label: 'WhatsApp' },\n { id: 'gitlab', icon: 'GL', label: 'GitLab' },\n { id: 'github', icon: 'GH', label: 'GitHub' },\n { id: 'webrtc', icon: '☎', label: 'Calls (WebRTC)' },\n];\n\n/** Per-connector card metadata — logo color, long description, setup time.\n * Keeps renderChannelsGrid() focused on wiring rather than content. */\nconst CONNECTOR_META = {\n telegram: { group: 'chat', brand: '#229ED9', desc: 'Replies in Telegram DMs and groups when tagged. Paste your BotFather token; we keep the value in .env.' },\n whatsapp: { group: 'chat', brand: '#25D366', desc: 'Answer on WhatsApp via a local session. Scan a QR once; AgentX pairs as an extra device.' },\n slack: { group: 'chat', brand: '#4A154B', desc: 'Agents can DM or be mentioned in channels. Uses the Events API with socket mode.' },\n discord: { group: 'chat', brand: '#5865F2', desc: 'Works in servers, threads, and DMs. Create a Discord bot, grant Read/Send Messages, paste the token.' },\n gitlab: { group: 'dev', brand: '#FC6D26', desc: 'Pings your agent on MRs, issues, and pipeline events. Works with self-hosted too.' },\n github: { group: 'dev', brand: '#24292e', desc: 'Pings your agent on PRs, issues, and CI runs. Configure per-repo.' },\n webrtc: { group: 'chat', brand: '#10B981', desc: 'Voice calls with a transcribing bot. Live on the /call page; history of recent calls below.' },\n};\n\nfunction renderChannelsGrid() {\n const chat = $('ch-chatapps');\n const dev = $('ch-devtools');\n if (!chat || !dev) return;\n chat.innerHTML = ''; dev.innerHTML = '';\n for (const def of CHANNEL_DEFS) {\n const meta = CONNECTOR_META[def.id] || { group: 'chat', brand: 'var(--ax-surface-3)', desc: '' };\n const status = channelStatus(def.id);\n const active = state.__activeChannel === def.id;\n const statusPill = status === 'on'\n ? '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>live</span>'\n : status === 'off'\n ? '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>off</span>'\n : '<span class=\"ax-pill ax-pill--off\">not set up</span>';\n const sub = channelSubtext(def.id, status);\n const card = document.createElement('div');\n card.className = 'ax-connector' + (status === 'on' ? ' is-on' : '') + (active ? ' is-active' : '');\n card.innerHTML =\n '<div class=\"ax-connector__top\">' +\n '<div class=\"ax-connector__logo\" style=\"background:' + meta.brand + ';color:#fff;border-color:' + meta.brand + '\">' + def.icon + '</div>' +\n '<div class=\"ax-connector__meta\">' +\n '<div class=\"ax-connector__name\">' + def.label + '</div>' +\n '<div class=\"ax-connector__sub\">' + sub + '</div>' +\n '</div>' +\n '<div class=\"ax-connector__status\">' + statusPill + '</div>' +\n '</div>' +\n '<div class=\"ax-connector__desc\">' + meta.desc + '</div>' +\n '<div class=\"ax-connector__foot\">' +\n '<span>' + (status === 'none' ? '~5 min setup' : 'Configured') + '</span>' +\n '<span class=\"ax-connector__cta\">' + (status === 'none' ? 'Set up' : 'Manage') + ' <svg viewBox=\"0 0 12 12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\"><path d=\"M4 2l4 4-4 4\"/></svg></span>' +\n '</div>';\n card.addEventListener('click', () => showChannelPane(def.id));\n (meta.group === 'chat' ? chat : dev).appendChild(card);\n }\n if (!state.__activeChannel) {\n // Default to telegram pane showing if nothing picked yet.\n showChannelPane('telegram');\n } else {\n showChannelPane(state.__activeChannel);\n }\n}\n\n/** Human-readable sub-text for the connector card under the name. */\nfunction channelSubtext(id, status) {\n if (id === 'telegram') {\n const t = state.telegram || {};\n const n = (t.accounts || []).length;\n if (n === 0) return 'Not set up';\n return n + ' bot' + (n === 1 ? '' : 's') + ' configured';\n }\n if (id === 'whatsapp') {\n const w = state.whatsapp || {};\n return w.routeCount ? w.routeCount + ' route' + (w.routeCount === 1 ? '' : 's') : 'Not set up';\n }\n if (id === 'slack') {\n const s = state.slack || {};\n return s.botTokenRef ? (s.workspace ? 'Workspace: ' + s.workspace : 'Token set') : 'Not set up';\n }\n if (id === 'discord') return status === 'none' ? 'Not set up' : 'Token set';\n if (id === 'gitlab') return status === 'none' ? 'Not set up' : 'Token set';\n if (id === 'github') {\n const n = ((state.webhooks || []).filter((w) => w.source === 'github')).length;\n return n ? n + ' webhook' + (n === 1 ? '' : 's') : 'Not set up';\n }\n return status === 'none' ? 'Not set up' : 'Configured';\n}\n\n/** Show the pane for the given channel id, hide the others, and highlight\n * the matching connector card. */\nfunction showChannelPane(id) {\n state.__activeChannel = id;\n const panes = ['telegram','whatsapp','slack','discord','gitlab','github','webrtc'];\n for (const p of panes) {\n const el = document.getElementById('ch-' + p);\n if (el) el.hidden = (p !== id);\n }\n // Reflect active on connector cards\n const chat = $('ch-chatapps');\n const dev = $('ch-devtools');\n [chat, dev].forEach((container) => {\n if (!container) return;\n container.querySelectorAll('.ax-connector').forEach((c, i) => {\n const defs = CHANNEL_DEFS.filter(d => (CONNECTOR_META[d.id]?.group === (container === chat ? 'chat' : 'dev')));\n const def = defs[i];\n if (def) c.classList.toggle('is-active', def.id === id);\n });\n });\n // Lazy-render the panes whose content isn't filled by renderChannels(). TG\n // and Slack are populated unconditionally there; the rest need an explicit\n // call or their panes stay empty when switched via the connector card.\n if (id === 'discord') renderDiscordPane();\n else if (id === 'gitlab') renderGitLabPane();\n else if (id === 'github') renderGitHubPane();\n else if (id === 'whatsapp') renderWhatsAppPane();\n else if (id === 'webrtc') renderWebRtcPane();\n}\n\nfunction channelStatus(id) {\n if (id === 'telegram') {\n const t = state.telegram || {};\n const configured = (t.accounts || []).length > 0;\n if (!configured) return 'none';\n return t.enabled ? 'on' : 'off';\n }\n if (id === 'whatsapp') {\n const w = state.whatsapp || {};\n if (!w.routeCount) return 'none';\n return w.enabled ? 'on' : 'off';\n }\n if (id === 'slack') {\n const s = state.slack || {};\n if (!s.botTokenRef || !s.appTokenRef) return 'none';\n return s.enabled ? 'on' : 'off';\n }\n if (id === 'discord') {\n const d = state.discord || {};\n if (!d.tokenRef) return 'none';\n return d.enabled ? 'on' : 'off';\n }\n if (id === 'gitlab') {\n const g = state.gitlab || {};\n if (!g.tokenRef) return 'none';\n return g.enabled ? 'on' : 'off';\n }\n if (id === 'github') {\n const webhooks = (state.webhooks || []).filter((w) => w.source === 'github');\n if (webhooks.length === 0) return 'none';\n return webhooks.some((w) => w.enabled) ? 'on' : 'off';\n }\n return 'none';\n}\n\nfunction renderChannelsMenu() {\n const menu = $('ch-menu');\n if (!menu) return;\n const activeId = menu.dataset.active || 'telegram';\n menu.innerHTML = CHANNEL_DEFS.map((c) => {\n const s = channelStatus(c.id);\n const dotCls = s === 'on' ? 'ch-dot ch-dot--on' : s === 'off' ? 'ch-dot ch-dot--off' : 'ch-dot';\n const dotTitle = s === 'on' ? 'enabled' : s === 'off' ? 'configured but off' : 'not set up';\n const isActive = c.id === activeId;\n return '<button data-ch=\"' + c.id + '\" class=\"' + (isActive ? 'is-active' : '') + '\">' +\n '<span class=\"ch-icon\">' + c.icon + '</span>' +\n '<span class=\"ch-label\">' + c.label + '</span>' +\n '<span class=\"' + dotCls + '\" title=\"' + dotTitle + '\"></span>' +\n '</button>';\n }).join('');\n for (const btn of menu.querySelectorAll('button')) {\n btn.addEventListener('click', () => selectChannel(btn.dataset.ch));\n }\n // Re-render the currently-selected pane's content.\n selectChannel(activeId, true);\n}\n\nfunction selectChannel(id, keep) {\n const menu = $('ch-menu');\n if (menu) menu.dataset.active = id;\n for (const btn of menu.querySelectorAll('button')) {\n btn.classList.toggle('is-active', btn.dataset.ch === id);\n }\n for (const v of document.querySelectorAll('.ch-view')) {\n v.hidden = v.id !== 'ch-' + id;\n }\n // Lazy-render the more static panes; telegram + slack re-render via their existing calls.\n if (id === 'discord') renderDiscordPane();\n else if (id === 'gitlab') renderGitLabPane();\n else if (id === 'github') renderGitHubPane();\n else if (id === 'whatsapp') renderWhatsAppPane();\n}\n\nfunction renderDiscordPane() {\n const d = state.discord || {};\n const stripRef = (ref) => (ref || '').replace(/^\\\\\\$\\\\{|\\\\}$/g, '');\n const agentOptions = state.agents.map((a) => '<option value=\"' + escapeHtml(a.id) + '\"' +\n (a.id === (d.agentBinding || '') ? ' selected' : '') + '>' +\n escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>').join('');\n const configured = !!d.tokenRef;\n const statusChip = configured\n ? (d.enabled ? '<span class=\"chip\" style=\"background:rgba(34,197,94,0.15);color:var(--ax-accent)\">enabled</span>' : '<span class=\"chip off\">disabled</span>')\n : '<span class=\"chip off\">not configured</span>';\n $('ch-discord').innerHTML =\n '<div class=\"row-card\" style=\"margin-bottom:12px\"><div class=\"info\"><h3>Discord bot</h3>' +\n '<div class=\"meta\">' + statusChip +\n (d.tokenRef ? ' token: <code style=\"font-family:var(--ax-mono)\">' + escapeHtml(d.tokenRef) + '</code>' : '') +\n (d.agentBinding ? ' · agent: <b>' + escapeHtml(d.agentBinding) + '</b>' : '') +\n '</div></div>' +\n (configured ? '<label class=\"toggle-switch\" style=\"margin:0\"><input type=\"checkbox\" id=\"dc-toggle\"' + (d.enabled ? ' checked' : '') + ' /> <span>On</span></label>' : '') +\n '</div>' +\n '<div class=\"add-form\"><h3>' + (configured ? 'Edit Discord' : 'Connect Discord') + '</h3>' +\n '<label>Bot token env-var<span class=\"hint\">(from the Discord Developer Portal)</span></label>' +\n '<input id=\"dc-env\" placeholder=\"DISCORD_BOT_TOKEN\" value=\"' + escapeHtml(stripRef(d.tokenRef)) + '\" />' +\n '<label>Default agent<span class=\"hint\">(optional)</span></label>' +\n '<select id=\"dc-agent\"><option value=\"\">(none)</option>' + agentOptions + '</select>' +\n '<div class=\"actions\"><button class=\"primary\" onclick=\"configureDiscord()\">' + (configured ? 'Save' : 'Connect') + '</button><div id=\"dc-msg\" class=\"msg\"></div></div>' +\n '<div class=\"hint-block\">Bot needs <code>MESSAGE_CONTENT</code> intent enabled in the Developer Portal. Routing: @-mention in channels, always in DMs.</div>' +\n '</div>';\n const t = $('dc-toggle');\n if (t) t.addEventListener('change', async (e) => {\n try { await req('POST', '/api/admin/channels/discord/toggle', { enabled: e.target.checked }); refresh(); }\n catch (err) { showMsg($('global-msg'), 'err', err.message); e.target.checked = !e.target.checked; }\n });\n}\nasync function configureDiscord() {\n const body = { tokenEnv: $('dc-env').value.trim(), agentBinding: $('dc-agent').value };\n try { const r = await req('POST', '/api/admin/channels/discord', body); showMsg($('dc-msg'), 'ok', (r.summary || 'Saved.') + (r.hint ? ' — ' + r.hint : '')); refresh(); }\n catch (e) { showMsg($('dc-msg'), 'err', e.message); }\n}\n\nfunction renderGitLabPane() {\n const g = state.gitlab || {};\n const stripRef = (ref) => (ref || '').replace(/^\\\\\\$\\\\{|\\\\}$/g, '');\n const configured = !!g.tokenRef;\n const statusChip = configured\n ? (g.enabled ? '<span class=\"chip\" style=\"background:rgba(34,197,94,0.15);color:var(--ax-accent)\">enabled</span>' : '<span class=\"chip off\">disabled</span>')\n : '<span class=\"chip off\">not configured</span>';\n $('ch-gitlab').innerHTML =\n '<div class=\"row-card\" style=\"margin-bottom:12px\"><div class=\"info\"><h3>GitLab</h3>' +\n '<div class=\"meta\">' + statusChip +\n (g.host ? ' <code style=\"font-family:var(--ax-mono)\">' + escapeHtml(g.host) + '</code>' : '') +\n (g.tokenRef ? ' · token: <code style=\"font-family:var(--ax-mono)\">' + escapeHtml(g.tokenRef) + '</code>' : '') +\n ' · ' + g.routeCount + ' route(s) · ' + g.agentMappingCount + ' agent mapping(s)' +\n '</div></div>' +\n (configured ? '<label class=\"toggle-switch\" style=\"margin:0\"><input type=\"checkbox\" id=\"gl-toggle\"' + (g.enabled ? ' checked' : '') + ' /> <span>On</span></label>' : '') +\n '</div>' +\n '<div class=\"add-form\"><h3>' + (configured ? 'Edit GitLab' : 'Connect GitLab') + '</h3>' +\n '<label>Host<span class=\"hint\">(e.g. <code>https://gitlab.com</code> or your self-hosted URL)</span></label>' +\n '<input id=\"gl-host\" placeholder=\"https://gitlab.com\" value=\"' + escapeHtml(g.host || 'https://gitlab.com') + '\" />' +\n '<label>Admin token env-var<span class=\"hint\">(personal access token with <code>api</code> scope)</span></label>' +\n '<input id=\"gl-env\" placeholder=\"GITLAB_TOKEN\" value=\"' + escapeHtml(stripRef(g.tokenRef)) + '\" />' +\n '<label>Webhook listen port<span class=\"hint\">(incoming GitLab webhooks)</span></label>' +\n '<input id=\"gl-port\" type=\"number\" min=\"1\" max=\"65535\" value=\"' + (g.webhookPort || 18810) + '\" />' +\n '<div class=\"actions\"><button class=\"primary\" onclick=\"configureGitLab()\">' + (configured ? 'Save' : 'Connect') + '</button><div id=\"gl-msg\" class=\"msg\"></div></div>' +\n '<div class=\"hint-block\">Per-project routes and per-agent tokens stay in the <b>Advanced</b> tab (raw JSON). Webhooks from GitLab are also registered in the <b>Webhooks</b> tab with source=<code>gitlab</code>.</div>' +\n '</div>';\n const t = $('gl-toggle');\n if (t) t.addEventListener('change', async (e) => {\n try { await req('POST', '/api/admin/channels/gitlab/toggle', { enabled: e.target.checked }); refresh(); }\n catch (err) { showMsg($('global-msg'), 'err', err.message); e.target.checked = !e.target.checked; }\n });\n}\nasync function configureGitLab() {\n const body = {\n host: $('gl-host').value.trim(),\n tokenEnv: $('gl-env').value.trim(),\n webhookPort: parseInt($('gl-port').value, 10),\n };\n try { const r = await req('POST', '/api/admin/channels/gitlab', body); showMsg($('gl-msg'), 'ok', (r.summary || 'Saved.') + (r.hint ? ' — ' + r.hint : '')); refresh(); }\n catch (e) { showMsg($('gl-msg'), 'err', e.message); }\n}\n\nfunction renderGitHubPane() {\n const daemonUrl = (state.daemonUrl || '').replace(/\\\\/+$/, '');\n const ghHooks = (state.webhooks || []).filter((w) => w.source === 'github');\n const hookList = ghHooks.length === 0\n ? '<div class=\"empty\">No GitHub webhooks registered yet.</div>'\n : ghHooks.map((w) => {\n const url = daemonUrl + '/webhook/' + encodeURIComponent(w.agentId) + '/github';\n const status = w.enabled\n ? '<span class=\"chip\" style=\"background:rgba(34,197,94,0.15);color:var(--ax-accent)\">enabled</span>'\n : '<span class=\"chip off\">disabled</span>';\n return '<div class=\"row-card\"><div class=\"info\"><h3>' + escapeHtml(w.id) + '</h3>' +\n '<div class=\"meta\">' + status + ' agent: <b>' + escapeHtml(w.agentId) + '</b> · ' +\n '<code style=\"font-family:var(--ax-mono);font-size:11px\">' + escapeHtml(url) + '</code>' +\n '</div></div></div>';\n }).join('');\n $('ch-github').innerHTML =\n '<div class=\"hint-block\" style=\"margin-bottom:14px\">GitHub doesn&rsquo;t need a bot adapter — it speaks webhooks. Register one in the <b>Webhooks</b> tab with <code>source=github</code>, bind it to an agent, then paste the generated URL into the repo&rsquo;s <i>Settings → Webhooks</i>.</div>' +\n '<div class=\"list\">' + hookList + '</div>' +\n '<div class=\"actions\"><button class=\"primary\" onclick=\"jumpToWebhooks()\">Add a GitHub webhook →</button></div>';\n}\nfunction jumpToWebhooks() {\n for (const btn of document.querySelectorAll('nav.tabs button')) {\n if (btn.dataset.tab === 'webhooks') btn.click();\n }\n const src = document.getElementById('w-source');\n if (src) src.value = 'github';\n}\n\nfunction renderWebRtcPane() {\n const pane = $('ch-webrtc');\n if (!pane) return;\n pane.innerHTML =\n '<div class=\"ax-section-card\"><div class=\"ax-section-card__head\">' +\n '<div class=\"ax-section-card__icon\" style=\"background:#10B981;color:#fff\">☎</div>' +\n '<div class=\"ax-section-card__info\"><h3>Calls (WebRTC)</h3><div class=\"ax-section-card__sub\">Live calls happen on <a href=\"/call\" target=\"_blank\">/call</a>. The transcribing bot can be invited per call from there. This panel shows active + recent sessions.</div></div></div>' +\n '<div class=\"ax-stack\" style=\"margin-top:14px\">' +\n '<div style=\"display:flex;align-items:center;gap:10px;margin-bottom:8px\">' +\n '<h3 style=\"margin:0;font-size:13px\">Active calls</h3>' +\n '<button class=\"ax-btn\" onclick=\"loadWebRtcHistory()\">Refresh</button>' +\n '</div>' +\n '<div id=\"webrtc-active\" style=\"border:1px solid var(--ax-border);border-radius:6px;padding:8px;font-family:&quot;IBM Plex Mono&quot;,monospace;font-size:11px\">click refresh</div>' +\n '<h3 style=\"margin:14px 0 8px;font-size:13px\">Recent sessions <span style=\"font-size:11px;color:var(--ax-muted);font-weight:normal\">(in-process ring buffer, last ~50)</span></h3>' +\n '<div id=\"webrtc-history\" style=\"border:1px solid var(--ax-border);border-radius:6px;padding:8px;font-family:&quot;IBM Plex Mono&quot;,monospace;font-size:11px;max-height:280px;overflow-y:auto\">click refresh</div>' +\n '</div>';\n loadWebRtcHistory();\n}\n\nwindow.loadWebRtcHistory = async function() {\n try {\n const data = await req('GET', '/api/admin/channels/webrtc/history');\n const active = data.active || [];\n const history = data.history || [];\n const fmtTs = function(ms) { const d = new Date(ms); return (d.getMonth()+1) + '/' + d.getDate() + ' ' + String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0') + ':' + String(d.getSeconds()).padStart(2,'0'); };\n const fmtDur = function(s) { if (s < 60) return s + 's'; const m = Math.floor(s/60); return m + 'm ' + (s % 60) + 's'; };\n $('webrtc-active').innerHTML = active.length\n ? active.map(function(b){ return '<div>· call=' + escapeHtml(b.callId.slice(0, 12)) + ' agent=<b>' + escapeHtml(b.agentId) + '</b> target=' + escapeHtml(b.target) + ' uptime=' + fmtDur(b.uptimeSec) + '</div>'; }).join('')\n : '<i style=\"color:var(--ax-muted)\">no active calls</i>';\n $('webrtc-history').innerHTML = history.length\n ? '<div style=\"display:grid;grid-template-columns:130px 110px 110px 1fr 70px 60px;gap:6px;font-size:10px;color:var(--ax-muted);text-transform:uppercase;letter-spacing:0.04em;padding-bottom:4px;border-bottom:1px solid var(--ax-border);margin-bottom:4px\"><div>started</div><div>agent</div><div>target</div><div>callId</div><div>duration</div><div>chunks</div></div>' +\n history.map(function(h){ return '<div style=\"display:grid;grid-template-columns:130px 110px 110px 1fr 70px 60px;gap:6px;padding:3px 0\">' +\n '<div>' + escapeHtml(fmtTs(h.startedAt)) + '</div>' +\n '<div><b>' + escapeHtml(h.agentId) + '</b></div>' +\n '<div>' + escapeHtml(h.target) + '</div>' +\n '<div style=\"color:var(--ax-muted)\">' + escapeHtml(h.callId.slice(0, 24)) + '</div>' +\n '<div>' + escapeHtml(fmtDur(h.durationSec)) + '</div>' +\n '<div>' + h.transcriptChunks + '</div>' +\n '</div>'; }).join('')\n : '<i style=\"color:var(--ax-muted)\">no completed sessions in this daemon&#39;s memory yet</i>';\n } catch (e) {\n $('webrtc-active').innerHTML = '<i style=\"color:var(--ax-err)\">failed: ' + e.message + '</i>';\n $('webrtc-history').innerHTML = '';\n }\n}\n\nfunction renderWhatsAppPane() {\n const w = state.whatsapp || {};\n const enabled = !!w.enabled;\n const statusChip = enabled\n ? '<span class=\"chip\" style=\"background:rgba(34,197,94,0.15);color:var(--ax-accent)\">enabled</span>'\n : '<span class=\"chip off\">disabled</span>';\n $('ch-whatsapp').innerHTML =\n '<div class=\"row-card\" style=\"margin-bottom:12px\"><div class=\"info\"><h3>WhatsApp</h3>' +\n '<div class=\"meta\">' + statusChip +\n ' · session dir: <code style=\"font-family:var(--ax-mono)\">' + escapeHtml(w.sessionDir) + '</code>' +\n ' · ' + w.routeCount + ' route(s)' +\n '</div></div></div>' +\n '<div id=\"wa-pairing\" class=\"add-form\"><h3>Pairing</h3>' +\n '<div id=\"wa-status\" style=\"margin-bottom:10px;font-size:12px;color:var(--ax-muted)\">checking…</div>' +\n '<div id=\"wa-qr-wrap\" style=\"display:none;text-align:center;padding:14px;background:var(--ax-bg);border:1px solid var(--ax-border);border-radius:6px\">' +\n '<img id=\"wa-qr\" alt=\"WhatsApp QR\" style=\"width:260px;height:260px;background:#fff;padding:8px;border-radius:4px\" />' +\n '<div style=\"margin-top:10px;font-size:11px;color:var(--ax-muted);line-height:1.55\">Open WhatsApp on your phone → <b>Settings</b> → <b>Linked devices</b> → <b>Link a device</b> and scan. The code refreshes every ~20s.</div>' +\n '</div>' +\n '<div id=\"wa-connected\" style=\"display:none;padding:14px;background:color-mix(in oklch,var(--ax-accent) 10%,transparent);border:1px solid color-mix(in oklch,var(--ax-accent) 35%,transparent);border-radius:6px;color:var(--ax-accent);font-size:13px\">✓ Paired — the daemon is signed in.</div>' +\n '<div id=\"wa-disabled\" style=\"display:none;padding:14px;font-size:12px;color:var(--ax-muted);line-height:1.6\">WhatsApp is currently disabled in <code>agentx.json</code>. Enable it there (or via the Advanced tab) and restart the daemon; the QR will appear once Baileys needs it.</div>' +\n '<div class=\"hint-block\" style=\"margin-top:12px\">Routes, phone numbers, and allow-lists live under <code>channels.whatsapp</code> in the <b>Advanced</b> tab. Requires <code>@whiskeysockets/baileys</code> installed in the daemon&rsquo;s dir.</div>' +\n '</div>' +\n '<details class=\"add-form\" style=\"margin-top:14px\"><summary class=\"primary\">📥 Wiki ingest from WhatsApp</summary>' +\n '<div style=\"margin-top:10px\">' +\n '<p style=\"font-size:11px;color:var(--ax-muted);margin:0 0 10px\">Sweep observed WhatsApp chats/contacts into an agent&#39;s wiki. Mirrors <code>agentx whatsapp ingest-all/list-chats/list-contacts</code>.</p>' +\n '<div style=\"display:flex;gap:8px;margin-bottom:10px\">' +\n '<button class=\"ax-btn\" onclick=\"loadWhatsAppLists()\">Reload chats + contacts</button>' +\n '<label class=\"ax-inline\" style=\"display:inline-flex;gap:6px;align-items:center;font-size:12px\"><input type=\"checkbox\" id=\"wa-ingest-dry\" checked /> dry-run</label>' +\n '<label class=\"ax-inline\" style=\"display:inline-flex;gap:6px;align-items:center;font-size:12px\"><input type=\"checkbox\" id=\"wa-ingest-force\" /> force (override <code>ingest.enabled=false</code>)</label>' +\n '</div>' +\n '<div style=\"display:flex;gap:8px;margin-bottom:10px\">' +\n '<input id=\"wa-ingest-agent\" placeholder=\"agent id (defaults to channels.whatsapp.defaultAgent)\" style=\"flex:1\" />' +\n '<button class=\"ax-btn ax-btn--primary\" onclick=\"runWhatsAppIngest()\">Sweep now</button>' +\n '</div>' +\n '<div id=\"wa-ingest-msg\" class=\"msg\" style=\"margin-bottom:10px\"></div>' +\n '<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:10px\">' +\n '<div><h4 style=\"font-size:11px;color:var(--ax-muted);margin:0 0 4px;text-transform:uppercase;letter-spacing:0.04em\">Chats</h4><div id=\"wa-chats\" style=\"max-height:200px;overflow-y:auto;border:1px solid var(--ax-border);border-radius:4px;padding:6px;font-family:&quot;IBM Plex Mono&quot;,monospace;font-size:11px\">click reload</div></div>' +\n '<div><h4 style=\"font-size:11px;color:var(--ax-muted);margin:0 0 4px;text-transform:uppercase;letter-spacing:0.04em\">Contacts</h4><div id=\"wa-contacts\" style=\"max-height:200px;overflow-y:auto;border:1px solid var(--ax-border);border-radius:4px;padding:6px;font-family:&quot;IBM Plex Mono&quot;,monospace;font-size:11px\">click reload</div></div>' +\n '</div>' +\n '</div>' +\n '</details>';\n startWhatsAppPolling();\n}\n\nwindow.loadWhatsAppLists = async function() {\n try {\n const [chats, contacts] = await Promise.all([\n req('GET', '/api/admin/channels/whatsapp/chats').catch(() => ({chats:[]})),\n req('GET', '/api/admin/channels/whatsapp/contacts').catch(() => ({contacts:[]})),\n ]);\n const cl = $('wa-chats');\n const cs = chats.chats || [];\n cl.innerHTML = cs.length ? cs.slice(0, 100).map(c => '<div>' + escapeHtml(c.name || c.jid || '?') + ' <span style=\"color:var(--ax-muted)\">' + (c.kind || '') + '</span></div>').join('') : '<i style=\"color:var(--ax-muted)\">empty</i>';\n const xl = $('wa-contacts');\n const xs = contacts.contacts || [];\n xl.innerHTML = xs.length ? xs.slice(0, 100).map(c => '<div>' + escapeHtml(c.name || c.jid || '?') + '</div>').join('') : '<i style=\"color:var(--ax-muted)\">empty</i>';\n } catch (e) { showMsg($('wa-ingest-msg'), 'err', e.message); }\n}\n\nwindow.runWhatsAppIngest = async function() {\n const agent = $('wa-ingest-agent').value.trim();\n const dryRun = $('wa-ingest-dry').checked;\n const force = $('wa-ingest-force').checked;\n const body = { dryRun, force };\n if (agent) body.agent = agent;\n showMsg($('wa-ingest-msg'), 'info', 'sweeping…');\n try {\n const r = await req('POST', '/api/admin/channels/whatsapp/ingest', body);\n showMsg($('wa-ingest-msg'), 'ok', 'Done — ' + (r.summary || JSON.stringify(r).slice(0, 120)));\n } catch (e) { showMsg($('wa-ingest-msg'), 'err', e.message); }\n}\n\nconst _wa = { timer: null, lastQRSeen: '' };\nfunction startWhatsAppPolling() {\n stopWhatsAppPolling();\n _wa.timer = setInterval(pollWhatsAppState, 3000);\n pollWhatsAppState();\n}\nfunction stopWhatsAppPolling() {\n if (_wa.timer) { clearInterval(_wa.timer); _wa.timer = null; }\n}\nasync function pollWhatsAppState() {\n // Stop polling if the user switched away from the WhatsApp pane.\n const pane = $('ch-whatsapp');\n if (!pane || pane.hidden) { stopWhatsAppPolling(); return; }\n try {\n const s = await req('GET', '/api/admin/channels/whatsapp/state');\n const enabled = !!(state.whatsapp && state.whatsapp.enabled);\n const status = $('wa-status');\n const qrWrap = $('wa-qr-wrap');\n const connected = $('wa-connected');\n const disabled = $('wa-disabled');\n const qrImg = $('wa-qr');\n if (!enabled) {\n status.textContent = 'disabled in config';\n qrWrap.style.display = 'none'; connected.style.display = 'none'; disabled.style.display = 'block';\n return;\n }\n disabled.style.display = 'none';\n const connState = s.connection || 'init';\n const detail = s.detail ? ' — ' + s.detail : '';\n if (connState === 'open') {\n status.innerHTML = '<span style=\"color:var(--ax-accent)\">● connected</span>';\n qrWrap.style.display = 'none'; connected.style.display = 'block';\n } else if (s.qr) {\n status.innerHTML = '<span style=\"color:var(--ax-warn)\">● waiting for scan</span>' + (detail ? escapeHtml(detail) : '');\n connected.style.display = 'none'; qrWrap.style.display = 'block';\n // Bust cache when the QR actually changed.\n if (s.qrUpdatedAt !== _wa.lastQRSeen) {\n _wa.lastQRSeen = s.qrUpdatedAt;\n qrImg.src = '/api/admin/channels/whatsapp/qr.svg?t=' + encodeURIComponent(s.qrUpdatedAt || Date.now());\n }\n } else {\n status.textContent = connState + detail + ' — waiting for the daemon to emit a QR…';\n qrWrap.style.display = 'none'; connected.style.display = 'none';\n }\n } catch (e) {\n $('wa-status').innerHTML = '<span style=\"color:var(--ax-err)\">' + escapeHtml(e.message) + '</span>';\n }\n}\n\nfunction renderSlackSection() {\n const s = state.slack || {};\n const agentOptions = state.agents.map((a) => '<option value=\"' + escapeHtml(a.id) + '\"' +\n (a.id === (s.agentBinding || '') ? ' selected' : '') + '>' +\n escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>').join('');\n // Strip dollar-brace wrappers so the edit form shows just the env-var name.\n const stripRef = (ref) => (ref || '').replace(/^\\\\\\$\\\\{|\\\\}$/g, '');\n const configured = s.botTokenRef && s.appTokenRef;\n const statusChip = configured\n ? (s.enabled\n ? '<span class=\"chip\" style=\"background:rgba(34,197,94,0.15);color:var(--ax-accent)\">enabled</span>'\n : '<span class=\"chip off\">disabled</span>')\n : '<span class=\"chip off\">not configured</span>';\n\n $('slack-section').innerHTML =\n '<h3 style=\"margin:0 0 8px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--ax-muted)\">Slack</h3>' +\n '<div class=\"row-card\" style=\"margin-bottom:12px\"><div class=\"info\"><h3># slack workspace</h3>' +\n '<div class=\"meta\">' + statusChip +\n (s.botTokenRef ? ' bot: <code style=\"font-family:var(--ax-mono)\">' + escapeHtml(s.botTokenRef) + '</code>' : '') +\n (s.appTokenRef ? ' · app: <code style=\"font-family:var(--ax-mono)\">' + escapeHtml(s.appTokenRef) + '</code>' : '') +\n (s.agentBinding ? ' · agent: <b>' + escapeHtml(s.agentBinding) + '</b>' : '') +\n '</div></div>' +\n (configured ? '<label class=\"toggle-switch\" style=\"margin:0\"><input type=\"checkbox\" id=\"slack-toggle\"' + (s.enabled ? ' checked' : '') + ' /> <span>On</span></label>' : '') +\n '</div>' +\n '<div class=\"add-form\"><h3>' + (configured ? 'Edit Slack connection' : 'Connect Slack') + '</h3>' +\n '<div class=\"rowf\">' +\n '<div><label>Bot token env-var<span class=\"hint\">(<code>xoxb-…</code>)</span></label><input id=\"slack-bot-env\" placeholder=\"SLACK_BOT_TOKEN\" value=\"' + escapeHtml(stripRef(s.botTokenRef)) + '\" /></div>' +\n '<div><label>App token env-var<span class=\"hint\">(<code>xapp-…</code> — Socket Mode)</span></label><input id=\"slack-app-env\" placeholder=\"SLACK_APP_TOKEN\" value=\"' + escapeHtml(stripRef(s.appTokenRef)) + '\" /></div>' +\n '</div>' +\n '<label>Default agent<span class=\"hint\">(optional — used when a Slack message has no explicit @mention of another agent)</span></label>' +\n '<select id=\"slack-agent\"><option value=\"\">(none)</option>' + agentOptions + '</select>' +\n '<div class=\"actions\"><button class=\"primary\" onclick=\"configureSlack()\">' + (configured ? 'Save changes' : 'Connect') + '</button>' +\n '<div id=\"slack-msg\" class=\"msg\"></div></div>' +\n '<div class=\"hint-block\">Create the Slack app at <code>api.slack.com/apps</code>, enable Socket Mode, and generate an app-level token with <code>connections:write</code>. Bot needs scopes <code>chat:write</code>, <code>app_mentions:read</code>, <code>channels:history</code>, <code>im:history</code>, <code>users:read</code>, and event subscriptions for <code>app_mention</code> + <code>message.*</code>. Add <code>xoxb-…</code> and <code>xapp-…</code> to <code>.env</code>, then install <code>@slack/socket-mode</code> + <code>@slack/web-api</code>.</div>' +\n '</div>';\n const t = $('slack-toggle');\n if (t) t.addEventListener('change', async (e) => {\n try { await req('POST', '/api/admin/channels/slack/toggle', { enabled: e.target.checked }); refresh(); }\n catch (err) { showMsg($('global-msg'), 'err', err.message); e.target.checked = !e.target.checked; }\n });\n}\n\nasync function configureSlack() {\n const body = {\n botTokenEnv: $('slack-bot-env').value.trim(),\n appTokenEnv: $('slack-app-env').value.trim(),\n agentBinding: $('slack-agent').value,\n };\n try {\n const r = await req('POST', '/api/admin/channels/slack', body);\n showMsg($('slack-msg'), 'ok', (r.summary || 'Saved.') + (r.hint ? ' — ' + r.hint : ''));\n refresh();\n } catch (e) { showMsg($('slack-msg'), 'err', e.message); }\n}\n\nfunction openTelegramEdit(acc) {\n const modal = $('tg-edit-modal');\n $('tg-edit-title').textContent = 'Edit Telegram · ' + acc.id;\n $('tg-edit-agent').innerHTML = state.agents.map((a) =>\n '<option value=\"' + escapeHtml(a.id) + '\"' + (a.id === acc.agentBinding ? ' selected' : '') + '>' +\n escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>').join('');\n $('tg-edit-user').value = acc.botUsername || '';\n // Current token ref is a dollar-brace placeholder — strip the wrapper for the form.\n const ref = acc.botTokenRef || '';\n $('tg-edit-env').value = ref.replace(/^\\\\\\$\\\\{|\\\\}$/g, '');\n $('tg-edit-id').value = acc.id;\n $('tg-edit-msg').className = 'msg';\n modal.classList.remove('hidden');\n modal.setAttribute('aria-hidden', 'false');\n}\n\nfunction closeTelegramEdit() {\n const modal = $('tg-edit-modal');\n modal.classList.add('hidden');\n modal.setAttribute('aria-hidden', 'true');\n}\n\nasync function saveTelegramEdit() {\n const id = $('tg-edit-id').value;\n const patch = {\n agentBinding: $('tg-edit-agent').value,\n botUsername: $('tg-edit-user').value.trim(),\n botTokenEnv: $('tg-edit-env').value.trim(),\n };\n try {\n await req('PATCH', '/api/admin/channels/telegram', { id, patch });\n showMsg($('tg-edit-msg'), 'ok', 'Saved.');\n setTimeout(() => { closeTelegramEdit(); refresh(); }, 500);\n } catch (e) { showMsg($('tg-edit-msg'), 'err', e.message); }\n}\n\nasync function addTelegram() {\n const body = {\n id: $('tg-id').value.trim(),\n agentBinding: $('tg-agent').value,\n botUsername: $('tg-user').value.trim(),\n botTokenEnv: $('tg-env').value.trim(),\n };\n try {\n const r = await req('POST', '/api/admin/channels/telegram', body);\n showMsg($('tg-msg'), 'ok', (r.summary || 'Added.') + (r.hint ? ' — ' + r.hint : ''));\n refresh();\n } catch (e) { showMsg($('tg-msg'), 'err', e.message); }\n}\n\nasync function deleteTelegram(id) {\n if (!confirm('Delete Telegram account \"' + id + '\"?')) return;\n try { await req('DELETE', '/api/admin/channels/telegram', { id }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nfunction renderCrons() {\n const list = $('cron-list');\n if (!state.crons.length) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:42px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><h3 style=\"margin:0 0 4px;color:var(--ax-text);font-size:15px;font-weight:600\">No schedules yet</h3><p style=\"margin:0;font-size:13px\">Create one below to nudge an agent on a timer.</p></div>';\n return;\n }\n list.innerHTML = '';\n for (const c of state.crons) {\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar ax-avatar--' + avatarVariant(c.id) + '\">' +\n '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>' +\n '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(c.id) + '</div>' +\n '<div class=\"ax-sub\">' +\n '<span class=\"ax-pill\"><span class=\"ax-pill__dot\" style=\"background:var(--ax-accent)\"></span>' + escapeHtml(c.schedule) + '</span>' +\n ' runs <b>' + escapeHtml(c.agent) + '</b>' +\n (c.prompt ? ' · ' + escapeHtml(c.prompt.slice(0, 80) + (c.prompt.length > 80 ? '…' : '')) : '') +\n '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n '<button class=\"ax-btn ax-btn--danger\" data-id=\"' + escapeHtml(c.id) + '\">Delete</button>' +\n '</div>' +\n '</div>';\n card.querySelector('[data-id]').addEventListener('click', () => deleteCron(c.id));\n list.appendChild(card);\n }\n}\n\nasync function addCron() {\n const body = {\n id: $('c-id').value.trim(),\n schedule: $('c-schedule').value.trim(),\n agent: $('c-agent').value,\n prompt: $('c-prompt').value.trim(),\n };\n try {\n const r = await req('POST', '/api/admin/crons', body);\n showMsg($('c-msg'), 'ok', r.summary || 'Added.');\n $('c-id').value = ''; $('c-schedule').value = ''; $('c-prompt').value = '';\n refresh();\n } catch (e) { showMsg($('c-msg'), 'err', e.message); }\n}\n\nasync function deleteCron(id) {\n if (!confirm('Delete schedule \"' + id + '\"?')) return;\n try { await req('DELETE', '/api/admin/crons', { id }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\n/* ========== Schedule builder (Guided mode) ==========\n * Translates sentence-builder selections (agent/frequency/days/hour) into a\n * cron expression and populates the hidden #c-schedule input used by\n * addCron(). Also keeps the /preview endpoint happy with the natural-\n * language preview line. */\n\nlet __scheduleState = { agent: '', freq: 'weekly', days: [1], hour: 9 };\n\nfunction scheduleCronFromState() {\n const s = __scheduleState;\n if (s.freq === 'hourly') return '0 * * * *';\n if (s.freq === '15min') return '*/15 * * * *';\n if (s.freq === 'daily') return '0 ' + s.hour + ' * * *';\n if (s.freq === 'weekdays') return '0 ' + s.hour + ' * * 1-5';\n // weekly → days\n const dow = s.days.length ? s.days.slice().sort().join(',') : '1';\n return '0 ' + s.hour + ' * * ' + dow;\n}\n\nfunction schedulePreviewText() {\n const s = __scheduleState;\n const hh = ((s.hour + 11) % 12) + 1;\n const ap = s.hour < 12 ? 'am' : 'pm';\n const time = hh + ':00 ' + ap;\n if (s.freq === 'hourly') return 'Every hour on the hour';\n if (s.freq === '15min') return 'Every 15 minutes';\n if (s.freq === 'daily') return 'Every day at ' + time;\n if (s.freq === 'weekdays') return 'Weekdays at ' + time;\n const names = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];\n const pretty = s.days.slice().sort().map(d => names[d]).join(', ');\n return (pretty || 'No day picked') + ' at ' + time;\n}\n\nfunction updateSchedulePreview() {\n const cron = scheduleCronFromState();\n const $id = (x) => document.getElementById(x);\n const preview = $id('s-preview-text');\n if (preview) preview.innerHTML = '<b>' + schedulePreviewText() + '</b> &nbsp;·&nbsp; cron: <span class=\"mono\" style=\"color:var(--ax-text)\">' + cron + '</span>';\n const echo = $id('s-cron-echo');\n if (echo) echo.textContent = cron;\n // Keep the hidden #c-schedule + #c-agent in sync so addCron() reads them.\n const schedInp = $id('c-schedule');\n if (schedInp) schedInp.value = cron;\n const agentSel = $id('c-agent');\n if (agentSel) agentSel.value = __scheduleState.agent || (agentSel.options[0] && agentSel.options[0].value);\n}\n\nfunction initScheduleBuilder() {\n const freq = document.getElementById('s-freq');\n const hour = document.getElementById('s-hour');\n const agent = document.getElementById('s-agent');\n const sAt = document.getElementById('s-at');\n if (!freq || !hour || !agent) return;\n // Default active day = Monday\n const dayBtns = document.querySelectorAll('#s-days button');\n dayBtns.forEach((b) => {\n if (b.dataset.day === '1') b.classList.add('is-active');\n b.addEventListener('click', () => {\n b.classList.toggle('is-active');\n __scheduleState.days = [...document.querySelectorAll('#s-days button.is-active')]\n .map(el => parseInt(el.dataset.day, 10));\n updateSchedulePreview();\n });\n });\n // Populate agent select from state\n agent.innerHTML = state.agents.map((a) =>\n '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + '</option>'\n ).join('');\n if (agent.options.length) __scheduleState.agent = agent.options[0].value;\n\n // Mode switch\n document.querySelectorAll('#sched-mode button').forEach((b) => {\n b.addEventListener('click', () => {\n document.querySelectorAll('#sched-mode button').forEach((x) => x.classList.toggle('is-active', x === b));\n const simple = document.getElementById('sched-simple');\n const expert = document.getElementById('sched-expert');\n if (!simple || !expert) return;\n const mode = b.dataset.mode;\n simple.style.display = mode === 'simple' ? '' : 'none';\n expert.style.display = mode === 'expert' ? '' : 'none';\n });\n });\n\n // Hide day-picker + \"at\" unless frequency is weekly\n const reflectFreq = () => {\n const days = document.getElementById('s-days');\n const show = __scheduleState.freq === 'weekly';\n if (days) days.style.display = show ? '' : 'none';\n const alwaysTimeless = (__scheduleState.freq === 'hourly' || __scheduleState.freq === '15min');\n if (sAt) sAt.style.display = alwaysTimeless ? 'none' : '';\n hour.style.display = alwaysTimeless ? 'none' : '';\n };\n\n agent.addEventListener('change', () => { __scheduleState.agent = agent.value; updateSchedulePreview(); });\n freq.addEventListener('change', () => { __scheduleState.freq = freq.value; reflectFreq(); updateSchedulePreview(); });\n hour.addEventListener('change', () => {\n // \"9:00 am\" → 9, \"2:00 pm\" → 14\n const text = hour.value.trim().toLowerCase();\n const m = text.match(/^(\\\\d+):\\\\d+\\\\s*(am|pm)/);\n if (m) {\n let h = parseInt(m[1], 10);\n if (m[2] === 'pm' && h !== 12) h += 12;\n if (m[2] === 'am' && h === 12) h = 0;\n __scheduleState.hour = h;\n }\n updateSchedulePreview();\n });\n\n reflectFreq();\n updateSchedulePreview();\n}\n\nasync function addCronExpert() {\n // Expert mode has its own id/agent/prompt inputs; mirror them into\n // the canonical #c-* ids before dispatching.\n const $id = (x) => document.getElementById(x);\n $id('c-id').value = $id('c-id-expert').value.trim();\n const expertAgent = $id('c-agent-expert');\n if (expertAgent && expertAgent.value) {\n $id('c-agent').value = expertAgent.value;\n }\n $id('c-prompt').value = $id('c-prompt-expert').value.trim();\n await addCron();\n}\n\n// Expert-mode agent dropdown mirrors the guided one.\nfunction initScheduleExpertAgentSelect() {\n const sel = document.getElementById('c-agent-expert');\n if (!sel) return;\n sel.innerHTML = state.agents.map((a) =>\n '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>'\n ).join('');\n}\n\n// Live cron preview — debounced so each keystroke doesn't hit the server.\nlet cronPreviewTimer = null;\nfunction scheduleCronPreview() {\n if (cronPreviewTimer) clearTimeout(cronPreviewTimer);\n cronPreviewTimer = setTimeout(runCronPreview, 300);\n}\nasync function runCronPreview() {\n const expr = $('c-schedule').value.trim();\n const box = $('c-preview');\n if (!expr) { box.textContent = ''; return; }\n try {\n const r = await req('POST', '/api/admin/crons/preview', { schedule: expr });\n const fires = r.next.map((iso) => new Date(iso).toLocaleString()).slice(0, 3);\n box.innerHTML =\n '<div style=\"color:var(--text)\"><b>' + escapeHtml(r.human) + '</b></div>' +\n '<div style=\"margin-top:4px\">Next runs (' + escapeHtml(r.timezone) + '): ' + fires.map(escapeHtml).join(' · ') + '</div>';\n } catch (e) {\n box.innerHTML = '<div style=\"color:var(--red)\">' + escapeHtml(e.message) + '</div>';\n }\n}\n$('c-schedule').addEventListener('input', scheduleCronPreview);\n\n// --- Files modal (identity + skills) ---\nconst filesModal = {\n el: $('files-modal'),\n agentId: null,\n agentName: null,\n currentTab: 'identity', // 'identity' | 'skills'\n currentPath: null, // path of file being edited\n originalContent: '', // for revert\n};\n\nasync function openFilesModal(agentId, agentName) {\n filesModal.agentId = agentId;\n filesModal.agentName = agentName;\n $('files-title').textContent = agentName + ' · Files';\n filesModal.el.classList.remove('hidden');\n filesModal.el.setAttribute('aria-hidden', 'false');\n switchFilesTab('identity');\n}\n\nfunction closeFilesModal() {\n filesModal.el.classList.add('hidden');\n filesModal.el.setAttribute('aria-hidden', 'true');\n filesModal.agentId = null;\n filesModal.currentPath = null;\n hideFileEditor();\n}\n\nfunction switchFilesTab(tab) {\n filesModal.currentTab = tab;\n filesModal.currentPath = null;\n $('files-tab-label').textContent = tab === 'skills' ? 'Skills' : 'Identity';\n $('files-tab-identity').style.background = tab === 'identity' ? 'rgba(99,102,241,0.15)' : 'transparent';\n $('files-tab-skills').style.background = tab === 'skills' ? 'rgba(99,102,241,0.15)' : 'transparent';\n $('files-add-skill').style.display = tab === 'skills' ? 'block' : 'none';\n hideFileEditor();\n loadFileList();\n}\n\nasync function loadFileList() {\n const picker = $('files-picker');\n picker.innerHTML = '<div class=\"empty\" style=\"padding:20px 10px;color:var(--muted)\">loading…</div>';\n try {\n const r = await req('GET', '/api/admin/files?agent=' + encodeURIComponent(filesModal.agentId));\n filesModal.overview = r;\n renderFilesPicker(r);\n } catch (e) {\n picker.innerHTML = '<div class=\"empty\" style=\"color:var(--red);padding:14px\">' + escapeHtml(e.message) + '</div>';\n }\n}\n\nfunction renderFilesPicker(overview) {\n const picker = $('files-picker');\n if (filesModal.currentTab === 'identity') {\n const rows = overview.identity.map((f) => {\n const activeCls = filesModal.currentPath === f.path ? ' active' : '';\n const missingCls = f.exists ? '' : ' missing';\n const sizeLabel = f.exists ? formatBytes(f.size) : 'create';\n return '<div class=\"file' + activeCls + missingCls + '\" data-path=\"' + escapeHtml(f.path) + '\">' +\n '<span class=\"info\">' + escapeHtml(f.title) + '</span>' +\n '<span class=\"size\">' + sizeLabel + '</span>' +\n '</div>';\n }).join('');\n picker.innerHTML = '<div class=\"group\">Identity files</div>' + (rows || '<div class=\"empty\">No files.</div>');\n } else {\n if (overview.skills.length === 0) {\n picker.innerHTML = '<div class=\"group\">Skills</div><div class=\"empty\" style=\"padding:14px;color:var(--muted)\">No skills yet.</div>';\n } else {\n const rows = overview.skills.map((s) => {\n const activeCls = filesModal.currentPath === s.path ? ' active' : '';\n return '<div class=\"file' + activeCls + '\" data-path=\"' + escapeHtml(s.path) + '\" data-slug=\"' + escapeHtml(s.slug) + '\">' +\n '<span class=\"info\">' + escapeHtml(s.title) + '<br><span class=\"size\">' + escapeHtml(s.slug) + '</span></span>' +\n '<button class=\"del\" data-rm-skill=\"' + escapeHtml(s.slug) + '\" title=\"Delete skill\">🗑</button>' +\n '</div>';\n }).join('');\n picker.innerHTML = '<div class=\"group\">Skills</div>' + rows;\n }\n }\n for (const row of picker.querySelectorAll('.file')) {\n row.addEventListener('click', (e) => {\n if (e.target.closest('[data-rm-skill]')) return;\n openFileInEditor(row.dataset.path);\n });\n }\n for (const btn of picker.querySelectorAll('[data-rm-skill]')) {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n removeSkill(btn.dataset.rmSkill);\n });\n }\n}\n\nfunction formatBytes(n) {\n if (!n) return '0 B';\n if (n < 1024) return n + ' B';\n if (n < 1024 * 1024) return (n / 1024).toFixed(1).replace(/\\\\.0$/, '') + ' KB';\n return (n / (1024 * 1024)).toFixed(2) + ' MB';\n}\n\nasync function openFileInEditor(path) {\n filesModal.currentPath = path;\n renderFilesPicker(filesModal.overview); // restyle active row\n try {\n const r = await req('GET', '/api/admin/files/read?agent=' + encodeURIComponent(filesModal.agentId) + '&path=' + encodeURIComponent(path));\n filesModal.originalContent = r.content;\n $('files-current-path').textContent = path;\n $('files-editor').value = r.content;\n $('files-editor-empty').style.display = 'none';\n $('files-editor-head').style.display = 'flex';\n $('files-editor-wrap').style.display = 'flex';\n $('files-save-msg').className = 'msg';\n } catch (e) { showMsg($('files-save-msg'), 'err', e.message); }\n}\n\nfunction hideFileEditor() {\n $('files-editor-empty').style.display = 'flex';\n $('files-editor-head').style.display = 'none';\n $('files-editor-wrap').style.display = 'none';\n $('files-save-msg').className = 'msg';\n $('files-editor').value = '';\n}\n\nasync function saveCurrentFile() {\n if (!filesModal.currentPath) return;\n const content = $('files-editor').value;\n try {\n const r = await req('PUT', '/api/admin/files', {\n agent: filesModal.agentId,\n path: filesModal.currentPath,\n content,\n });\n filesModal.originalContent = content;\n showMsg($('files-save-msg'), 'ok', r.summary);\n loadFileList();\n } catch (e) { showMsg($('files-save-msg'), 'err', e.message); }\n}\n\nfunction revertCurrentFile() {\n $('files-editor').value = filesModal.originalContent;\n $('files-save-msg').className = 'msg';\n}\n\nasync function addSkill() {\n const slug = $('skill-slug').value.trim();\n const title = $('skill-title').value.trim();\n const fileInput = $('skill-file');\n const file = fileInput && fileInput.files && fileInput.files[0];\n let content;\n if (file) {\n try { content = await readFileAsText(file); }\n catch (e) { showMsg($('skill-msg'), 'err', 'Could not read file: ' + e.message); return; }\n if (content.length > 200 * 1024) {\n showMsg($('skill-msg'), 'err', 'File too large (>200 KB). Edit in-place after creation instead.');\n return;\n }\n }\n try {\n const body = { agent: filesModal.agentId, slug, title };\n if (content != null) body.content = content;\n const r = await req('POST', '/api/admin/files/skill', body);\n showMsg($('skill-msg'), 'ok', r.summary);\n $('skill-slug').value = ''; $('skill-title').value = '';\n if (fileInput) fileInput.value = '';\n await loadFileList();\n openFileInEditor(r.path);\n } catch (e) { showMsg($('skill-msg'), 'err', e.message); }\n}\n\nfunction readFileAsText(file) {\n return new Promise((resolve, reject) => {\n const r = new FileReader();\n r.onload = () => resolve(String(r.result || ''));\n r.onerror = () => reject(r.error || new Error('read failed'));\n r.readAsText(file);\n });\n}\n\n// Upload-into-editor: replaces the current file's buffer with the uploaded text.\n// User still has to click Save for the write to land on disk, so accidents are reversible.\nfunction wireFilesUpload() {\n const input = $('files-upload');\n if (!input) return;\n input.addEventListener('change', async (e) => {\n const file = e.target.files && e.target.files[0];\n if (!file) return;\n try {\n const text = await readFileAsText(file);\n if (text.length > 200 * 1024) throw new Error('File too large (>200 KB).');\n $('files-editor').value = text;\n showMsg($('files-save-msg'), 'ok', 'Loaded into editor — click Save to persist.');\n } catch (err) {\n showMsg($('files-save-msg'), 'err', err.message);\n }\n e.target.value = '';\n });\n}\nwireFilesUpload();\n\nasync function removeSkill(slug) {\n if (!confirm('Delete skill \"' + slug + '\"? The skill folder and SKILL.md will be removed from disk.')) return;\n try {\n await req('DELETE', '/api/admin/files/skill', { agent: filesModal.agentId, slug });\n if (filesModal.currentPath && filesModal.currentPath.includes('/' + slug + '/')) hideFileEditor();\n loadFileList();\n } catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\n$('files-close').addEventListener('click', closeFilesModal);\n$('files-modal').querySelector('.td-backdrop').addEventListener('click', closeFilesModal);\n$('files-tab-identity').addEventListener('click', () => switchFilesTab('identity'));\n$('files-tab-skills').addEventListener('click', () => switchFilesTab('skills'));\n$('files-save').addEventListener('click', saveCurrentFile);\n$('files-revert').addEventListener('click', revertCurrentFile);\n$('files-editor').addEventListener('keydown', (e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveCurrentFile(); }\n});\ndocument.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && !filesModal.el.classList.contains('hidden')) closeFilesModal();\n});\n\n// --- Edit agent modal ---\nconst editModal = {\n el: $('edit-modal'),\n currentId: null,\n};\n\nfunction openAgentEdit(agent) {\n editModal.currentId = agent.id;\n $('edit-title').textContent = agent.name + ' · ' + agent.id;\n $('e-name').value = agent.name || '';\n $('e-tier').value = agent.tier || 'claude-code';\n $('e-model').value = agent.model || '';\n $('e-triggers').value = (agent.mentions || []).join(', ');\n $('e-personality').value = agent.systemPrompt || '';\n $('e-max-concurrent').value = agent.maxConcurrent || 1;\n $('e-max-exec').value = agent.maxExecutionMinutes || 20;\n $('e-perm').value = agent.permissionMode || 'default';\n $('e-access').checked = agent.access === 'public';\n $('e-msg').className = 'msg';\n editModal.el.classList.remove('hidden');\n editModal.el.setAttribute('aria-hidden', 'false');\n}\n\nfunction closeAgentEdit() {\n editModal.el.classList.add('hidden');\n editModal.el.setAttribute('aria-hidden', 'true');\n editModal.currentId = null;\n}\n\nasync function saveAgentEdit() {\n if (!editModal.currentId) return;\n const id = editModal.currentId;\n const maxConcurrent = parseInt($('e-max-concurrent').value, 10);\n const maxExecutionMinutes = parseInt($('e-max-exec').value, 10);\n const patch = {\n name: $('e-name').value.trim(),\n tier: $('e-tier').value,\n model: $('e-model').value.trim(),\n triggerWords: $('e-triggers').value.trim(),\n systemPrompt: $('e-personality').value,\n maxConcurrent: Number.isFinite(maxConcurrent) ? maxConcurrent : undefined,\n maxExecutionMinutes: Number.isFinite(maxExecutionMinutes) ? maxExecutionMinutes : undefined,\n permissionMode: $('e-perm').value,\n access: $('e-access').checked ? 'public' : 'private',\n };\n try {\n await req('PATCH', '/api/admin/agents', { id, patch });\n showMsg($('e-msg'), 'ok', 'Saved. The change is live (hot-reloaded).');\n setTimeout(() => { closeAgentEdit(); refresh(); }, 600);\n } catch (e) { showMsg($('e-msg'), 'err', e.message); }\n}\n\n// Telegram edit wiring\n$('tg-edit-close').addEventListener('click', closeTelegramEdit);\n$('tg-edit-cancel').addEventListener('click', closeTelegramEdit);\n$('tg-edit-save').addEventListener('click', saveTelegramEdit);\n$('tg-edit-modal').querySelector('.td-backdrop').addEventListener('click', closeTelegramEdit);\n\n$('edit-close').addEventListener('click', closeAgentEdit);\n$('edit-cancel').addEventListener('click', closeAgentEdit);\n$('edit-save').addEventListener('click', saveAgentEdit);\neditModal.el.querySelector('.td-backdrop').addEventListener('click', closeAgentEdit);\ndocument.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && !editModal.el.classList.contains('hidden')) closeAgentEdit();\n});\n\n// --- Test drive chat modal ---\nconst testDrive = {\n modal: $('td-modal'),\n body: $('td-body'),\n title: $('td-title'),\n input: $('td-input'),\n sendBtn: $('td-send'),\n hint: $('td-hint'),\n closeBtn: $('td-close'),\n agentId: null,\n chatId: null, // stable per modal open so follow-ups share session context\n thinkingEl: null,\n busy: false,\n};\n\nfunction openTestDrive(agentId, agentName) {\n testDrive.agentId = agentId;\n testDrive.chatId = 'admin-' + Date.now();\n testDrive.title.textContent = agentName + ' · ' + agentId;\n testDrive.body.innerHTML = '<div class=\"td-empty\">Send a message to sanity-check this agent before wiring it to a real channel.</div>';\n testDrive.hint.textContent = 'Session: ' + testDrive.chatId + ' · channel: test-drive';\n testDrive.input.value = '';\n testDrive.modal.classList.remove('hidden');\n testDrive.modal.setAttribute('aria-hidden', 'false');\n setTimeout(() => testDrive.input.focus(), 50);\n}\n\nfunction closeTestDrive() {\n testDrive.modal.classList.add('hidden');\n testDrive.modal.setAttribute('aria-hidden', 'true');\n testDrive.agentId = null;\n testDrive.chatId = null;\n}\n\nfunction appendChat(kind, text) {\n const empty = testDrive.body.querySelector('.td-empty');\n if (empty) empty.remove();\n const el = document.createElement('div');\n el.className = 'td-msg ' + kind;\n el.textContent = text;\n testDrive.body.appendChild(el);\n testDrive.body.scrollTop = testDrive.body.scrollHeight;\n return el;\n}\n\nasync function sendTestDrive() {\n if (testDrive.busy) return;\n const msg = testDrive.input.value.trim();\n if (!msg || !testDrive.agentId) return;\n appendChat('user', msg);\n testDrive.input.value = '';\n testDrive.input.style.height = 'auto';\n testDrive.thinkingEl = appendChat('thinking', 'thinking…');\n testDrive.busy = true;\n testDrive.sendBtn.disabled = true;\n try {\n const r = await req('POST', '/api/admin/agents/test', {\n agent: testDrive.agentId,\n message: msg,\n chatId: testDrive.chatId,\n });\n testDrive.thinkingEl?.remove();\n testDrive.thinkingEl = null;\n const reply = r?.response?.content || r?.response?.error || '(empty reply)';\n appendChat(r?.response?.error ? 'err' : 'agent', reply);\n } catch (e) {\n testDrive.thinkingEl?.remove();\n testDrive.thinkingEl = null;\n appendChat('err', e.message);\n } finally {\n testDrive.busy = false;\n testDrive.sendBtn.disabled = false;\n testDrive.input.focus();\n }\n}\n\ntestDrive.sendBtn.addEventListener('click', sendTestDrive);\ntestDrive.closeBtn.addEventListener('click', closeTestDrive);\ntestDrive.modal.querySelector('.td-backdrop').addEventListener('click', closeTestDrive);\ntestDrive.input.addEventListener('keydown', (e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); sendTestDrive(); }\n});\ntestDrive.input.addEventListener('input', () => {\n // Autogrow the textarea up to 120px.\n testDrive.input.style.height = 'auto';\n testDrive.input.style.height = Math.min(testDrive.input.scrollHeight, 120) + 'px';\n});\ndocument.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && !testDrive.modal.classList.contains('hidden')) closeTestDrive();\n});\n\n// --- Tokens tab ---\nconst SCOPE_CHOICES = [\n { value: 'dashboard:read', label: 'Read dashboard + live view' },\n { value: 'dashboard:write', label: 'Write (admin + config) — full control' },\n { value: 'agent:*', label: 'Message any public agent' },\n { value: 'mesh:peer', label: 'Mesh peer auth (cross-node)' },\n];\n\nfunction renderScopeChoices() {\n const publicAgents = state.agents.filter((a) => a.access === 'public');\n const agentScopes = publicAgents.map((a) => ({ value: 'agent:' + a.id, label: 'Message agent: ' + a.name + ' (' + a.id + ')' }));\n const all = SCOPE_CHOICES.concat(agentScopes);\n const host = $('t-scopes');\n host.innerHTML = all.map((c) => {\n return '<label style=\"display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text);font-weight:400;margin:0\">' +\n '<input type=\"checkbox\" value=\"' + escapeHtml(c.value) + '\" style=\"width:auto\" /> ' +\n '<code style=\"background:#0e1119;padding:1px 6px;border-radius:3px;font-family:ui-monospace,monospace;font-size:11px;color:var(--accent)\">' + escapeHtml(c.value) + '</code>' +\n '<span style=\"color:var(--muted)\">— ' + escapeHtml(c.label) + '</span>' +\n '</label>';\n }).join('');\n}\n\nasync function loadTokens() {\n renderScopeChoices();\n try {\n const tokens = await req('GET', '/api/admin/tokens');\n const list = $('token-list');\n if (!tokens.length) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:42px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><h3 style=\"margin:0 0 4px;color:var(--ax-text);font-size:15px;font-weight:600\">No tokens yet</h3><p style=\"margin:0;font-size:13px\">Create one below to let a script, a chat bridge, or another AgentX machine talk to this one.</p></div>';\n return;\n }\n list.innerHTML = '';\n for (const t of tokens) {\n const now = Date.now();\n const expired = t.expiresAt && Date.parse(t.expiresAt) < now;\n const stale = t.lastUsedAt && (now - Date.parse(t.lastUsedAt)) > 30 * 24 * 60 * 60 * 1000 && !t.revokedAt;\n const statusPill = t.revokedAt\n ? '<span class=\"ax-pill ax-pill--off\">revoked</span>'\n : (expired\n ? '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>expired</span>'\n : (stale\n ? '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>stale</span>'\n : '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>active</span>'));\n const sub = [\n (t.scopes && t.scopes.length) ? 'scopes: ' + t.scopes.map(escapeHtml).join(', ') : 'no scopes',\n t.lastUsedAt ? 'last used ' + new Date(t.lastUsedAt).toLocaleString() : 'never used',\n t.expiresAt ? 'expires ' + new Date(t.expiresAt).toLocaleDateString() : 'no expiry',\n ].join(' · ');\n const avClass = t.revokedAt ? '' : (stale || expired ? 'ax-avatar--amber' : 'ax-avatar--teal');\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar ' + avClass + '\">' +\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"8\" cy=\"15\" r=\"4\"/><path d=\"M10.85 12.15L19 4M18 5l2 2M15 8l2 2\"/></svg>' +\n '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(t.name) + ' ' + statusPill + '</div>' +\n '<div class=\"ax-sub\"><span class=\"mono\">' + escapeHtml(t.prefix) + '…</span> · ' + sub + '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n (t.revokedAt ? '' : '<button class=\"ax-btn ax-btn--danger\" data-revoke=\"' + escapeHtml(t.id) + '\">Revoke</button>') +\n '</div>' +\n '</div>';\n const del = card.querySelector('[data-revoke]');\n if (del) del.addEventListener('click', () => revokeToken(t.id));\n list.appendChild(card);\n }\n } catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nasync function createToken() {\n const name = $('t-name').value.trim();\n const scopes = [...document.querySelectorAll('#t-scopes input:checked')].map((el) => el.value);\n const expires = $('t-expires').value.trim();\n try {\n const body = { name, scopes, expiresInDays: expires ? parseInt(expires, 10) : undefined };\n const r = await req('POST', '/api/admin/tokens', body);\n $('t-secret').textContent = r.token;\n $('t-reveal').style.display = 'block';\n showMsg($('t-msg'), 'ok', r.summary);\n $('t-name').value = ''; $('t-expires').value = '';\n document.querySelectorAll('#t-scopes input:checked').forEach((el) => { el.checked = false; });\n loadTokens();\n } catch (e) { showMsg($('t-msg'), 'err', e.message); }\n}\n\nasync function revokeToken(id) {\n if (!confirm('Revoke this token? Any callers using it will immediately fail.')) return;\n try { await req('DELETE', '/api/admin/tokens', { id }); loadTokens(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\n/* ========== Advanced tab: JSON tree viewer ==========\n *\n * Three modes:\n * tree — collapsible key/value tree with search + inline hints\n * raw — pretty-printed read-only JSON\n * edit — writable textarea (same behavior as the legacy Advanced tab)\n *\n * All three populate from the SAME /api/admin/config payload. When saving,\n * we send the text back to /api/admin/config/raw (only \"edit\" mode has a\n * writable textarea; tree and raw are read-only). */\n\nconst JV_HINTS = {\n 'node.name': 'friendly name of this daemon',\n 'node.id': 'stable identifier, used in logs',\n 'agents[].id': 'lowercase handle, used in logs',\n 'agents[].tier': 'which AI engine runs this agent (claude-code / codex-cli / opencode / sdk / orchestrator)',\n 'agents[].mentions': 'trigger words that wake this agent',\n 'channels.telegram.accounts[].token': 'reference to an env-var, not the token itself',\n 'channels.telegram.accounts[].agentBinding': 'which agent answers messages to this bot',\n 'crons[].schedule': 'standard five-field cron',\n 'mesh.peers[].url': 'full URL including port',\n 'tokens[].scopes': 'permissions this token grants',\n};\n\nlet __jvMode = 'tree';\nlet __jvRawConfig = null;\n\nfunction renderJvLine(value, path, keyLabel, parent) {\n const isArr = Array.isArray(value);\n const isObj = value && typeof value === 'object' && !isArr;\n const line = document.createElement('div');\n line.className = 'ax-jv-line';\n line.dataset.path = path;\n\n const tog = document.createElement('span');\n tog.className = 'ax-jv-toggle';\n const empty = (isArr ? value.length === 0 : (isObj ? Object.keys(value).length === 0 : true));\n if ((isArr || isObj) && !empty) {\n tog.textContent = '▾';\n tog.addEventListener('click', () => {\n const kids = line.nextElementSibling;\n if (kids && kids.classList.contains('ax-jv-children')) {\n const willCollapse = !kids.classList.contains('is-collapsed');\n kids.classList.toggle('is-collapsed');\n tog.textContent = willCollapse ? '▸' : '▾';\n }\n });\n } else {\n tog.className += ' empty';\n }\n line.appendChild(tog);\n\n if (keyLabel !== null) {\n const k = document.createElement('span');\n k.className = 'ax-jv-key';\n k.textContent = '\"' + keyLabel + '\"';\n line.appendChild(k);\n const colon = document.createElement('span');\n colon.className = 'ax-jv-punc';\n colon.textContent = ': ';\n line.appendChild(colon);\n }\n\n if (isArr || isObj) {\n const open = document.createElement('span');\n open.className = 'ax-jv-punc';\n open.textContent = isArr ? '[' : '{';\n line.appendChild(open);\n if (empty) {\n const close = document.createElement('span');\n close.className = 'ax-jv-empty-brace';\n close.textContent = isArr ? ']' : '}';\n line.appendChild(close);\n }\n } else if (value === null) {\n const v = document.createElement('span'); v.className = 'ax-jv-null'; v.textContent = 'null'; line.appendChild(v);\n } else if (typeof value === 'boolean') {\n const v = document.createElement('span'); v.className = 'ax-jv-bool'; v.textContent = String(value); line.appendChild(v);\n } else if (typeof value === 'number') {\n const v = document.createElement('span'); v.className = 'ax-jv-number'; v.textContent = String(value); line.appendChild(v);\n } else {\n const v = document.createElement('span'); v.className = 'ax-jv-string'; v.textContent = '\"' + String(value) + '\"'; line.appendChild(v);\n }\n\n const hintKey = path.replace(/\\\\[\\\\d+\\\\]/g, '[]');\n const hint = JV_HINTS[hintKey];\n if (hint) {\n const c = document.createElement('span');\n c.className = 'ax-jv-comment';\n c.textContent = '// ' + hint;\n line.appendChild(c);\n }\n\n parent.appendChild(line);\n\n if ((isArr || isObj) && !empty) {\n const wrap = document.createElement('div');\n wrap.className = 'ax-jv-children';\n const entries = isArr ? value.map((v, i) => [i, v]) : Object.entries(value);\n entries.forEach(([k, v]) => {\n const childPath = isArr ? path + '[' + k + ']' : (path ? path + '.' + k : String(k));\n renderJvLine(v, childPath, isArr ? null : k, wrap);\n });\n parent.appendChild(wrap);\n const close = document.createElement('div');\n close.className = 'ax-jv-line';\n const spacer = document.createElement('span');\n spacer.className = 'ax-jv-toggle empty';\n close.appendChild(spacer);\n const b = document.createElement('span');\n b.className = 'ax-jv-punc';\n b.textContent = isArr ? ']' : '}';\n close.appendChild(b);\n parent.appendChild(close);\n }\n}\n\nfunction renderJvTree(cfg) {\n const root = document.getElementById('jv-viewer');\n if (!root) return;\n root.innerHTML = '';\n root.style.whiteSpace = '';\n renderJvLine(cfg, '', null, root);\n}\n\nfunction renderJvRaw(cfg) {\n const root = document.getElementById('jv-viewer');\n if (!root) return;\n root.innerHTML = '<pre style=\"margin:0;font:inherit;color:var(--ax-text-2);white-space:pre-wrap\">' +\n escapeHtml(JSON.stringify(cfg, null, 2)) + '</pre>';\n}\n\nfunction renderJvEdit(cfg) {\n const root = document.getElementById('jv-viewer');\n const ta = document.getElementById('raw-editor');\n if (!root || !ta) return;\n root.innerHTML = '';\n ta.value = JSON.stringify(cfg, null, 2);\n ta.style.cssText = 'display:block;width:100%;min-height:500px;background:var(--ax-bg-elev);color:var(--ax-text);border:1px solid var(--ax-border);border-top:0;border-radius:0 0 var(--ax-radius) var(--ax-radius);font:inherit;font-family:var(--ax-mono);font-size:12.5px;padding:14px 18px;resize:vertical;outline:none';\n root.style.display = 'none';\n}\n\nfunction applyJvMode(mode) {\n __jvMode = mode;\n const root = document.getElementById('jv-viewer');\n const ta = document.getElementById('raw-editor');\n if (!root || !ta) return;\n if (mode !== 'edit') { root.style.display = ''; ta.style.display = 'none'; }\n if (mode === 'tree') renderJvTree(__jvRawConfig);\n if (mode === 'raw') renderJvRaw(__jvRawConfig);\n if (mode === 'edit') renderJvEdit(__jvRawConfig);\n document.querySelectorAll('#jv-mode button').forEach((b) => {\n b.classList.toggle('is-active', b.dataset.jvMode === mode);\n });\n}\n\nasync function loadRaw() {\n try {\n const r = await fetch('/api/admin/config');\n const txt = await r.text();\n // Try parsing; if it fails, fall back to edit mode with the raw text.\n try {\n __jvRawConfig = JSON.parse(txt);\n applyJvMode(__jvMode);\n const meta = document.getElementById('jv-meta');\n if (meta) {\n const kb = (txt.length / 1024).toFixed(1);\n meta.textContent = 'agentx.json · ' + kb + ' KB';\n }\n showMsg($('r-msg'), 'ok', 'Loaded.');\n } catch (pe) {\n __jvRawConfig = null;\n applyJvMode('edit');\n $('raw-editor').value = txt;\n showMsg($('r-msg'), 'err', 'Config is not valid JSON — open Edit mode to fix it.');\n }\n } catch (e) { showMsg($('r-msg'), 'err', e.message); }\n}\n\nasync function saveRaw() {\n // Save from the editor textarea if in edit mode; otherwise serialize the\n // in-memory config (useful after users read the Tree view and just click\n // Save to re-validate).\n const raw = __jvMode === 'edit' ? $('raw-editor').value : JSON.stringify(__jvRawConfig, null, 2);\n try {\n const r = await req('POST', '/api/admin/config/raw', { raw });\n showMsg($('r-msg'), 'ok', (r.summary || 'Saved.') + (r.backupPath ? ' (backup: ' + r.backupPath.split('/').pop() + ')' : ''));\n refresh();\n loadRaw();\n } catch (e) { showMsg($('r-msg'), 'err', e.message); }\n}\n\nfunction initJvControls() {\n const modeWrap = document.getElementById('jv-mode');\n if (modeWrap) {\n modeWrap.querySelectorAll('button').forEach((b) => {\n b.addEventListener('click', () => applyJvMode(b.dataset.jvMode));\n });\n }\n const search = document.getElementById('jv-search');\n if (search) {\n search.addEventListener('input', (e) => {\n const q = String(e.target.value || '').trim().toLowerCase();\n document.querySelectorAll('.ax-jv-line').forEach((line) => {\n const text = line.textContent.toLowerCase();\n line.classList.toggle('is-match', !!q && text.includes(q));\n });\n if (q) {\n document.querySelectorAll('.ax-jv-children.is-collapsed').forEach((c) => {\n if (c.textContent.toLowerCase().includes(q)) {\n c.classList.remove('is-collapsed');\n const sib = c.previousElementSibling;\n const tog = sib && sib.querySelector('.ax-jv-toggle:not(.empty)');\n if (tog) tog.textContent = '▾';\n }\n });\n }\n });\n }\n const expand = document.getElementById('jv-expand-all');\n if (expand) expand.addEventListener('click', () => {\n document.querySelectorAll('.ax-jv-children').forEach((c) => c.classList.remove('is-collapsed'));\n document.querySelectorAll('.ax-jv-toggle').forEach((t) => { if (!t.classList.contains('empty')) t.textContent = '▾'; });\n });\n const collapse = document.getElementById('jv-collapse-all');\n if (collapse) collapse.addEventListener('click', () => {\n document.querySelectorAll('.ax-jv-children').forEach((c) => c.classList.add('is-collapsed'));\n document.querySelectorAll('.ax-jv-toggle').forEach((t) => { if (!t.classList.contains('empty')) t.textContent = '▸'; });\n });\n const save = document.getElementById('jv-save');\n if (save) save.addEventListener('click', () => saveRaw());\n}\n// Wire up once on script load (doesn't depend on /api/admin/state).\ninitJvControls();\n\nfunction escapeHtml(s) {\n return String(s || '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]));\n}\n\n// Tab switching\nfor (const btn of document.querySelectorAll('nav.tabs button')) {\n btn.addEventListener('click', () => {\n document.querySelectorAll('nav.tabs button').forEach((b) => b.classList.remove('active'));\n document.querySelectorAll('section.tab').forEach((s) => s.classList.remove('active'));\n btn.classList.add('active');\n $('tab-' + btn.dataset.tab).classList.add('active');\n if (btn.dataset.tab === 'advanced') loadRaw();\n if (btn.dataset.tab === 'tokens') loadTokens();\n });\n}\n\n// --- Peer-proxy banner: shown when admin writes target a non-primary peer ---\n(function(){\n var cur = currentPeer();\n if (cur === 'primary') return;\n // Try to resolve a friendly name from the topbar menu (server-rendered).\n var friendly = cur;\n var picked = document.querySelector('.ax-mesh-menu a[data-peer-id=\"' + CSS.escape(cur) + '\"]');\n if (picked) {\n var span = picked.querySelector('.row > span:last-child');\n if (span) friendly = span.textContent.replace(/ · primary/,'').trim();\n }\n $('peer-banner-name').textContent = friendly;\n $('peer-banner').classList.add('is-active');\n $('peer-banner-back').addEventListener('click', function(){\n setCurrentPeer('primary');\n location.reload();\n });\n})();\n\nrefresh();\n`\n","import type { IncomingMessage, ServerResponse } from \"http\"\nimport { resolve } from \"path\"\nimport { loadDaemonConfig } from \"./config\"\nimport { GraphStore, type GraphNode, type Classification } from \"@/graph\"\nimport type { TopbarPeer } from \"./topbar\"\nimport { renderGraphPage } from \"./ui/pages/graph\"\n\n// --- /admin/graph panel: Intent Knowledge Graph management ---\n//\n// Three surfaces:\n// 1. Pending queue — LLM-proposed classifications awaiting human approval.\n// Approve / reject / edit-path inline.\n// 2. Tree view — current nodes grouped by level, editable axes.\n// 3. Schema — raw JSON editor for the level definitions (advanced).\n//\n// Matches admin-panel.ts in chrome + form-driven style. All persistence goes\n// through GraphStore so the disk layout stays canonical.\n\nexport function handleGraphGet(\n _req: IncomingMessage,\n res: ServerResponse,\n peers: TopbarPeer[] = [],\n localToken?: string,\n): void {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderGraphPage({ peers, localToken }))\n}\n\nexport async function handleGraphApi(\n req: IncomingMessage,\n res: ServerResponse,\n path: string,\n): Promise<void> {\n try {\n const body = req.method === \"GET\" ? undefined : await readJsonBody(req)\n const store = openStore()\n const dispatch: Record<string, () => unknown> = {\n \"GET /api/admin/graph/state\": () => getGraphState(store),\n \"POST /api/admin/graph/classifications/approve\": () => approveClassification(store, body),\n \"POST /api/admin/graph/classifications/reject\": () => rejectClassification(store, body),\n \"POST /api/admin/graph/nodes\": () => createNode(store, body),\n \"PATCH /api/admin/graph/nodes\": () => updateNode(store, body),\n \"DELETE /api/admin/graph/nodes\": () => deleteNode(store, body),\n \"PUT /api/admin/graph/schema\": () => saveSchema(store, body),\n }\n const key = `${req.method} ${path}`\n const handler = dispatch[key]\n if (!handler) {\n sendJson(res, 404, { error: `unknown graph endpoint: ${key}` })\n return\n }\n const result = await handler()\n sendJson(res, 200, result)\n } catch (e: any) {\n sendJson(res, 400, { error: e?.message || \"graph op failed\" })\n }\n}\n\n// ========================================================================\n// Store open — reads graph.baseDir out of agentx.json each call (cheap).\n// ========================================================================\n\nfunction openStore(): GraphStore {\n const cfg = loadDaemonConfig()\n const baseDir = resolve(process.cwd(), cfg.graph.baseDir)\n return new GraphStore({ baseDir })\n}\n\n// ========================================================================\n// Read side\n// ========================================================================\n\nfunction getGraphState(store: GraphStore) {\n const cfg = loadDaemonConfig()\n const schema = store.loadSchema()\n const nodes = store.loadNodes().nodes\n const recent = store.readRecentClassifications(100)\n const index = store.loadIndex().entries\n\n // A classification is \"pending\" if its fingerprint hasn't landed in the\n // snap-to-path index yet AND its latest log row isn't rejected. We scan the\n // recent log newest-first so later entries (approved/rejected) win over\n // earlier pending ones for the same msgHash.\n const seen = new Map<string, Classification>()\n for (const c of recent) {\n if (!seen.has(c.msgHash)) seen.set(c.msgHash, c)\n }\n const pending: Classification[] = []\n for (const c of seen.values()) {\n if (c.status === \"pending\" && !index[c.msgHash]) pending.push(c)\n }\n\n return {\n enabled: cfg.graph.enabled,\n schema,\n nodes,\n pending,\n recent: recent.slice(0, 25),\n counts: {\n nodes: nodes.length,\n pending: pending.length,\n fingerprints: Object.keys(index).length,\n },\n }\n}\n\n// ========================================================================\n// Mutations\n// ========================================================================\n\nfunction approveClassification(store: GraphStore, body: any) {\n const msgHash = String(body?.msgHash || \"\").trim()\n if (!msgHash) throw new Error(\"msgHash is required\")\n const c = findPending(store, msgHash)\n // Allow the UI to override the path / axes before approving.\n if (Array.isArray(body?.path) && body.path.length > 0) {\n c.path = body.path.map((s: any) => String(s))\n }\n if (body?.proposedAxes && typeof body.proposedAxes === \"object\") {\n c.proposedAxes = sanitizeAxes(body.proposedAxes)\n }\n store.approveClassification(c)\n return { summary: `Approved ${c.msgHash}` }\n}\n\nfunction rejectClassification(store: GraphStore, body: any) {\n const msgHash = String(body?.msgHash || \"\").trim()\n if (!msgHash) throw new Error(\"msgHash is required\")\n const c = findPending(store, msgHash)\n store.rejectClassification(c)\n return { summary: `Rejected ${c.msgHash}` }\n}\n\nfunction findPending(store: GraphStore, msgHash: string): Classification {\n const recent = store.readRecentClassifications(500)\n const hit = recent.find((c) => c.msgHash === msgHash)\n if (!hit) throw new Error(`No classification found for ${msgHash}`)\n return hit\n}\n\nfunction createNode(store: GraphStore, body: any): { node: GraphNode } {\n const id = slug(String(body?.id || \"\"))\n const level = String(body?.level || \"\").trim()\n const parentId = body?.parentId ? slug(String(body.parentId)) : null\n const axes = sanitizeAxes({ [id]: body?.axes || {} })[id] || {}\n if (!id) throw new Error(\"id is required\")\n if (!level) throw new Error(\"level is required\")\n const node: GraphNode = {\n id,\n level,\n parentId,\n axes,\n createdAt: new Date().toISOString(),\n createdBy: \"admin-ui\",\n }\n store.addNode(node)\n return { node }\n}\n\nfunction updateNode(store: GraphStore, body: any): { node: GraphNode } {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"id is required\")\n const axes = sanitizeAxes({ [id]: body?.axes || {} })[id] || {}\n const node = store.updateNodeAxes(id, axes)\n return { node }\n}\n\nfunction deleteNode(store: GraphStore, body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"id is required\")\n store.deleteNode(id)\n return { summary: `Deleted ${id}` }\n}\n\nfunction saveSchema(store: GraphStore, body: any) {\n if (!body?.schema || typeof body.schema !== \"object\") {\n throw new Error(\"schema object is required\")\n }\n store.saveSchema(body.schema) // GraphStore validates via zod; throws on invalid\n return { summary: \"Schema saved\" }\n}\n\n// ========================================================================\n// Helpers\n// ========================================================================\n\nfunction sanitizeAxes(\n src: Record<string, any>,\n): Record<string, Record<string, string>> {\n const out: Record<string, Record<string, string>> = {}\n for (const [nodeId, axes] of Object.entries(src || {})) {\n if (!axes || typeof axes !== \"object\") continue\n const clean: Record<string, string> = {}\n for (const [k, v] of Object.entries(axes as Record<string, unknown>)) {\n if (typeof v === \"string\") clean[k] = v\n }\n out[nodeId] = clean\n }\n return out\n}\n\nfunction slug(s: string): string {\n return s\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n}\n\nfunction sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(data))\n}\n\nfunction readJsonBody(req: IncomingMessage): Promise<any> {\n return new Promise((ok, err) => {\n let raw = \"\"\n req.on(\"data\", (c) => {\n raw += c\n })\n req.on(\"end\", () => {\n try {\n ok(raw ? JSON.parse(raw) : {})\n } catch (e) {\n err(e)\n }\n })\n req.on(\"error\", err)\n })\n}\n\n","// --- Intent Graph page (/admin/graph) ---\n//\n// Three surfaces inside one page:\n// 1. Pending approval queue — LLM-proposed classifications the operator\n// triages (approve / reject / edit).\n// 2. Taxonomy tree — nodes grouped by level, inline axes editing.\n// 3. Schema editor — raw JSON for advanced level/axis changes.\n//\n// API is in graph-panel.ts (kept there because it's the module that owns\n// GraphStore); this file owns ONLY the HTML/CSS/client JS.\n\nimport { renderShell, type TopbarPeer } from \"..\"\n\nexport interface GraphPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n localToken?: string\n}\n\nexport function renderGraphPage(opts: GraphPageOpts = {}): string {\n const tokenScript = opts.localToken\n ? `<script>window.AX_LOCAL_TOKEN = ${JSON.stringify(opts.localToken)};</script>`\n : \"\"\n\n const body = `<div id=\"msg\" class=\"msg\"></div>\n\n <div id=\"disabled-banner\" class=\"banner warn\" style=\"display:none\">\n <b>Graph is disabled.</b> Set <code class=\"mono\">graph.enabled: true</code>\n in <code class=\"mono\">agentx.json</code> to start classifying traffic.\n The admin UI works either way — nothing classifies until the daemon reloads with graph enabled.\n </div>\n\n <div class=\"counts\">\n <div class=\"count\"><div class=\"n\" id=\"c-nodes\">—</div><div class=\"l\">Nodes</div></div>\n <div class=\"count\"><div class=\"n\" id=\"c-pending\">—</div><div class=\"l\">Pending</div></div>\n <div class=\"count\"><div class=\"n\" id=\"c-fp\">—</div><div class=\"l\">Cached fingerprints</div></div>\n </div>\n\n <div class=\"grid grid-2\">\n <section class=\"card\">\n <h2>Pending approval</h2>\n <p class=\"muted\">The LLM proposed a path for these messages. Approve to add the fingerprint to the snap-to-path cache so similar messages don't hit the LLM again.</p>\n <div id=\"pending-list\"></div>\n </section>\n\n <section class=\"card\">\n <h2>Taxonomy</h2>\n <p class=\"muted\">Current nodes, grouped by level. Edit axes inline; add nodes with the form below.</p>\n <div id=\"tree\"></div>\n\n <details>\n <summary>Add node</summary>\n <div class=\"row\" style=\"margin-top:10px\">\n <select id=\"n-level\"></select>\n <input id=\"n-id\" placeholder=\"node id (e.g. acme-devops)\" />\n <select id=\"n-parent\"><option value=\"\">— no parent —</option></select>\n </div>\n <div class=\"row\" style=\"margin-top:6px\">\n <input id=\"n-axes\" placeholder='axes JSON, e.g. {\"name\":\"DevOps\",\"orgKind\":\"team\"}' />\n <button class=\"primary\" id=\"btn-add-node\">Add</button>\n </div>\n </details>\n </section>\n </div>\n\n <section class=\"card\" style=\"margin-top:20px\">\n <h2>Schema (advanced)</h2>\n <p class=\"muted\">Levels and their required axes. Edit carefully — changing enum values or removing axes can orphan existing nodes.</p>\n <textarea id=\"schema-editor\" spellcheck=\"false\"></textarea>\n <div class=\"row\" style=\"margin-top:10px;justify-content:flex-end\">\n <button id=\"btn-reload-schema\">Reload</button>\n <button class=\"primary\" id=\"btn-save-schema\">Save schema</button>\n </div>\n </section>\n\n <section class=\"card\" style=\"margin-top:20px\">\n <h2>Recent classifications</h2>\n <p class=\"muted\">Last 25 log rows, newest first — approved / pending / rejected.</p>\n <div id=\"recent\"></div>\n </section>`\n\n return renderShell({\n title: \"AgentX · Intent Graph\",\n activeTab: \"graph\",\n subtitle: \"Intent Graph\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: GRAPH_PAGE_CSS,\n scripts: `${tokenScript}<script>${GRAPH_PAGE_SCRIPT}</script>`,\n })\n}\n\nconst GRAPH_PAGE_CSS = `\nmain { max-width: 1200px; margin: 0 auto; padding: 24px 20px 60px; }\nh2 { margin: 0 0 12px; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }\n.grid { display: grid; gap: 20px; grid-template-columns: 1fr; }\n@media (min-width: 920px) { .grid-2 { grid-template-columns: 3fr 2fr; } }\n.card {\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 8px; padding: 16px;\n}\n.card h2 { margin-top: 0; }\n.muted { color: var(--ax-muted); font-size: 12px; }\n.mono { font-family: var(--ax-mono); }\n.chip {\n display: inline-block; padding: 1px 8px; border-radius: 3px;\n font-size: 11px; background: var(--ax-surface); color: var(--ax-text-2);\n border: 1px solid var(--ax-border); margin-right: 4px;\n}\n.chip.pending {\n background: color-mix(in oklch, var(--ax-warn) 15%, transparent);\n color: var(--ax-warn); border-color: transparent;\n}\n.chip.approved {\n background: color-mix(in oklch, var(--ax-accent) 15%, transparent);\n color: var(--ax-accent); border-color: transparent;\n}\n.chip.rejected {\n background: color-mix(in oklch, var(--ax-err) 15%, transparent);\n color: var(--ax-err); border-color: transparent;\n}\nbutton, .btn {\n font: inherit; font-size: 12px; padding: 6px 12px; border-radius: 4px;\n border: 1px solid var(--ax-border-2); background: var(--ax-surface);\n color: var(--ax-text); cursor: pointer;\n}\nbutton:hover { border-color: var(--ax-accent); color: var(--ax-accent); }\nbutton.primary {\n background: var(--ax-accent); color: var(--ax-bg);\n border-color: var(--ax-accent); font-weight: 600;\n}\nbutton.primary:hover { filter: brightness(1.08); color: var(--ax-bg); }\nbutton.danger { color: var(--ax-err); border-color: var(--ax-err); }\nbutton.danger:hover { background: var(--ax-err); color: #fff; }\ninput, textarea, select {\n font: inherit; font-size: 12px; padding: 6px 10px;\n background: var(--ax-surface); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 4px; width: 100%;\n font-family: var(--ax-mono);\n}\ntextarea { min-height: 220px; white-space: pre; line-height: 1.45; }\n.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n.row > * { flex: 0 0 auto; }\n.row > input, .row > select { flex: 1 1 160px; min-width: 0; }\n.pending-item { border-bottom: 1px solid var(--ax-border); padding: 12px 0; }\n.pending-item:last-child { border-bottom: none; }\n.pending-preview {\n color: var(--ax-text-2); font-size: 12px; margin: 6px 0 8px;\n padding: 8px 10px; background: var(--ax-surface); border-radius: 4px;\n border-left: 2px solid var(--ax-accent); white-space: pre-wrap;\n}\n.path-crumbs {\n font-family: var(--ax-mono); font-size: 12px; color: var(--ax-accent); margin: 4px 0;\n}\n.level-group { margin-bottom: 16px; }\n.level-group h3 {\n font-size: 12px; margin: 0 0 6px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ax-muted);\n}\n.node-row {\n display: flex; align-items: center; gap: 10px; padding: 6px 0;\n border-bottom: 1px dotted var(--ax-border);\n}\n.node-row:last-child { border: none; }\n.node-id { font-family: var(--ax-mono); font-weight: 600; min-width: 180px; }\n.axes { display: flex; gap: 6px; flex-wrap: wrap; flex: 1; }\n.axis { font-size: 11px; color: var(--ax-text-2); }\n.axis b { color: var(--ax-text); font-weight: 500; }\n.empty { color: var(--ax-muted); font-style: italic; padding: 16px; text-align: center; }\n.banner { padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 12px; }\n.banner.warn {\n background: color-mix(in oklch, var(--ax-warn) 10%, transparent);\n color: var(--ax-warn);\n border: 1px solid color-mix(in oklch, var(--ax-warn) 30%, transparent);\n}\n.msg { font-size: 11px; padding: 6px 10px; border-radius: 4px; margin-top: 8px; display: none; }\n.msg.ok {\n background: color-mix(in oklch, var(--ax-accent) 10%, transparent);\n color: var(--ax-accent); display: block;\n}\n.msg.err {\n background: color-mix(in oklch, var(--ax-err) 10%, transparent);\n color: var(--ax-err); display: block;\n}\n.counts { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }\n.count {\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 6px; padding: 10px 14px; min-width: 110px;\n}\n.count .n { font-size: 20px; font-weight: 600; line-height: 1; }\n.count .l { font-size: 11px; color: var(--ax-muted); margin-top: 4px; }\ndetails {\n background: var(--ax-surface); border: 1px solid var(--ax-border);\n border-radius: 4px; padding: 8px 12px; margin-top: 8px;\n}\ndetails summary { cursor: pointer; font-size: 11px; color: var(--ax-muted); user-select: none; }\ndetails[open] summary { margin-bottom: 8px; }`\n\n/** Client script — 220-ish lines. Unchanged from the pre-refactor version. */\nconst GRAPH_PAGE_SCRIPT = `\nconst $ = (id) => document.getElementById(id);\nconst state = { schema: null, nodes: [], pending: [], recent: [] };\n\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,c=>({\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\"}[c]));}\n\nasync function req(method, path, body) {\n const headers = { 'Content-Type': 'application/json' };\n if (window.AX_LOCAL_TOKEN) headers['Authorization'] = 'Bearer ' + window.AX_LOCAL_TOKEN;\n const r = await fetch(path, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n const data = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));\n return data;\n}\n\nfunction showMsg(kind, text) {\n const m = $('msg');\n m.className = 'msg ' + kind;\n m.textContent = text;\n setTimeout(() => { m.className = 'msg'; }, 4000);\n}\n\nfunction pathLabel(path) {\n return path.map(id => {\n const n = state.nodes.find(x => x.id === id);\n return n ? (n.axes?.name || n.axes?.what || id) : id;\n }).join(' › ');\n}\n\nfunction renderCounts(counts, enabled) {\n $('c-nodes').textContent = counts.nodes;\n $('c-pending').textContent = counts.pending;\n $('c-fp').textContent = counts.fingerprints;\n $('disabled-banner').style.display = enabled ? 'none' : 'block';\n}\n\nfunction renderTree() {\n const host = $('tree');\n if (!state.schema || state.nodes.length === 0) {\n host.innerHTML = '<div class=\"empty\">No nodes yet. The classifier populates these as new intents arrive.</div>';\n return;\n }\n let html = '';\n for (const level of state.schema.levels) {\n const nodes = state.nodes.filter(n => n.level === level.id);\n html += '<div class=\"level-group\"><h3>' + esc(level.id) + '</h3>';\n if (!nodes.length) {\n html += '<div class=\"muted\" style=\"padding-left:4px\">—</div>';\n } else {\n for (const n of nodes) {\n const axes = Object.entries(n.axes || {}).map(([k, v]) =>\n '<span class=\"axis\"><b>' + esc(k) + ':</b> ' + esc(v) + '</span>'\n ).join(' ');\n html += '<div class=\"node-row\">'\n + '<span class=\"node-id\">' + esc(n.id) + '</span>'\n + '<div class=\"axes\">' + axes + '</div>'\n + '<button data-edit=\"' + esc(n.id) + '\">Edit</button>'\n + '<button class=\"danger\" data-del=\"' + esc(n.id) + '\">Delete</button>'\n + '</div>';\n }\n }\n html += '</div>';\n }\n host.innerHTML = html;\n host.querySelectorAll('[data-edit]').forEach(b =>\n b.addEventListener('click', () => editNode(b.getAttribute('data-edit')))\n );\n host.querySelectorAll('[data-del]').forEach(b =>\n b.addEventListener('click', () => deleteNode(b.getAttribute('data-del')))\n );\n}\n\nfunction renderPending() {\n const host = $('pending-list');\n if (!state.pending.length) {\n host.innerHTML = '<div class=\"empty\">Nothing pending. New traffic will appear here when the classifier proposes a novel path.</div>';\n return;\n }\n host.innerHTML = state.pending.map(c => {\n const conf = c.confidence != null ? ' · confidence ' + (c.confidence * 100).toFixed(0) + '%' : '';\n const preview = c.preview || '(no preview)';\n const path = pathLabel(c.path);\n return '<div class=\"pending-item\" data-hash=\"' + esc(c.msgHash) + '\">'\n + '<div class=\"muted\" style=\"font-size:11px\">' + esc(c.channel || '?') + ' · ' + esc(c.sender || '?') + conf + ' · ' + esc(c.ts) + '</div>'\n + '<div class=\"path-crumbs\">' + esc(path) + '</div>'\n + '<div class=\"pending-preview\">' + esc(preview) + '</div>'\n + '<div class=\"row\">'\n + '<button class=\"primary\" data-approve=\"' + esc(c.msgHash) + '\">Approve</button>'\n + '<button class=\"danger\" data-reject=\"' + esc(c.msgHash) + '\">Reject</button>'\n + '</div>'\n + '</div>';\n }).join('');\n host.querySelectorAll('[data-approve]').forEach(b =>\n b.addEventListener('click', () => approve(b.getAttribute('data-approve')))\n );\n host.querySelectorAll('[data-reject]').forEach(b =>\n b.addEventListener('click', () => reject(b.getAttribute('data-reject')))\n );\n}\n\nfunction renderRecent() {\n const host = $('recent');\n if (!state.recent.length) {\n host.innerHTML = '<div class=\"empty\">No classifications yet.</div>';\n return;\n }\n host.innerHTML = state.recent.map(c =>\n '<div class=\"node-row\">'\n + '<span class=\"chip ' + esc(c.status) + '\">' + esc(c.status) + '</span>'\n + '<span class=\"mono muted\" style=\"font-size:11px\">' + esc(c.ts) + '</span>'\n + '<span style=\"flex:1\">' + esc(pathLabel(c.path)) + '</span>'\n + '<span class=\"muted\" style=\"font-size:11px\">' + esc(c.source) + '</span>'\n + '</div>'\n ).join('');\n}\n\nfunction renderSchemaEditor() {\n $('schema-editor').value = JSON.stringify(state.schema, null, 2);\n const levelSel = $('n-level');\n levelSel.innerHTML = state.schema.levels.map(l =>\n '<option value=\"' + esc(l.id) + '\">' + esc(l.id) + '</option>'\n ).join('');\n const parentSel = $('n-parent');\n parentSel.innerHTML = '<option value=\"\">— no parent —</option>' +\n state.nodes.map(n => '<option value=\"' + esc(n.id) + '\">'\n + esc(n.id) + ' (' + esc(n.level) + ')</option>').join('');\n}\n\nasync function refresh() {\n try {\n const r = await req('GET', '/api/admin/graph/state');\n state.schema = r.schema;\n state.nodes = r.nodes;\n state.pending = r.pending;\n state.recent = r.recent;\n renderCounts(r.counts, r.enabled);\n renderTree();\n renderPending();\n renderRecent();\n renderSchemaEditor();\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function approve(msgHash) {\n try {\n await req('POST', '/api/admin/graph/classifications/approve', { msgHash });\n showMsg('ok', 'Approved.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function reject(msgHash) {\n if (!confirm('Reject this classification? It will not be committed and no fingerprint is cached.')) return;\n try {\n await req('POST', '/api/admin/graph/classifications/reject', { msgHash });\n showMsg('ok', 'Rejected.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function editNode(id) {\n const n = state.nodes.find(x => x.id === id);\n if (!n) return;\n const current = JSON.stringify(n.axes || {}, null, 2);\n const next = prompt('Edit axes for \"' + id + '\" (JSON):', current);\n if (next == null) return;\n let axes;\n try { axes = JSON.parse(next); } catch { showMsg('err', 'Invalid JSON'); return; }\n try {\n await req('PATCH', '/api/admin/graph/nodes', { id, axes });\n showMsg('ok', 'Node updated.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function deleteNode(id) {\n if (!confirm('Delete node \"' + id + '\"? Fails if it has children or references.')) return;\n try {\n await req('DELETE', '/api/admin/graph/nodes', { id });\n showMsg('ok', 'Deleted.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n}\n\n$('btn-add-node').addEventListener('click', async () => {\n const level = $('n-level').value;\n const id = $('n-id').value.trim();\n const parentId = $('n-parent').value || null;\n let axes = {};\n const raw = $('n-axes').value.trim();\n if (raw) {\n try { axes = JSON.parse(raw); }\n catch { showMsg('err', 'Axes JSON invalid.'); return; }\n }\n try {\n await req('POST', '/api/admin/graph/nodes', { id, level, parentId, axes });\n $('n-id').value = ''; $('n-axes').value = '';\n showMsg('ok', 'Node added.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n});\n\n$('btn-save-schema').addEventListener('click', async () => {\n let schema;\n try { schema = JSON.parse($('schema-editor').value); }\n catch { showMsg('err', 'Schema JSON invalid.'); return; }\n try {\n await req('PUT', '/api/admin/graph/schema', { schema });\n showMsg('ok', 'Schema saved.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n});\n\n$('btn-reload-schema').addEventListener('click', refresh);\n\nrefresh();\n`\n","import type { IncomingMessage, ServerResponse } from \"http\"\nimport { resolve } from \"path\"\nimport { existsSync } from \"fs\"\nimport Database from \"better-sqlite3\"\nimport { renderObservabilityPage } from \"./ui/pages/observability\"\nimport type { TopbarPeer } from \"./topbar\"\n\n// Anthropic public per-million-token rates. Same numbers\n// src/observability/tracker.ts uses; duplicated here so this module stays\n// independent of the in-process token tracker.\n//\n// \"external\" is a pricing-attribution stub for models we don't bill against\n// the Anthropic spend — codex-cli routes through OpenAI, sdk agents may\n// route through OpenAI / Bedrock / Vertex. Showing them at $0 here is the\n// honest answer: this dashboard tracks Anthropic spend; cross-provider\n// rollups belong on a separate surface that knows the OpenAI keys / bill.\nconst RATE_PER_M: Record<string, { input: number; output: number; cacheRead: number; cacheCreate: number }> = {\n opus: { input: 15, output: 75, cacheRead: 1.5, cacheCreate: 18.75 },\n sonnet: { input: 3, output: 15, cacheRead: 0.3, cacheCreate: 3.75 },\n haiku: { input: 0.25, output: 1.25, cacheRead: 0.025, cacheCreate: 0.3125 },\n external: { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 },\n}\nfunction modelFamily(model: string): \"opus\" | \"sonnet\" | \"haiku\" | \"external\" {\n const m = (model || \"\").toLowerCase()\n if (m.includes(\"opus\")) return \"opus\"\n if (m.includes(\"haiku\")) return \"haiku\"\n if (m.includes(\"sonnet\") || m.includes(\"claude\")) return \"sonnet\"\n // Anything else — gpt-*, codex, gemini, llama, … — routed to the external\n // bucket so it doesn't masquerade as Sonnet spend on the dashboard.\n return \"external\"\n}\nfunction rowCost(row: {\n model: string;\n input_tokens: number; output_tokens: number;\n cache_read_tokens: number; cache_create_tokens: number;\n tier2_input_tokens?: number; tier2_output_tokens?: number;\n tier2_cache_read_tokens?: number; tier2_cache_create_tokens?: number;\n}): number {\n const r = RATE_PER_M[modelFamily(row.model)]\n // Tier-2 columns ALSO live in input_tokens etc. (they're a subset, not a\n // disjoint count). The tier-2 multiplier (1.5×) is the additional charge.\n const base =\n (row.input_tokens / 1_000_000) * r.input +\n (row.output_tokens / 1_000_000) * r.output +\n (row.cache_read_tokens / 1_000_000) * r.cacheRead +\n (row.cache_create_tokens / 1_000_000) * r.cacheCreate\n const tier2Surcharge =\n ((row.tier2_input_tokens ?? 0) / 1_000_000) * r.input * 0.5 +\n ((row.tier2_output_tokens ?? 0) / 1_000_000) * r.output * 0.5 +\n ((row.tier2_cache_read_tokens ?? 0) / 1_000_000) * r.cacheRead * 0.5 +\n ((row.tier2_cache_create_tokens ?? 0) / 1_000_000) * r.cacheCreate * 0.5\n return base + tier2Surcharge\n}\n\n// --- /admin/observability — surface the SQLite tables that have no UI ---\n//\n// route_traces, rotations, and task_history(error rows) were CLI-only before\n// this panel landed. The page is a read-only window over the same tables\n// `agentx db routes / db rotations / db tasks --status error` already\n// expose; the JSON API just makes them visible in the dashboard.\n//\n// Each request opens a short-lived read-only handle. The dashboard process\n// doesn't keep one — the daemon owns the writes via WAL — so we don't fight\n// for the SHM lock; concurrent reads are the WAL's whole point.\n\ninterface OpenedDb {\n db: Database.Database\n close: () => void\n}\n\nfunction openReadOnly(): OpenedDb | null {\n const path = resolve(process.cwd(), \".agentx/db.sqlite\")\n if (!existsSync(path)) return null\n try {\n const db = new Database(path, { readonly: true, fileMustExist: true })\n return { db, close: () => db.close() }\n } catch {\n return null\n }\n}\n\nexport function handleObservabilityGet(_req: IncomingMessage, res: ServerResponse, peers: TopbarPeer[] = []): void {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderObservabilityPage({ peers }))\n}\n\ninterface QueryParams {\n limit: number\n agent?: string\n}\n\nfunction parseQuery(req: IncomingMessage): QueryParams {\n const url = new URL(req.url || \"/\", \"http://_\")\n const limitRaw = parseInt(url.searchParams.get(\"limit\") || \"50\", 10)\n const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(500, limitRaw)) : 50\n const agent = url.searchParams.get(\"agent\") || undefined\n return { limit, agent: agent && agent.length > 0 ? agent : undefined }\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(body))\n}\n\nexport async function handleObservabilityApi(req: IncomingMessage, res: ServerResponse, path: string): Promise<boolean> {\n if (!path.startsWith(\"/api/admin/observability/\")) return false\n const slug = path.replace(/^\\/api\\/admin\\/observability\\//, \"\").split(\"?\")[0]\n\n const opened = openReadOnly()\n if (!opened) {\n sendJson(res, 503, { error: \"operational SQLite not available\", rows: [] })\n return true\n }\n\n try {\n const { limit, agent } = parseQuery(req)\n if (slug === \"routing\") {\n const where: string[] = []\n const params: Record<string, unknown> = { limit }\n if (agent) { where.push(\"agent_id = @agent\"); params.agent = agent }\n const sql = `\n SELECT at, channel, chat_id, account_id, kind, deciding_stage, agent_id, reason\n FROM route_traces\n ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"}\n ORDER BY at DESC LIMIT @limit\n `\n const rows = opened.db.prepare(sql).all(params)\n sendJson(res, 200, { rows })\n return true\n }\n if (slug === \"rotations\") {\n const where: string[] = []\n const params: Record<string, unknown> = { limit }\n if (agent) { where.push(\"agent_id = @agent\"); params.agent = agent }\n const sql = `\n SELECT rotated_at, agent_id, channel, reason, last_turn_input_tokens\n FROM rotations\n ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"}\n ORDER BY rotated_at DESC LIMIT @limit\n `\n const rows = opened.db.prepare(sql).all(params)\n sendJson(res, 200, { rows })\n return true\n }\n if (slug === \"errors\") {\n const where: string[] = [\"status = 'error'\"]\n const params: Record<string, unknown> = { limit }\n if (agent) { where.push(\"agent_id = @agent\"); params.agent = agent }\n const sql = `\n SELECT started_at, agent_id, channel, message_preview, error\n FROM task_history\n WHERE ${where.join(\" AND \")}\n ORDER BY started_at DESC LIMIT @limit\n `\n const rows = opened.db.prepare(sql).all(params) as Array<{ error?: string | null }>\n // Plain-English label for non-technical operators. Pattern-match the\n // common failure modes; fall through to the raw string for unknowns.\n const labelled = rows.map((r) => ({ ...r, plain: plainErrorLabel(r.error) }))\n sendJson(res, 200, { rows: labelled })\n return true\n }\n // The page calls `/overview`; legacy callers hit `/summary`. Same data.\n if (slug === \"summary\" || slug === \"overview\") {\n sendJson(res, 200, buildSummary(opened.db))\n return true\n }\n if (slug === \"activity\") {\n sendJson(res, 200, buildActivity(opened.db, { agent }))\n return true\n }\n if (slug === \"cost\") {\n const url = new URL(req.url || \"/\", \"http://_\")\n const range = parseRange(url.searchParams.get(\"range\"))\n sendJson(res, 200, buildCost(opened.db, range))\n return true\n }\n if (slug === \"cost.csv\") {\n const url = new URL(req.url || \"/\", \"http://_\")\n const range = parseRange(url.searchParams.get(\"range\"))\n const data = buildCost(opened.db, range)\n const csv = toCostCsv(data.days)\n res.writeHead(200, {\n \"Content-Type\": \"text/csv; charset=utf-8\",\n \"Content-Disposition\": `attachment; filename=\"agentx-cost-${range || \"all\"}d-${Date.now()}.csv\"`,\n })\n res.end(csv)\n return true\n }\n sendJson(res, 404, { error: \"unknown observability slice\", rows: [] })\n return true\n } catch (e: any) {\n sendJson(res, 500, { error: e?.message ?? String(e), rows: [] })\n return true\n } finally {\n opened.close()\n }\n}\n\n// --- Plain-English error labels --------------------------------------------\n\nfunction plainErrorLabel(error: string | null | undefined): string {\n const e = String(error || \"\")\n if (!e) return \"unknown failure\"\n if (/timed out|SIGTERM|exit 143|maxExecutionMinutes/i.test(e)) return \"Agent ran past its time limit\"\n if (/ECONNREFUSED|fetch failed|getaddrinfo|ENOTFOUND/i.test(e)) return \"Couldn't reach a service over the network\"\n if (/401|403|Unauthorized|Forbidden|invalid token|missing scope/i.test(e)) return \"Authentication or permission denied\"\n if (/rate limit|429|too many requests/i.test(e)) return \"Hit a rate limit (too many calls)\"\n if (/quota|insufficient_quota/i.test(e)) return \"Provider account out of quota or credits\"\n if (/tier-?2|tier two|200K|200,000/i.test(e)) return \"Hit Anthropic tier-2 billing threshold\"\n if (/JSON|parse|unexpected token/i.test(e)) return \"Got a response in the wrong shape\"\n if (/permission denied|EACCES|EPERM/i.test(e)) return \"File permission denied\"\n if (/ENOENT|no such file/i.test(e)) return \"Expected file is missing\"\n if (/abort|cancel/i.test(e)) return \"Task was cancelled before it finished\"\n return e.split(\"\\n\")[0].slice(0, 80) + (e.length > 80 ? \"…\" : \"\")\n}\n\n// --- Summary (Overview tab) -------------------------------------------------\n\nfunction buildSummary(db: Database.Database) {\n const now = new Date()\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString()\n const yesterdayStart = new Date(now.getTime() - 86400_000)\n const yStart = new Date(yesterdayStart.getFullYear(), yesterdayStart.getMonth(), yesterdayStart.getDate()).toISOString()\n const last24 = new Date(now.getTime() - 86400_000).toISOString()\n\n const tasksToday = (db.prepare(`SELECT COUNT(*) AS n FROM task_history WHERE started_at >= ?`).get(todayStart) as { n: number }).n\n const tasksYesterday = (db.prepare(`SELECT COUNT(*) AS n FROM task_history WHERE started_at >= ? AND started_at < ?`).get(yStart, todayStart) as { n: number }).n\n const errorsToday = (db.prepare(`SELECT COUNT(*) AS n FROM task_history WHERE status='error' AND started_at >= ?`).get(todayStart) as { n: number }).n\n const avgDuration = (db.prepare(`SELECT AVG(duration_ms) AS d FROM task_history WHERE status='ok' AND started_at >= ?`).get(todayStart) as { d: number | null }).d ?? 0\n\n // p50/p95 via SQLite — manual from a small sample, cheap.\n const dRows = db.prepare(`SELECT duration_ms FROM task_history WHERE status='ok' AND started_at >= ? AND duration_ms IS NOT NULL ORDER BY duration_ms ASC`).all(todayStart) as Array<{ duration_ms: number }>\n const p = (q: number) => dRows.length ? dRows[Math.min(dRows.length - 1, Math.floor(dRows.length * q))].duration_ms : 0\n const p50 = p(0.5)\n const p95 = p(0.95)\n\n // Spend today: sum cost across all usage_daily rows for today.\n const usageRows = db.prepare(`SELECT * FROM usage_daily WHERE day = date('now')`).all() as any[]\n const spendToday = usageRows.reduce((s, r) => s + rowCost(r), 0)\n const spendYesterday = (db.prepare(`SELECT * FROM usage_daily WHERE day = date('now', '-1 day')`).all() as any[])\n .reduce((s, r) => s + rowCost(r), 0)\n\n // Per-agent today (top 10 by tasks).\n const perAgent = db.prepare(`\n SELECT agent_id,\n COUNT(*) AS tasks,\n SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors,\n ROUND(AVG(duration_ms)) AS avg_duration_ms,\n MAX(started_at) AS last_active\n FROM task_history\n WHERE started_at >= ?\n GROUP BY agent_id\n ORDER BY tasks DESC LIMIT 10\n `).all(todayStart)\n\n // Per-channel today.\n const perChannel = db.prepare(`\n SELECT COALESCE(channel, 'unknown') AS channel,\n COUNT(*) AS tasks,\n SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors\n FROM task_history\n WHERE started_at >= ?\n GROUP BY channel\n ORDER BY tasks DESC\n `).all(todayStart)\n\n // Idle agents — listed in usage_daily over the last 7 days but no activity in the last 24h.\n const knownAgents = (db.prepare(`SELECT DISTINCT agent_id FROM usage_daily WHERE day >= date('now', '-7 day')`).all() as Array<{ agent_id: string }>).map((r) => r.agent_id)\n const recentlyActive = new Set((db.prepare(`SELECT DISTINCT agent_id FROM task_history WHERE started_at >= ?`).all(last24) as Array<{ agent_id: string }>).map((r) => r.agent_id))\n const idleAgents = knownAgents.filter((a) => !recentlyActive.has(a))\n\n // Recent errors (last 24h, top 5).\n const recentErrors = db.prepare(`\n SELECT started_at, agent_id, channel, message_preview, error\n FROM task_history\n WHERE status='error' AND started_at >= ?\n ORDER BY started_at DESC LIMIT 5\n `).all(last24) as Array<{ error?: string | null }>\n const recentErrorsLabelled = recentErrors.map((r) => ({ ...r, plain: plainErrorLabel(r.error) }))\n\n return {\n kpis: {\n tasksToday,\n tasksDelta: tasksToday - tasksYesterday,\n errorsToday,\n errorRate: tasksToday > 0 ? errorsToday / tasksToday : 0,\n avgDurationMs: Math.round(avgDuration),\n p50DurationMs: p50,\n p95DurationMs: p95,\n spendToday,\n spendDelta: spendToday - spendYesterday,\n idleAgentCount: idleAgents.length,\n },\n perAgent,\n perChannel,\n idleAgents,\n recentErrors: recentErrorsLabelled,\n }\n}\n\n// --- Activity (Activity tab) ------------------------------------------------\n\nfunction buildActivity(db: Database.Database, opts: { agent?: string }) {\n const last24 = new Date(Date.now() - 86400_000).toISOString()\n const last7d = new Date(Date.now() - 7 * 86400_000).toISOString()\n\n const where = opts.agent ? \"AND agent_id = @agent\" : \"\"\n const params = opts.agent ? { agent: opts.agent } : {}\n\n // Hourly buckets for last 24h.\n const hourly = db.prepare(`\n SELECT strftime('%Y-%m-%dT%H:00', started_at) AS hour,\n COUNT(*) AS tasks,\n SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors\n FROM task_history\n WHERE started_at >= @since ${where}\n GROUP BY hour\n ORDER BY hour\n `).all({ since: last24, ...params })\n\n // Daily counts for last 7 days.\n const daily = db.prepare(`\n SELECT date(started_at) AS day,\n COUNT(*) AS tasks,\n SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors\n FROM task_history\n WHERE started_at >= @since ${where}\n GROUP BY day\n ORDER BY day\n `).all({ since: last7d, ...params })\n\n // Per-agent table (7d).\n const perAgent = db.prepare(`\n SELECT agent_id,\n COUNT(*) AS tasks_7d,\n SUM(CASE WHEN started_at >= @last24 THEN 1 ELSE 0 END) AS tasks_24h,\n SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors_7d,\n ROUND(AVG(duration_ms)) AS avg_duration_ms,\n MAX(started_at) AS last_active\n FROM task_history\n WHERE started_at >= @since\n GROUP BY agent_id\n ORDER BY tasks_24h DESC, tasks_7d DESC\n `).all({ since: last7d, last24 })\n\n return { hourly, daily, perAgent }\n}\n\n// --- Cost (Cost tab) --------------------------------------------------------\n\n/** Parse `range` query: 7|14|30|90 days, or \"all\" → 0 (no cutoff). */\nfunction parseRange(raw: string | null): number {\n if (!raw) return 30\n if (raw === \"all\" || raw === \"0\") return 0\n const n = parseInt(raw, 10)\n if (!Number.isFinite(n) || n <= 0) return 30\n return Math.min(n, 365)\n}\n\ninterface CostAgentDay {\n tasks: number; cost: number;\n input: number; output: number; cacheRead: number; cacheCreate: number;\n tier2: number; model?: string;\n}\ninterface CostDay {\n date: string;\n tasks: number;\n input: number; output: number; cacheRead: number; cacheCreate: number;\n cost: number; tier2: number;\n agents: Record<string, CostAgentDay>;\n}\n\nfunction buildCost(db: Database.Database, range: number) {\n // range=0 means \"all\" — no cutoff. Otherwise day ≥ today − range.\n const where = range > 0 ? `WHERE day >= date('now', '-${range} day')` : \"\"\n const usageAll = db.prepare(`\n SELECT day, agent_id, model,\n input_tokens, output_tokens, cache_read_tokens, cache_create_tokens,\n tier2_input_tokens, tier2_output_tokens, tier2_cache_read_tokens, tier2_cache_create_tokens,\n tasks\n FROM usage_daily\n ${where}\n ORDER BY day\n `).all() as any[]\n\n const today = new Date().toISOString().slice(0, 10)\n const yesterday = new Date(Date.now() - 86400_000).toISOString().slice(0, 10)\n const last7Cutoff = new Date(Date.now() - 7 * 86400_000).toISOString().slice(0, 10)\n const last14Cutoff = new Date(Date.now() - 14 * 86400_000).toISOString().slice(0, 10)\n\n // KPI accumulators (always over the full 30-day window for comparable\n // top-of-page numbers; the per-day chart respects the user's range).\n let spendToday = 0, spendYesterday = 0, spend7d = 0, spend30d = 0\n let tier2Today = 0, tier2_7d = 0\n let tasks7d = 0, tasksPrev7d = 0, spendPrev7d = 0\n\n // Per-day, per-agent rollup over the requested range — drives every\n // panel below the KPI strip (hero chart, mix, leaderboard, gauge, table).\n const dayMap: Record<string, CostDay> = {}\n const byAgentRange: Record<string, CostAgentDay> = {}\n\n for (const r of usageAll) {\n const c = rowCost(r)\n const family = modelFamily(r.model)\n const rate = RATE_PER_M[family]\n const tier2Surcharge =\n ((r.tier2_input_tokens || 0) / 1e6) * rate.input * 0.5 +\n ((r.tier2_output_tokens || 0) / 1e6) * rate.output * 0.5 +\n ((r.tier2_cache_read_tokens || 0) / 1e6) * rate.cacheRead * 0.5 +\n ((r.tier2_cache_create_tokens || 0) / 1e6) * rate.cacheCreate * 0.5\n\n // Tier-2 token columns on usage_daily are SUBSETS of the base columns\n // (same tokens, just billed at the higher rate). The dashboard surfaces\n // total tokens processed so display sums tier1 + tier2 here.\n const inTok = (r.input_tokens || 0) + (r.tier2_input_tokens || 0)\n const outTok = (r.output_tokens || 0) + (r.tier2_output_tokens || 0)\n const crTok = (r.cache_read_tokens || 0) + (r.tier2_cache_read_tokens || 0)\n const cwTok = (r.cache_create_tokens || 0) + (r.tier2_cache_create_tokens || 0)\n\n spend30d += c\n if (r.day >= last7Cutoff) { spend7d += c; tasks7d += r.tasks || 0 }\n if (r.day >= last14Cutoff && r.day < last7Cutoff) {\n spendPrev7d += c\n tasksPrev7d += r.tasks || 0\n }\n if (r.day === today) spendToday += c\n if (r.day === yesterday) spendYesterday += c\n if (r.day === today) tier2Today += tier2Surcharge\n if (r.day >= last7Cutoff) tier2_7d += tier2Surcharge\n\n const d = dayMap[r.day] || (dayMap[r.day] = {\n date: r.day, tasks: 0, input: 0, output: 0, cacheRead: 0, cacheCreate: 0,\n cost: 0, tier2: 0, agents: {},\n })\n d.tasks += r.tasks || 0\n d.input += inTok; d.output += outTok\n d.cacheRead += crTok; d.cacheCreate += cwTok\n d.cost += c; d.tier2 += tier2Surcharge\n\n const a = d.agents[r.agent_id] || (d.agents[r.agent_id] = {\n tasks: 0, cost: 0, input: 0, output: 0, cacheRead: 0, cacheCreate: 0, tier2: 0, model: r.model,\n })\n a.tasks += r.tasks || 0\n a.cost += c; a.tier2 += tier2Surcharge\n a.input += inTok; a.output += outTok\n a.cacheRead += crTok; a.cacheCreate += cwTok\n if (r.model) a.model = r.model\n\n const agg = byAgentRange[r.agent_id] || (byAgentRange[r.agent_id] = {\n tasks: 0, cost: 0, input: 0, output: 0, cacheRead: 0, cacheCreate: 0, tier2: 0, model: r.model,\n })\n agg.tasks += r.tasks || 0\n agg.cost += c; agg.tier2 += tier2Surcharge\n agg.input += inTok; agg.output += outTok\n agg.cacheRead += crTok; agg.cacheCreate += cwTok\n if (r.model) agg.model = r.model\n }\n\n const days: CostDay[] = Object.values(dayMap).sort((a, b) => (a.date < b.date ? -1 : 1))\n\n // Range-wide totals for KPIs that follow the user's range pick.\n let tasksRange = 0, inputRange = 0, outputRange = 0, cacheReadRange = 0, cacheCreateRange = 0, costRange = 0\n for (const d of days) {\n tasksRange += d.tasks\n inputRange += d.input; outputRange += d.output\n cacheReadRange += d.cacheRead; cacheCreateRange += d.cacheCreate\n costRange += d.cost\n }\n\n const perAgent = Object.entries(byAgentRange)\n .map(([agent_id, v]) => ({\n agent_id, ...v,\n tokens: v.input + v.output + v.cacheRead + v.cacheCreate,\n }))\n .sort((a, b) => b.cost - a.cost)\n\n return {\n range,\n generatedAt: new Date().toISOString(),\n kpis: {\n spendToday, spendYesterday, spend7d, spend30d,\n tier2Today, tier2_7d,\n tier2PctToday: spendToday > 0 ? tier2Today / spendToday : 0,\n // Range-pinned numbers used by the new 6-KPI strip.\n spendRange: costRange,\n avgDailyRange: days.length ? costRange / days.length : 0,\n tasksRange,\n tokensRange: inputRange + outputRange + cacheReadRange + cacheCreateRange,\n cacheHitRange: (cacheReadRange + inputRange) > 0\n ? cacheReadRange / (cacheReadRange + inputRange)\n : 0,\n costPerTaskRange: tasksRange > 0 ? costRange / tasksRange : 0,\n // Week-over-week deltas (always 7d-vs-prev-7d, independent of range).\n spendDeltaWoW: spendPrev7d > 0 ? (spend7d - spendPrev7d) / spendPrev7d : 0,\n tasksDeltaWoW: tasksPrev7d > 0 ? (tasks7d - tasksPrev7d) / tasksPrev7d : 0,\n },\n days,\n perAgent,\n }\n}\n\n/** CSV export for `/api/admin/observability/cost.csv` — same column order as\n * the legacy `/api/usage.csv`, picking the top-spend agent of each day. */\nfunction toCostCsv(days: CostDay[]): string {\n const header = \"date,tasks,input,output,cache_read,cache_create,cost_usd,top_agent\"\n const rows = days.map((d) => {\n const top = Object.entries(d.agents).sort(([, a], [, b]) => b.cost - a.cost)[0]\n return [d.date, d.tasks, d.input, d.output, d.cacheRead, d.cacheCreate, d.cost.toFixed(6), top ? top[0] : \"\"].join(\",\")\n })\n return header + \"\\n\" + rows.join(\"\\n\") + \"\\n\"\n}\n","import { renderShell, type TopbarPeer } from \"..\"\n\n// --- /admin/observability page ---\n//\n// Three tables that were write-only-from-the-CLI before this page existed:\n// - Routing (route_traces): why each inbound message went to which agent.\n// Critical for \"why didn't agent X pick up this message?\" debugging.\n// - Rotations: stale / tier-2 / max-turns session rotations per agent.\n// Pairs with the usage dashboard's tier-2 hotspot view.\n// - Errors: task_history rows where status='error' with truncated stack.\n//\n// Read-only. Filters are client-side; backing API is /api/admin/observability/*.\n\nexport interface ObservabilityPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderObservabilityPage(opts: ObservabilityPageOpts = {}): string {\n const body = `<div class=\"ax-obs\">\n <header class=\"ax-obs__head\">\n <h1>Health</h1>\n <p class=\"ax-obs__sub\">Platform-health view: what each agent ran, what failed, and how the router decided. <span class=\"ax-obs__sub-link\">Spend lives on the <a href=\"/admin/cost\">Cost</a> page.</span></p>\n <nav class=\"ax-obs__tabs\" role=\"tablist\">\n <button class=\"ax-obs__tab is-active\" data-tab=\"overview\" role=\"tab\">Overview</button>\n <button class=\"ax-obs__tab\" data-tab=\"activity\" role=\"tab\">Activity</button>\n <button class=\"ax-obs__tab\" data-tab=\"errors\" role=\"tab\">Errors</button>\n <button class=\"ax-obs__tab\" data-tab=\"routing\" role=\"tab\">Routing</button>\n <button class=\"ax-obs__tab\" data-tab=\"rotations\" role=\"tab\">Rotations</button>\n <button class=\"ax-obs__tab\" data-tab=\"logs\" role=\"tab\">Logs</button>\n <button class=\"ax-obs__tab\" data-tab=\"doctor\" role=\"tab\">Doctor</button>\n </nav>\n <div class=\"ax-obs__filters\" id=\"obs-filters\">\n <label>Agent\n <input id=\"obs-agent\" type=\"text\" placeholder=\"filter by agent id\" />\n </label>\n <label class=\"js-tech-only\">Limit\n <input id=\"obs-limit\" type=\"number\" min=\"1\" max=\"500\" value=\"50\" />\n </label>\n <button id=\"obs-refresh\" type=\"button\">Refresh</button>\n </div>\n </header>\n\n <section class=\"ax-obs__pane is-active\" data-pane=\"overview\">\n <div class=\"ax-obs__kpis\" id=\"obs-overview-kpis\"></div>\n <div class=\"ax-obs__split\">\n <div class=\"ax-obs__card\">\n <h3>Top agents today</h3>\n <table class=\"ax-obs__table\">\n <thead><tr><th>Agent</th><th class=\"num\">Tasks</th><th class=\"num\">Errors</th><th class=\"num\">Avg time</th><th>Last active</th></tr></thead>\n <tbody id=\"obs-overview-agents\"></tbody>\n </table>\n <p class=\"ax-obs__empty\" id=\"obs-overview-agents-empty\" hidden>No tasks yet today.</p>\n </div>\n <div class=\"ax-obs__card\">\n <h3>Channels today</h3>\n <table class=\"ax-obs__table\">\n <thead><tr><th>Channel</th><th class=\"num\">Tasks</th><th class=\"num\">Errors</th></tr></thead>\n <tbody id=\"obs-overview-channels\"></tbody>\n </table>\n <p class=\"ax-obs__empty\" id=\"obs-overview-channels-empty\" hidden>No traffic yet today.</p>\n </div>\n </div>\n <div class=\"ax-obs__card\">\n <h3>Idle agents <span class=\"ax-obs__hint\">(no activity in the last 24h)</span></h3>\n <p id=\"obs-overview-idle\" class=\"ax-obs__idle\"></p>\n </div>\n <div class=\"ax-obs__card\">\n <h3>Recent failures (last 24h) <span class=\"ax-obs__hint\">— top 5</span></h3>\n <ul id=\"obs-overview-errors\" class=\"ax-obs__error-list\"></ul>\n <p class=\"ax-obs__empty\" id=\"obs-overview-errors-empty\" hidden>No failures in the last 24 hours. ✓</p>\n </div>\n </section>\n\n <section class=\"ax-obs__pane\" data-pane=\"activity\">\n <div class=\"ax-obs__card\">\n <h3>Last 24 hours <span class=\"ax-obs__hint\">— hourly</span></h3>\n <div id=\"obs-activity-hourly\" class=\"ax-obs__chart\"></div>\n <p class=\"ax-obs__empty\" id=\"obs-activity-hourly-empty\" hidden>No activity in the last 24 hours.</p>\n </div>\n <div class=\"ax-obs__card\">\n <h3>Last 7 days <span class=\"ax-obs__hint\">— daily</span></h3>\n <div id=\"obs-activity-daily\" class=\"ax-obs__chart\"></div>\n </div>\n <div class=\"ax-obs__card\">\n <h3>Per-agent activity (7-day window)</h3>\n <table class=\"ax-obs__table\">\n <thead><tr><th>Agent</th><th class=\"num\">Tasks 24h</th><th class=\"num\">Tasks 7d</th><th class=\"num\">Errors 7d</th><th class=\"num\">Avg time</th><th>Last active</th></tr></thead>\n <tbody id=\"obs-activity-agents\"></tbody>\n </table>\n <p class=\"ax-obs__empty\" id=\"obs-activity-agents-empty\" hidden>(no rows)</p>\n </div>\n </section>\n\n <section class=\"ax-obs__pane\" data-pane=\"errors\">\n <div class=\"ax-obs__table-wrap\"><table class=\"ax-obs__table\">\n <thead><tr><th>When</th><th>Agent</th><th>Channel</th><th>What happened</th><th>Message</th></tr></thead>\n <tbody id=\"obs-errors\"></tbody>\n </table></div>\n <p class=\"ax-obs__empty\" id=\"obs-errors-empty\" hidden>No errors found.</p>\n </section>\n\n <section class=\"ax-obs__pane\" data-pane=\"routing\">\n <p class=\"ax-obs__hint\">Technical: every routing decision the daemon made — which stage decided which agent gets a message. Useful for debugging \"why didn't agent X pick up this?\".</p>\n <div class=\"ax-obs__table-wrap\"><table class=\"ax-obs__table\">\n <thead><tr><th>at</th><th>channel</th><th>chat_id</th><th>kind</th><th>stage</th><th>agent</th><th>reason</th></tr></thead>\n <tbody id=\"obs-routing\"></tbody>\n </table></div>\n <p class=\"ax-obs__empty\" id=\"obs-routing-empty\" hidden>(no rows)</p>\n </section>\n\n <section class=\"ax-obs__pane\" data-pane=\"rotations\">\n <p class=\"ax-obs__hint\">Technical: each time an agent's Claude session was rotated — <code>stale</code> (idle), <code>tier-2</code> (input crossed billing threshold), or <code>max-turns</code> (per-session cap).</p>\n <div class=\"ax-obs__table-wrap\"><table class=\"ax-obs__table\">\n <thead><tr><th>at</th><th>agent</th><th>channel</th><th>reason</th><th>last turn input tokens</th></tr></thead>\n <tbody id=\"obs-rotations\"></tbody>\n </table></div>\n <p class=\"ax-obs__empty\" id=\"obs-rotations-empty\" hidden>(no rows)</p>\n </section>\n\n <section class=\"ax-obs__pane\" data-pane=\"logs\">\n <p class=\"ax-obs__hint\">Live tail of the daemon's stdout — same source as <code>agentx daemon logs -f</code>. Streams via SSE; falls back to /tmp/agentx-daemon.log when the daemon isn't under systemd.</p>\n <div class=\"ax-obs__logs-toolbar\">\n <input id=\"obs-logs-filter\" type=\"text\" placeholder=\"grep (case-insensitive)\" />\n <label class=\"ax-obs__inline\"><input id=\"obs-logs-autoscroll\" type=\"checkbox\" checked /> auto-scroll</label>\n <button id=\"obs-logs-pause\" type=\"button\">Pause</button>\n <button id=\"obs-logs-clear\" type=\"button\">Clear</button>\n <span id=\"obs-logs-status\" class=\"ax-obs__hint\">connecting…</span>\n </div>\n <pre id=\"obs-logs-body\" class=\"ax-obs__logs-body\"></pre>\n </section>\n\n <section class=\"ax-obs__pane\" data-pane=\"doctor\">\n <p class=\"ax-obs__hint\">Preflight checks — same as <code>agentx doctor</code>. Read-only. Re-run any time.</p>\n <div class=\"ax-obs__doctor-toolbar\">\n <button id=\"obs-doctor-run\" type=\"button\">Re-run checks</button>\n <label class=\"ax-obs__inline\"><input id=\"obs-doctor-running\" type=\"checkbox\" checked /> probe running daemon</label>\n <span id=\"obs-doctor-summary\" class=\"ax-obs__hint\">—</span>\n </div>\n <div id=\"obs-doctor-body\" class=\"ax-obs__doctor-body\"></div>\n </section>\n</div>`\n\n return renderShell({\n title: \"AgentX · Health\",\n activeTab: \"health\",\n subtitle: \"Health\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: OBS_PAGE_CSS,\n scripts: `<script>${OBS_PAGE_SCRIPT}</script>`,\n })\n}\n\nconst OBS_PAGE_CSS = `\n.ax-obs { padding: 16px 24px; }\n.ax-obs__head { margin-bottom: 16px; }\n.ax-obs__head h1 { margin: 0 0 4px; font-size: 18px; }\n.ax-obs__sub { color: var(--ax-muted); font-size: 13px; margin: 0 0 12px; }\n.ax-obs__sub code { font-size: 12px; padding: 1px 5px; background: var(--ax-surface-2); border-radius: 4px; }\n.ax-obs__tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--ax-border); margin-bottom: 12px; align-items: center; }\n.ax-obs__tab {\n background: transparent; border: 0; padding: 8px 14px; cursor: pointer;\n font: inherit; color: var(--ax-muted); border-bottom: 2px solid transparent;\n}\n.ax-obs__tab:hover { color: var(--ax-text); }\n.ax-obs__tab.is-active { color: var(--ax-text); border-bottom-color: var(--ax-accent, #3a7bd5); }\n.ax-obs__filters { display: flex; gap: 12px; align-items: end; flex-wrap: wrap; padding: 8px 0; }\n.ax-obs__filters label { display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: var(--ax-muted); }\n.ax-obs__filters input {\n font: inherit; padding: 6px 8px; border: 1px solid var(--ax-border);\n border-radius: 6px; background: var(--ax-bg); color: var(--ax-text); width: 160px;\n}\n.ax-obs__filters input[type=\"number\"] { width: 80px; }\n.ax-obs__filters button {\n padding: 6px 14px; border: 1px solid var(--ax-border); background: var(--ax-bg);\n color: var(--ax-text); border-radius: 6px; cursor: pointer; font: inherit;\n}\n.ax-obs__filters button:hover { background: var(--ax-surface-2); }\n.ax-obs__pane { display: none; }\n.ax-obs__pane.is-active { display: block; }\n.ax-obs__hint { color: var(--ax-muted); font-size: 11px; font-weight: normal; }\n.ax-obs__table-wrap { overflow-x: auto; max-height: calc(100vh - 280px); overflow-y: auto; border: 1px solid var(--ax-border); border-radius: 6px; }\n.ax-obs__table { width: 100%; border-collapse: collapse; font-size: 12px; }\n.ax-obs__table thead th {\n position: sticky; top: 0; background: var(--ax-bg); padding: 8px 10px;\n text-align: left; border-bottom: 1px solid var(--ax-border);\n font-weight: 600; font-size: 11px; color: var(--ax-muted); text-transform: uppercase; letter-spacing: 0.04em;\n}\n.ax-obs__table td { padding: 6px 10px; border-bottom: 1px solid var(--ax-border); vertical-align: top; }\n.ax-obs__table tr:hover td { background: var(--ax-surface-2); }\n.ax-obs__table .num { text-align: right; font-variant-numeric: tabular-nums; }\n.ax-obs__table .small { color: var(--ax-muted); font-size: 11px; }\n\n/* KPI cards */\n.ax-obs__kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 16px; }\n.ax-obs__kpi { padding: 14px 16px; border: 1px solid var(--ax-border); border-radius: 8px; background: var(--ax-bg); }\n.ax-obs__kpi-label { color: var(--ax-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; }\n.ax-obs__kpi-value { font-size: 22px; font-weight: 600; line-height: 1.1; }\n.ax-obs__kpi-sub { color: var(--ax-muted); font-size: 11px; margin-top: 4px; }\n.ax-obs__kpi--alert .ax-obs__kpi-value { color: #cf222e; }\n.ax-obs__kpi--good .ax-obs__kpi-value { color: #2da44e; }\n.ax-obs__delta { font-size: 11px; }\n.ax-obs__delta--up { color: #2da44e; }\n.ax-obs__delta--down { color: #cf222e; }\n\n.ax-obs__card { border: 1px solid var(--ax-border); border-radius: 8px; padding: 14px 16px; margin-bottom: 12px; background: var(--ax-bg); }\n.ax-obs__card h3 { margin: 0 0 10px; font-size: 13px; font-weight: 600; }\n.ax-obs__split { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }\n@media (max-width: 900px) { .ax-obs__split { grid-template-columns: 1fr; } }\n.ax-obs__idle { margin: 0; font-size: 12px; color: var(--ax-muted); }\n.ax-obs__idle .ax-obs__pill { margin: 2px 4px 2px 0; }\n.ax-obs__error-list { list-style: none; margin: 0; padding: 0; }\n.ax-obs__error-list li {\n padding: 8px 10px; border-radius: 6px; background: var(--ax-surface-2);\n margin-bottom: 6px; font-size: 12px; display: grid; gap: 4px;\n}\n.ax-obs__error-plain { font-weight: 600; color: #cf222e; }\n.ax-obs__error-meta { color: var(--ax-muted); font-size: 11px; }\n.ax-obs__error-message { color: var(--ax-muted); font-size: 11px; font-family: monospace; word-break: break-word; }\n\n/* Bar charts */\n.ax-obs__chart { display: flex; align-items: flex-end; gap: 2px; height: 120px; padding: 4px 0; }\n.ax-obs__bar { flex: 1; min-width: 4px; background: var(--ax-accent, #3a7bd5); border-radius: 2px 2px 0 0; opacity: 0.8; position: relative; cursor: default; transition: opacity 0.1s; }\n.ax-obs__bar:hover { opacity: 1; }\n.ax-obs__bar--with-error { background: linear-gradient(to top, #cf222e var(--err-pct), var(--ax-accent, #3a7bd5) var(--err-pct)); }\n.ax-obs__bar[data-tooltip]:hover::after {\n content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%;\n transform: translateX(-50%); white-space: nowrap; padding: 4px 8px;\n background: var(--ax-text); color: var(--ax-bg); border-radius: 4px;\n font-size: 11px; pointer-events: none; z-index: 10;\n}\n.ax-obs__chart--cost .ax-obs__bar { background: #6b7280; }\n\n.ax-obs__pill {\n display: inline-block; padding: 1px 7px; border-radius: 10px;\n font-size: 10px; font-weight: 600; text-transform: uppercase;\n background: var(--ax-surface-2); color: var(--ax-text);\n}\n.ax-obs__pill--match { background: rgba(46, 160, 67, 0.15); color: #2da44e; }\n.ax-obs__pill--drop { background: rgba(207, 34, 46, 0.15); color: #cf222e; }\n.ax-obs__pill--tier-2 { background: rgba(218, 119, 6, 0.15); color: #bf8700; }\n.ax-obs__pill--stale { background: rgba(99, 110, 123, 0.15); color: #57606a; }\n.ax-obs__pill--max-turns { background: rgba(99, 110, 123, 0.15); color: #57606a; }\n.ax-obs__pill--idle { background: rgba(218, 119, 6, 0.15); color: #bf8700; }\n.ax-obs__empty { color: var(--ax-muted); font-style: italic; padding: 18px 0; margin: 0; }\n.ax-obs__logs-toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; flex-wrap: wrap; }\n.ax-obs__logs-toolbar input[type=\"text\"] { font-family: 'IBM Plex Mono', monospace; font-size: 12px; padding: 5px 9px; min-width: 220px; background: var(--ax-bg-elev); border: 1px solid var(--ax-border); border-radius: 4px; color: var(--ax-text); }\n.ax-obs__logs-toolbar button { font-size: 11px; padding: 5px 11px; border-radius: 4px; border: 1px solid var(--ax-border); background: var(--ax-bg); color: var(--ax-text); cursor: pointer; }\n.ax-obs__logs-toolbar button:hover { background: var(--ax-surface); }\n.ax-obs__inline { font-size: 11px; color: var(--ax-muted); display: inline-flex; align-items: center; gap: 4px; }\n.ax-obs__logs-body { font-family: 'IBM Plex Mono', monospace; font-size: 11px; line-height: 1.45; height: calc(100vh - 320px); min-height: 360px; overflow-y: auto; background: var(--ax-bg-elev); border: 1px solid var(--ax-border); border-radius: 4px; padding: 10px 12px; margin: 0; white-space: pre-wrap; word-break: break-word; }\n.ax-obs__logs-body .err { color: var(--ax-err, #e74c3c); }\n.ax-obs__logs-body .warn { color: #d4ac0d; }\n.ax-obs__logs-body .info { color: var(--ax-text); }\n.ax-obs__logs-body .dim { color: var(--ax-muted); }\n.ax-obs__doctor-toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }\n.ax-obs__doctor-toolbar button { font-size: 11px; padding: 5px 11px; border-radius: 4px; border: 1px solid var(--ax-border); background: var(--ax-bg); color: var(--ax-text); cursor: pointer; }\n.ax-obs__doctor-toolbar button:hover { background: var(--ax-surface); }\n.ax-obs__doctor-body { display: flex; flex-direction: column; gap: 10px; }\n.ax-obs__doctor-group { background: var(--ax-bg-elev); border: 1px solid var(--ax-border); border-radius: 6px; padding: 10px 14px; }\n.ax-obs__doctor-group h4 { font-family: 'IBM Plex Sans', sans-serif; font-size: 11px; font-weight: 600; margin: 0 0 8px; color: var(--ax-muted); text-transform: uppercase; letter-spacing: 0.04em; }\n.ax-obs__doctor-row { display: grid; grid-template-columns: 60px 1fr; gap: 10px; padding: 5px 0; border-top: 1px solid var(--ax-border); align-items: baseline; }\n.ax-obs__doctor-row:first-child { border-top: 0; }\n.ax-obs__doctor-sev { font-size: 10px; text-transform: uppercase; font-weight: 600; letter-spacing: 0.04em; padding: 2px 6px; border-radius: 3px; text-align: center; }\n.ax-obs__doctor-sev.ok { color: #2ecc71; background: rgba(46,204,113,0.12); }\n.ax-obs__doctor-sev.warn { color: #d4ac0d; background: rgba(212,172,13,0.14); }\n.ax-obs__doctor-sev.fail { color: var(--ax-err, #e74c3c); background: rgba(231,76,60,0.14); }\n.ax-obs__doctor-title { font-size: 12px; font-weight: 500; }\n.ax-obs__doctor-detail { font-size: 11px; color: var(--ax-muted); margin-top: 2px; font-family: 'IBM Plex Mono', monospace; word-break: break-word; }\n.ax-obs__doctor-fix { font-size: 11px; color: var(--ax-accent, #3a7bd5); margin-top: 2px; }\n`\n\nconst OBS_PAGE_SCRIPT = `\n(function(){\n var panes = document.querySelectorAll('.ax-obs__pane');\n var tabs = document.querySelectorAll('.ax-obs__tab');\n var current = 'overview';\n\n tabs.forEach(function(t){\n t.addEventListener('click', function(){\n tabs.forEach(function(x){ x.classList.toggle('is-active', x === t); });\n panes.forEach(function(p){ p.classList.toggle('is-active', p.dataset.pane === t.dataset.tab); });\n current = t.dataset.tab;\n load(current);\n });\n });\n\n function fmtTs(s){\n if (!s) return '';\n return String(s).replace('T', ' ').replace('Z', '').slice(0, 19);\n }\n function esc(s){\n if (s == null) return '';\n return String(s).replace(/[&<>\"']/g, function(c){\n return c === '&' ? '&amp;' : c === '<' ? '&lt;' : c === '>' ? '&gt;' : c === '\"' ? '&quot;' : '&#39;';\n });\n }\n function fmtMoney(n){\n if (n == null) return '$0.00';\n if (n < 0.01) return '$' + n.toFixed(4);\n if (n < 1) return '$' + n.toFixed(3);\n return '$' + n.toFixed(2);\n }\n function fmtMs(n){\n if (!n) return '–';\n if (n < 1000) return Math.round(n) + 'ms';\n if (n < 60000) return (n / 1000).toFixed(1) + 's';\n return Math.round(n / 60000) + 'm';\n }\n function fmtNum(n){\n if (n == null) return '–';\n return Number(n).toLocaleString();\n }\n function fmtRel(iso){\n if (!iso) return '–';\n var d = new Date(iso).getTime();\n if (isNaN(d)) return '–';\n var diff = (Date.now() - d) / 1000;\n if (diff < 60) return Math.floor(diff) + 's ago';\n if (diff < 3600) return Math.floor(diff / 60) + 'm ago';\n if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';\n return Math.floor(diff / 86400) + 'd ago';\n }\n function delta(n, label){\n if (n === 0 || n == null) return '<span class=\"ax-obs__delta\">no change</span>';\n var arrow = n > 0 ? '▲' : '▼';\n var cls = n > 0 ? 'ax-obs__delta--up' : 'ax-obs__delta--down';\n return '<span class=\"ax-obs__delta ' + cls + '\">' + arrow + ' ' + Math.abs(n).toLocaleString() + ' ' + (label || '') + '</span>';\n }\n function deltaMoney(n){\n if (Math.abs(n) < 0.001) return '<span class=\"ax-obs__delta\">no change</span>';\n var arrow = n > 0 ? '▲' : '▼';\n var cls = n > 0 ? 'ax-obs__delta--down' : 'ax-obs__delta--up'; // more cost = bad\n return '<span class=\"ax-obs__delta ' + cls + '\">' + arrow + ' ' + fmtMoney(Math.abs(n)) + ' vs yesterday</span>';\n }\n\n async function load(tab){\n if (tab === 'logs') return ensureLogsStream();\n if (logsState.es) closeLogsStream();\n if (tab === 'doctor') return loadDoctor();\n var agent = document.getElementById('obs-agent').value || '';\n var limit = document.getElementById('obs-limit').value || 50;\n\n var url, qs = (agent ? '?agent=' + encodeURIComponent(agent) : '');\n if (tab === 'overview' || tab === 'activity') {\n url = '/api/admin/observability/' + tab + qs;\n } else {\n qs = '?limit=' + encodeURIComponent(limit) + (agent ? '&agent=' + encodeURIComponent(agent) : '');\n url = '/api/admin/observability/' + tab + qs;\n }\n\n try {\n var r = await fetch(url, { credentials: 'same-origin' });\n var data = await r.json();\n render(tab, data);\n } catch (e) {\n console.error('observability fetch failed', e);\n }\n }\n\n // ----- Logs tab: SSE-driven live tail -----\n var logsState = { es: null, paused: false, buffer: [], maxLines: 2000 };\n function classifyLine(s){\n var l = s.toLowerCase();\n if (l.indexOf('error') >= 0 || l.indexOf('fail') >= 0) return 'err';\n if (l.indexOf('warn') >= 0) return 'warn';\n if (l.indexOf('debug') >= 0 || l.indexOf('[graph]') >= 0) return 'dim';\n return 'info';\n }\n function renderLogsLine(line){\n if (!line) return;\n var body = document.getElementById('obs-logs-body');\n var filter = (document.getElementById('obs-logs-filter').value || '').toLowerCase();\n if (filter && line.toLowerCase().indexOf(filter) === -1) return;\n var span = document.createElement('span');\n span.className = classifyLine(line);\n span.textContent = line + '\\\\n';\n body.appendChild(span);\n while (body.children.length > logsState.maxLines) body.removeChild(body.firstChild);\n if (document.getElementById('obs-logs-autoscroll').checked) {\n body.scrollTop = body.scrollHeight;\n }\n }\n function ensureLogsStream(){\n if (logsState.es || logsState.paused) {\n document.getElementById('obs-logs-status').textContent = logsState.paused ? 'paused' : 'live';\n return;\n }\n document.getElementById('obs-logs-status').textContent = 'connecting…';\n var es = new EventSource('/api/admin/logs/stream', { withCredentials: true });\n logsState.es = es;\n es.addEventListener('line', function(ev){ renderLogsLine(ev.data); });\n es.addEventListener('open', function(){ document.getElementById('obs-logs-status').textContent = 'live'; });\n es.addEventListener('error', function(){\n document.getElementById('obs-logs-status').textContent = 'disconnected (retrying)';\n });\n }\n function closeLogsStream(){\n if (logsState.es) { try { logsState.es.close(); } catch (e) {} logsState.es = null; }\n }\n // ----- Doctor tab: preflight checks (mirror of agentx doctor) -----\n async function loadDoctor(){\n var running = document.getElementById('obs-doctor-running').checked;\n document.getElementById('obs-doctor-summary').textContent = 'running…';\n try {\n var r = await fetch('/api/admin/doctor?running=' + (running ? 'true' : 'false'), { credentials: 'same-origin' });\n var data = await r.json();\n var s = data.summary || {};\n document.getElementById('obs-doctor-summary').textContent =\n 'ok=' + (s.ok || 0) + ' warn=' + (s.warnings || 0) + ' fail=' + (s.errors || 0);\n var grouped = {};\n for (var i = 0; i < (data.checks || []).length; i++) {\n var c = data.checks[i];\n (grouped[c.group] = grouped[c.group] || []).push(c);\n }\n var html = '';\n Object.keys(grouped).forEach(function(g){\n html += '<div class=\"ax-obs__doctor-group\"><h4>' + esc(g) + '</h4>';\n grouped[g].forEach(function(c){\n html += '<div class=\"ax-obs__doctor-row\">' +\n '<span class=\"ax-obs__doctor-sev ' + esc(c.severity) + '\">' + esc(c.severity) + '</span>' +\n '<div>' +\n '<div class=\"ax-obs__doctor-title\">' + esc(c.title) + '</div>' +\n (c.detail ? '<div class=\"ax-obs__doctor-detail\">' + esc(c.detail) + '</div>' : '') +\n (c.fix ? '<div class=\"ax-obs__doctor-fix\">→ ' + esc(c.fix) + '</div>' : '') +\n '</div>' +\n '</div>';\n });\n html += '</div>';\n });\n document.getElementById('obs-doctor-body').innerHTML = html || '<p class=\"ax-obs__empty\">no checks</p>';\n } catch (e) {\n document.getElementById('obs-doctor-summary').textContent = 'failed: ' + e.message;\n }\n }\n\n function wireLogsToolbar(){\n var pauseBtn = document.getElementById('obs-logs-pause');\n var clearBtn = document.getElementById('obs-logs-clear');\n if (!pauseBtn || pauseBtn.dataset.wired) return;\n pauseBtn.dataset.wired = '1';\n pauseBtn.addEventListener('click', function(){\n logsState.paused = !logsState.paused;\n pauseBtn.textContent = logsState.paused ? 'Resume' : 'Pause';\n if (logsState.paused) closeLogsStream();\n else ensureLogsStream();\n document.getElementById('obs-logs-status').textContent = logsState.paused ? 'paused' : 'connecting…';\n });\n clearBtn.addEventListener('click', function(){\n document.getElementById('obs-logs-body').innerHTML = '';\n });\n }\n\n function render(tab, data){\n if (tab === 'overview') return renderOverview(data);\n if (tab === 'activity') return renderActivity(data);\n if (tab === 'errors') return renderErrors(data);\n if (tab === 'routing') return renderRouting(data);\n if (tab === 'rotations') return renderRotations(data);\n }\n\n function renderOverview(data){\n if (!data) return;\n var k = data.kpis || {};\n var errAlert = k.errorsToday > 0 ? ' ax-obs__kpi--alert' : ' ax-obs__kpi--good';\n document.getElementById('obs-overview-kpis').innerHTML = [\n kpi('Tasks today', fmtNum(k.tasksToday), delta(k.tasksDelta, 'vs yesterday')),\n kpi('Errors today', fmtNum(k.errorsToday) + ' <span class=\"ax-obs__kpi-sub\">(' + (k.errorRate * 100).toFixed(1) + '% error rate)</span>', '', errAlert),\n kpi('Avg response', fmtMs(k.avgDurationMs), 'p50 ' + fmtMs(k.p50DurationMs) + ' · p95 ' + fmtMs(k.p95DurationMs)),\n kpi('Spend today', fmtMoney(k.spendToday), deltaMoney(k.spendDelta)),\n ].join('');\n\n var pa = data.perAgent || [];\n if (pa.length) {\n document.getElementById('obs-overview-agents-empty').hidden = true;\n document.getElementById('obs-overview-agents').innerHTML = pa.map(function(r){\n return '<tr>'\n + '<td>' + esc(r.agent_id) + '</td>'\n + '<td class=\"num\">' + fmtNum(r.tasks) + '</td>'\n + '<td class=\"num\" ' + (r.errors > 0 ? 'style=\"color:#cf222e;font-weight:600\"' : '') + '>' + fmtNum(r.errors) + '</td>'\n + '<td class=\"num\">' + fmtMs(r.avg_duration_ms) + '</td>'\n + '<td class=\"small\">' + fmtRel(r.last_active) + '</td>'\n + '</tr>';\n }).join('');\n } else {\n document.getElementById('obs-overview-agents-empty').hidden = false;\n document.getElementById('obs-overview-agents').innerHTML = '';\n }\n\n var pc = data.perChannel || [];\n if (pc.length) {\n document.getElementById('obs-overview-channels-empty').hidden = true;\n document.getElementById('obs-overview-channels').innerHTML = pc.map(function(r){\n return '<tr>'\n + '<td>' + esc(r.channel) + '</td>'\n + '<td class=\"num\">' + fmtNum(r.tasks) + '</td>'\n + '<td class=\"num\" ' + (r.errors > 0 ? 'style=\"color:#cf222e;font-weight:600\"' : '') + '>' + fmtNum(r.errors) + '</td>'\n + '</tr>';\n }).join('');\n } else {\n document.getElementById('obs-overview-channels-empty').hidden = false;\n document.getElementById('obs-overview-channels').innerHTML = '';\n }\n\n var idle = data.idleAgents || [];\n document.getElementById('obs-overview-idle').innerHTML = idle.length\n ? idle.map(function(a){ return '<span class=\"ax-obs__pill ax-obs__pill--idle\">' + esc(a) + '</span>'; }).join('')\n : '<span class=\"ax-obs__pill ax-obs__pill--match\">All recently-active agents posted a task in the last 24h ✓</span>';\n\n var er = data.recentErrors || [];\n var ulEmpty = document.getElementById('obs-overview-errors-empty');\n var ulList = document.getElementById('obs-overview-errors');\n if (er.length === 0) {\n ulEmpty.hidden = false;\n ulList.innerHTML = '';\n } else {\n ulEmpty.hidden = true;\n ulList.innerHTML = er.map(function(r){\n return '<li>'\n + '<div class=\"ax-obs__error-plain\">' + esc(r.plain) + '</div>'\n + '<div class=\"ax-obs__error-meta\">' + fmtRel(r.started_at) + ' · ' + esc(r.agent_id) + ' · ' + esc(r.channel) + '</div>'\n + (r.message_preview ? '<div class=\"ax-obs__error-message\">' + esc(r.message_preview.slice(0, 200)) + '</div>' : '')\n + '</li>';\n }).join('');\n }\n }\n\n function renderActivity(data){\n if (!data) return;\n var hourly = data.hourly || [];\n if (hourly.length === 0) {\n document.getElementById('obs-activity-hourly-empty').hidden = false;\n document.getElementById('obs-activity-hourly').innerHTML = '';\n } else {\n document.getElementById('obs-activity-hourly-empty').hidden = true;\n document.getElementById('obs-activity-hourly').innerHTML = renderBarChart(hourly, 'hour', 'tasks', 'errors');\n }\n document.getElementById('obs-activity-daily').innerHTML = renderBarChart(data.daily || [], 'day', 'tasks', 'errors');\n\n var rows = data.perAgent || [];\n if (rows.length === 0) {\n document.getElementById('obs-activity-agents-empty').hidden = false;\n document.getElementById('obs-activity-agents').innerHTML = '';\n } else {\n document.getElementById('obs-activity-agents-empty').hidden = true;\n document.getElementById('obs-activity-agents').innerHTML = rows.map(function(r){\n return '<tr>'\n + '<td>' + esc(r.agent_id) + '</td>'\n + '<td class=\"num\">' + fmtNum(r.tasks_24h) + '</td>'\n + '<td class=\"num\">' + fmtNum(r.tasks_7d) + '</td>'\n + '<td class=\"num\" ' + (r.errors_7d > 0 ? 'style=\"color:#cf222e;font-weight:600\"' : '') + '>' + fmtNum(r.errors_7d) + '</td>'\n + '<td class=\"num\">' + fmtMs(r.avg_duration_ms) + '</td>'\n + '<td class=\"small\">' + fmtRel(r.last_active) + '</td>'\n + '</tr>';\n }).join('');\n }\n }\n\n function renderErrors(data){\n var rows = data && data.rows || [];\n var body = document.getElementById('obs-errors');\n var empty = document.getElementById('obs-errors-empty');\n if (!rows.length) { body.innerHTML = ''; empty.hidden = false; return; }\n empty.hidden = true;\n body.innerHTML = rows.map(function(r){\n return '<tr>'\n + '<td>' + fmtTs(r.started_at) + '</td>'\n + '<td>' + esc(r.agent_id) + '</td>'\n + '<td>' + esc(r.channel) + '</td>'\n + '<td><span style=\"color:#cf222e;font-weight:600\">' + esc(r.plain) + '</span></td>'\n + '<td class=\"small\">' + esc((r.message_preview || '').slice(0, 60)) + '</td>'\n + '</tr>';\n }).join('');\n }\n\n function renderRouting(data){\n var rows = data && data.rows || [];\n var body = document.getElementById('obs-routing');\n var empty = document.getElementById('obs-routing-empty');\n if (!rows.length) { body.innerHTML = ''; empty.hidden = false; return; }\n empty.hidden = true;\n body.innerHTML = rows.map(function(r){\n return '<tr>'\n + '<td>' + fmtTs(r.at) + '</td>'\n + '<td>' + esc(r.channel) + '</td>'\n + '<td class=\"small\">' + esc(r.chat_id) + (r.account_id ? ' / ' + esc(r.account_id) : '') + '</td>'\n + '<td><span class=\"ax-obs__pill ax-obs__pill--' + esc(r.kind) + '\">' + esc(r.kind || '?') + '</span></td>'\n + '<td>' + esc(r.deciding_stage) + '</td>'\n + '<td>' + esc(r.agent_id || '-') + '</td>'\n + '<td class=\"small\">' + esc(r.reason || '') + '</td>'\n + '</tr>';\n }).join('');\n }\n\n function renderRotations(data){\n var rows = data && data.rows || [];\n var body = document.getElementById('obs-rotations');\n var empty = document.getElementById('obs-rotations-empty');\n if (!rows.length) { body.innerHTML = ''; empty.hidden = false; return; }\n empty.hidden = true;\n body.innerHTML = rows.map(function(r){\n return '<tr>'\n + '<td>' + fmtTs(r.rotated_at) + '</td>'\n + '<td>' + esc(r.agent_id) + '</td>'\n + '<td>' + esc(r.channel) + '</td>'\n + '<td><span class=\"ax-obs__pill ax-obs__pill--' + esc(r.reason) + '\">' + esc(r.reason) + '</span></td>'\n + '<td class=\"num\">' + (r.last_turn_input_tokens != null ? r.last_turn_input_tokens.toLocaleString() : '-') + '</td>'\n + '</tr>';\n }).join('');\n }\n\n function kpi(label, value, sub, extraCls){\n return '<div class=\"ax-obs__kpi' + (extraCls || '') + '\">'\n + '<div class=\"ax-obs__kpi-label\">' + label + '</div>'\n + '<div class=\"ax-obs__kpi-value\">' + value + '</div>'\n + '<div class=\"ax-obs__kpi-sub\">' + (sub || '') + '</div>'\n + '</div>';\n }\n\n function renderBarChart(rows, xKey, valueKey, errorKey, money){\n if (!rows.length) return '<p class=\"ax-obs__empty\">(no data)</p>';\n var max = 0;\n for (var i = 0; i < rows.length; i++) {\n if (rows[i][valueKey] > max) max = rows[i][valueKey];\n }\n if (max === 0) return '<p class=\"ax-obs__empty\">(no activity)</p>';\n return rows.map(function(r){\n var v = r[valueKey] || 0;\n var pct = (v / max) * 100;\n var label = money ? fmtMoney(v) : fmtNum(v);\n var tip = esc(r[xKey]) + ': ' + label + (errorKey && r[errorKey] ? ' (' + r[errorKey] + ' errors)' : '');\n var errPct = errorKey && v > 0 ? Math.round((r[errorKey] / v) * 100) + '%' : '0%';\n var cls = errorKey && r[errorKey] > 0 ? 'ax-obs__bar ax-obs__bar--with-error' : 'ax-obs__bar';\n return '<div class=\"' + cls + '\" style=\"height:' + pct + '%;--err-pct:' + errPct + '\" data-tooltip=\"' + tip + '\"></div>';\n }).join('');\n }\n\n document.getElementById('obs-refresh').addEventListener('click', function(){ load(current); });\n document.getElementById('obs-agent').addEventListener('change', function(){ load(current); });\n var lim = document.getElementById('obs-limit');\n if (lim) lim.addEventListener('change', function(){ load(current); });\n\n document.getElementById('obs-doctor-run').addEventListener('click', loadDoctor);\n document.getElementById('obs-doctor-running').addEventListener('change', loadDoctor);\n wireLogsToolbar();\n document.getElementById('obs-logs-filter').addEventListener('input', function(){\n // re-render existing buffered lines through the filter\n var body = document.getElementById('obs-logs-body');\n var filter = (this.value || '').toLowerCase();\n Array.from(body.children).forEach(function(s){\n s.style.display = (filter && s.textContent.toLowerCase().indexOf(filter) === -1) ? 'none' : '';\n });\n });\n\n load(current);\n})();\n`\n","import type { IncomingMessage, ServerResponse } from \"http\"\nimport { resolve } from \"path\"\nimport { existsSync } from \"fs\"\nimport Database from \"better-sqlite3\"\nimport { renderShell, type TopbarPeer } from \"./ui\"\n\n// --- /admin/ledger — intent-ledger explorer ---\n//\n// Read-only window over .agentx/intent/ledger.sqlite. Same data the\n// `agentx ledger` CLI commands surface (stats / events / divergences /\n// active), but rendered as a single-page tab UI. Replay and lineage\n// stay CLI-only for now — they're interactive enough that piping\n// through dashboard JSON would lose value.\n\ninterface OpenedDb {\n db: Database.Database\n close: () => void\n}\n\nfunction openReadOnly(): OpenedDb | null {\n const path = resolve(process.cwd(), \".agentx/intent/ledger.sqlite\")\n if (!existsSync(path)) return null\n try {\n const db = new Database(path, { readonly: true, fileMustExist: true })\n return { db, close: () => db.close() }\n } catch (e: any) {\n const now = Date.now()\n if (now - _lastOpenReadOnlyErrAt > 60_000) {\n _lastOpenReadOnlyErrAt = now\n console.error(`[ledger-panel] openReadOnly failed: ${e?.message ?? e}`)\n }\n return null\n }\n}\nlet _lastOpenReadOnlyErrAt = 0\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(body))\n}\n\n/** Parse `?since=24h|7d|1h` into a unix-ms cutoff. Returns undefined when\n * unset so SQL queries omit the WHERE clause entirely. */\nfunction parseSince(raw: string | null): number | undefined {\n if (!raw) return undefined\n const m = String(raw).trim().match(/^(\\d+)\\s*(s|m|h|d|w)?$/i)\n if (!m) return undefined\n const n = parseInt(m[1], 10)\n const unit = (m[2] || \"h\").toLowerCase()\n const ms = unit === \"s\" ? 1000\n : unit === \"m\" ? 60_000\n : unit === \"h\" ? 3_600_000\n : unit === \"d\" ? 86_400_000\n : unit === \"w\" ? 7 * 86_400_000\n : 0\n return Date.now() - n * ms\n}\n\nexport async function handleLedgerApi(\n req: IncomingMessage,\n res: ServerResponse,\n path: string,\n): Promise<boolean> {\n if (!path.startsWith(\"/api/admin/ledger/\")) return false\n const slug = path.replace(/^\\/api\\/admin\\/ledger\\//, \"\").split(\"?\")[0]\n const opened = openReadOnly()\n if (!opened) {\n sendJson(res, 503, { error: \"intent ledger not available\", rows: [] })\n return true\n }\n try {\n const url = new URL(req.url || \"/\", \"http://_\")\n const since = parseSince(url.searchParams.get(\"since\"))\n const source = url.searchParams.get(\"source\") || \"\"\n const project = url.searchParams.get(\"project\") || \"\"\n const limitRaw = parseInt(url.searchParams.get(\"limit\") || \"50\", 10)\n const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(500, limitRaw)) : 50\n\n if (slug === \"stats\") {\n const sinceClause = since !== undefined ? \"WHERE ts >= @since\" : \"\"\n const sinceClauseDecisions = since !== undefined ? \"WHERE decided_at >= @since\" : \"\"\n const eventsBySource = opened.db.prepare(\n `SELECT source, COUNT(*) AS n FROM intent_events ${sinceClause} GROUP BY source ORDER BY n DESC`,\n ).all({ since })\n const totalDecisions = (opened.db.prepare(\n `SELECT COUNT(*) AS n FROM intent_decisions ${sinceClauseDecisions}`,\n ).get({ since }) as { n: number }).n\n const decisionsByOutcome = opened.db.prepare(\n `SELECT outcome, COUNT(*) AS n FROM intent_decisions ${sinceClauseDecisions} GROUP BY outcome ORDER BY n DESC`,\n ).all({ since })\n const totalDivergences = (opened.db.prepare(\n `SELECT COUNT(*) AS n FROM intent_divergences ${sinceClause}`,\n ).get({ since }) as { n: number }).n\n const divergencesBySource = opened.db.prepare(\n `SELECT source, COUNT(*) AS n FROM intent_divergences ${sinceClause} GROUP BY source ORDER BY n DESC`,\n ).all({ since })\n const inFlight = (opened.db.prepare(`\n SELECT COUNT(*) AS n FROM intent_decisions d\n LEFT JOIN intent_resolutions r\n ON r.decision_event_id = d.event_id AND r.decision_decided_by = d.decided_by\n WHERE d.outcome = 'dispatched' AND r.decision_event_id IS NULL\n `).get() as { n: number }).n\n sendJson(res, 200, {\n since: since ?? null,\n eventsBySource, totalDecisions, decisionsByOutcome,\n totalDivergences, divergencesBySource, inFlight,\n })\n return true\n }\n\n if (slug === \"events\") {\n const where: string[] = []\n const params: Record<string, unknown> = { limit }\n if (since !== undefined) { where.push(\"ts >= @since\"); params.since = since }\n if (source) { where.push(\"source = @source\"); params.source = source }\n if (project) { where.push(\"project = @project\"); params.project = project }\n const sql = `\n SELECT id, ts, source, source_event_id, project, subject, intent\n FROM intent_events\n ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"}\n ORDER BY ts DESC LIMIT @limit\n `\n sendJson(res, 200, { rows: opened.db.prepare(sql).all(params) })\n return true\n }\n\n if (slug === \"divergences\") {\n const where: string[] = []\n const params: Record<string, unknown> = { limit }\n if (since !== undefined) { where.push(\"ts >= @since\"); params.since = since }\n if (source) { where.push(\"source = @source\"); params.source = source }\n const sql = `\n SELECT ts, source, decided_by,\n legacy_agent_id, legacy_outcome, legacy_reason,\n ledger_agent_id, ledger_outcome, ledger_reason,\n event_id\n FROM intent_divergences\n ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"}\n ORDER BY ts DESC LIMIT @limit\n `\n sendJson(res, 200, { rows: opened.db.prepare(sql).all(params) })\n return true\n }\n\n if (slug === \"active\") {\n const where: string[] = [\"d.outcome = 'dispatched'\", \"r.decision_event_id IS NULL\"]\n const params: Record<string, unknown> = { limit }\n if (source) { where.push(\"e.source = @source\"); params.source = source }\n const sql = `\n SELECT e.id AS event_id, e.ts, e.source, e.project, e.subject, e.intent,\n d.decided_by, d.decided_at, d.agent_id, d.reason\n FROM intent_decisions d\n JOIN intent_events e ON e.id = d.event_id\n LEFT JOIN intent_resolutions r\n ON r.decision_event_id = d.event_id AND r.decision_decided_by = d.decided_by\n WHERE ${where.join(\" AND \")}\n ORDER BY d.decided_at DESC LIMIT @limit\n `\n sendJson(res, 200, { rows: opened.db.prepare(sql).all(params) })\n return true\n }\n\n sendJson(res, 404, { error: \"unknown ledger slice\", rows: [] })\n return true\n } catch (e: any) {\n sendJson(res, 500, { error: e?.message ?? String(e), rows: [] })\n return true\n } finally {\n opened.close()\n }\n}\n\nexport interface LedgerPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderLedgerPage(opts: LedgerPageOpts = {}): string {\n const body = `<div class=\"ax-led\">\n <header class=\"ax-led__head\">\n <h1>Intent ledger</h1>\n <p class=\"ax-led__sub\">Read-only view over <code>.agentx/intent/ledger.sqlite</code> — same source as <code>agentx ledger</code>. Replay and lineage live in the CLI for now.</p>\n <nav class=\"ax-led__tabs\" role=\"tablist\">\n <button class=\"ax-led__tab is-active\" data-tab=\"stats\" role=\"tab\">Stats</button>\n <button class=\"ax-led__tab\" data-tab=\"events\" role=\"tab\">Events</button>\n <button class=\"ax-led__tab\" data-tab=\"divergences\" role=\"tab\">Divergences</button>\n <button class=\"ax-led__tab\" data-tab=\"active\" role=\"tab\">In-flight</button>\n </nav>\n <div class=\"ax-led__filters\">\n <label>Since\n <select id=\"led-since\">\n <option value=\"\">all-time</option>\n <option value=\"1h\">last 1h</option>\n <option value=\"24h\" selected>last 24h</option>\n <option value=\"7d\">last 7d</option>\n <option value=\"30d\">last 30d</option>\n </select>\n </label>\n <label>Source\n <input id=\"led-source\" type=\"text\" placeholder=\"telegram, gitlab, mesh, …\" />\n </label>\n <label>Limit\n <input id=\"led-limit\" type=\"number\" min=\"1\" max=\"500\" value=\"50\" />\n </label>\n <button id=\"led-refresh\" type=\"button\">Refresh</button>\n </div>\n </header>\n\n <section class=\"ax-led__pane is-active\" data-pane=\"stats\">\n <div class=\"ax-led__kpis\" id=\"led-stats-kpis\"></div>\n <div class=\"ax-led__split\">\n <div class=\"ax-led__card\"><h3>Events by source</h3><table class=\"ax-led__table\"><thead><tr><th>source</th><th class=\"num\">count</th></tr></thead><tbody id=\"led-stats-events\"></tbody></table></div>\n <div class=\"ax-led__card\"><h3>Decisions by outcome</h3><table class=\"ax-led__table\"><thead><tr><th>outcome</th><th class=\"num\">count</th></tr></thead><tbody id=\"led-stats-decisions\"></tbody></table></div>\n <div class=\"ax-led__card\"><h3>Divergences by source</h3><table class=\"ax-led__table\"><thead><tr><th>source</th><th class=\"num\">count</th></tr></thead><tbody id=\"led-stats-divergences\"></tbody></table></div>\n </div>\n </section>\n\n <section class=\"ax-led__pane\" data-pane=\"events\">\n <div class=\"ax-led__table-wrap\"><table class=\"ax-led__table\">\n <thead><tr><th>at</th><th>source</th><th>intent</th><th>project</th><th>subject</th><th class=\"mono small\">id</th></tr></thead>\n <tbody id=\"led-events\"></tbody>\n </table></div>\n <p class=\"ax-led__empty\" id=\"led-events-empty\" hidden>(no rows)</p>\n </section>\n\n <section class=\"ax-led__pane\" data-pane=\"divergences\">\n <p class=\"ax-led__hint\">When the legacy router and the ledger disagree on what to dispatch — agentx logs both decisions and the difference here. A growing count means the new ledger is observing edge cases the old code path missed (or vice versa); investigate.</p>\n <div class=\"ax-led__table-wrap\"><table class=\"ax-led__table\">\n <thead><tr><th>at</th><th>source</th><th>decided by</th><th>legacy → agent</th><th>ledger → agent</th><th>reason</th></tr></thead>\n <tbody id=\"led-divergences\"></tbody>\n </table></div>\n <p class=\"ax-led__empty\" id=\"led-divergences-empty\" hidden>(no rows)</p>\n </section>\n\n <section class=\"ax-led__pane\" data-pane=\"active\">\n <p class=\"ax-led__hint\">Decisions where the dispatch landed but no resolution row exists yet. Long-lived rows here are the operationally interesting ones — they're the running tasks.</p>\n <div class=\"ax-led__table-wrap\"><table class=\"ax-led__table\">\n <thead><tr><th>started</th><th>source</th><th>agent</th><th>intent</th><th>subject</th><th>reason</th></tr></thead>\n <tbody id=\"led-active\"></tbody>\n </table></div>\n <p class=\"ax-led__empty\" id=\"led-active-empty\" hidden>(no rows)</p>\n </section>\n</div>`\n\n const css = `\n .ax-led{padding:24px 28px;max-width:1240px;margin:0 auto}\n .ax-led__head h1{font-family:'IBM Plex Sans',sans-serif;font-weight:600;font-size:20px;letter-spacing:-0.01em;margin:0 0 6px}\n .ax-led__sub{color:var(--ax-muted);font-size:12px;margin:0 0 12px}\n .ax-led__tabs{display:flex;gap:2px;border-bottom:1px solid var(--ax-border);margin-bottom:12px;align-items:center}\n .ax-led__tab{font:inherit;font-size:12px;background:none;border:0;border-bottom:2px solid transparent;color:var(--ax-muted);padding:8px 12px;cursor:pointer}\n .ax-led__tab:hover{color:var(--ax-text)}\n .ax-led__tab.is-active{color:var(--ax-text);border-bottom-color:var(--ax-accent,#3a7bd5)}\n .ax-led__filters{display:flex;align-items:center;gap:14px;margin-bottom:12px;flex-wrap:wrap}\n .ax-led__filters label{font-size:11px;color:var(--ax-muted);display:inline-flex;align-items:center;gap:6px}\n .ax-led__filters input,.ax-led__filters select{font-family:'IBM Plex Mono',monospace;font-size:12px;padding:4px 8px;background:var(--ax-bg-elev);border:1px solid var(--ax-border);border-radius:4px;color:var(--ax-fg)}\n .ax-led__filters button{font-size:11px;padding:5px 11px;border-radius:4px;border:1px solid var(--ax-border);background:var(--ax-bg);color:var(--ax-fg);cursor:pointer}\n .ax-led__filters button:hover{background:var(--ax-surface)}\n .ax-led__pane{display:none}\n .ax-led__pane.is-active{display:block}\n .ax-led__kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:14px}\n .ax-led__kpi{background:var(--ax-bg-elev);border:1px solid var(--ax-border);border-radius:6px;padding:10px 12px}\n .ax-led__kpi-label{font-size:10px;text-transform:uppercase;letter-spacing:0.04em;color:var(--ax-muted);font-weight:600;margin-bottom:4px}\n .ax-led__kpi-value{font-family:'IBM Plex Mono',monospace;font-size:18px;font-weight:600}\n .ax-led__split{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}\n .ax-led__card{background:var(--ax-bg-elev);border:1px solid var(--ax-border);border-radius:6px;padding:12px 14px}\n .ax-led__card h3{font-family:'IBM Plex Sans',sans-serif;font-size:12px;font-weight:600;margin:0 0 8px;color:var(--ax-fg);text-transform:uppercase;letter-spacing:0.04em}\n .ax-led__table{width:100%;border-collapse:collapse;font-size:11px;font-family:'IBM Plex Mono',monospace}\n .ax-led__table thead th{text-align:left;padding:5px 7px;border-bottom:1px solid var(--ax-border);font-weight:500;color:var(--ax-muted);font-size:10px;text-transform:uppercase;letter-spacing:0.04em}\n .ax-led__table tbody td{padding:5px 7px;border-bottom:1px solid var(--ax-border);vertical-align:top}\n .ax-led__table tr:hover td{background:var(--ax-surface)}\n .ax-led__table .num{text-align:right;font-variant-numeric:tabular-nums}\n .ax-led__table .small{color:var(--ax-muted);font-size:10px}\n .ax-led__table .mono{font-family:'IBM Plex Mono',monospace}\n .ax-led__table-wrap{overflow-x:auto;max-height:calc(100vh - 280px);overflow-y:auto;border:1px solid var(--ax-border);border-radius:6px}\n .ax-led__empty{color:var(--ax-muted);font-style:italic;padding:18px 0;margin:0}\n .ax-led__hint{color:var(--ax-muted);font-size:11px;margin:0 0 8px}\n .ax-led__pill{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;background:var(--ax-bg);border:1px solid var(--ax-border)}\n .ax-led__pill--err{color:var(--ax-err,#e74c3c);border-color:rgba(231,76,60,0.5)}\n .ax-led__pill--ok{color:#2ecc71}\n `\n\n const script = `\n (function(){\n var current = 'stats';\n function $(id){ return document.getElementById(id); }\n function esc(s){ if (s == null) return ''; return String(s).replace(/[&<>\"']/g, function(c){ return ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'})[c]; }); }\n function fmtTs(ms){ if (!ms) return ''; var d = new Date(Number(ms)); var pad = function(n){ return n < 10 ? '0' + n : n; }; return d.getMonth()+1 + '/' + d.getDate() + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); }\n function fmtNum(n){ return (n||0).toLocaleString(); }\n function kpi(label, value){ return '<div class=\"ax-led__kpi\"><div class=\"ax-led__kpi-label\">' + esc(label) + '</div><div class=\"ax-led__kpi-value\">' + value + '</div></div>'; }\n\n document.querySelectorAll('.ax-led__tab').forEach(function(t){\n t.addEventListener('click', function(){\n document.querySelectorAll('.ax-led__tab').forEach(function(x){ x.classList.toggle('is-active', x === t); });\n document.querySelectorAll('.ax-led__pane').forEach(function(p){ p.classList.toggle('is-active', p.dataset.pane === t.dataset.tab); });\n current = t.dataset.tab;\n load();\n });\n });\n $('led-refresh').addEventListener('click', load);\n $('led-since').addEventListener('change', load);\n $('led-source').addEventListener('change', load);\n $('led-limit').addEventListener('change', load);\n\n async function load(){\n var since = $('led-since').value;\n var source = $('led-source').value.trim();\n var limit = $('led-limit').value || 50;\n var qs = '?limit=' + encodeURIComponent(limit);\n if (since) qs += '&since=' + encodeURIComponent(since);\n if (source) qs += '&source=' + encodeURIComponent(source);\n try {\n var r = await fetch('/api/admin/ledger/' + current + qs, { credentials: 'same-origin' });\n var data = await r.json();\n render(current, data);\n } catch (e) {\n console.error('ledger fetch failed', e);\n }\n }\n function render(tab, data){\n if (tab === 'stats') return renderStats(data);\n if (tab === 'events') return renderEvents(data);\n if (tab === 'divergences') return renderDivergences(data);\n if (tab === 'active') return renderActive(data);\n }\n function renderStats(d){\n $('led-stats-kpis').innerHTML = [\n kpi('Decisions', fmtNum(d.totalDecisions)),\n kpi('Divergences', fmtNum(d.totalDivergences)),\n kpi('In-flight', fmtNum(d.inFlight)),\n ].join('');\n $('led-stats-events').innerHTML = (d.eventsBySource || []).map(function(r){\n return '<tr><td>' + esc(r.source) + '</td><td class=\"num\">' + fmtNum(r.n) + '</td></tr>';\n }).join('') || '<tr><td colspan=\"2\" class=\"small\">(none)</td></tr>';\n $('led-stats-decisions').innerHTML = (d.decisionsByOutcome || []).map(function(r){\n return '<tr><td>' + esc(r.outcome) + '</td><td class=\"num\">' + fmtNum(r.n) + '</td></tr>';\n }).join('') || '<tr><td colspan=\"2\" class=\"small\">(none)</td></tr>';\n $('led-stats-divergences').innerHTML = (d.divergencesBySource || []).map(function(r){\n return '<tr><td>' + esc(r.source) + '</td><td class=\"num\">' + fmtNum(r.n) + '</td></tr>';\n }).join('') || '<tr><td colspan=\"2\" class=\"small\">(none)</td></tr>';\n }\n function renderEvents(d){\n var rows = d.rows || [];\n $('led-events-empty').hidden = rows.length > 0;\n $('led-events').innerHTML = rows.map(function(r){\n return '<tr>' +\n '<td>' + esc(fmtTs(r.ts)) + '</td>' +\n '<td><span class=\"ax-led__pill\">' + esc(r.source) + '</span></td>' +\n '<td>' + esc(r.intent || '') + '</td>' +\n '<td>' + esc(r.project || '') + '</td>' +\n '<td class=\"small\">' + esc((r.subject || '').slice(0, 80)) + '</td>' +\n '<td class=\"small mono\">' + esc((r.id || '').slice(0, 16)) + '</td>' +\n '</tr>';\n }).join('');\n }\n function renderDivergences(d){\n var rows = d.rows || [];\n $('led-divergences-empty').hidden = rows.length > 0;\n $('led-divergences').innerHTML = rows.map(function(r){\n var legacy = (r.legacy_outcome || '') + (r.legacy_agent_id ? ' → ' + r.legacy_agent_id : '');\n var ledger = (r.ledger_outcome || '') + (r.ledger_agent_id ? ' → ' + r.ledger_agent_id : '');\n var reason = (r.legacy_reason || r.ledger_reason || '').slice(0, 100);\n return '<tr>' +\n '<td>' + esc(fmtTs(r.ts)) + '</td>' +\n '<td><span class=\"ax-led__pill\">' + esc(r.source) + '</span></td>' +\n '<td>' + esc(r.decided_by || '') + '</td>' +\n '<td>' + esc(legacy) + '</td>' +\n '<td>' + esc(ledger) + '</td>' +\n '<td class=\"small\">' + esc(reason) + '</td>' +\n '</tr>';\n }).join('');\n }\n function renderActive(d){\n var rows = d.rows || [];\n $('led-active-empty').hidden = rows.length > 0;\n $('led-active').innerHTML = rows.map(function(r){\n return '<tr>' +\n '<td>' + esc(fmtTs(r.decided_at)) + '</td>' +\n '<td><span class=\"ax-led__pill\">' + esc(r.source) + '</span></td>' +\n '<td>' + esc(r.agent_id || '') + '</td>' +\n '<td>' + esc(r.intent || '') + '</td>' +\n '<td class=\"small\">' + esc((r.subject || '').slice(0, 80)) + '</td>' +\n '<td class=\"small\">' + esc((r.reason || '').slice(0, 80)) + '</td>' +\n '</tr>';\n }).join('');\n }\n load();\n })();\n `\n\n return renderShell({\n title: \"AgentX · Ledger\",\n activeTab: \"graph\",\n subtitle: \"Ledger\",\n body,\n css,\n scripts: `<script>${script}</script>`,\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n })\n}\n","import { renderShell, type TopbarPeer } from \"..\"\n\n// --- /admin/cost — token-spend dashboard ---\n//\n// Replaces the standalone `agentx usage serve` (port 4201) at full fidelity.\n// Six KPIs with sparklines + WoW deltas, hero stacked-cost canvas, token mix\n// card with cache-savings chip, top-agent leaderboard, cache efficiency\n// gauge, daily detail table with filter chips, CSV export, and a range\n// selector (7/14/30/90/all). Backed by /api/admin/observability/cost which\n// now returns full per-day per-category breakdown + per-agent model+tokens.\n// Theme follows the dashboard's CSS variables — no separate palette.\n\nexport interface CostPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderCostPage(opts: CostPageOpts = {}): string {\n const body = `<div class=\"ax-cost\">\n <header class=\"ax-cost__head\">\n <div>\n <h1>Cost</h1>\n <p class=\"ax-cost__sub\"><span class=\"ax-cost__live\"></span>Anthropic spend across all agents. Last ingest <span id=\"cost-last\" class=\"ax-mono\">—</span>.</p>\n </div>\n <div class=\"ax-cost__controls\">\n <div class=\"ax-seg\" id=\"cost-range\" role=\"tablist\">\n <button data-r=\"7\">7d</button>\n <button data-r=\"14\">14d</button>\n <button data-r=\"30\" class=\"active\">30d</button>\n <button data-r=\"90\">90d</button>\n <button data-r=\"all\">All</button>\n </div>\n <a class=\"ax-cost__export\" id=\"cost-export\" href=\"#\">Export CSV</a>\n </div>\n </header>\n\n <section class=\"ax-cost__kpis\" id=\"cost-kpis\"></section>\n\n <section class=\"ax-cost__hero\">\n <div class=\"ax-cost__card\">\n <div class=\"ax-cost__card-head\">\n <div>\n <div class=\"ax-cost__card-title\">Cost over time</div>\n <div class=\"ax-cost__card-sub\">Daily spend, stacked by token category</div>\n </div>\n <div class=\"ax-cost__legend\">\n <span><i class=\"sw sw-input\"></i>Input</span>\n <span><i class=\"sw sw-output\"></i>Output</span>\n <span><i class=\"sw sw-cr\"></i>Cache read</span>\n <span><i class=\"sw sw-cw\"></i>Cache write</span>\n </div>\n </div>\n <div class=\"ax-cost__hero-wrap\">\n <div class=\"ax-cost__hero-overlay\">\n <div><div class=\"v ax-mono\" id=\"cost-hero-total\">$0</div><div class=\"l\">Total · range</div></div>\n <div><div class=\"v ax-mono\" id=\"cost-hero-peak\">$0</div><div class=\"l\">Peak day</div></div>\n <div><div class=\"v ax-mono\" id=\"cost-hero-avg\">$0</div><div class=\"l\">Daily avg</div></div>\n </div>\n <canvas id=\"cost-hero\" class=\"ax-cost__hero-canvas\"></canvas>\n </div>\n </div>\n\n <div class=\"ax-cost__card ax-cost__mix\">\n <div class=\"ax-cost__card-head\">\n <div>\n <div class=\"ax-cost__card-title\">Token mix</div>\n <div class=\"ax-cost__card-sub\">Volume by category · range</div>\n </div>\n </div>\n <div id=\"cost-mix\"></div>\n <div class=\"ax-cost__saved\">\n <div class=\"l\">Estimated savings from cache</div>\n <div class=\"v ax-mono\" id=\"cost-saved\">$0</div>\n </div>\n </div>\n </section>\n\n <section class=\"ax-cost__row2\">\n <div class=\"ax-cost__card\">\n <div class=\"ax-cost__card-head\">\n <div>\n <div class=\"ax-cost__card-title\">Top agents by spend</div>\n <div class=\"ax-cost__card-sub\">Ranked across the selected range</div>\n </div>\n </div>\n <div class=\"ax-cost__agents\" id=\"cost-agents\"></div>\n </div>\n\n <div class=\"ax-cost__card\">\n <div class=\"ax-cost__card-head\">\n <div>\n <div class=\"ax-cost__card-title\">Cache efficiency</div>\n <div class=\"ax-cost__card-sub\">Hit rate and avoided input cost</div>\n </div>\n <span class=\"ax-cost__health\" id=\"cost-health\">—</span>\n </div>\n <div class=\"ax-cost__gauge\">\n <div class=\"ax-cost__dial\">\n <svg width=\"140\" height=\"140\" viewBox=\"0 0 140 140\">\n <circle cx=\"70\" cy=\"70\" r=\"58\" stroke=\"var(--ax-surface-2)\" stroke-width=\"12\" fill=\"none\"/>\n <circle id=\"cost-arc\" cx=\"70\" cy=\"70\" r=\"58\" stroke=\"var(--ax-accent)\" stroke-width=\"12\" fill=\"none\" stroke-linecap=\"round\" stroke-dasharray=\"364\" stroke-dashoffset=\"364\" transform=\"rotate(-90 70 70)\"/>\n </svg>\n <div class=\"ax-cost__readout\"><div class=\"num ax-mono\" id=\"cost-hit\">0%</div><div class=\"lbl\">Hit rate</div></div>\n </div>\n <div class=\"ax-cost__cstats\">\n <div><div class=\"lbl\">Cache reads</div><div class=\"val ax-mono\" id=\"cost-cr\">0 <span>tok</span></div></div>\n <div><div class=\"lbl\">Cache writes</div><div class=\"val ax-mono\" id=\"cost-cw\">0 <span>tok</span></div></div>\n <div><div class=\"lbl\">Reuse multiplier</div><div class=\"val ax-mono\" id=\"cost-mult\">0×</div></div>\n <div><div class=\"lbl\">Avoided input</div><div class=\"val ax-mono\" id=\"cost-avoided\">0</div></div>\n </div>\n </div>\n </div>\n </section>\n\n <section class=\"ax-cost__card ax-cost__table-card\">\n <div class=\"ax-cost__table-head\">\n <div>\n <div class=\"ax-cost__card-title\">Daily detail</div>\n <div class=\"ax-cost__card-sub\">Per-day breakdown with agent attribution</div>\n </div>\n <div class=\"ax-cost__chips\" id=\"cost-filter\">\n <button class=\"active\" data-f=\"all\">All days</button>\n <button data-f=\"weekdays\">Weekdays</button>\n <button data-f=\"over10\">&gt; $10</button>\n </div>\n </div>\n <div class=\"ax-cost__table-scroll\">\n <table class=\"ax-cost__table\">\n <thead>\n <tr>\n <th>Date</th><th class=\"num\">Tasks</th><th>Mix</th>\n <th class=\"num\">Input</th><th class=\"num\">Output</th>\n <th class=\"num\">Cache R</th><th class=\"num\">Cache W</th>\n <th class=\"num\">Cost</th><th>Top agents</th>\n </tr>\n </thead>\n <tbody id=\"cost-tbody\"></tbody>\n </table>\n </div>\n </section>\n</div>`\n\n const css = `\n .ax-cost{padding:24px 28px;max-width:1440px;margin:0 auto}\n .ax-cost__head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;flex-wrap:wrap;margin-bottom:20px}\n .ax-cost__head h1{font-family:var(--ax-font);font-weight:600;font-size:22px;letter-spacing:-0.015em;margin:0 0 6px}\n .ax-cost__sub{color:var(--ax-muted);font-size:12px;margin:0}\n .ax-cost__live{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--ax-accent);margin-right:6px;vertical-align:middle;animation:ax-cost-pulse 2s ease-in-out infinite}\n @keyframes ax-cost-pulse{0%,100%{opacity:1}50%{opacity:0.4}}\n .ax-cost__controls{display:flex;gap:8px;align-items:center}\n .ax-seg{display:inline-flex;padding:2px;background:var(--ax-surface);border:1px solid var(--ax-border);border-radius:8px}\n .ax-seg button{background:transparent;border:0;color:var(--ax-muted);padding:5px 10px;font-size:12px;font-weight:500;border-radius:6px;cursor:pointer;font-family:var(--ax-font);transition:color .12s,background .12s}\n .ax-seg button:hover{color:var(--ax-text)}\n .ax-seg button.active{background:var(--ax-surface-2);color:var(--ax-text)}\n .ax-cost__export{background:var(--ax-surface);color:var(--ax-text-2);border:1px solid var(--ax-border);padding:5px 12px;font-size:12px;font-weight:500;border-radius:8px;text-decoration:none}\n .ax-cost__export:hover{border-color:var(--ax-border-2);color:var(--ax-text);text-decoration:none}\n\n .ax-cost__card{background:var(--ax-surface);border:1px solid var(--ax-border);border-radius:var(--ax-radius-lg);display:flex;flex-direction:column;min-width:0}\n .ax-cost__card-head{padding:14px 18px 10px;display:flex;align-items:center;justify-content:space-between;gap:12px}\n .ax-cost__card-title{font-size:13px;font-weight:600;letter-spacing:-0.005em}\n .ax-cost__card-sub{font-size:12px;color:var(--ax-muted);margin-top:2px}\n .ax-cost__legend{display:flex;gap:12px;flex-wrap:wrap}\n .ax-cost__legend span{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--ax-text-2);font-weight:500}\n .ax-cost__legend .sw{width:8px;height:8px;border-radius:2px;display:inline-block}\n .sw-input{background:var(--ax-info)}\n .sw-output{background:var(--ax-accent)}\n .sw-cr{background:oklch(0.72 0.10 290)}\n .sw-cw{background:var(--ax-warn)}\n\n .ax-cost__kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:0;background:var(--ax-surface);border:1px solid var(--ax-border);border-radius:var(--ax-radius-lg);overflow:hidden;margin-bottom:16px}\n .ax-cost__kpi{padding:16px 18px;border-right:1px solid var(--ax-border);display:flex;flex-direction:column;gap:6px;min-width:0}\n .ax-cost__kpi:last-child{border-right:0}\n .ax-cost__kpi-label{font-size:10.5px;color:var(--ax-muted);text-transform:uppercase;letter-spacing:0.06em;font-weight:600}\n .ax-cost__kpi-value{font-family:var(--ax-mono);font-size:22px;font-weight:600;letter-spacing:-0.02em;line-height:1.1;display:flex;align-items:baseline;gap:4px}\n .ax-cost__kpi-value .unit{font-size:12px;color:var(--ax-muted);font-weight:500;font-family:var(--ax-font)}\n .ax-cost__kpi--accent .ax-cost__kpi-value{color:var(--ax-accent)}\n .ax-cost__kpi-delta{font-size:11px;color:var(--ax-muted);font-weight:500}\n .ax-cost__kpi-delta--up{color:var(--ax-err)}\n .ax-cost__kpi-delta--down{color:var(--ax-accent)}\n .ax-cost__kpi-spark{height:22px;margin-top:auto;display:flex;align-items:flex-end;gap:1.5px}\n .ax-cost__kpi-spark .bar{flex:1;background:var(--ax-muted);border-radius:1px;min-height:2px;opacity:0.5}\n .ax-cost__kpi--accent .ax-cost__kpi-spark .bar{background:var(--ax-accent);opacity:0.6}\n .ax-cost__kpi-spark .bar:last-child{opacity:1;background:var(--ax-accent)}\n @media (max-width:1100px){.ax-cost__kpis{grid-template-columns:repeat(3,1fr)}.ax-cost__kpi{border-bottom:1px solid var(--ax-border)}.ax-cost__kpi:nth-child(3),.ax-cost__kpi:nth-last-child(-n+3){border-right:0}.ax-cost__kpi:nth-last-child(-n+3){border-bottom:0}}\n @media (max-width:680px){.ax-cost__kpis{grid-template-columns:repeat(2,1fr)}.ax-cost__kpi{border-right:1px solid var(--ax-border);border-bottom:1px solid var(--ax-border)}.ax-cost__kpi:nth-child(2n){border-right:0}}\n\n .ax-cost__hero{display:grid;grid-template-columns:1fr 320px;gap:16px;margin-bottom:16px}\n @media (max-width:1200px){.ax-cost__hero{grid-template-columns:1fr}}\n .ax-cost__hero-wrap{position:relative;padding:4px 18px 16px}\n .ax-cost__hero-canvas{width:100%;height:300px;display:block}\n .ax-cost__hero-overlay{position:absolute;top:8px;left:18px;display:flex;gap:22px;pointer-events:none}\n .ax-cost__hero-overlay .v{font-size:20px;font-weight:600;letter-spacing:-0.02em}\n .ax-cost__hero-overlay .l{font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--ax-muted);font-weight:600;margin-top:2px}\n\n .ax-cost__mix{padding:14px 18px 16px}\n .ax-cost__mix-row{display:grid;grid-template-columns:84px 1fr 92px;align-items:center;gap:10px;padding:9px 0;border-bottom:1px dashed var(--ax-border)}\n .ax-cost__mix-row:last-of-type{border-bottom:0}\n .ax-cost__mix-label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--ax-text-2);font-weight:500}\n .ax-cost__mix-dot{width:8px;height:8px;border-radius:2px}\n .ax-cost__mix-bar{position:relative;height:6px;background:var(--ax-surface-2);border-radius:3px;overflow:hidden}\n .ax-cost__mix-bar-fill{height:100%;border-radius:3px;transition:width .6s cubic-bezier(0.22,1,0.36,1)}\n .ax-cost__mix-val{font-family:var(--ax-mono);font-size:11px;color:var(--ax-text-2);text-align:right;font-weight:500}\n .ax-cost__saved{margin-top:14px;padding:10px 12px;background:color-mix(in oklch,var(--ax-accent) 14%,transparent);border:1px solid color-mix(in oklch,var(--ax-accent) 35%,transparent);border-radius:8px;display:flex;align-items:center;justify-content:space-between}\n .ax-cost__saved .l{font-size:11px;color:var(--ax-text-2)}\n .ax-cost__saved .v{font-size:14px;font-weight:600;color:var(--ax-accent)}\n\n .ax-cost__row2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}\n @media (max-width:1100px){.ax-cost__row2{grid-template-columns:1fr}}\n\n .ax-cost__agents{display:flex;flex-direction:column}\n .ax-cost__agent{display:grid;grid-template-columns:22px 1fr 60px 90px 90px;align-items:center;gap:12px;padding:10px 18px;border-top:1px solid var(--ax-border)}\n .ax-cost__agent:hover{background:var(--ax-bg-elev)}\n .ax-cost__agent-rank{font-family:var(--ax-mono);font-size:11px;color:var(--ax-muted);font-weight:500}\n .ax-cost__agent-namewrap{display:flex;align-items:center;gap:10px;min-width:0}\n .ax-cost__agent-ico{width:24px;height:24px;border-radius:6px;display:grid;place-items:center;font-size:11px;font-weight:700;flex-shrink:0}\n .ax-cost__agent-name{font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n .ax-cost__agent-model{font-size:10.5px;color:var(--ax-muted);margin-top:1px}\n .ax-cost__agent-tasks{font-family:var(--ax-mono);font-size:11.5px;color:var(--ax-text-2);text-align:right}\n .ax-cost__agent-bar{position:relative;height:5px;background:var(--ax-surface-2);border-radius:2px;overflow:hidden}\n .ax-cost__agent-bar-fill{height:100%;border-radius:2px;transition:width .7s cubic-bezier(0.22,1,0.36,1)}\n .ax-cost__agent-cost{font-family:var(--ax-mono);font-size:12.5px;font-weight:600;text-align:right;color:var(--ax-text)}\n .ax-cost__agent-cost .sub{display:block;font-size:10px;color:var(--ax-muted);font-weight:400;margin-top:1px}\n .ax-cost__health{display:inline-flex;align-items:center;gap:6px;padding:3px 8px 3px 6px;border:1px solid var(--ax-border);border-radius:999px;font-size:11px;color:var(--ax-accent);font-weight:600;background:var(--ax-surface)}\n\n .ax-cost__gauge{display:grid;grid-template-columns:140px 1fr;gap:24px;align-items:center;padding:14px 18px 18px}\n .ax-cost__dial{position:relative;width:140px;height:140px}\n .ax-cost__readout{position:absolute;inset:0;display:grid;place-items:center;text-align:center}\n .ax-cost__readout .num{font-size:24px;font-weight:600;letter-spacing:-0.02em}\n .ax-cost__readout .lbl{font-size:10px;color:var(--ax-muted);letter-spacing:0.06em;text-transform:uppercase;margin-top:2px}\n .ax-cost__cstats{display:flex;flex-direction:column;gap:10px}\n .ax-cost__cstats > div{display:flex;justify-content:space-between;align-items:baseline;border-bottom:1px dashed var(--ax-border);padding-bottom:8px}\n .ax-cost__cstats > div:last-child{border-bottom:0}\n .ax-cost__cstats .lbl{font-size:11px;color:var(--ax-muted);text-transform:uppercase;letter-spacing:0.05em}\n .ax-cost__cstats .val{font-size:14px;font-weight:600}\n .ax-cost__cstats .val span{font-size:10px;color:var(--ax-muted);margin-left:4px;font-weight:400}\n\n .ax-cost__table-card{padding-bottom:8px}\n .ax-cost__table-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px 12px;flex-wrap:wrap;gap:8px}\n .ax-cost__chips{display:flex;gap:6px}\n .ax-cost__chips button{background:transparent;border:1px solid var(--ax-border);color:var(--ax-muted);font-size:11px;padding:3px 10px;border-radius:999px;cursor:pointer;font-family:var(--ax-font);transition:color .12s,background .12s,border-color .12s}\n .ax-cost__chips button:hover,.ax-cost__chips button.active{color:var(--ax-text);background:var(--ax-surface-2);border-color:var(--ax-border-2)}\n .ax-cost__table-scroll{overflow-x:auto}\n .ax-cost__table{width:100%;border-collapse:collapse;font-size:12.5px}\n .ax-cost__table thead th{text-align:left;padding:8px 18px;background:var(--ax-bg-elev);border-top:1px solid var(--ax-border);border-bottom:1px solid var(--ax-border);font-size:10.5px;text-transform:uppercase;letter-spacing:0.06em;color:var(--ax-muted);font-weight:500;white-space:nowrap}\n .ax-cost__table thead th.num,.ax-cost__table tbody td.num{text-align:right;font-family:var(--ax-mono)}\n .ax-cost__table tbody td{padding:10px 18px;border-bottom:1px solid var(--ax-border);color:var(--ax-text-2);vertical-align:middle;white-space:nowrap}\n .ax-cost__table tbody tr:hover td{background:var(--ax-bg-elev);color:var(--ax-text)}\n .ax-cost__table tbody tr:last-child td{border-bottom:0}\n .ax-cost__table .row-date{color:var(--ax-text);font-weight:500}\n .ax-cost__table .row-date .day{color:var(--ax-muted);font-weight:400;margin-left:6px;font-size:11px}\n .ax-cost__table .row-cost{color:var(--ax-accent);font-weight:600}\n .ax-cost__minibar{display:inline-flex;gap:1px;height:12px;width:80px;align-items:flex-end}\n .ax-cost__minibar span{flex:1;border-radius:1px;min-height:1px;display:block;height:100%}\n .ax-cost__chips-row{display:inline-flex;gap:4px;flex-wrap:wrap}\n .ax-cost__agent-chip{display:inline-flex;align-items:center;gap:5px;padding:2px 7px 2px 5px;background:var(--ax-surface-2);border:1px solid var(--ax-border);border-radius:999px;font-size:10.5px;color:var(--ax-text-2)}\n .ax-cost__agent-chip i{width:6px;height:6px;border-radius:50%;display:inline-block}\n .ax-cost__empty{padding:32px 18px;text-align:center;color:var(--ax-muted);font-size:12px;font-style:italic}\n `\n\n const script = `\n (function(){\n var state = { data: null, range: 30, filter: 'all' };\n\n function fmtTok(n){ if(!n)return '0'; if(n>=1e9)return (n/1e9).toFixed(2)+'B'; if(n>=1e6)return (n/1e6).toFixed(2)+'M'; if(n>=1e3)return (n/1e3).toFixed(1)+'K'; return String(Math.round(n)); }\n function fmtCost(n,d){ if(d==null)d=2; if(!isFinite(n))n=0; if(Math.abs(n)>=10000)return '$'+(n/1000).toFixed(1)+'K'; if(n===0)return '$0'; if(Math.abs(n)<0.01)return '$'+n.toFixed(4); return '$'+n.toFixed(d); }\n function fmtDate(iso){ var dt=new Date(iso+'T00:00:00'); return dt.toLocaleDateString(undefined,{month:'short',day:'numeric'}); }\n function fmtDay(iso){ var dt=new Date(iso+'T00:00:00'); return dt.toLocaleDateString(undefined,{weekday:'short'}); }\n function isWeekday(iso){ var dt=new Date(iso+'T00:00:00'); var d=dt.getDay(); return d!==0 && d!==6; }\n function esc(s){ return String(s||'').replace(/[&<>\"']/g,function(c){ return ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]); }); }\n function getCss(name){ return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }\n\n var COL_INPUT = function(){ return getCss('--ax-info') || '#7aa6e0'; };\n var COL_OUTPUT = function(){ return getCss('--ax-accent') || '#6ec4a7'; };\n var COL_CR = function(){ return 'oklch(0.72 0.10 290)'; };\n var COL_CW = function(){ return getCss('--ax-warn') || '#d4a437'; };\n\n /* deterministic per-agent color — hashed hue, OKLCH-stable across themes. */\n var agentColorCache = {};\n function agentColor(id){\n if (agentColorCache[id]) return agentColorCache[id];\n var h = 0;\n for (var i=0;i<id.length;i++) h = ((h<<5)-h+id.charCodeAt(i))|0;\n var hue = Math.abs(h) % 360;\n var c = 'oklch(0.74 0.13 ' + hue + ')';\n agentColorCache[id] = c;\n return c;\n }\n\n /* Range selector */\n document.querySelectorAll('#cost-range button').forEach(function(b){\n b.addEventListener('click', function(){\n document.querySelectorAll('#cost-range button').forEach(function(x){ x.classList.remove('active'); });\n b.classList.add('active');\n var r = b.getAttribute('data-r');\n state.range = r;\n load();\n });\n });\n /* Filter chips */\n document.querySelectorAll('#cost-filter button').forEach(function(c){\n c.addEventListener('click', function(){\n document.querySelectorAll('#cost-filter button').forEach(function(x){ x.classList.remove('active'); });\n c.classList.add('active');\n state.filter = c.getAttribute('data-f');\n renderTable();\n });\n });\n /* Export CSV */\n document.getElementById('cost-export').addEventListener('click', function(e){\n e.preventDefault();\n window.location.href = '/api/admin/observability/cost.csv?range=' + encodeURIComponent(state.range);\n });\n\n async function load(){\n try {\n var r = await fetch('/api/admin/observability/cost?range=' + encodeURIComponent(state.range), { credentials: 'same-origin' });\n state.data = await r.json();\n render();\n } catch (e) { console.error('cost fetch failed', e); }\n }\n\n function render(){\n if (!state.data) return;\n renderKPIs();\n renderHero();\n renderMix();\n renderAgents();\n renderCacheGauge();\n renderTable();\n var li = document.getElementById('cost-last');\n var days = state.data.days || [];\n li.textContent = days.length ? days[days.length-1].date : '—';\n }\n\n function kpi(opts){\n var sparks = opts.sparks || [];\n var max = 0; for (var i=0;i<sparks.length;i++){ if (sparks[i] > max) max = sparks[i]; }\n var bars = sparks.map(function(v){ var h = max>0 ? Math.max(2,(v/max)*100) : 2; return '<div class=\"bar\" style=\"height:'+h+'%\"></div>'; }).join('');\n var deltaHtml = '';\n if (opts.delta != null && isFinite(opts.delta) && opts.delta !== 0) {\n var up = opts.delta > 0;\n var cls = up ? 'ax-cost__kpi-delta--up' : 'ax-cost__kpi-delta--down';\n var arrow = up ? '\\\\u25B2' : '\\\\u25BC';\n deltaHtml = '<div class=\"ax-cost__kpi-delta '+cls+'\">'+arrow+' '+(Math.abs(opts.delta)*100).toFixed(1)+'% vs prev 7d</div>';\n } else if (opts.sub) {\n deltaHtml = '<div class=\"ax-cost__kpi-delta\">'+esc(opts.sub)+'</div>';\n }\n var unit = opts.unit ? '<span class=\"unit\">'+esc(opts.unit)+'</span>' : '';\n return '<div class=\"ax-cost__kpi '+(opts.accent?'ax-cost__kpi--accent':'')+'\">'\n + '<div class=\"ax-cost__kpi-label\">'+esc(opts.label)+'</div>'\n + '<div class=\"ax-cost__kpi-value\">'+opts.value+unit+'</div>'\n + deltaHtml\n + '<div class=\"ax-cost__kpi-spark\">'+bars+'</div>'\n + '</div>';\n }\n\n function renderKPIs(){\n var d = state.data; var days = d.days || []; var k = d.kpis || {};\n var sparkCost = days.map(function(x){ return x.cost; });\n var sparkTasks = days.map(function(x){ return x.tasks; });\n var sparkTok = days.map(function(x){ return x.input + x.output + x.cacheRead + x.cacheCreate; });\n var sparkHit = days.map(function(x){ var d2 = x.cacheRead + x.input; return d2>0 ? (x.cacheRead/d2)*100 : 0; });\n var sparkPerTask= days.map(function(x){ return x.tasks>0 ? x.cost/x.tasks : 0; });\n document.getElementById('cost-kpis').innerHTML = [\n kpi({ label:'Total spend', value: fmtCost(k.spendRange||0), sparks: sparkCost, delta: k.spendDeltaWoW, accent: true }),\n kpi({ label:'Daily average', value: fmtCost(k.avgDailyRange||0), sparks: sparkCost, sub: '/ day' }),\n kpi({ label:'Tasks run', value: (k.tasksRange||0).toLocaleString(), sparks: sparkTasks, delta: k.tasksDeltaWoW }),\n kpi({ label:'Tokens processed',value: fmtTok(k.tokensRange||0), sparks: sparkTok, sub: 'tok' }),\n kpi({ label:'Cache hit rate', value: ((k.cacheHitRange||0)*100).toFixed(0)+'%', sparks: sparkHit }),\n kpi({ label:'Cost / task', value: fmtCost(k.costPerTaskRange||0,3), sparks: sparkPerTask }),\n ].join('');\n }\n\n /* Hero stacked-bar chart on canvas. Each bar's segment heights are\n derived from cost shares per category (cost × tokens_in_cat / total_tokens). */\n function setupCanvas(id){\n var canvas = document.getElementById(id);\n var dpr = window.devicePixelRatio || 1;\n var rect = canvas.parentElement.getBoundingClientRect();\n var w = rect.width;\n var h = canvas.clientHeight || 300;\n canvas.width = Math.max(1, Math.round(w * dpr));\n canvas.height = Math.max(1, Math.round(h * dpr));\n canvas.style.width = w + 'px';\n canvas.style.height = h + 'px';\n var ctx = canvas.getContext('2d');\n ctx.scale(dpr, dpr);\n ctx.clearRect(0, 0, w, h);\n return { ctx: ctx, w: w, h: h };\n }\n function roundRect(ctx, x, y, w, h, r){\n if (w <= 0 || h <= 0) return;\n r = Math.min(r, w/2, h/2);\n ctx.beginPath();\n ctx.moveTo(x+r, y); ctx.lineTo(x+w-r, y);\n ctx.quadraticCurveTo(x+w, y, x+w, y+r);\n ctx.lineTo(x+w, y+h); ctx.lineTo(x, y+h);\n ctx.lineTo(x, y+r);\n ctx.quadraticCurveTo(x, y, x+r, y);\n ctx.closePath();\n }\n function renderHero(){\n var c = setupCanvas('cost-hero'); var ctx = c.ctx, w = c.w, h = c.h;\n var data = state.data.days || [];\n var totalEl = document.getElementById('cost-hero-total');\n var peakEl = document.getElementById('cost-hero-peak');\n var avgEl = document.getElementById('cost-hero-avg');\n if (!data.length){\n ctx.fillStyle = getCss('--ax-muted'); ctx.font = '12px '+getCss('--ax-font'); ctx.textAlign = 'center';\n ctx.fillText('No usage data in this range', w/2, h/2);\n totalEl.textContent = '$0'; peakEl.textContent = '$0'; avgEl.textContent = '$0';\n return;\n }\n var pad = { t: 70, r: 16, b: 28, l: 48 };\n var cw = w - pad.l - pad.r;\n var ch = h - pad.t - pad.b;\n\n var series = data.map(function(d){\n var tot = d.input + d.output + d.cacheRead + d.cacheCreate;\n if (!tot) return { input:0, output:0, cacheR:0, cacheW:0, total: d.cost };\n return {\n input: d.cost * (d.input / tot),\n output: d.cost * (d.output / tot),\n cacheR: d.cost * (d.cacheRead / tot),\n cacheW: d.cost * (d.cacheCreate / tot),\n total: d.cost,\n };\n });\n var maxTot = Math.max.apply(null, series.map(function(s){return s.total;}).concat([0.001]));\n\n ctx.font = '10.5px '+getCss('--ax-mono');\n ctx.textAlign = 'right'; ctx.textBaseline = 'middle';\n for (var i = 0; i <= 4; i++){\n var v = (maxTot / 4) * i;\n var y = pad.t + ch - (i / 4) * ch;\n ctx.strokeStyle = getCss('--ax-border');\n ctx.lineWidth = 1;\n ctx.beginPath(); ctx.moveTo(pad.l, y + 0.5); ctx.lineTo(pad.l + cw, y + 0.5); ctx.stroke();\n ctx.fillStyle = getCss('--ax-muted');\n ctx.fillText(fmtCost(v, 0), pad.l - 8, y);\n }\n ctx.fillStyle = getCss('--ax-muted');\n ctx.textAlign = 'center'; ctx.textBaseline = 'top';\n ctx.font = '10.5px '+getCss('--ax-font');\n var nLabels = Math.min(7, data.length);\n for (var j = 0; j < nLabels; j++){\n var idx = Math.round((j / Math.max(1, nLabels - 1)) * (data.length - 1));\n var x = pad.l + (idx / Math.max(1, data.length - 1)) * cw;\n ctx.fillText(fmtDate(data[idx].date), x, pad.t + ch + 10);\n }\n var cats = [\n { k:'input', c: COL_INPUT() },\n { k:'output', c: COL_OUTPUT() },\n { k:'cacheR', c: COL_CR() },\n { k:'cacheW', c: COL_CW() },\n ];\n var barW = Math.max(2, (cw / data.length) * 0.7);\n var gap = cw / data.length;\n data.forEach(function(d, i){\n var s = series[i];\n var x = pad.l + i * gap + (gap - barW) / 2;\n var yOff = 0;\n cats.forEach(function(cat, ci){\n var v = s[cat.k];\n var barH = (v / maxTot) * ch;\n var y = pad.t + ch - yOff - barH;\n ctx.fillStyle = cat.c;\n var radius = ci === cats.length - 1 ? 2 : 0;\n roundRect(ctx, x, y, barW, barH + 0.5, radius);\n ctx.fill();\n yOff += barH;\n });\n });\n\n var total = series.reduce(function(s, p){ return s + p.total; }, 0);\n var peak = Math.max.apply(null, series.map(function(p){ return p.total; }));\n var avg = total / Math.max(1, series.length);\n totalEl.textContent = fmtCost(total);\n peakEl.textContent = fmtCost(peak);\n avgEl.textContent = fmtCost(avg);\n }\n\n function renderMix(){\n var data = state.data.days || [];\n var tot = { input:0, output:0, cacheRead:0, cacheCreate:0 };\n data.forEach(function(d){ tot.input+=d.input; tot.output+=d.output; tot.cacheRead+=d.cacheRead; tot.cacheCreate+=d.cacheCreate; });\n var grand = tot.input + tot.output + tot.cacheRead + tot.cacheCreate;\n var items = [\n { lbl:'Input', v: tot.input, c: COL_INPUT() },\n { lbl:'Output', v: tot.output, c: COL_OUTPUT() },\n { lbl:'Cache read', v: tot.cacheRead, c: COL_CR() },\n { lbl:'Cache write', v: tot.cacheCreate, c: COL_CW() },\n ];\n var max = Math.max.apply(null, items.map(function(i){ return i.v; }).concat([1]));\n document.getElementById('cost-mix').innerHTML = items.map(function(it){\n var pct = grand > 0 ? (it.v / grand * 100) : 0;\n var barPct = (it.v / max) * 100;\n return '<div class=\"ax-cost__mix-row\">'\n + '<div class=\"ax-cost__mix-label\"><span class=\"ax-cost__mix-dot\" style=\"background:'+it.c+'\"></span>'+esc(it.lbl)+'</div>'\n + '<div class=\"ax-cost__mix-bar\"><div class=\"ax-cost__mix-bar-fill\" style=\"width:'+barPct+'%;background:'+it.c+'\"></div></div>'\n + '<div class=\"ax-cost__mix-val\">'+fmtTok(it.v)+' · '+pct.toFixed(0)+'%</div>'\n + '</div>';\n }).join('');\n // Rough cache savings: cache-read tokens priced at ~10% of input rate (sonnet).\n var savedCost = (tot.cacheRead * 0.9) / 1e6 * 3;\n document.getElementById('cost-saved').textContent = fmtCost(savedCost);\n }\n\n function renderAgents(){\n var rows = (state.data.perAgent || []).slice(0, 12);\n var max = rows[0] ? rows[0].cost : 1;\n var container = document.getElementById('cost-agents');\n if (!rows.length){ container.innerHTML = '<div class=\"ax-cost__empty\">No agent activity in this range.</div>'; return; }\n container.innerHTML = rows.map(function(a, i){\n var c = agentColor(a.agent_id);\n var initials = a.agent_id.split('-').map(function(s){ return s[0]||''; }).join('').slice(0,2).toUpperCase();\n var iconBg = 'color-mix(in oklch, ' + c + ' 22%, var(--ax-surface-2))';\n var barPct = (a.cost / max) * 100;\n return '<div class=\"ax-cost__agent\">'\n + '<div class=\"ax-cost__agent-rank\">'+String(i+1).padStart(2,'0')+'</div>'\n + '<div class=\"ax-cost__agent-namewrap\">'\n + '<div class=\"ax-cost__agent-ico\" style=\"background:'+iconBg+';color:'+c+'\">'+esc(initials)+'</div>'\n + '<div style=\"min-width:0\">'\n + '<div class=\"ax-cost__agent-name\">'+esc(a.agent_id)+'</div>'\n + (a.model ? '<div class=\"ax-cost__agent-model\">'+esc(a.model)+'</div>' : '')\n + '</div>'\n + '</div>'\n + '<div class=\"ax-cost__agent-tasks\">'+(a.tasks||0).toLocaleString()+'</div>'\n + '<div class=\"ax-cost__agent-bar\"><div class=\"ax-cost__agent-bar-fill\" style=\"width:'+barPct+'%;background:'+c+'\"></div></div>'\n + '<div class=\"ax-cost__agent-cost\">'+fmtCost(a.cost)+'<span class=\"sub\">'+fmtTok(a.tokens||0)+' tok</span></div>'\n + '</div>';\n }).join('');\n }\n\n function renderCacheGauge(){\n var data = state.data.days || [];\n var tot = data.reduce(function(s,d){ return { r: s.r + d.cacheRead, w: s.w + d.cacheCreate, i: s.i + d.input }; }, { r:0, w:0, i:0 });\n var hit = (tot.r + tot.i) > 0 ? (tot.r / (tot.r + tot.i)) * 100 : 0;\n var circ = 2 * Math.PI * 58;\n var offset = circ - (hit / 100) * circ;\n var arc = document.getElementById('cost-arc');\n arc.style.strokeDasharray = circ.toFixed(1);\n arc.style.strokeDashoffset = circ.toFixed(1);\n requestAnimationFrame(function(){\n arc.style.transition = 'stroke-dashoffset 1s cubic-bezier(0.22,1,0.36,1)';\n arc.style.strokeDashoffset = offset.toFixed(1);\n });\n document.getElementById('cost-hit').textContent = hit.toFixed(0) + '%';\n document.getElementById('cost-cr').innerHTML = fmtTok(tot.r) + ' <span>tok</span>';\n document.getElementById('cost-cw').innerHTML = fmtTok(tot.w) + ' <span>tok</span>';\n var mult = tot.w > 0 ? (tot.r / tot.w) : 0;\n document.getElementById('cost-mult').textContent = mult.toFixed(1) + '\\\\u00D7';\n document.getElementById('cost-avoided').textContent = fmtTok(tot.r);\n var label = hit >= 60 ? 'Healthy' : hit >= 30 ? 'Warming up' : 'Cold';\n document.getElementById('cost-health').textContent = label;\n }\n\n function renderTable(){\n var tbody = document.getElementById('cost-tbody');\n var data = state.data ? state.data.days || [] : [];\n var rows = data.slice().reverse();\n if (state.filter === 'weekdays') rows = rows.filter(function(r){ return isWeekday(r.date); });\n else if (state.filter === 'over10') rows = rows.filter(function(r){ return r.cost > 10; });\n if (!rows.length){ tbody.innerHTML = '<tr><td colspan=\"9\" class=\"ax-cost__empty\">No rows match this filter.</td></tr>'; return; }\n tbody.innerHTML = rows.map(function(d){\n var tot = d.input + d.output + d.cacheRead + d.cacheCreate || 1;\n var mix = [\n { v: d.input, c: COL_INPUT() },\n { v: d.output, c: COL_OUTPUT() },\n { v: d.cacheRead, c: COL_CR() },\n { v: d.cacheCreate, c: COL_CW() },\n ];\n var mixHtml = mix.map(function(m){ return '<span style=\"flex:'+(m.v/tot)+';background:'+m.c+'\"></span>'; }).join('');\n var topAgents = Object.keys(d.agents || {}).map(function(id){ return [id, d.agents[id]]; }).sort(function(a,b){ return b[1].cost - a[1].cost; }).slice(0, 3);\n var chips = topAgents.map(function(pair){\n return '<span class=\"ax-cost__agent-chip\"><i style=\"background:'+agentColor(pair[0])+'\"></i>'+esc(pair[0])+'</span>';\n }).join('');\n return '<tr>'\n + '<td class=\"row-date\">'+fmtDate(d.date)+'<span class=\"day\">'+fmtDay(d.date)+'</span></td>'\n + '<td class=\"num\">'+(d.tasks||0).toLocaleString()+'</td>'\n + '<td><div class=\"ax-cost__minibar\">'+mixHtml+'</div></td>'\n + '<td class=\"num\">'+fmtTok(d.input)+'</td>'\n + '<td class=\"num\">'+fmtTok(d.output)+'</td>'\n + '<td class=\"num\">'+fmtTok(d.cacheRead)+'</td>'\n + '<td class=\"num\">'+fmtTok(d.cacheCreate)+'</td>'\n + '<td class=\"num row-cost\">'+fmtCost(d.cost)+'</td>'\n + '<td><div class=\"ax-cost__chips-row\">'+chips+'</div></td>'\n + '</tr>';\n }).join('');\n }\n\n window.addEventListener('resize', function(){ if (state.data) renderHero(); });\n load();\n setInterval(load, 30000);\n })();\n `\n\n return renderShell({\n title: \"AgentX · Cost\",\n activeTab: \"cost\",\n subtitle: \"Cost\",\n body,\n css,\n scripts: `<script>${script}</script>`,\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n })\n}\n","// --- Projects page ---\n//\n// Read-only aggregated view: one row per project (rule file or\n// workflow-tagged or channel-route), expandable to show its bound\n// agents, workflows, channels, and contacts.\n//\n// Data source: GET /api/admin/projects (daemon, aggregator in\n// src/daemon/projects-api.ts). Follows the topbar mesh selector via\n// the existing /api/admin/* proxy contract.\n//\n// Scope is intentionally tight for v1:\n// - list view + detail panel\n// - links cross-navigate to /workflows, /admin/agents/<id>, etc.\n// - no editing controls yet — those land once read view is shaped\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\n\nexport interface ProjectsPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderProjectsPage(opts: ProjectsPageOpts = {}): string {\n const body = `<div class=\"ax-pj__root\">\n <aside class=\"ax-pj__list\">\n <header>\n <h2>Projects</h2>\n <div style=\"display:flex;gap:4px;align-items:center;\">\n <button id=\"pj-new\" class=\"ax-pj__icon\" title=\"New project\">+</button>\n <button id=\"pj-refresh\" class=\"ax-pj__icon\" title=\"Refresh (r)\">↻</button>\n </div>\n </header>\n <div class=\"ax-pj__search\">\n <input id=\"pj-filter\" type=\"search\" placeholder=\"Filter by project key…\" autocomplete=\"off\" />\n </div>\n <ul id=\"pj-list\" class=\"ax-pj__cards\" aria-live=\"polite\"></ul>\n <div id=\"pj-empty\" class=\"ax-pj__empty\" hidden>\n <p>No projects yet.</p>\n <p class=\"hint\">Add a rule under <code>.agentx/projects/&lt;org&gt;/&lt;project&gt;.yaml</code>\n OR set <code>project: org/repo</code> on a workflow to make it visible here.</p>\n </div>\n <div id=\"pj-warn\" class=\"ax-pj__warn\" hidden></div>\n </aside>\n\n <section class=\"ax-pj__detail\">\n <header id=\"pj-detail-head\">\n <span class=\"hint\">Select a project to inspect.</span>\n </header>\n <div id=\"pj-detail-body\" class=\"ax-pj__detail-body\"></div>\n </section>\n</div>`\n\n return renderShell({\n title: \"AgentX · Projects\",\n activeTab: \"projects\",\n subtitle: \"Projects\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: PROJECTS_PAGE_CSS,\n scripts: `<script>${PROJECTS_PAGE_SCRIPT}</script>`,\n })\n}\n\nconst PROJECTS_PAGE_CSS = `\n.ax-pj__root {\n display: grid;\n grid-template-columns: 360px 1fr;\n height: calc(100vh - var(--ax-topbar-h, 48px));\n background: var(--ax-bg);\n}\n.ax-pj__list {\n border-right: 1px solid var(--ax-border);\n display: flex; flex-direction: column;\n background: var(--ax-bg-elev);\n min-height: 0;\n}\n.ax-pj__list > header {\n display: flex; align-items: center; justify-content: space-between;\n padding: 14px 18px; border-bottom: 1px solid var(--ax-border);\n}\n.ax-pj__list > header h2 { margin: 0; font-size: 14px; }\n.ax-pj__icon {\n background: transparent; color: var(--ax-fg-muted, var(--ax-text));\n border: 1px solid transparent; border-radius: 4px;\n padding: 4px 8px; cursor: pointer; font-size: 14px;\n}\n.ax-pj__icon:hover { background: var(--ax-surface); }\n.ax-pj__search { padding: 10px 14px; border-bottom: 1px solid var(--ax-border); }\n.ax-pj__search input {\n width: 100%; font: inherit; font-size: 12px;\n padding: 6px 8px; border: 1px solid var(--ax-border);\n border-radius: 4px; background: var(--ax-bg); color: var(--ax-text);\n}\n.ax-pj__cards {\n list-style: none; margin: 0; padding: 0;\n flex: 1; overflow-y: auto;\n}\n.ax-pj__card {\n padding: 12px 18px; border-bottom: 1px solid var(--ax-border);\n cursor: pointer; transition: background 0.1s;\n}\n.ax-pj__card:hover { background: var(--ax-surface); }\n.ax-pj__card.is-active { background: var(--ax-surface); box-shadow: inset 3px 0 0 var(--ax-accent); }\n.ax-pj__card-title { font-weight: 500; font-size: 13px; margin-bottom: 4px; }\n.ax-pj__card-meta {\n display: flex; gap: 6px; flex-wrap: wrap;\n font-size: 11px; color: var(--ax-fg-muted, var(--ax-text));\n}\n.ax-pj__card-meta .stat { padding: 1px 6px; border-radius: 3px; background: var(--ax-bg); border: 1px solid var(--ax-border); }\n.ax-pj__card-meta .stat.zero { opacity: 0.4; }\n.ax-pj__detail {\n display: flex; flex-direction: column; min-height: 0;\n}\n.ax-pj__detail > header {\n padding: 18px 24px; border-bottom: 1px solid var(--ax-border);\n display: flex; align-items: center; justify-content: space-between;\n}\n.ax-pj__detail > header h2 { margin: 0; font-size: 18px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.ax-pj__kind {\n font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;\n padding: 2px 8px; border-radius: 3px; font-weight: 500;\n border: 1px solid transparent;\n}\n.ax-pj__kind--gitlab { color: #fc6d26; background: rgba(252, 109, 38, 0.1); border-color: rgba(252, 109, 38, 0.3); }\n.ax-pj__kind--github { color: #c9d1d9; background: rgba(110, 118, 129, 0.2); border-color: rgba(110, 118, 129, 0.4); }\n.ax-pj__kind--jira { color: #2684ff; background: rgba(38, 132, 255, 0.1); border-color: rgba(38, 132, 255, 0.3); }\n.ax-pj__kind--linear { color: #5e6ad2; background: rgba(94, 106, 210, 0.1); border-color: rgba(94, 106, 210, 0.3); }\n.ax-pj__kind--other { color: var(--ax-fg-muted, var(--ax-text)); background: var(--ax-bg); border-color: var(--ax-border); }\n.ax-pj__keyhint { font-size: 12px; font-weight: normal; color: var(--ax-fg-muted, var(--ax-text)); font-family: var(--ax-mono, monospace); }\n.ax-pj__home {\n font-size: 12px; text-decoration: none; padding: 2px 6px;\n color: var(--ax-fg-muted, var(--ax-text)); border: 1px solid var(--ax-border);\n border-radius: 3px;\n}\n.ax-pj__home:hover { color: var(--ax-accent); border-color: var(--ax-accent); }\n.ax-pj__section h3 {\n display: flex; align-items: center; gap: 8px;\n}\n.ax-pj__btn {\n font: inherit; font-size: 11px;\n padding: 3px 8px; border: 1px solid var(--ax-border);\n border-radius: 3px; background: var(--ax-bg); color: var(--ax-text);\n cursor: pointer; text-transform: none; letter-spacing: 0;\n margin-left: auto;\n}\n.ax-pj__btn:hover { border-color: var(--ax-accent); color: var(--ax-accent); }\n.ax-pj__unlink {\n margin-left: auto;\n background: transparent; border: 1px solid transparent;\n color: var(--ax-fg-muted, var(--ax-text));\n cursor: pointer; font-size: 14px; padding: 0 6px; border-radius: 3px;\n}\n.ax-pj__unlink:hover { color: var(--ax-err, #ef4444); border-color: var(--ax-err, #ef4444); }\n/* Workflow picker modal */\n.ax-pj__modal-bg {\n position: fixed; inset: 0; background: rgba(0,0,0,0.4);\n display: flex; align-items: center; justify-content: center; z-index: 100;\n}\n.ax-pj__modal {\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 6px; min-width: 480px; max-width: 720px;\n max-height: 80vh; display: flex; flex-direction: column;\n}\n.ax-pj__modal-head {\n padding: 14px 18px; border-bottom: 1px solid var(--ax-border);\n display: flex; align-items: center; justify-content: space-between;\n}\n.ax-pj__modal-head h3 { margin: 0; font-size: 14px; }\n.ax-pj__modal-body { padding: 14px 18px; overflow-y: auto; }\n.ax-pj__modal-search input {\n width: 100%; font: inherit; font-size: 13px;\n padding: 6px 10px; border: 1px solid var(--ax-border);\n border-radius: 4px; background: var(--ax-bg); color: var(--ax-text);\n margin-bottom: 12px;\n}\n.ax-pj__pick {\n display: flex; align-items: center; gap: 10px;\n padding: 8px 10px; border-radius: 4px; cursor: pointer;\n border: 1px solid transparent;\n}\n.ax-pj__pick:hover { background: var(--ax-bg); border-color: var(--ax-border); }\n.ax-pj__pick .pname { font-weight: 500; flex: 1; }\n.ax-pj__pick .pmeta { font-size: 11px; color: var(--ax-fg-muted, var(--ax-text)); }\n.ax-pj__pick .ptag {\n font-size: 11px; padding: 1px 6px; border-radius: 3px;\n background: var(--ax-warn, #facc15); color: #1a1a1a;\n}\n/* Form fields */\n.ax-pj__form { display: flex; flex-direction: column; gap: 12px; }\n.ax-pj__fld { display: flex; flex-direction: column; gap: 4px; }\n.ax-pj__fld-label { font-size: 12px; font-weight: 500; color: var(--ax-fg-muted, var(--ax-text)); }\n.ax-pj__fld input, .ax-pj__fld select {\n font: inherit; font-size: 13px;\n padding: 6px 10px; border: 1px solid var(--ax-border);\n border-radius: 4px; background: var(--ax-bg); color: var(--ax-text);\n}\n.ax-pj__fld-hint { font-size: 11px; color: var(--ax-fg-muted, var(--ax-text)); }\n.ax-pj__form-actions {\n display: flex; gap: 8px; align-items: center;\n margin-top: 8px; padding-top: 14px;\n border-top: 1px solid var(--ax-border);\n}\n.ax-pj__btn--primary {\n background: var(--ax-accent, #6366f1); color: white;\n border-color: var(--ax-accent, #6366f1);\n}\n.ax-pj__btn--primary:hover { opacity: 0.9; }\n.ax-pj__btn--danger { color: var(--ax-err, #ef4444); border-color: var(--ax-err, #ef4444); }\n.ax-pj__btn--danger:hover { background: var(--ax-err, #ef4444); color: white; }\n.ax-pj__fld textarea {\n font: inherit; font-family: var(--ax-mono, monospace); font-size: 12px;\n padding: 6px 10px; border: 1px solid var(--ax-border);\n border-radius: 4px; background: var(--ax-bg); color: var(--ax-text);\n resize: vertical;\n}\n.ax-pj__clause {\n border: 1px solid var(--ax-border); border-radius: 4px;\n padding: 12px; margin-bottom: 12px; background: var(--ax-bg);\n}\n.ax-pj__clause h4 {\n margin: 0 0 10px 0; font-size: 12px;\n font-family: var(--ax-mono, monospace);\n color: var(--ax-accent); text-transform: lowercase;\n}\n.ax-pj__form details summary {\n cursor: pointer; padding: 6px 0; font-weight: 500;\n border-bottom: 1px solid var(--ax-border); margin-bottom: 12px;\n}\n.ax-pj__detail-body { padding: 24px; overflow-y: auto; flex: 1; }\n.ax-pj__section {\n margin-bottom: 28px;\n}\n.ax-pj__section h3 {\n font-size: 13px; text-transform: uppercase; letter-spacing: 0.6px;\n color: var(--ax-fg-muted, var(--ax-text)); margin: 0 0 10px 0;\n}\n.ax-pj__row {\n display: flex; align-items: center; gap: 10px;\n padding: 8px 12px; border: 1px solid var(--ax-border);\n border-radius: 4px; margin-bottom: 6px; background: var(--ax-bg);\n}\n.ax-pj__row .name { font-weight: 500; }\n.ax-pj__row .meta { font-size: 11px; color: var(--ax-fg-muted, var(--ax-text)); }\n.ax-pj__row a { color: var(--ax-accent); text-decoration: none; }\n.ax-pj__row a:hover { text-decoration: underline; }\n.ax-pj__empty, .ax-pj__warn {\n padding: 18px; font-size: 12px; color: var(--ax-fg-muted, var(--ax-text));\n}\n.ax-pj__warn { color: var(--ax-warn, #facc15); border-top: 1px solid var(--ax-border); }\n.ax-pj__hint { font-size: 11px; color: var(--ax-fg-muted, var(--ax-text)); margin-top: 4px; }\n`\n\nconst PROJECTS_PAGE_SCRIPT = `\n(function(){\n const $ = (sel) => document.querySelector(sel);\n const esc = (s) => String(s == null ? \"\" : s).replace(/[&<>\"]/g, (c) => ({\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",\"\\\\\"\":\"&quot;\"}[c]));\n const state = { rows: [], warnings: [], selectedKey: null, filter: \"\" };\n\n async function load() {\n const r = await fetch(\"/api/admin/projects\");\n const data = await r.json().catch(() => ({}));\n state.rows = Array.isArray(data.projects) ? data.projects : [];\n state.warnings = Array.isArray(data.warnings) ? data.warnings : [];\n if (!state.selectedKey && state.rows[0]) state.selectedKey = state.rows[0].project;\n render();\n }\n\n function render() {\n renderList();\n renderDetail();\n renderWarnings();\n }\n\n function renderList() {\n const list = $(\"#pj-list\");\n const empty = $(\"#pj-empty\");\n const needle = state.filter.toLowerCase();\n const visible = state.rows.filter(p => !needle || p.project.toLowerCase().includes(needle));\n if (visible.length === 0) {\n list.innerHTML = \"\";\n empty.hidden = false;\n return;\n }\n empty.hidden = true;\n list.innerHTML = visible.map(p => {\n const isActive = p.project === state.selectedKey;\n const wfCount = p.workflows.length;\n const agentCount = p.agents.length;\n const contactCount = p.contacts.length;\n return '<li class=\"ax-pj__card ' + (isActive ? 'is-active' : '') + '\" data-key=\"' + esc(p.project) + '\">'\n + '<div class=\"ax-pj__card-title\">' + esc(p.project) + '</div>'\n + '<div class=\"ax-pj__card-meta\">'\n + '<span class=\"stat ' + (agentCount === 0 ? 'zero' : '') + '\" title=\"Agents bound to this project\">' + agentCount + ' agents</span>'\n + '<span class=\"stat ' + (wfCount === 0 ? 'zero' : '') + '\" title=\"Workflows tagged with this project\">' + wfCount + ' workflows</span>'\n + '<span class=\"stat ' + (contactCount === 0 ? 'zero' : '') + '\" title=\"Contacts associated with this project\">' + contactCount + ' contacts</span>'\n + (p.channels.length ? '<span class=\"stat\" title=\"Channels carrying this project\">' + esc(p.channels.join(\", \")) + '</span>' : '')\n + '</div>'\n + '</li>';\n }).join(\"\");\n list.querySelectorAll(\".ax-pj__card\").forEach(el => {\n el.addEventListener(\"click\", () => {\n state.selectedKey = el.dataset.key;\n render();\n });\n });\n }\n\n function renderDetail() {\n const head = $(\"#pj-detail-head\");\n const body = $(\"#pj-detail-body\");\n const p = state.rows.find(r => r.project === state.selectedKey);\n if (!p) {\n head.innerHTML = '<span class=\"hint\">Select a project to inspect.</span>';\n body.innerHTML = \"\";\n return;\n }\n const kindBadge = '<span class=\"ax-pj__kind ax-pj__kind--' + esc(p.kind || 'other') + '\">' + esc(p.kind || 'other') + '</span>';\n const homeLink = p.homeUrl ? ' <a href=\"' + esc(p.homeUrl) + '\" target=\"_blank\" rel=\"noopener\" class=\"ax-pj__home\" title=\"Open project home\">↗</a>' : '';\n const heading = p.displayName ? esc(p.displayName) + ' <span class=\"ax-pj__keyhint\">' + esc(p.project) + '</span>' : esc(p.project);\n const canEdit = !!p.rulePath; // only rule-backed projects are mutable\n const editBtn = canEdit\n ? '<button class=\"ax-pj__btn\" data-action=\"edit-project\" data-project=\"' + esc(p.project) + '\" style=\"margin-left:auto\">Edit</button>'\n : '<span class=\"ax-pj__hint\" style=\"margin-left:auto\">no rule file</span>';\n head.innerHTML = '<div style=\"display:flex;align-items:center;gap:10px;flex-wrap:wrap;width:100%\">'\n + '<h2>' + kindBadge + ' ' + heading + homeLink + '</h2>'\n + editBtn\n + '</div>'\n + '<div class=\"ax-pj__hint\">'\n + (p.runbook ? 'runbook: <code>' + esc(p.runbook) + '</code>' : '<em>no runbook</em>')\n + (p.rulePath ? ' · rule: <code>' + esc(p.rulePath.split('/').slice(-3).join('/')) + '</code>' : '')\n + '</div>';\n\n let html = \"\";\n\n // Channels\n html += '<section class=\"ax-pj__section\">'\n + '<h3>Channels (' + p.channels.length + ')'\n + (p.rulePath ? ' <button class=\"ax-pj__btn\" data-action=\"edit-clauses\" data-project=\"' + esc(p.project) + '\" title=\"Edit channel-clause filters (actions / labels / states / authors)\">Edit clauses</button>' : '')\n + '</h3>';\n if (p.channels.length === 0) html += '<div class=\"ax-pj__hint\">No channel route configured. ' + (p.rulePath ? 'Click <strong>Edit clauses</strong> above to add a gitlab or github clause, or' : 'A') + ' add a route under <code>channels.gitlab.routes</code> or <code>channels.github.routes</code>.</div>';\n else html += p.channels.map(c => {\n const cl = p.clauses && p.clauses[c];\n const summary = cl ? Object.keys(cl).join(\", \") : \"no rules\";\n return '<div class=\"ax-pj__row\"><span class=\"name\">' + esc(c) + '</span><span class=\"meta\">' + esc(summary) + '</span></div>';\n }).join(\"\");\n html += '</section>';\n\n // Agents\n html += '<section class=\"ax-pj__section\">'\n + '<h3>Agents (' + p.agents.length + ')'\n + (p.rulePath ? ' <button class=\"ax-pj__btn\" data-action=\"set-agent\" data-project=\"' + esc(p.project) + '\" title=\"Bind a default agent (writes rule.agent)\">Set default agent</button>' : '')\n + '</h3>';\n if (p.agents.length === 0) html += '<div class=\"ax-pj__hint\">No agents bound. Click <strong>Set default agent</strong> above, or add a channel route in agentx.json.</div>';\n else html += p.agents.map(a => {\n const canUnbind = a.via === \"rule.agent\" && p.rulePath;\n return '<div class=\"ax-pj__row\">'\n + '<span class=\"name\"><a href=\"/admin/agents/' + esc(a.agentId) + '\">' + esc(a.agentId) + '</a></span>'\n + '<span class=\"meta\">' + esc(a.channel) + (a.gitlabUsername ? ' · @' + esc(a.gitlabUsername) : '') + ' · via ' + esc(a.via) + '</span>'\n + (canUnbind ? '<button class=\"ax-pj__unlink\" data-action=\"unbind-agent\" data-project=\"' + esc(p.project) + '\" title=\"Clear rule.agent\">×</button>' : '')\n + '</div>';\n }).join(\"\");\n html += '</section>';\n\n // Workflows\n html += '<section class=\"ax-pj__section\">'\n + '<h3>Workflows (' + p.workflows.length + ')'\n + ' <button class=\"ax-pj__btn\" data-action=\"add-workflow\" data-project=\"' + esc(p.project) + '\" title=\"Tag a workflow with this project\">+ Add workflow</button>'\n + '</h3>';\n if (p.workflows.length === 0) html += '<div class=\"ax-pj__hint\">No workflows tagged with this project. Click <strong>+ Add workflow</strong> above, or set <code>project: ' + esc(p.project) + '</code> in a workflow YAML.</div>';\n else html += p.workflows.map(w =>\n '<div class=\"ax-pj__row\">'\n + '<span class=\"name\"><a href=\"/workflows#' + esc(w.id) + '\">' + esc(w.title || w.id) + '</a></span>'\n + '<span class=\"meta\">' + esc(w.id) + (w.trigger ? ' · ' + esc(w.trigger) : '') + ' · ' + esc(w.status) + '</span>'\n + '<button class=\"ax-pj__unlink\" data-action=\"unlink-workflow\" data-workflow-id=\"' + esc(w.id) + '\" data-project=\"' + esc(p.project) + '\" title=\"Strip the project tag from this workflow\">×</button>'\n + '</div>').join(\"\");\n html += '</section>';\n\n // Contacts\n html += '<section class=\"ax-pj__section\">'\n + '<h3>Contacts (' + p.contacts.length + ')'\n + (p.rulePath ? ' <button class=\"ax-pj__btn\" data-action=\"add-contact\" data-project=\"' + esc(p.project) + '\" title=\"Link a contact id to this project\">+ Add contact</button>' : '')\n + '</h3>';\n if (p.contacts.length === 0) html += '<div class=\"ax-pj__hint\">No contacts associated. Click <strong>+ Add contact</strong> above, or set <code>\"project\": \"' + esc(p.project) + '\"</code> on a contact in <code>.agentx/contacts.json</code>.</div>';\n else html += p.contacts.map(c =>\n '<div class=\"ax-pj__row\">'\n + '<span class=\"name\">' + esc(c.name) + '</span>'\n + '<span class=\"meta\">' + esc(c.id) + (c.channels.length ? ' · ' + esc(c.channels.join(\", \")) : '') + '</span>'\n + (p.rulePath ? '<button class=\"ax-pj__unlink\" data-action=\"unlink-contact\" data-project=\"' + esc(p.project) + '\" data-contact-id=\"' + esc(c.id) + '\" title=\"Remove from rule.contacts\">×</button>' : '')\n + '</div>').join(\"\");\n html += '</section>';\n\n body.innerHTML = html;\n }\n\n function renderWarnings() {\n const w = $(\"#pj-warn\");\n if (!state.warnings.length) { w.hidden = true; return; }\n w.hidden = false;\n w.innerHTML = state.warnings.map(x => '⚠ ' + esc(x)).join(\"<br>\");\n }\n\n // ── Workflow link/unlink ──────────────────────────────────────────\n // Both go through /api/admin/projects/workflows/{link,unlink} (the\n // existing /api/admin/* proxy + admin-panel forwarder). On success\n // we reload the page state — the aggregator picks up the new tag.\n\n async function unlinkWorkflow(workflowId, projectKey) {\n if (!confirm(\"Strip project tag '\" + projectKey + \"' from workflow '\" + workflowId + \"'?\")) return;\n try {\n const r = await fetch(\"/api/admin/projects/workflows/unlink\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-Requested-With\": \"agentx-board\" },\n body: JSON.stringify({ projectKey, workflowId }),\n });\n const data = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error(data.error || (\"HTTP \" + r.status));\n await load();\n } catch (e) {\n alert(\"Unlink failed: \" + (e.message || e));\n }\n }\n\n async function linkWorkflow(workflowId, projectKey) {\n try {\n const r = await fetch(\"/api/admin/projects/workflows/link\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-Requested-With\": \"agentx-board\" },\n body: JSON.stringify({ projectKey, workflowId }),\n });\n const data = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error(data.error || (\"HTTP \" + r.status));\n closeModal();\n await load();\n } catch (e) {\n alert(\"Link failed: \" + (e.message || e));\n }\n }\n\n // ── Workflow picker modal ────────────────────────────────────────\n\n function closeModal() {\n const bg = document.querySelector(\".ax-pj__modal-bg\");\n if (bg) bg.remove();\n }\n\n async function openWorkflowPicker(projectKey) {\n // Fetch the full workflow list via the existing /api/workflows\n // endpoint (proxied through the mesh selector — the picker shows\n // workflows from the SELECTED node, not local). Workflows already\n // tagged with this project are still shown but flagged.\n let workflows = [];\n try {\n const r = await fetch(\"/api/workflows\");\n const data = await r.json();\n workflows = Array.isArray(data.workflows) ? data.workflows : [];\n } catch (e) { alert(\"Could not load workflows: \" + e.message); return; }\n\n // Build modal.\n const bg = document.createElement(\"div\");\n bg.className = \"ax-pj__modal-bg\";\n bg.innerHTML =\n '<div class=\"ax-pj__modal\" role=\"dialog\" aria-label=\"Pick workflow\">'\n + '<div class=\"ax-pj__modal-head\">'\n + '<h3>Tag a workflow with <code>' + esc(projectKey) + '</code></h3>'\n + '<button class=\"ax-pj__icon\" data-pj-close>×</button>'\n + '</div>'\n + '<div class=\"ax-pj__modal-body\">'\n + '<div class=\"ax-pj__modal-search\"><input type=\"search\" id=\"pj-pick-search\" placeholder=\"Filter by id or title…\" autofocus /></div>'\n + '<div id=\"pj-pick-list\"></div>'\n + '</div>'\n + '</div>';\n document.body.appendChild(bg);\n bg.addEventListener(\"click\", (e) => { if (e.target === bg) closeModal(); });\n bg.querySelector(\"[data-pj-close]\").addEventListener(\"click\", closeModal);\n\n function renderPickList() {\n const needle = (document.getElementById(\"pj-pick-search\").value || \"\").toLowerCase();\n const filtered = workflows.filter(w =>\n !needle || w.id.toLowerCase().includes(needle) || (w.title || \"\").toLowerCase().includes(needle));\n const html = filtered.map(w => {\n const alreadyHere = w.project === projectKey;\n const otherProject = w.project && w.project !== projectKey;\n return '<div class=\"ax-pj__pick\" data-wf-id=\"' + esc(w.id) + '\">'\n + '<span class=\"pname\">' + esc(w.title || w.id) + '</span>'\n + '<span class=\"pmeta\">' + esc(w.id) + '</span>'\n + (alreadyHere ? '<span class=\"ptag\">already linked</span>' : \"\")\n + (otherProject ? '<span class=\"ptag\" title=\"Currently on ' + esc(w.project) + '\">' + esc(w.project) + ' → ' + esc(projectKey) + '</span>' : \"\")\n + '</div>';\n }).join(\"\") || '<div class=\"ax-pj__hint\">No workflows match.</div>';\n document.getElementById(\"pj-pick-list\").innerHTML = html;\n document.querySelectorAll(\".ax-pj__pick\").forEach(el => {\n el.addEventListener(\"click\", () => linkWorkflow(el.dataset.wfId, projectKey));\n });\n }\n renderPickList();\n document.getElementById(\"pj-pick-search\").addEventListener(\"input\", renderPickList);\n document.addEventListener(\"keydown\", function escClose(e) {\n if (e.key === \"Escape\") { closeModal(); document.removeEventListener(\"keydown\", escClose); }\n });\n }\n\n // ── POST helper used by every mutation. Returns parsed json or\n // throws with the server-side error message.\n async function postJson(path, body) {\n const r = await fetch(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-Requested-With\": \"agentx-board\" },\n body: JSON.stringify(body || {}),\n });\n const data = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error(data.error || (\"HTTP \" + r.status));\n return data;\n }\n\n // ── Header edit modal ─────────────────────────────────────────────\n function openHeaderEditModal(projectKey) {\n const p = state.rows.find(r => r.project === projectKey);\n if (!p) return;\n const bg = document.createElement(\"div\");\n bg.className = \"ax-pj__modal-bg\";\n bg.innerHTML =\n '<div class=\"ax-pj__modal\" role=\"dialog\" aria-label=\"Edit project header\">'\n + '<div class=\"ax-pj__modal-head\">'\n + '<h3>Edit <code>' + esc(projectKey) + '</code></h3>'\n + '<button class=\"ax-pj__icon\" data-pj-close>×</button>'\n + '</div>'\n + '<div class=\"ax-pj__modal-body\">'\n + '<form id=\"pj-edit-form\" class=\"ax-pj__form\">'\n + fld(\"Display name\", \"displayName\", p.displayName || \"\", \"Friendly name shown on this page\")\n + fld(\"Home URL\", \"homeUrl\", p.homeUrl || \"\", \"External link to repo / board\")\n + selFld(\"Kind\", \"kind\", p.kind || \"other\", [\"gitlab\",\"github\",\"jira\",\"linear\",\"other\"])\n + fld(\"Runbook path\", \"runbook\", p.runbook || \"\", \"Filesystem path; injected into agent system prefix\")\n + fld(\"Default agent\", \"agent\", \"\", \"agentId — leave blank to keep / clear field\")\n + '<div class=\"ax-pj__form-actions\">'\n + '<button type=\"submit\" class=\"ax-pj__btn ax-pj__btn--primary\">Save</button>'\n + '<button type=\"button\" class=\"ax-pj__btn\" data-pj-close>Cancel</button>'\n + '<button type=\"button\" class=\"ax-pj__btn ax-pj__btn--danger\" data-pj-delete style=\"margin-left:auto\">Delete project</button>'\n + '</div>'\n + '</form>'\n + '</div>'\n + '</div>';\n document.body.appendChild(bg);\n bg.addEventListener(\"click\", (e) => { if (e.target === bg) closeModal(); });\n bg.querySelectorAll(\"[data-pj-close]\").forEach(b => b.addEventListener(\"click\", closeModal));\n bg.querySelector(\"#pj-edit-form\").addEventListener(\"submit\", async (e) => {\n e.preventDefault();\n const f = new FormData(e.target);\n const patch = {};\n for (const k of [\"displayName\", \"homeUrl\", \"kind\", \"runbook\", \"agent\"]) {\n const v = (f.get(k) || \"\").toString();\n // Only include fields the operator actually filled — preserves\n // current values on the server when the input was left blank.\n // Exception: when the field had a value before and is now empty,\n // we DO send \"\" to clear it. We can't distinguish those two\n // cases with FormData alone, so the convention here is: blank\n // submit means clear. Operator wanting to keep a value should\n // close the modal without saving. Documented in the help text\n // above each field.\n patch[k] = v;\n }\n try {\n await postJson(\"/api/admin/projects/header\", { projectKey, patch });\n closeModal();\n await load();\n } catch (err) { alert(\"Save failed: \" + err.message); }\n });\n bg.querySelector(\"[data-pj-delete]\").addEventListener(\"click\", async () => {\n if (!confirm(\"Delete project '\" + projectKey + \"'? Rule file will be backed up.\")) return;\n try {\n await postJson(\"/api/admin/projects/delete\", { projectKey });\n closeModal();\n state.selectedKey = null;\n await load();\n } catch (err) { alert(\"Delete failed: \" + err.message); }\n });\n }\n\n function fld(label, name, value, hint) {\n return '<label class=\"ax-pj__fld\">'\n + '<span class=\"ax-pj__fld-label\">' + esc(label) + '</span>'\n + '<input type=\"text\" name=\"' + esc(name) + '\" value=\"' + esc(value) + '\" />'\n + (hint ? '<span class=\"ax-pj__fld-hint\">' + esc(hint) + '</span>' : '')\n + '</label>';\n }\n function selFld(label, name, value, options) {\n const opts = options.map(o => '<option value=\"' + esc(o) + '\"' + (o === value ? ' selected' : '') + '>' + esc(o) + '</option>').join(\"\");\n return '<label class=\"ax-pj__fld\">'\n + '<span class=\"ax-pj__fld-label\">' + esc(label) + '</span>'\n + '<select name=\"' + esc(name) + '\">' + opts + '</select>'\n + '</label>';\n }\n\n // ── Set/unbind default agent ──────────────────────────────────────\n async function setDefaultAgent(projectKey) {\n // Pull agents from the SELECTED node (mesh-aware /api/agents).\n let agents = [];\n try {\n const r = await fetch(\"/api/agents\");\n const data = await r.json();\n agents = Array.isArray(data) ? data : (data.agents || []);\n } catch (e) { alert(\"Could not load agents: \" + e.message); return; }\n const id = prompt(\"Agent id to set as default for '\" + projectKey + \"':\\\\n\\\\n\"\n + \"Available: \" + agents.map(a => a.id).join(\", \"));\n if (!id) return;\n try {\n await postJson(\"/api/admin/projects/header\", { projectKey, patch: { agent: id } });\n await load();\n } catch (e) { alert(\"Set agent failed: \" + e.message); }\n }\n\n async function unbindAgent(projectKey) {\n if (!confirm(\"Clear default agent on '\" + projectKey + \"'?\")) return;\n try {\n await postJson(\"/api/admin/projects/header\", { projectKey, patch: { agent: \"\" } });\n await load();\n } catch (e) { alert(\"Unbind failed: \" + e.message); }\n }\n\n // ── Contact add/remove ───────────────────────────────────────────\n async function addContact(projectKey) {\n const cid = prompt(\"Contact id to link to '\" + projectKey + \"':\\\\n\\\\n\"\n + \"Use the id from .agentx/contacts.json (e.g. 'alex', 'jordan').\");\n if (!cid) return;\n try {\n await postJson(\"/api/admin/projects/contacts/link\", { projectKey, contactId: cid.trim() });\n await load();\n } catch (e) { alert(\"Link contact failed: \" + e.message); }\n }\n\n async function unlinkContactAction(projectKey, contactId) {\n if (!confirm(\"Unlink '\" + contactId + \"' from '\" + projectKey + \"'?\")) return;\n try {\n await postJson(\"/api/admin/projects/contacts/unlink\", { projectKey, contactId });\n await load();\n } catch (e) { alert(\"Unlink contact failed: \" + e.message); }\n }\n\n // ── Channel-clause editor modal ───────────────────────────────────\n //\n // Renders one section per clause (gitlab.issue, gitlab.merge_request,\n // gitlab.note, gitlab.pipeline, github.issues, github.pull_request).\n // Each list field is a <textarea> where one line = one entry. On\n // save we parse back into the shape the rule expects and POST the\n // entire gitlab + github block. Operator can clear a clause by\n // emptying ALL its textareas (the prune step on the server side\n // drops empty objects).\n //\n // Triggers field on note clauses uses a mini DSL — one line each:\n // auto → { auto-resolve from agentMappings }\n // @username → { mention: \"@username\" }\n // keyword: phrase → { keyword: \"phrase\" }\n // Empty lines ignored. Format is line-oriented to keep the textarea\n // usable; structured editors can land later.\n\n const CLAUSE_FIELDS = {\n issue: [\"actions\", \"requireLabels\", \"excludeLabels\", \"excludeStates\", \"excludeAuthors\"],\n merge_request: [\"actions\", \"requireLabels\", \"excludeLabels\", \"excludeStates\", \"excludeAuthors\"],\n pipeline: [\"actions\"],\n issues: [\"actions\", \"requireLabels\", \"excludeLabels\", \"excludeStates\", \"excludeAuthors\"],\n pull_request: [\"actions\", \"requireLabels\", \"excludeLabels\", \"excludeStates\", \"excludeAuthors\"],\n };\n\n function listToText(arr) { return Array.isArray(arr) ? arr.join(\"\\\\n\") : \"\"; }\n function textToList(s) {\n return String(s || \"\").split(\"\\\\n\").map(x => x.trim()).filter(x => x.length > 0);\n }\n function triggersToText(trigs) {\n if (!Array.isArray(trigs)) return \"\";\n return trigs.map(t => {\n if (t === \"auto\" || (t && t.auto)) return \"auto\";\n if (t && t.mention) return t.mention;\n if (t && t.keyword) return \"keyword: \" + t.keyword;\n return \"\";\n }).filter(Boolean).join(\"\\\\n\");\n }\n function textToTriggers(s) {\n const out = [];\n for (const line of String(s || \"\").split(\"\\\\n\").map(l => l.trim()).filter(Boolean)) {\n if (line === \"auto\") out.push(\"auto\");\n else if (line.toLowerCase().startsWith(\"keyword:\")) out.push({ keyword: line.slice(8).trim() });\n else if (line.startsWith(\"@\")) out.push({ mention: line });\n else out.push({ keyword: line }); // fallback\n }\n return out;\n }\n\n function clauseForm(channel, clauseName, current) {\n const cur = current || {};\n let html = '<div class=\"ax-pj__clause\" data-clause=\"' + esc(channel + \".\" + clauseName) + '\">'\n + '<h4>' + esc(channel) + '.' + esc(clauseName) + '</h4>';\n if (clauseName === \"note\") {\n html += listFld(\"Only on (one per line: issue / merge_request / commit / snippet)\", \"onlyOn\", listToText(cur.onlyOn));\n html += listFld(\"Triggers (auto, @user, or 'keyword: phrase' — one per line)\", \"triggers\", triggersToText(cur.triggers));\n html += listFld(\"Exclude authors (substring match — one per line)\", \"excludeAuthors\", listToText(cur.excludeAuthors));\n } else {\n const fields = CLAUSE_FIELDS[clauseName] || [];\n for (const f of fields) {\n const labels = {\n actions: \"Actions (one per line: open, reopen, update, close, …)\",\n requireLabels: \"Require labels (at least one must match — one per line)\",\n excludeLabels: \"Exclude labels (any present blocks — one per line)\",\n excludeStates: \"Exclude states (one per line: closed, opened, merged, …)\",\n excludeAuthors: \"Exclude authors (substring match — one per line)\",\n };\n html += listFld(labels[f] || f, f, listToText(cur[f]));\n }\n }\n html += '</div>';\n return html;\n }\n function listFld(label, name, value) {\n return '<label class=\"ax-pj__fld\">'\n + '<span class=\"ax-pj__fld-label\">' + esc(label) + '</span>'\n + '<textarea name=\"' + esc(name) + '\" rows=\"3\">' + esc(value) + '</textarea>'\n + '</label>';\n }\n\n function readClauseFromForm(scopeEl, clauseName) {\n const out = {};\n const fieldNames = clauseName === \"note\"\n ? [\"onlyOn\", \"triggers\", \"excludeAuthors\"]\n : CLAUSE_FIELDS[clauseName] || [];\n for (const f of fieldNames) {\n const ta = scopeEl.querySelector('textarea[name=\"' + f + '\"]');\n if (!ta) continue;\n const v = ta.value;\n if (f === \"triggers\") {\n const t = textToTriggers(v);\n if (t.length > 0) out[f] = t;\n } else {\n const list = textToList(v);\n if (list.length > 0) out[f] = list;\n }\n }\n return out;\n }\n\n function openClauseEditor(projectKey) {\n const p = state.rows.find(r => r.project === projectKey);\n if (!p) return;\n const cur = p.clauses || {};\n const gl = cur.gitlab || {};\n const gh = cur.github || {};\n\n const bg = document.createElement(\"div\");\n bg.className = \"ax-pj__modal-bg\";\n bg.innerHTML =\n '<div class=\"ax-pj__modal\" style=\"min-width:640px\" role=\"dialog\" aria-label=\"Edit channel clauses\">'\n + '<div class=\"ax-pj__modal-head\">'\n + '<h3>Channel clauses · <code>' + esc(projectKey) + '</code></h3>'\n + '<button class=\"ax-pj__icon\" data-pj-close>×</button>'\n + '</div>'\n + '<div class=\"ax-pj__modal-body\">'\n + '<form id=\"pj-clauses-form\" class=\"ax-pj__form\">'\n + '<details open><summary>GitLab</summary>'\n + clauseForm(\"gitlab\", \"issue\", gl.issue)\n + clauseForm(\"gitlab\", \"merge_request\", gl.merge_request)\n + clauseForm(\"gitlab\", \"note\", gl.note)\n + clauseForm(\"gitlab\", \"pipeline\", gl.pipeline)\n + '</details>'\n + '<details><summary>GitHub</summary>'\n + clauseForm(\"github\", \"issues\", gh.issues)\n + clauseForm(\"github\", \"pull_request\", gh.pull_request)\n + '</details>'\n + '<div class=\"ax-pj__form-actions\">'\n + '<button type=\"submit\" class=\"ax-pj__btn ax-pj__btn--primary\">Save clauses</button>'\n + '<button type=\"button\" class=\"ax-pj__btn\" data-pj-close>Cancel</button>'\n + '</div>'\n + '</form>'\n + '</div>'\n + '</div>';\n document.body.appendChild(bg);\n bg.addEventListener(\"click\", (e) => { if (e.target === bg) closeModal(); });\n bg.querySelectorAll(\"[data-pj-close]\").forEach(b => b.addEventListener(\"click\", closeModal));\n bg.querySelector(\"#pj-clauses-form\").addEventListener(\"submit\", async (e) => {\n e.preventDefault();\n // Read each clause section back into the shape the rule expects.\n // Empty clauses (every textarea blank) drop out via pruneEmpty on\n // the server, so the YAML stays clean.\n const gitlabOut = {\n issue: readClauseFromForm(bg.querySelector('[data-clause=\"gitlab.issue\"]'), \"issue\"),\n merge_request: readClauseFromForm(bg.querySelector('[data-clause=\"gitlab.merge_request\"]'), \"merge_request\"),\n note: readClauseFromForm(bg.querySelector('[data-clause=\"gitlab.note\"]'), \"note\"),\n pipeline: readClauseFromForm(bg.querySelector('[data-clause=\"gitlab.pipeline\"]'), \"pipeline\"),\n };\n const githubOut = {\n issues: readClauseFromForm(bg.querySelector('[data-clause=\"github.issues\"]'), \"issues\"),\n pull_request: readClauseFromForm(bg.querySelector('[data-clause=\"github.pull_request\"]'), \"pull_request\"),\n };\n // Strip empty inner objects so the rule doesn't end up with\n // ghost-clauses (e.g. issue:{} when no fields were set).\n const cleanInner = (o) => {\n const r = {};\n for (const [k, v] of Object.entries(o)) {\n if (v && typeof v === \"object\" && Object.keys(v).length > 0) r[k] = v;\n }\n return r;\n };\n const cleanGl = cleanInner(gitlabOut);\n const cleanGh = cleanInner(githubOut);\n try {\n await postJson(\"/api/admin/projects/clauses\", {\n projectKey,\n gitlab: Object.keys(cleanGl).length > 0 ? cleanGl : null,\n github: Object.keys(cleanGh).length > 0 ? cleanGh : null,\n });\n closeModal();\n await load();\n } catch (err) { alert(\"Save clauses failed: \" + err.message); }\n });\n }\n\n // ── New project modal ─────────────────────────────────────────────\n function openNewProjectModal() {\n const bg = document.createElement(\"div\");\n bg.className = \"ax-pj__modal-bg\";\n bg.innerHTML =\n '<div class=\"ax-pj__modal\" role=\"dialog\" aria-label=\"New project\">'\n + '<div class=\"ax-pj__modal-head\">'\n + '<h3>New project</h3>'\n + '<button class=\"ax-pj__icon\" data-pj-close>×</button>'\n + '</div>'\n + '<div class=\"ax-pj__modal-body\">'\n + '<form id=\"pj-new-form\" class=\"ax-pj__form\">'\n + fld(\"Project key\", \"projectKey\", \"\", \"org/repo for VCS, or any slug for jira/linear/other\")\n + selFld(\"Kind\", \"kind\", \"gitlab\", [\"gitlab\",\"github\",\"jira\",\"linear\",\"other\"])\n + fld(\"Display name\", \"displayName\", \"\", \"Optional human-friendly title\")\n + fld(\"Home URL\", \"homeUrl\", \"\", \"Optional link to repo / board\")\n + fld(\"Runbook path\", \"runbook\", \"\", \"Optional filesystem path\")\n + fld(\"Default agent\", \"agent\", \"\", \"Optional agentId\")\n + '<div class=\"ax-pj__form-actions\">'\n + '<button type=\"submit\" class=\"ax-pj__btn ax-pj__btn--primary\">Create</button>'\n + '<button type=\"button\" class=\"ax-pj__btn\" data-pj-close>Cancel</button>'\n + '</div>'\n + '</form>'\n + '</div>'\n + '</div>';\n document.body.appendChild(bg);\n bg.addEventListener(\"click\", (e) => { if (e.target === bg) closeModal(); });\n bg.querySelectorAll(\"[data-pj-close]\").forEach(b => b.addEventListener(\"click\", closeModal));\n bg.querySelector(\"#pj-new-form\").addEventListener(\"submit\", async (e) => {\n e.preventDefault();\n const f = new FormData(e.target);\n const projectKey = (f.get(\"projectKey\") || \"\").toString().trim();\n if (!projectKey) { alert(\"project key required\"); return; }\n const body = {\n projectKey,\n kind: (f.get(\"kind\") || \"other\").toString(),\n displayName: (f.get(\"displayName\") || \"\").toString() || undefined,\n homeUrl: (f.get(\"homeUrl\") || \"\").toString() || undefined,\n runbook: (f.get(\"runbook\") || \"\").toString() || undefined,\n agent: (f.get(\"agent\") || \"\").toString() || undefined,\n };\n try {\n await postJson(\"/api/admin/projects/create\", body);\n closeModal();\n state.selectedKey = projectKey;\n await load();\n } catch (err) { alert(\"Create failed: \" + err.message); }\n });\n }\n\n // Delegate clicks for unlink + add buttons since detail re-renders.\n document.addEventListener(\"click\", (e) => {\n const t = e.target;\n if (!t || !t.closest) return;\n const unlinkBtn = t.closest(\"[data-action='unlink-workflow']\");\n if (unlinkBtn) {\n e.preventDefault();\n unlinkWorkflow(unlinkBtn.dataset.workflowId, unlinkBtn.dataset.project);\n return;\n }\n const addBtn = t.closest(\"[data-action='add-workflow']\");\n if (addBtn) {\n e.preventDefault();\n openWorkflowPicker(addBtn.dataset.project);\n return;\n }\n const editBtn = t.closest(\"[data-action='edit-project']\");\n if (editBtn) { e.preventDefault(); openHeaderEditModal(editBtn.dataset.project); return; }\n const editClausesBtn = t.closest(\"[data-action='edit-clauses']\");\n if (editClausesBtn) { e.preventDefault(); openClauseEditor(editClausesBtn.dataset.project); return; }\n const setAgentBtn = t.closest(\"[data-action='set-agent']\");\n if (setAgentBtn) { e.preventDefault(); setDefaultAgent(setAgentBtn.dataset.project); return; }\n const unbindAgentBtn = t.closest(\"[data-action='unbind-agent']\");\n if (unbindAgentBtn) { e.preventDefault(); unbindAgent(unbindAgentBtn.dataset.project); return; }\n const addContactBtn = t.closest(\"[data-action='add-contact']\");\n if (addContactBtn) { e.preventDefault(); addContact(addContactBtn.dataset.project); return; }\n const unlinkContactBtn = t.closest(\"[data-action='unlink-contact']\");\n if (unlinkContactBtn) { e.preventDefault(); unlinkContactAction(unlinkContactBtn.dataset.project, unlinkContactBtn.dataset.contactId); return; }\n });\n\n document.addEventListener(\"DOMContentLoaded\", () => {\n $(\"#pj-refresh\").addEventListener(\"click\", load);\n $(\"#pj-new\").addEventListener(\"click\", openNewProjectModal);\n $(\"#pj-filter\").addEventListener(\"input\", (e) => { state.filter = e.target.value; renderList(); });\n document.addEventListener(\"keydown\", (e) => {\n if (e.key === \"r\" && !e.metaKey && document.activeElement?.tagName !== \"INPUT\") load();\n });\n load();\n });\n})();\n`\n","import type { IncomingMessage, ServerResponse } from \"http\"\nimport { clientFromProject, agentClients, matchContact as matchContactRule, type ContactRule } from \"@/business/clients\"\nimport { resolve, join } from \"path\"\nimport { existsSync, readdirSync, readFileSync } from \"fs\"\nimport Database from \"better-sqlite3\"\nimport { inferProject, projectFromPreview } from \"./activity-graph-attribution\"\nimport { fetchForgeStatus, refsToLookUp, type ForgeItem } from \"./activity-graph-forge\"\nimport type { DaemonConfig } from \"./config\"\n\n// --- /admin/activity-graph — Fleet activity perspective view ---\n//\n// Server-side snapshot builder. Reads .agentx/intent/ledger.sqlite\n// (read-only) and the daemon config; produces a \"fleet snapshot\"\n// JSON the client React app consumes:\n//\n// {\n// now: epoch ms,\n// clients: [{ id, name, color, projects: string[] }],\n// agents: [{ id, name, tier, model, role }],\n// channels: [{ id, label, color }],\n// initiators: [{ id, name, avatar }],\n// dispatches: [{ id, agentId, clientId, projectId, channelId,\n// initiatorId, subject, intent, startedAt,\n// resolvedAt, duration, active, outcome, tokens }]\n// }\n//\n// One row per dispatched decision in the ledger window. Active = no\n// resolution row yet. Client/initiator extracted from the source-\n// specific raw_json payload.\n\ninterface OpenedDb { db: Database.Database; close: () => void }\n\nfunction openLedger(): OpenedDb | null {\n const path = resolve(process.cwd(), \".agentx/intent/ledger.sqlite\")\n if (!existsSync(path)) return null\n try {\n const db = new Database(path, { readonly: true, fileMustExist: true })\n return { db, close: () => db.close() }\n } catch (e: any) {\n // Surface the real reason — silent failure previously masked a\n // better-sqlite3 ABI mismatch as an empty \"intent ledger not available\"\n // UI for days. Log once per minute to avoid flooding the dashboard log.\n const now = Date.now()\n if (now - _lastOpenLedgerErrAt > 60_000) {\n _lastOpenLedgerErrAt = now\n console.error(`[activity-graph] openLedger failed: ${e?.message ?? e}`)\n }\n return null\n }\n}\nlet _lastOpenLedgerErrAt = 0\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(body))\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot types\n\nexport interface FleetClient { id: string; name: string; color: string; projects: string[] }\nexport interface FleetAgent {\n id: string; name: string; tier: \"lead\" | \"worker\"; model: string; role: string\n /** Org-chart role title (\"Marketing Lead\"), when the agent has one. */\n title?: string\n}\nexport interface FleetChannel { id: string; label: string; color: string }\nexport interface FleetInitiator { id: string; name: string; avatar: string; kind: InitiatorKind }\nexport interface FleetDispatch {\n id: string\n agentId: string\n clientId: string\n projectId: string\n channelId: string\n initiatorId: string\n /** What kind of trigger started this dispatch — \"telegram\", \"gitlab\",\n * \"github\", \"cron\", \"workflow\", \"a2a\", … Lets the UI render a small\n * pill (\"Alex · GitLab MR\") instead of the catch-all \"Schedule\". */\n initiatorKind?: InitiatorKind\n subject: string\n intent: string\n startedAt: number\n resolvedAt: number | null\n duration: number\n active: boolean\n outcome: \"completed\" | \"active\" | \"error\"\n tokens: number\n /** First ~200 chars of the inbound message text. Inline so the drawer\n * can render the trigger without an extra round-trip. Full text +\n * agent response come from the detail endpoint. */\n inputPreview: string\n /** True when this dispatch is internal infrastructure (intent classifier,\n * background workers) rather than user-facing work. The activity graph\n * hides these by default — they bloat the view without telling the\n * business operator anything actionable. Toggle on to see them. */\n system: boolean\n /** Which mesh node produced this row. Set by the fleet merger when\n * combining snapshots from multiple peers; left undefined for\n * single-node responses. The UI renders it as a small badge so two\n * agents that share an id across nodes are still distinguishable. */\n nodeId?: string\n}\nexport interface FleetSnapshot {\n now: number\n windowH: number\n clients: FleetClient[]\n agents: FleetAgent[]\n channels: FleetChannel[]\n initiators: FleetInitiator[]\n dispatches: FleetDispatch[]\n /** Set by the fleet merger: the node that served the merged view. */\n localNodeId?: string\n /** Forge base URLs, so the UI can link issues and MRs. */\n forges?: { gitlab?: string; github?: string }\n /** Live issue/MR state keyed by refKey (\"ns/repo!51\"); see withForgeStatus. */\n forge?: Record<string, ForgeItem>\n}\n\n// ---------------------------------------------------------------------------\n// Lookups: client palette + channel palette\n\nconst CHANNEL_DEF: Record<string, { label: string; color: string }> = {\n gitlab: { label: \"GitLab\", color: \"#fc6d26\" },\n github: { label: \"GitHub\", color: \"#6e7681\" },\n telegram: { label: \"Telegram\", color: \"#0088cc\" },\n whatsapp: { label: \"WhatsApp\", color: \"#25d366\" },\n slack: { label: \"Slack\", color: \"#4a154b\" },\n discord: { label: \"Discord\", color: \"#5865f2\" },\n mesh: { label: \"Mesh (a2a)\", color: \"#bf8700\" },\n a2a: { label: \"Agent → Agent\", color: \"#bf8700\" },\n cron: { label: \"Cron\", color: \"#6b7280\" },\n workflow: { label: \"Workflow\", color: \"#9333ea\" },\n api: { label: \"Web API\", color: \"#3a7bd5\" },\n mcp: { label: \"Agent tools (MCP)\", color: \"#bf8700\" },\n voice: { label: \"Voice\", color: \"#e5534b\" },\n desktop: { label: \"Desktop assistant\", color: \"#8957e5\" },\n opencode: { label: \"OpenCode\", color: \"#57606a\" },\n}\n\n/** Collapse the storage `source` to the channel the operator thinks\n * they're looking at. Examples:\n * source=mesh, intent=mesh.github → \"github\"\n * source=mesh, intent=mesh.gitlab → \"gitlab\"\n * source=mesh, intent=mesh.a2a → \"a2a\"\n * source=mesh, intent=mesh.task → \"mesh\"\n * source=workflow, intent=workflow.whatsapp-msg → \"whatsapp\"\n * source=workflow, intent=workflow.hook → \"gitlab\"|\"github\" (per payload)\n * Everything else passes through unchanged. */\nfunction upstreamChannel(source: string, intent: string, raw: any): string {\n if (source === \"mesh\") {\n const ctxChan = raw?.context?.channel\n if (typeof ctxChan === \"string\" && ctxChan in CHANNEL_DEF) return ctxChan\n if (intent.startsWith(\"mesh.\")) {\n const suffix = intent.slice(5)\n if (suffix === \"a2a\") return \"a2a\"\n if (suffix in CHANNEL_DEF) return suffix\n }\n return \"mesh\"\n }\n if (source === \"workflow\") {\n // Trigger payload tells us the upstream channel for chat-style triggers.\n const payload = raw?.event?.payload\n const chan = typeof payload?.channel === \"string\" ? payload.channel : \"\"\n if (chan && chan in CHANNEL_DEF) return chan\n // Hook triggers — peek at the issueEvent shape to disambiguate.\n const issueEvent = payload?.issueEvent || payload?.webhookEvent\n if (issueEvent) {\n if (issueEvent.user?.username || issueEvent.project?.path_with_namespace) return \"gitlab\"\n if (issueEvent.sender?.login) return \"github\"\n }\n // Intent suffix as a final hint (e.g. `workflow.whatsapp-message`).\n if (intent.startsWith(\"workflow.\")) {\n const suffix = intent.slice(\"workflow.\".length).split(\"-\")[0]\n if (suffix in CHANNEL_DEF) return suffix\n }\n return \"workflow\"\n }\n return source\n}\n\n// Stable client color from id hash — semantically muted but distinct.\nfunction colorForClient(id: string): string {\n // Reserve known clients to nice colours; everything else picks from a\n // deterministic palette by hashing the id.\n const known: Record<string, string> = {\n globex: \"#e07a3a\",\n initech: \"#3a7bd5\",\n acme: \"#10b981\",\n umbrella: \"#bc8cff\",\n \"umbrella-lab\": \"#bc8cff\",\n hexastack: \"#06b6d4\",\n demosite: \"#ec775c\",\n internal: \"#6b7280\",\n // \"unmapped\" is rendered amber so it's visually obvious that the\n // operator needs to add a contactMap or business.projects entry.\n unmapped: \"#d29922\",\n }\n if (known[id]) return known[id]\n const palette = [\"#e07a3a\", \"#3a7bd5\", \"#10b981\", \"#bc8cff\", \"#ec775c\", \"#bf8700\", \"#5865f2\", \"#cf222e\"]\n let h = 0\n for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) | 0\n return palette[Math.abs(h) % palette.length]\n}\n\n/** Returns true when this agent is internal infrastructure (intent\n * classifier, background workers) that shouldn't show as user-facing\n * work in the activity graph by default. Heuristics:\n * - explicit `agents[X].system === true` in agentx.json (NEW)\n * - well-known classifier names (graph-agent, *-classifier)\n * Operators can opt anything in/out via the config flag. */\nfunction isSystemAgent(agentId: string, daemonConfig: DaemonConfig | null): boolean {\n if (!agentId) return false\n const cfg = (daemonConfig as any)?.agents?.[agentId]\n if (cfg && typeof cfg.system === \"boolean\") return cfg.system\n // Default heuristic: the intent classifier is a known internal name,\n // and any agent ending in \"-classifier\" is by convention internal.\n if (agentId === \"graph-agent\" || agentId === \"intent-classifier\") return true\n if (/-classifier$/.test(agentId)) return true\n return false\n}\n\n/** Pull the identity fields out of a raw channel payload, then defer to the\n * shared matcher so the graph and the monitor agree on attribution. */\nfunction matchContact(source: string, raw: any, contactMap: ContactRule[]): ContactRule | undefined {\n return matchContactRule(source, {\n chatId: String(raw?.chatId ?? raw?.message?.chat?.id ?? \"\"),\n username: String(raw?.sender?.username ?? raw?.message?.from?.username ?? \"\"),\n senderId: String(raw?.sender?.id ?? raw?.message?.from?.id ?? \"\"),\n }, contactMap)\n}\n\n// Best-effort initiator extraction per source. The raw_json shape differs:\n// - chat-style channels (telegram/whatsapp/slack/discord): the IncomingMessage\n// is what's stored, with sender.{id,username,name} at the top level.\n// - gitlab webhook: the raw GitLab payload is stored as-is, with user.* at\n// the top level (no IncomingMessage wrapper).\n// - github webhook: raw GitHub payload, sender.login at top level.\n// - mesh: a routed task — the original channel + sender live under\n// `raw.context.{channel,sender,senderUsername,senderId}`, NOT at top level.\n// - cron: { jobId, agentId, firedAt } — no human; we name the cron job.\n// - workflow: workflow run envelope with {workflowId|runId}.\nfunction initiatorFrom(source: string, intent: string, raw: any): { id: string; name: string; avatar: string; kind: InitiatorKind } | null {\n if (!raw || typeof raw !== \"object\") return null\n\n // ---- mesh-routed events: dig into raw.context.* + raw.senderAgentId ----\n // Three caller variants seen in the wild:\n // 1. modern A2A protocol (mesh + MCP): raw.senderAgentId = \"atlas\"\n // 2. /task with channel context: raw.context.{channel,sender,senderUsername}\n // 3. legacy A2A executor: raw.context.sender = \"agent:atlas\"\n // 4. bare A2A from old callers: {agentId, message} — no caller info\n // we still bucket these as \"A2A\" (not Schedule)\n // so the operator sees \"this is internal mesh traffic\" rather than a fake\n // cron entry.\n if (source === \"mesh\") {\n // 1. Direct senderAgentId — most informative, future-proof.\n if (typeof raw.senderAgentId === \"string\" && raw.senderAgentId.trim()) {\n const aid = raw.senderAgentId.trim()\n return { id: aid, name: aid, avatar: initialsFor(aid), kind: \"a2a\" }\n }\n const ctx = raw.context && typeof raw.context === \"object\" ? raw.context : null\n if (ctx) {\n const senderUsername = typeof ctx.senderUsername === \"string\" ? ctx.senderUsername : null\n const senderName = typeof ctx.sender === \"string\" ? ctx.sender : null\n const senderId = typeof ctx.senderId === \"string\" ? ctx.senderId : null\n // 3. legacy A2A — context.sender comes through as \"agent:<name>\".\n if (senderName && senderName.startsWith(\"agent:\")) {\n const aid = senderName.slice(\"agent:\".length)\n if (aid && !isSystemSender(aid)) {\n return { id: aid, name: aid, avatar: initialsFor(aid), kind: \"a2a\" }\n }\n }\n // 2. Channel-context A2A (telegram/gitlab/github routed through mesh).\n const display = senderName || senderUsername || senderId\n if (display && !isSystemSender(senderUsername || senderName || \"\")) {\n const id = senderUsername || senderName || senderId!\n const kind: InitiatorKind =\n intent === \"mesh.github\" ? \"github\"\n : intent === \"mesh.gitlab\" ? \"gitlab\"\n : intent === \"mesh.a2a\" ? \"a2a\"\n : intent === \"mesh.voice\" ? \"voice\"\n : intent === \"mesh.desktop\" ? \"desktop\"\n : \"mesh\"\n return { id, name: senderName || senderUsername || id, avatar: initialsFor(display), kind }\n }\n }\n // 4. Bare A2A — no sender info but it IS an internal mesh dispatch.\n // Single bucket keeps the operator from confusing it with cron/system.\n if (intent === \"mesh.task\" || intent === \"mesh.a2a\") {\n return { id: \"__a2a\", name: \"Internal (A2A)\", avatar: \"↻\", kind: \"a2a\" }\n }\n return null\n }\n\n // ---- cron: name the job, never bucket it as anonymous \"Schedule\" ----\n if (source === \"cron\") {\n const jobId = typeof raw.jobId === \"string\" ? raw.jobId : null\n if (jobId) {\n return { id: `cron:${jobId}`, name: `Cron: ${jobId}`, avatar: \"⏱\", kind: \"cron\" }\n }\n }\n\n // ---- workflow: dig into the wrapped trigger payload first ----\n // Workflow events envelope the original event under raw.event.payload.\n // Two shapes seen in the wild:\n // - chat trigger: raw.event.payload.{channel,sender,fromJid}\n // - hook trigger: raw.event.payload.issueEvent.{user,project} (gitlab)\n // raw.event.payload.issueEvent.{sender,...} (github)\n // Without this, workflow events fall through to \"Schedule\" — exactly what\n // the user reported for an inbound WhatsApp message.\n if (source === \"workflow\") {\n const payload = raw.event?.payload\n if (payload && typeof payload === \"object\") {\n // Chat trigger (WhatsApp/Telegram/Slack/Discord wrapped by a workflow).\n if (payload.sender && typeof payload.sender === \"object\") {\n const s = payload.sender\n const username = typeof s.username === \"string\" ? s.username : null\n const name = typeof s.name === \"string\" ? s.name : null\n const id = username || (s.id != null ? String(s.id) : null) || name\n if (id && !isSystemSender(username || name || \"\")) {\n const chan = typeof payload.channel === \"string\" ? payload.channel : \"\"\n const kind = chatKindFor(chan)\n return { id, name: name || username || id, avatar: initialsFor(name || username || id), kind }\n }\n }\n // GitLab webhook nested in a workflow hook trigger.\n const issueEvent = payload.issueEvent || payload.webhookEvent\n if (issueEvent && typeof issueEvent === \"object\") {\n const u = issueEvent.user\n if (u && typeof u === \"object\" && (u.username || u.name)) {\n const id = u.username || u.name\n const name = u.name || u.username\n return { id, name, avatar: initialsFor(name), kind: \"gitlab\" }\n }\n // GitHub-shaped hook (sender.login)\n const ghs = issueEvent.sender\n if (ghs?.login) return { id: ghs.login, name: ghs.login, avatar: initialsFor(ghs.login), kind: \"github\" }\n }\n }\n // No upstream sender we can attribute → name the workflow itself.\n const wid =\n (typeof raw.workflowId === \"string\" && raw.workflowId) ||\n (typeof raw.runId === \"string\" && raw.runId) ||\n (typeof raw.workflow === \"string\" && raw.workflow) ||\n (typeof raw.event?.id === \"string\" && raw.event.id.split(\":\")[0])\n if (wid) return { id: `workflow:${wid}`, name: `Workflow: ${wid}`, avatar: \"▶\", kind: \"workflow\" }\n }\n\n // Chat-style: IncomingMessage shape with sender.{id,username,name}.\n if (raw.sender && typeof raw.sender === \"object\") {\n const s = raw.sender\n const username = typeof s.username === \"string\" ? s.username : null\n const name = typeof s.name === \"string\" ? s.name : null\n const id = username || (s.id != null ? String(s.id) : null) || name\n if (id) {\n const display = name || username || id\n return { id, name: display, avatar: initialsFor(display), kind: chatKindFor(source) }\n }\n }\n\n // GitLab webhook payload as-is.\n if (source === \"gitlab\") {\n const u = raw.user || raw.payload?.user\n if (u && typeof u === \"object\" && (u.username || u.name)) {\n const id = u.username || u.name\n const name = u.name || u.username\n return { id, name, avatar: initialsFor(name), kind: \"gitlab\" }\n }\n }\n\n // GitHub webhook payload — sender.login at top level.\n if (source === \"github\") {\n const s = raw.sender || raw.payload?.sender\n if (s?.login) return { id: s.login, name: s.login, avatar: initialsFor(s.login), kind: \"github\" }\n }\n\n // Last-ditch fallbacks for chat-style raw payloads we didn't catch above.\n if (source === \"telegram\") {\n const f = raw.message?.from || raw.from\n if (f?.username) {\n const display = f.first_name || f.username\n return { id: f.username, name: display, avatar: initialsFor(display), kind: \"telegram\" }\n }\n if (f?.id) return { id: String(f.id), name: f.first_name || `tg:${f.id}`, avatar: initialsFor(f.first_name || `tg${f.id}`), kind: \"telegram\" }\n }\n if (source === \"whatsapp\") {\n const f = raw.from || raw.payload?.from\n if (f) {\n const id = String(f)\n const name = id.replace(/@.*$/, \"\")\n return { id, name, avatar: initialsFor(name), kind: \"whatsapp\" }\n }\n }\n return null\n}\n\n/** Initiator origin — used by the UI to render a small pill next to the\n * initiator name (e.g. \"Alex · GitLab MR\", \"Cron: daily-brief\"). */\nexport type InitiatorKind =\n | \"telegram\" | \"whatsapp\" | \"slack\" | \"discord\"\n | \"gitlab\" | \"github\"\n | \"cron\" | \"workflow\"\n | \"mesh\" | \"a2a\"\n | \"voice\" | \"desktop\"\n | \"system\"\n\nfunction chatKindFor(source: string): InitiatorKind {\n if (source === \"telegram\" || source === \"whatsapp\" || source === \"slack\" || source === \"discord\") {\n return source\n }\n return \"system\"\n}\n\n/** Pseudo-senders the orchestrator generates on internal hops (the graph\n * classifier sub-call, hook fan-outs, etc.). These don't represent real\n * initiators — they're system traffic. Flag them so the activity graph\n * can group them under \"system\" instead of inventing a fake person. */\nfunction isSystemSender(s: string): boolean {\n if (!s) return false\n const v = s.toLowerCase()\n return (\n v === \"graph-classifier\" ||\n v === \"graph-agent\" ||\n v.startsWith(\"system:\") ||\n v.startsWith(\"internal:\") ||\n v.endsWith(\"-system\")\n )\n}\n\nfunction initialsFor(name: string): string {\n const parts = String(name || \"?\").trim().split(/\\s+/).filter(Boolean)\n if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase()\n return String(name || \"?\").slice(0, 2).toUpperCase()\n}\n\n/** Pull a short text preview of what triggered this event from its\n * raw payload. Each source stashes the human-meaningful text in a\n * different place; we try the most-likely paths in order and trim\n * to a fixed cap. Empty string when nothing readable was found. */\nfunction inputPreviewFrom(source: string, raw: any): string {\n if (!raw || typeof raw !== \"object\") return \"\"\n const candidates: Array<unknown> = []\n // Chat-style normalized IncomingMessage\n if (typeof raw.text === \"string\") candidates.push(raw.text)\n // Telegram raw payload\n if (raw.message?.text) candidates.push(raw.message.text)\n if (raw.message?.caption) candidates.push(raw.message.caption)\n // GitLab webhook\n if (raw.object_attributes?.note) candidates.push(raw.object_attributes.note)\n if (raw.object_attributes?.title) {\n const t = raw.object_attributes.title\n const d = raw.object_attributes.description\n candidates.push(d ? `${t}\\n${d}` : t)\n }\n // GitHub webhook\n if (raw.comment?.body) candidates.push(raw.comment.body)\n if (raw.issue?.title) candidates.push(raw.issue.title + (raw.issue.body ? `\\n${raw.issue.body}` : \"\"))\n if (raw.pull_request?.title) candidates.push(raw.pull_request.title)\n // mesh / cron / workflow envelopes\n if (typeof raw.message === \"string\") candidates.push(raw.message)\n if (typeof raw.prompt === \"string\") candidates.push(raw.prompt)\n if (typeof raw.input === \"string\") candidates.push(raw.input)\n if (raw.payload && typeof raw.payload.text === \"string\") candidates.push(raw.payload.text)\n if (raw.payload && typeof raw.payload.message === \"string\") candidates.push(raw.payload.message)\n\n for (const c of candidates) {\n if (typeof c === \"string\" && c.trim().length > 0) {\n const s = c.trim()\n return s.length > 220 ? s.slice(0, 217) + \"…\" : s\n }\n }\n return \"\"\n}\n\n// Trim \"merge_request:957:...\" → \"MR #957\", \"issue:42:...\" → \"issue #42\", etc.\nfunction shortSubject(subject: string | null): string {\n if (!subject) return \"(no subject)\"\n let m: RegExpMatchArray | null\n if ((m = subject.match(/^merge_request:(\\d+)(?::note:(\\d+))?/))) {\n return m[2] ? `MR #${m[1]} · note ${m[2]}` : `MR #${m[1]}`\n }\n if ((m = subject.match(/^issue:(\\d+)(?::note:(\\d+))?/))) {\n return m[2] ? `Issue #${m[1]} · note ${m[2]}` : `Issue #${m[1]}`\n }\n if ((m = subject.match(/^chat:(.+?):(.+)$/))) return `${m[1]} · ${m[2].slice(0, 40)}`\n if ((m = subject.match(/^mesh:agent:(.+)$/))) return `→ ${m[1]}`\n if ((m = subject.match(/^workflow:([^:]+):(.+)$/))) return `${m[1]} · ${m[2].slice(0, 30)}`\n return subject.length > 60 ? subject.slice(0, 57) + \"…\" : subject\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot builder\n\nfunction buildFleetSnapshot(db: Database.Database, daemonConfig: DaemonConfig | null, windowH: number): FleetSnapshot {\n const now = Date.now()\n const sinceMs = now - windowH * 3600 * 1000\n\n // Pull events in the window.\n const events = db.prepare(`\n SELECT id, ts, source, project, subject, intent, raw_json\n FROM intent_events WHERE ts >= ? ORDER BY ts ASC\n `).all(sinceMs) as Array<{ id: string; ts: number; source: string; project: string | null; subject: string | null; intent: string | null; raw_json: string }>\n\n // Decisions for those events\n const decisionsByEvent = new Map<string, Array<{ event_id: string; decided_at: number; decided_by: string; agent_id: string | null; outcome: string; reason: string | null }>>()\n const resolutionsByDecision = new Map<string, { resolved_at: number; status: string; duration_ms: number | null; result_summary: string | null }>()\n\n if (events.length > 0) {\n const ids = events.map((e) => e.id)\n const placeholders = ids.map((_, i) => `@id${i}`).join(\",\")\n const params = Object.fromEntries(ids.map((id, i) => [`id${i}`, id]))\n\n const decRows = db.prepare(`\n SELECT event_id, decided_at, decided_by, agent_id, outcome, reason\n FROM intent_decisions WHERE event_id IN (${placeholders}) ORDER BY decided_at ASC\n `).all(params) as Array<{ event_id: string; decided_at: number; decided_by: string; agent_id: string | null; outcome: string; reason: string | null }>\n for (const d of decRows) {\n if (!decisionsByEvent.has(d.event_id)) decisionsByEvent.set(d.event_id, [])\n decisionsByEvent.get(d.event_id)!.push(d)\n }\n\n const resRows = db.prepare(`\n SELECT decision_event_id, decision_decided_by, resolved_at, status, duration_ms, result_summary\n FROM intent_resolutions WHERE decision_event_id IN (${placeholders})\n `).all(params) as Array<{ decision_event_id: string; decision_decided_by: string; resolved_at: number; status: string; duration_ms: number | null; result_summary: string | null }>\n for (const r of resRows) {\n resolutionsByDecision.set(`${r.decision_event_id}|${r.decision_decided_by}`, r)\n }\n }\n\n // Build entity caches\n const clientMap = new Map<string, FleetClient>()\n const projectsByClient = new Map<string, Set<string>>()\n const channelMap = new Map<string, FleetChannel>()\n const initiatorMap = new Map<string, FleetInitiator>()\n const dispatches: FleetDispatch[] = []\n\n // Pull config knobs once per snapshot.\n const businessProjects = (daemonConfig as any)?.business?.projects ?? []\n const contactMap: ContactRule[] = (daemonConfig as any)?.business?.contactMap ?? []\n const agentToClient = agentClients(((daemonConfig as any)?.business) ?? {})\n\n for (const ev of events) {\n let raw: any = null\n try { raw = JSON.parse(ev.raw_json) } catch { /* ignore */ }\n\n // Resolve client + project. Attribution precedence:\n // 1. contactMap match (operator-explicit)\n // 2. ev.project namespace prefix (with projects[].client override)\n // 3. agent's default client via orgChart (agent reportsTo PM whose\n // project has a known client — covers mesh dispatches with no\n // project metadata, like a2a calls to globex-v2)\n // 4. \"unmapped\" fallback\n //\n // Step 3 is computed per-decision below since it's agent-dependent.\n const contact = matchContact(ev.source, raw, contactMap)\n const inputPreview = inputPreviewFrom(ev.source, raw)\n const project = ev.project ?? inferProject(raw, ev.subject, inputPreview)\n let baseClientId: string | null = null\n let baseProjectId: string | null = null\n if (contact) {\n baseClientId = contact.client\n baseProjectId = contact.project || `${contact.client}/_chat`\n } else if (project) {\n baseClientId = clientFromProject(project, businessProjects)\n baseProjectId = project\n }\n\n // Origin channel — what the operator thinks of as \"where this came\n // from\". For mesh-routed events the source is \"mesh\" but the user\n // experiences a GitLab MR / GitHub push / a2a hop; collapse to the\n // upstream channel via raw.context.channel or the intent suffix.\n const upstream = upstreamChannel(ev.source, ev.intent || \"\", raw)\n const chanDef = CHANNEL_DEF[upstream] || { label: upstream, color: \"#6b7280\" }\n if (!channelMap.has(upstream)) channelMap.set(upstream, { id: upstream, label: chanDef.label, color: chanDef.color })\n\n let initiatorId = \"__system\"\n let initiatorKind: InitiatorKind | undefined\n const init = initiatorFrom(ev.source, ev.intent || \"\", raw)\n if (init) {\n initiatorId = init.id\n initiatorKind = init.kind\n // Contact-map can override the display name (e.g. map \"rivera\" → \"Alex\")\n const display = contact?.displayName || init.name\n if (!initiatorMap.has(init.id) || contact?.displayName) {\n initiatorMap.set(init.id, { id: init.id, name: display, avatar: initialsFor(display), kind: init.kind })\n }\n } else if (!initiatorMap.has(\"__system\")) {\n initiatorMap.set(\"__system\", { id: \"__system\", name: \"Schedule\", avatar: \"⏱\", kind: \"system\" })\n }\n\n // For each dispatched decision on this event, emit a dispatch row.\n // Client/project resolves per-decision because the orgChart fallback\n // depends on which agent received the dispatch.\n for (const d of decisionsByEvent.get(ev.id) ?? []) {\n if (d.outcome !== \"dispatched\" || !d.agent_id) continue\n const resKey = `${ev.id}|${d.decided_by}`\n const res = resolutionsByDecision.get(resKey)\n const startedAt = d.decided_at\n const resolvedAt = res?.resolved_at ?? null\n const active = resolvedAt === null\n const duration = res?.duration_ms ?? (active ? now - startedAt : 0)\n const outcome: FleetDispatch[\"outcome\"] = active\n ? \"active\"\n : (res?.status === \"error\" || res?.status === \"fail\" ? \"error\" : \"completed\")\n\n // Resolved client/project: contactMap > project > orgChart > unmapped\n let clientId: string\n let projectId: string\n if (baseClientId) {\n clientId = baseClientId\n projectId = baseProjectId!\n } else {\n const orgClient = agentToClient.get(d.agent_id)\n if (orgClient) {\n clientId = orgClient\n projectId = `${orgClient}/_${ev.source}`\n } else {\n clientId = \"unmapped\"\n projectId = `unmapped/_${ev.source}`\n }\n }\n\n if (!clientMap.has(clientId)) {\n clientMap.set(clientId, { id: clientId, name: clientId, color: colorForClient(clientId), projects: [] })\n }\n if (!projectsByClient.has(clientId)) projectsByClient.set(clientId, new Set())\n projectsByClient.get(clientId)!.add(projectId)\n\n dispatches.push({\n id: `${ev.id}|${d.decided_by}`,\n agentId: d.agent_id,\n clientId,\n projectId,\n channelId: upstream,\n initiatorId,\n initiatorKind,\n subject: shortSubject(ev.subject),\n intent: ev.intent || \"\",\n startedAt,\n resolvedAt,\n duration,\n active,\n outcome,\n tokens: 0,\n inputPreview,\n system: isSystemAgent(d.agent_id, daemonConfig),\n })\n }\n }\n\n // Materialize clients with their project list\n const clients: FleetClient[] = [...clientMap.values()].map((c) => ({\n ...c,\n projects: [...(projectsByClient.get(c.id) || [])].sort(),\n })).sort((a, b) => a.id.localeCompare(b.id))\n\n // Agents: union of (configured agents) + (any agent that appears in dispatches)\n const agents: FleetAgent[] = []\n const agentSeen = new Set<string>()\n if (daemonConfig) {\n const business = (daemonConfig as any).business ?? {}\n const titleOf = (id: string): string | undefined => {\n const role = business.orgChart?.[id]?.role\n const title = role ? business.roles?.[role]?.title : undefined\n return typeof title === \"string\" && title.trim() ? title.trim() : undefined\n }\n for (const [id, def] of Object.entries(daemonConfig.agents || {})) {\n const a: FleetAgent = {\n id,\n name: def.name || id,\n tier: def.tier === \"claude-code\" ? \"lead\" : \"worker\",\n model: def.model || \"\",\n role: def.systemPrompt ? def.systemPrompt.split(/[.\\n]/)[0].slice(0, 60) : (def.tier || \"\"),\n title: titleOf(id),\n }\n agents.push(a)\n agentSeen.add(id)\n }\n }\n for (const d of dispatches) {\n if (!agentSeen.has(d.agentId)) {\n agents.push({ id: d.agentId, name: d.agentId, tier: \"worker\", model: \"\", role: \"\" })\n agentSeen.add(d.agentId)\n }\n }\n agents.sort((a, b) => a.id.localeCompare(b.id))\n\n // Channels: include all known kinds (so the Flow view shows zero-traffic\n // channels too) plus any unknown sources we observed.\n const channels: FleetChannel[] = []\n const channelSeen = new Set<string>()\n for (const [id, def] of Object.entries(CHANNEL_DEF)) {\n channels.push({ id, label: def.label, color: def.color })\n channelSeen.add(id)\n }\n for (const c of channelMap.values()) {\n if (!channelSeen.has(c.id)) channels.push(c)\n }\n\n const initiators: FleetInitiator[] = [...initiatorMap.values()]\n if (!initiators.find((i) => i.id === \"__system\")) {\n initiators.push({ id: \"__system\", name: \"Schedule\", avatar: \"⏱\", kind: \"system\" })\n }\n\n const gitlabHost = (daemonConfig as any)?.channels?.gitlab?.host\n return {\n now,\n windowH,\n clients,\n agents,\n channels,\n initiators,\n dispatches,\n forges: { gitlab: typeof gitlabHost === \"string\" ? gitlabHost : undefined, github: \"https://github.com\" },\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTTP handlers\n\n/** Compute the local-only snapshot without going through HTTP. Lets the\n * fleet merger call us in-process for the local node instead of hitting\n * ourselves over the wire. */\nexport function buildLocalActivityGraphSnapshot(windowH: number): FleetSnapshot | null {\n const opened = openLedger()\n if (!opened) return null\n try {\n return buildFleetSnapshot(opened.db, _daemonConfigRef, windowH)\n } finally {\n opened.close()\n }\n}\n\n/** Merge several activity-graph snapshots into one. Each input is tagged\n * with the nodeId it came from; that nodeId is stamped onto every\n * dispatch so the UI can render a small node badge. Clients, agents,\n * channels, and initiators dedupe by id (first writer wins for the\n * display fields like color/name) — they're concept-level entities,\n * not node-local processes, so a `globex` client on Mac and a `globex`\n * client on peer should collapse into one row. Agents share names\n * across nodes too, but the dispatch's nodeId carries the disambiguation. */\nexport function mergeFleetSnapshots(parts: Array<{ nodeId: string; snap: FleetSnapshot }>): FleetSnapshot {\n const clients = new Map<string, FleetClient>()\n const agents = new Map<string, FleetAgent>()\n const channels = new Map<string, FleetChannel>()\n const initiators = new Map<string, FleetInitiator>()\n const dispatches: FleetDispatch[] = []\n let now = 0\n let windowH = 0\n const forges: NonNullable<FleetSnapshot[\"forges\"]> = {}\n for (const { nodeId, snap } of parts) {\n forges.gitlab ??= snap.forges?.gitlab\n forges.github ??= snap.forges?.github\n if (snap.now > now) now = snap.now\n if (snap.windowH > windowH) windowH = snap.windowH\n for (const c of snap.clients) {\n const existing = clients.get(c.id)\n if (!existing) clients.set(c.id, { ...c, projects: [...c.projects] })\n else {\n // Merge project lists across nodes; dedup with a Set.\n const merged = Array.from(new Set([...existing.projects, ...c.projects])).sort()\n clients.set(c.id, { ...existing, projects: merged })\n }\n }\n for (const a of snap.agents) if (!agents.has(a.id)) agents.set(a.id, a)\n for (const ch of snap.channels) if (!channels.has(ch.id)) channels.set(ch.id, ch)\n for (const i of snap.initiators) if (!initiators.has(i.id)) initiators.set(i.id, i)\n for (const raw of snap.dispatches) {\n const d = reattribute(raw)\n if (d.clientId !== raw.clientId && !clients.has(d.clientId)) {\n clients.set(d.clientId, { id: d.clientId, name: d.clientId, color: colorForClient(d.clientId), projects: [] })\n }\n if (d.projectId !== raw.projectId) {\n const c = clients.get(d.clientId)!\n if (!c.projects.includes(d.projectId)) clients.set(d.clientId, { ...c, projects: [...c.projects, d.projectId].sort() })\n }\n // Tag the dispatch row with its source node so the UI can show a badge.\n // Also rewrite the dispatch id to be globally unique — peers share the\n // same `<eventId>|<decidedBy>` shape, and a collision would make the\n // browser's React key reuse the wrong row across snapshots.\n dispatches.push({ ...d, nodeId, id: `${nodeId}::${d.id}` })\n }\n }\n return {\n now: now || Date.now(),\n windowH: windowH || 6,\n localNodeId: parts[0]?.nodeId,\n forges,\n clients: [...clients.values()].sort((a, b) => a.id.localeCompare(b.id)),\n agents: [...agents.values()].sort((a, b) => a.id.localeCompare(b.id)),\n channels: [...channels.values()],\n initiators: [...initiators.values()],\n dispatches: dispatches.sort((a, b) => b.startedAt - a.startedAt),\n }\n}\n\n/** Peers running older code leave relayed webhooks \"unmapped\" even though\n * the message names the project. Re-read it so the merged view is true. */\nfunction reattribute(d: FleetDispatch): FleetDispatch {\n if (d.clientId !== \"unmapped\") return d\n const project = projectFromPreview(d.inputPreview)\n if (!project) return d\n const businessProjects = (_daemonConfigRef as any)?.business?.projects ?? []\n return { ...d, projectId: project, clientId: clientFromProject(project, businessProjects) }\n}\n\n/** Attach live issue/MR/pipeline state for the refs in the window. A forge\n * outage leaves `forge` partial; the snapshot itself never fails on it. */\nexport async function withForgeStatus(snap: FleetSnapshot): Promise<FleetSnapshot> {\n const gl = (_daemonConfigRef as any)?.channels?.gitlab\n const forge = await fetchForgeStatus(refsToLookUp(snap.dispatches), {\n gitlab: gl?.host && gl?.token ? { host: gl.host, token: gl.token } : undefined,\n githubToken: process.env.GITHUB_TOKEN || undefined,\n })\n return { ...snap, forge }\n}\n\nlet _daemonConfigRef: DaemonConfig | null = null\n/** Wired by board-dashboard.ts when the dashboard starts so we can read\n * agent metadata (tier, model, name) for the snapshot. */\nexport function setDaemonConfigForActivityGraph(cfg: DaemonConfig | null): void {\n _daemonConfigRef = cfg\n}\n\nexport async function handleActivityGraphApi(req: IncomingMessage, res: ServerResponse, path: string): Promise<boolean> {\n if (path !== \"/api/admin/activity-graph\" && !path.startsWith(\"/api/admin/activity-graph?\")) return false\n const opened = openLedger()\n if (!opened) {\n sendJson(res, 503, { error: \"intent ledger not available\", clients: [], agents: [], channels: [], initiators: [], dispatches: [] })\n return true\n }\n try {\n const url = new URL(req.url || \"/\", \"http://_\")\n const windowH = clampWindow(parseInt(url.searchParams.get(\"hours\") || \"6\", 10))\n sendJson(res, 200, await withForgeStatus(buildFleetSnapshot(opened.db, _daemonConfigRef, windowH)))\n } catch (e: any) {\n sendJson(res, 500, { error: e?.message ?? String(e) })\n } finally {\n opened.close()\n }\n return true\n}\n\nconst TICK_MS = 5000\n\n/** `build` defaults to the local ledger; the dashboard passes the fleet\n * merger in fleet mode so live updates don't collapse the view to one node. */\nexport async function handleActivityGraphStream(\n req: IncomingMessage,\n res: ServerResponse,\n path: string,\n build: (windowH: number) => FleetSnapshot | null | Promise<FleetSnapshot | null> = buildLocalActivityGraphSnapshot,\n): Promise<boolean> {\n if (path !== \"/api/admin/activity-graph/stream\" && !path.startsWith(\"/api/admin/activity-graph/stream?\")) return false\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n \"Connection\": \"keep-alive\",\n \"X-Accel-Buffering\": \"no\",\n })\n res.write(`: connected\\n\\n`)\n\n const url = new URL(req.url || \"/\", \"http://_\")\n const windowH = clampWindow(parseInt(url.searchParams.get(\"hours\") || \"6\", 10))\n\n let stopped = false\n const stop = () => { stopped = true }\n req.on(\"close\", stop); req.on(\"error\", stop)\n res.on(\"close\", stop); res.on(\"error\", stop)\n const hb = setInterval(() => { if (!stopped) try { res.write(`: hb\\n\\n`) } catch { stop() } }, 25_000)\n\n try {\n while (!stopped) {\n try {\n const built = await build(windowH)\n const snap = built && await withForgeStatus(built)\n if (stopped) break\n if (snap) {\n res.write(`event: snapshot\\n`)\n res.write(`data: ${JSON.stringify(snap)}\\n\\n`)\n } else {\n res.write(`event: error\\ndata: ${JSON.stringify({ error: \"ledger not available\" })}\\n\\n`)\n }\n } catch (e: any) {\n res.write(`event: error\\ndata: ${JSON.stringify({ error: e?.message ?? String(e) })}\\n\\n`)\n }\n const start = Date.now()\n while (!stopped && Date.now() - start < TICK_MS) await new Promise((r) => setTimeout(r, 200))\n }\n } finally {\n clearInterval(hb)\n try { res.end() } catch { /* nothing */ }\n }\n return true\n}\n\nfunction clampWindow(h: number): number {\n if (!Number.isFinite(h) || h < 1) return 6\n return Math.min(720, Math.max(1, h))\n}\n\n// ---------------------------------------------------------------------------\n// Dispatch detail — joins the file-based task-history JSON to recover the\n// full input + agent's response for the drawer. The ledger doesn't store\n// responses (only resolutions), so we look them up by (agentId, startedAt±)\n// in .agentx/task-history/<agent>/<day>/*.json.\n\ninterface TaskRecordLite {\n id?: string\n agentId?: string\n channel?: string\n message?: string\n responseText?: string\n startedAt?: string | number\n durationMs?: number\n ok?: boolean\n}\n\nfunction findTaskRecord(agentId: string, startedAtMs: number, fuzzMs = 5_000): TaskRecordLite | null {\n const day = new Date(startedAtMs).toISOString().slice(0, 10)\n const dir = resolve(process.cwd(), \".agentx/task-history\", agentId, day)\n if (!existsSync(dir)) {\n // Try yesterday too — UTC day boundary can put a 22:59 dispatch on\n // either side depending on local TZ.\n const yDay = new Date(startedAtMs - 86400_000).toISOString().slice(0, 10)\n const yDir = resolve(process.cwd(), \".agentx/task-history\", agentId, yDay)\n if (!existsSync(yDir)) return null\n return scanDir(yDir, startedAtMs, fuzzMs)\n }\n return scanDir(dir, startedAtMs, fuzzMs)\n}\n\nfunction scanDir(dir: string, startedAtMs: number, fuzzMs: number): TaskRecordLite | null {\n let best: TaskRecordLite | null = null\n let bestDelta = Infinity\n for (const f of readdirSync(dir)) {\n if (!f.endsWith(\".json\")) continue\n let parsed: TaskRecordLite\n try { parsed = JSON.parse(readFileSync(join(dir, f), \"utf-8\")) }\n catch { continue }\n const ts = parsed.startedAt\n let recMs: number\n if (typeof ts === \"number\") recMs = ts\n else if (typeof ts === \"string\") {\n const n = Date.parse(ts)\n if (!Number.isFinite(n)) continue\n recMs = n\n } else continue\n const d = Math.abs(recMs - startedAtMs)\n if (d < bestDelta && d <= fuzzMs) {\n best = parsed\n bestDelta = d\n }\n }\n return best\n}\n\nexport async function handleActivityGraphDetail(req: IncomingMessage, res: ServerResponse, path: string): Promise<boolean> {\n const m = path.match(/^\\/api\\/admin\\/activity-graph\\/dispatch\\/([^/?]+)(?:\\?.*)?$/)\n if (!m) return false\n const dispatchId = decodeURIComponent(m[1])\n const sep = dispatchId.indexOf(\"|\")\n if (sep < 0) { sendJson(res, 400, { error: \"invalid dispatch id\" }); return true }\n const eventId = dispatchId.slice(0, sep)\n const decidedBy = dispatchId.slice(sep + 1)\n\n const opened = openLedger()\n if (!opened) { sendJson(res, 503, { error: \"ledger not available\" }); return true }\n try {\n const ev = opened.db\n .prepare(`SELECT id, ts, source, project, subject, intent, raw_json FROM intent_events WHERE id = ?`)\n .get(eventId) as { id: string; ts: number; source: string; project: string | null; subject: string | null; intent: string | null; raw_json: string } | undefined\n if (!ev) { sendJson(res, 404, { error: \"event not found\" }); return true }\n\n const dec = opened.db\n .prepare(`SELECT decided_at, decided_by, agent_id, outcome, reason FROM intent_decisions WHERE event_id = ? AND decided_by = ?`)\n .get(eventId, decidedBy) as { decided_at: number; decided_by: string; agent_id: string | null; outcome: string; reason: string | null } | undefined\n if (!dec) { sendJson(res, 404, { error: \"decision not found\" }); return true }\n\n const resn = opened.db\n .prepare(`SELECT resolved_at, status, duration_ms, result_summary FROM intent_resolutions WHERE decision_event_id = ? AND decision_decided_by = ?`)\n .get(eventId, decidedBy) as { resolved_at: number; status: string; duration_ms: number | null; result_summary: string | null } | undefined\n\n let raw: any = null\n try { raw = JSON.parse(ev.raw_json) } catch { /* ignore */ }\n const fullInput = inputPreviewFrom(ev.source, raw) // returns truncated; pull a fuller version below\n const longInput =\n (raw && typeof raw === \"object\" && (raw.text || raw.message?.text || raw.message?.caption || raw.object_attributes?.note || raw.object_attributes?.description || raw.comment?.body || raw.issue?.body || (typeof raw.message === \"string\" ? raw.message : null) || (typeof raw.prompt === \"string\" ? raw.prompt : null) || null)) || fullInput\n\n let response: string | null = null\n let transcriptLen: number | null = null\n if (dec.agent_id) {\n const rec = findTaskRecord(dec.agent_id, dec.decided_at)\n if (rec) {\n if (typeof rec.responseText === \"string\") response = rec.responseText\n const t = (rec as any).transcript\n if (Array.isArray(t)) transcriptLen = t.length\n }\n }\n\n // Re-derive the attribution chain so the drawer can explain why a\n // particular client was assigned. Same precedence the snapshot uses;\n // we just record which step won.\n const businessProjects = (_daemonConfigRef as any)?.business?.projects ?? []\n const contactMap: ContactRule[] = (_daemonConfigRef as any)?.business?.contactMap ?? []\n const agentToClient = agentClients(((_daemonConfigRef as any)?.business) ?? {})\n let attribution: { client: string; project: string; via: string } = { client: \"unmapped\", project: `unmapped/_${ev.source}`, via: \"fallback\" }\n const contact = matchContact(ev.source, raw, contactMap)\n if (contact) {\n attribution = {\n client: contact.client,\n project: contact.project || `${contact.client}/_chat`,\n via: `contactMap (channel=${contact.channel ?? \"*\"}${contact.username ? ` username=${contact.username}` : \"\"}${contact.chatId ? ` chatId=${contact.chatId}` : \"\"})`,\n }\n } else if (ev.project || inferProject(raw, ev.subject, fullInput)) {\n const project = ev.project || inferProject(raw, ev.subject, fullInput)!\n attribution = {\n client: clientFromProject(project, businessProjects),\n project,\n via: ev.project ? `project namespace prefix (${project})` : `forge path in chat id / message (${project})`,\n }\n } else if (dec.agent_id) {\n const orgClient = agentToClient.get(dec.agent_id)\n if (orgClient) {\n attribution = {\n client: orgClient,\n project: `${orgClient}/_${ev.source}`,\n via: `orgChart fallback (agent ${dec.agent_id} reports up to a PM whose project belongs to ${orgClient})`,\n }\n }\n }\n\n sendJson(res, 200, {\n id: dispatchId,\n eventId,\n source: ev.source,\n subject: ev.subject,\n intent: ev.intent,\n project: ev.project,\n decidedBy: dec.decided_by,\n agentId: dec.agent_id,\n outcome: dec.outcome,\n reason: dec.reason,\n startedAt: dec.decided_at,\n resolvedAt: resn?.resolved_at ?? null,\n durationMs: resn?.duration_ms ?? null,\n input: longInput,\n response,\n transcriptLen,\n resolutionStatus: resn?.status ?? null,\n resultSummary: resn?.result_summary ?? null,\n attribution,\n })\n } catch (e: any) {\n sendJson(res, 500, { error: e?.message ?? String(e) })\n } finally {\n opened.close()\n }\n return true\n}\n","// --- Which project is a dispatch about? ---\n//\n// Most mesh-routed events arrive with `project = null`: the GitLab/GitHub\n// channel forwards the forge path inside the chat id\n// (\"noqta/minbar:issue:152\", \"owner/repo:pull:16\") and peers that relay a\n// webhook as bare A2A only mention it in the message text\n// (\"[GitLab noqta/minbar MR !51 update]: …\"). Without reading those, every\n// dispatch lands on \"unmapped\". Kept separate from the snapshot builder so\n// the fleet merger can re-attribute rows from peers running older code.\n\nconst FORGE_PATH = String.raw`[\\w.-]+(?:/[\\w.-]+)+`\nconst CHAT_ID_RE = new RegExp(`^(?:chat:)?(${FORGE_PATH}):(?:issue|issues|merge_request|mr|pull|pr)s?:\\\\d+`)\nconst PREVIEW_RE = new RegExp(`^\\\\[(?:GitLab|GitHub) (${FORGE_PATH}) `)\n\n/** Forge project path from a chat id or subject, e.g.\n * \"chat:noqta/minbar:merge_request:51\" → \"noqta/minbar\". */\nexport function projectFromChatId(chatId: string | null | undefined): string | null {\n if (!chatId) return null\n const m = chatId.match(CHAT_ID_RE)\n return m ? m[1] : null\n}\n\n/** Forge project path from a relayed webhook summary line. */\nexport function projectFromPreview(text: string | null | undefined): string | null {\n if (!text) return null\n const m = text.trimStart().match(PREVIEW_RE)\n return m ? m[1] : null\n}\n\n/** Best project guess for an event whose ledger row has no project:\n * explicit forwarded context, then the chat id, then the message text. */\nexport function inferProject(raw: any, subject: string | null, preview: string): string | null {\n const ctx = raw && typeof raw === \"object\" ? raw.context : null\n if (ctx && typeof ctx.project === \"string\" && ctx.project.trim()) return ctx.project.trim()\n return (\n projectFromChatId(typeof ctx?.chatId === \"string\" ? ctx.chatId : null) ??\n projectFromChatId(subject) ??\n projectFromPreview(preview)\n )\n}\n\n// --- Which issues / MRs is a dispatch about? ---\n\nexport interface ForgeRef { kind: \"issue\" | \"mr\"; n: number }\n\n/** Forge path of a project id, or null for synthetic ids (\"mtgl/_mesh\"). */\nexport function forgePath(projectId: string): string | null {\n const [head, ...rest] = projectId.split(\"/\")\n if (!rest.length || head === \"unmapped\" || rest[0].startsWith(\"_\")) return null\n return projectId\n}\n\nexport function refKey(projectId: string, r: ForgeRef): string {\n return `${projectId}${r.kind === \"mr\" ? \"!\" : \"#\"}${r.n}`\n}\n\nconst SUBJECT_REF_RE = /\\b(issue|merge_request|pull|MR|Issue)s?[:\\s]+[#!]?(\\d+)/\nconst HEADER_REF_RE = /^\\[(?:GitLab|GitHub) \\S+ (issue|merge_request|pull|MR|Issue)s? [#!]?(\\d+)/i\nconst kindOf = (word: string): ForgeRef[\"kind\"] => (/^issue/i.test(word) ? \"issue\" : \"mr\")\n\n/** The one issue / MR / PR a dispatch is about: the subject first\n * (\"issue:152\", \"MR #51\", \"pull:16\"), then only the leading header of a\n * relayed webhook (\"[GitLab ns/repo MR !51 update]\") — a body can mention\n * any number of unrelated MRs. */\nexport function primaryRef(d: { subject: string; inputPreview: string }): ForgeRef | null {\n const m = d.subject.match(SUBJECT_REF_RE) ?? d.inputPreview.trimStart().match(HEADER_REF_RE)\n return m ? { kind: kindOf(m[1]), n: Number(m[2]) } : null\n}\n\n/** MRs a hand-off names in its text (\"review !445–!448\"). Only for\n * dispatches without a primary ref — those are about one thing. */\nexport function mentionedMrs(text: string, limit = 8): ForgeRef[] {\n const out: ForgeRef[] = []\n const seen = new Set<number>()\n for (const m of text.matchAll(/(?:^|[\\s(,])!(\\d{1,6})\\b/g)) {\n const n = Number(m[1])\n if (seen.has(n)) continue\n seen.add(n)\n out.push({ kind: \"mr\", n })\n if (out.length >= limit) break\n }\n return out\n}\n\n/** Every ref a dispatch is about, primary first. Empty when the project\n * has no forge path — a bare \"!445\" could belong to any repo. */\nexport function refsOf(d: { subject: string; inputPreview: string; projectId: string }): ForgeRef[] {\n if (!forgePath(d.projectId)) return []\n const primary = primaryRef(d)\n return primary ? [primary] : mentionedMrs(d.inputPreview)\n}\n","// --- Live state of the issues / MRs the fleet is working on ---\n//\n// The Fleet Map's line status (\"Delays · 3 pipelines failed\", \"1 held\")\n// has to come from the forge, not from the ledger: a run can finish fine\n// while the MR it pushed fails CI. For the refs named by dispatches in the\n// window, read MR/PR/issue state and the head pipeline. Cached per ref so\n// the 5 s snapshot stream costs at most one forge call per ref per TTL.\n\nimport { refKey, refsOf, type ForgeRef } from \"./activity-graph-attribution\"\n\nexport interface ForgeItem {\n kind: \"issue\" | \"mr\"\n project: string\n n: number\n title: string\n /** opened | merged | closed */\n state: string\n draft: boolean\n url: string\n /** Head pipeline (GitLab MRs only): success | failed | running | pending | canceled | … */\n pipeline: { status: string; url: string } | null\n mergedAt: number | null\n}\n\nexport interface ForgeConfig {\n gitlab?: { host: string; token: string }\n githubToken?: string\n}\n\ntype Fetch = typeof fetch\ninterface Want { project: string; ref: ForgeRef; github: boolean }\n\nconst TTL_MS = 60_000\nconst GITHUB_TTL_MS = 5 * 60_000 // unauthenticated GitHub allows 60 calls/hour\nconst MAX_REFS = 40\nconst CONCURRENCY = 6\n\nconst cache = new Map<string, { at: number; item: ForgeItem | null }>()\n\n/** Refs worth looking up, newest dispatches first, capped. */\nexport function refsToLookUp(dispatches: Array<{ subject: string; inputPreview: string; projectId: string; channelId: string; startedAt: number }>): Want[] {\n const out = new Map<string, Want>()\n const sorted = [...dispatches].sort((a, b) => b.startedAt - a.startedAt)\n for (const d of sorted) {\n const github = d.channelId === \"github\" || /\\bpull:\\d+/.test(d.subject)\n for (const ref of refsOf(d)) {\n const key = refKey(d.projectId, ref)\n if (!out.has(key)) out.set(key, { project: d.projectId, ref, github })\n if (out.size >= MAX_REFS) return [...out.values()]\n }\n }\n return [...out.values()]\n}\n\nexport async function fetchForgeStatus(\n wants: Want[],\n cfg: ForgeConfig,\n fetchImpl: Fetch = fetch,\n now = Date.now(),\n): Promise<Record<string, ForgeItem>> {\n const result: Record<string, ForgeItem> = {}\n const todo: Want[] = []\n for (const w of wants) {\n const key = refKey(w.project, w.ref)\n const hit = cache.get(key)\n if (hit && now - hit.at < (w.github ? GITHUB_TTL_MS : TTL_MS)) {\n if (hit.item) result[key] = hit.item\n } else todo.push(w)\n }\n for (let i = 0; i < todo.length; i += CONCURRENCY) {\n await Promise.all(todo.slice(i, i + CONCURRENCY).map(async (w) => {\n const key = refKey(w.project, w.ref)\n let item: ForgeItem | null = null\n try {\n item = w.github ? await fetchGithub(w, cfg, fetchImpl) : await fetchGitlab(w, cfg, fetchImpl)\n } catch {\n // Unreachable forge: the map shows the ref without a status rather\n // than failing the snapshot. Cached like a miss so we don't hammer it.\n }\n cache.set(key, { at: now, item })\n if (item) result[key] = item\n }))\n }\n return result\n}\n\nasync function getJson(url: string, headers: Record<string, string>, fetchImpl: Fetch): Promise<any | null> {\n const ac = new AbortController()\n const timer = setTimeout(() => ac.abort(), 5000)\n try {\n const r = await fetchImpl(url, { headers, signal: ac.signal })\n return r.ok ? await r.json() : null\n } finally {\n clearTimeout(timer)\n }\n}\n\nasync function fetchGitlab(w: Want, cfg: ForgeConfig, fetchImpl: Fetch): Promise<ForgeItem | null> {\n const gl = cfg.gitlab\n if (!gl?.host || !gl.token) return null\n const base = `${gl.host.replace(/\\/+$/, \"\")}/api/v4/projects/${encodeURIComponent(w.project)}`\n const path = w.ref.kind === \"mr\" ? `merge_requests/${w.ref.n}` : `issues/${w.ref.n}`\n const j = await getJson(`${base}/${path}`, { \"PRIVATE-TOKEN\": gl.token }, fetchImpl)\n if (!j) return null\n const hp = j.head_pipeline ?? j.pipeline\n return {\n kind: w.ref.kind, project: w.project, n: w.ref.n,\n title: String(j.title ?? \"\"),\n state: String(j.state ?? \"opened\"),\n draft: Boolean(j.draft ?? j.work_in_progress),\n url: String(j.web_url ?? \"\"),\n pipeline: hp && typeof hp.status === \"string\" ? { status: hp.status, url: String(hp.web_url ?? \"\") } : null,\n mergedAt: j.merged_at ? Date.parse(j.merged_at) : null,\n }\n}\n\nasync function fetchGithub(w: Want, cfg: ForgeConfig, fetchImpl: Fetch): Promise<ForgeItem | null> {\n const path = w.ref.kind === \"mr\" ? `pulls/${w.ref.n}` : `issues/${w.ref.n}`\n const headers: Record<string, string> = { Accept: \"application/vnd.github+json\", \"User-Agent\": \"agentx-dashboard\" }\n if (cfg.githubToken) headers.Authorization = `Bearer ${cfg.githubToken}`\n const j = await getJson(`https://api.github.com/repos/${w.project}/${path}`, headers, fetchImpl)\n if (!j) return null\n return {\n kind: w.ref.kind, project: w.project, n: w.ref.n,\n title: String(j.title ?? \"\"),\n state: j.merged_at ? \"merged\" : j.state === \"open\" ? \"opened\" : String(j.state ?? \"opened\"),\n draft: Boolean(j.draft),\n url: String(j.html_url ?? \"\"),\n pipeline: null,\n mergedAt: j.merged_at ? Date.parse(j.merged_at) : null,\n }\n}\n\n/** Test hook. */\nexport function clearForgeCache(): void {\n cache.clear()\n}\n","import type { IncomingMessage, ServerResponse } from \"http\"\nimport { existsSync, readFileSync } from \"fs\"\nimport { resolve } from \"path\"\nimport {\n listAgentFiles,\n readAgentFile,\n writeAgentFile,\n createAgentSkill,\n deleteAgentSkill,\n listSkillTree,\n readSkillFile,\n writeSkillFile,\n detectSkillDeps,\n} from \"./file-ops\"\nimport { HandoverStore } from \"@/channels/handover-store\"\nimport { loadDaemonConfig } from \"./config\"\nimport { mutateAgentxConfig } from \"./config-mutate\"\nimport type { TopbarPeer } from \"./topbar\"\nimport { renderAgentPage } from \"./ui/pages/agent\"\n\n// --- /admin/agents/<id> — one dedicated page per agent -------------------\n//\n// Layout:\n// Header — name, tier, model, mentions, access\n// Overview — editable systemPrompt + metadata form\n// Identity — CLAUDE.md / SOUL.md / IDENTITY.md etc, EasyMDE editor\n// Skills — directory tree per skill, file editor, install/deps actions\n// Channels — read-only list of bindings\n// Activity — recent tasks, today's KPI row\n//\n// \"Install package\" and \"Install deps\" DISPATCH through the daemon's /task\n// endpoint using devops-agent (or node.defaultAgent) as the worker, with a\n// confirm dialog in the UI. That way the user sees the action stream in\n// the normal task view and it's logged like any other task.\n\n// =======================================================================\n// Route handlers\n// =======================================================================\n\nexport function handleAgentPageGet(\n _req: IncomingMessage,\n res: ServerResponse,\n agentId: string,\n peers: TopbarPeer[] = [],\n localToken?: string,\n): void {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderAgentPage({ agentId, peers, localToken }))\n}\n\nexport async function handleAgentApi(\n req: IncomingMessage,\n res: ServerResponse,\n path: string,\n): Promise<void> {\n try {\n const body = req.method === \"GET\" ? undefined : await readJsonBody(req)\n const url = new URL(req.url || \"/\", \"http://localhost\")\n const qs = Object.fromEntries(url.searchParams)\n const match = path.match(/^\\/api\\/admin\\/agent\\/([a-z0-9][a-z0-9_-]*)(?:\\/(.+))?$/)\n if (!match) {\n sendJson(res, 404, { error: `unknown agent endpoint: ${path}` })\n return\n }\n const agentId = match[1]\n const sub = match[2] || \"\"\n const key = `${req.method} ${sub}`\n\n const dispatch: Record<string, () => unknown | Promise<unknown>> = {\n \"GET \": () => getAgentState(agentId),\n \"PATCH \": () => patchAgent(agentId, body),\n \"PATCH capability\": () => patchAgentCapability(agentId, body),\n \"POST mcp\": () => upsertAgentMcp(agentId, body),\n \"DELETE mcp\": () => deleteAgentMcp(agentId, body),\n \"GET identity\": () => getIdentityFiles(agentId),\n \"GET identity/file\": () => getIdentityFile(agentId, String(qs.path || \"\")),\n \"PUT identity/file\": () => putIdentityFile(agentId, body),\n \"GET skills\": () => getSkills(agentId),\n \"POST skills\": () => postCreateSkill(agentId, body),\n \"DELETE skills\": () => deleteSkill(agentId, body),\n \"GET skills/tree\": () => getSkillTree(agentId, String(qs.slug || \"\")),\n \"GET skills/file\": () =>\n getSkillFile(agentId, String(qs.slug || \"\"), String(qs.path || \"\")),\n \"PUT skills/file\": () => putSkillFile(agentId, body),\n \"GET skills/deps\": () => getDepsHint(agentId, String(qs.slug || \"\")),\n \"POST skills/install\": () => dispatchInstallPackage(agentId, body, req),\n \"POST skills/deps\": () => dispatchInstallDeps(agentId, body, req),\n \"GET channels\": () => getChannelsForAgent(agentId),\n \"GET activity\": () => getRecentActivity(agentId, req),\n \"GET handovers\": () => listHandovers(agentId),\n \"POST handovers\": () => createHandover(agentId, body),\n \"DELETE handovers\": () => releaseHandover(body),\n }\n\n const handler = dispatch[key]\n if (!handler) {\n sendJson(res, 404, { error: `unknown agent sub-endpoint: ${key}` })\n return\n }\n const result = await handler()\n sendJson(res, 200, result)\n } catch (e: any) {\n sendJson(res, 400, { error: e?.message || \"agent op failed\" })\n }\n}\n\n// =======================================================================\n// Helpers — reading & mutating agent config\n// =======================================================================\n\nfunction readConfigRaw(): any {\n const file = resolve(process.cwd(), \"agentx.json\")\n if (!existsSync(file)) return null\n return JSON.parse(readFileSync(file, \"utf-8\"))\n}\n\nfunction getAgentDef(agentId: string): any {\n const cfg = readConfigRaw()\n if (!cfg) throw new Error(\"agentx.json not found\")\n const def = cfg.agents?.[agentId]\n if (!def) throw new Error(`Agent \"${agentId}\" not found`)\n return { def, cfg }\n}\n\nfunction getAgentState(agentId: string) {\n const { def, cfg } = getAgentDef(agentId)\n // A minimal workspace snapshot so the page header can render without a\n // second round trip.\n const overview = def.workspace ? listAgentFiles(def.workspace) : null\n return {\n id: agentId,\n name: def.name || agentId,\n tier: def.tier,\n model: def.model,\n workspace: def.workspace,\n systemPrompt: def.systemPrompt || \"\",\n mentions: def.mentions || [],\n access: def.access || \"private\",\n maxConcurrent: def.maxConcurrent ?? 1,\n maxExecutionMinutes: def.maxExecutionMinutes ?? 20,\n permissionMode: def.permissionMode || \"default\",\n // Phase 5/8 capability fields — exposed so the Capability tab can\n // render current values without a second round-trip.\n intents: Array.isArray((def as any).intents) ? (def as any).intents : [],\n maxDelegationDepth: (def as any).maxDelegationDepth ?? 5,\n contextReferences: !!(def as any).contextReferences,\n contextStrategy: (def as any).contextStrategy || \"\",\n // MCP servers — { name → { command, args, env? } }. Synced to\n // <workspace>/.mcp.json by agent-mcp.ts at daemon boot.\n mcp: ((def as any).mcp && typeof (def as any).mcp === \"object\") ? (def as any).mcp : {},\n overview,\n defaultWorker: cfg.node?.defaultAgent,\n draftAgent: cfg.dashboard?.draftAgent,\n }\n}\n\nfunction patchAgent(agentId: string, body: any) {\n const fields: Record<string, any> = {}\n for (const k of [\n \"name\", \"tier\", \"model\", \"systemPrompt\", \"mentions\",\n \"access\", \"maxConcurrent\", \"maxExecutionMinutes\", \"permissionMode\",\n ]) {\n if (body?.[k] !== undefined) fields[k] = body[k]\n }\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.agents?.[agentId]) throw new Error(`Agent \"${agentId}\" not found`)\n cfg.agents[agentId] = { ...cfg.agents[agentId], ...fields }\n return `agent \"${agentId}\" updated (${Object.keys(fields).join(\", \")})`\n })\n return { summary, agent: getAgentState(agentId) }\n}\n\n/** Add or update one MCP server entry on an agent. Body shape:\n * { name: string, command: string, args?: string[], env?: Record<string,string> }\n * The agent.mcp record is keyed by name, and agent-mcp.ts syncs the merged\n * result to <workspace>/.mcp.json the next time the daemon boots / reloads.\n * Operator edits to .mcp.json are respected (the sync layer marks them\n * operator-owned and skips them) — this endpoint only writes to agentx.json. */\nfunction upsertAgentMcp(agentId: string, body: any) {\n const name = String(body?.name || \"\").trim()\n const command = String(body?.command || \"\").trim()\n if (!name) throw new Error(\"name required\")\n if (!/^[a-zA-Z0-9_-]+$/.test(name)) throw new Error(\"name must be identifier-safe (alnum, _, -)\")\n if (!command) throw new Error(\"command required\")\n const args = Array.isArray(body?.args) ? body.args.map((a: unknown) => String(a)) : []\n let env: Record<string, string> | undefined\n if (body?.env && typeof body.env === \"object\") {\n env = {}\n for (const [k, v] of Object.entries(body.env as Record<string, unknown>)) {\n if (typeof v === \"string\") env[k] = v\n }\n if (Object.keys(env).length === 0) env = undefined\n }\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.agents?.[agentId]) throw new Error(`Agent \"${agentId}\" not found`)\n cfg.agents[agentId].mcp = cfg.agents[agentId].mcp || {}\n cfg.agents[agentId].mcp[name] = { command, args, ...(env ? { env } : {}) }\n return `agent \"${agentId}\" mcp.${name} upserted`\n })\n return { summary, agent: getAgentState(agentId) }\n}\n\nfunction deleteAgentMcp(agentId: string, body: any) {\n const name = String(body?.name || \"\").trim()\n if (!name) throw new Error(\"name required\")\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.agents?.[agentId]) throw new Error(`Agent \"${agentId}\" not found`)\n if (!cfg.agents[agentId].mcp || !(name in cfg.agents[agentId].mcp)) {\n throw new Error(`no mcp.${name} on \"${agentId}\"`)\n }\n delete cfg.agents[agentId].mcp[name]\n if (Object.keys(cfg.agents[agentId].mcp).length === 0) delete cfg.agents[agentId].mcp\n return `agent \"${agentId}\" mcp.${name} removed`\n })\n return { summary, agent: getAgentState(agentId) }\n}\n\n/** Phase 5/8 capability fields — separated from patchAgent so the\n * validation can be stricter (intent allow-list, depth ranges,\n * context-strategy enum) without affecting the basic patch path the\n * Overview tab uses. Mirrors `agentx agent capability`. */\nfunction patchAgentCapability(agentId: string, body: any) {\n const fields: Record<string, any> = {}\n if (Array.isArray(body?.intents)) fields.intents = body.intents.map((s: unknown) => String(s)).filter(Boolean)\n if (body?.maxDelegationDepth !== undefined) {\n const n = Number(body.maxDelegationDepth)\n if (!Number.isFinite(n) || n < 0 || n > 50) throw new Error(\"maxDelegationDepth must be 0..50\")\n fields.maxDelegationDepth = n\n }\n if (body?.contextReferences !== undefined) fields.contextReferences = !!body.contextReferences\n if (body?.contextStrategy !== undefined) {\n if (body.contextStrategy && ![\"layered\", \"planner\"].includes(body.contextStrategy)) {\n throw new Error(\"contextStrategy must be layered|planner|empty\")\n }\n if (body.contextStrategy) fields.contextStrategy = body.contextStrategy\n else fields.contextStrategy = undefined\n }\n if (body?.maxExecutionMinutes !== undefined) {\n const n = Number(body.maxExecutionMinutes)\n if (!Number.isFinite(n) || n < 1 || n > 240) throw new Error(\"maxExecutionMinutes must be 1..240\")\n fields.maxExecutionMinutes = n\n }\n if (Object.keys(fields).length === 0) throw new Error(\"nothing to update\")\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.agents?.[agentId]) throw new Error(`Agent \"${agentId}\" not found`)\n const next = { ...cfg.agents[agentId] }\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) delete next[k]\n else next[k] = v\n }\n cfg.agents[agentId] = next\n return `agent \"${agentId}\" capability updated (${Object.keys(fields).join(\", \")})`\n })\n return { summary, agent: getAgentState(agentId) }\n}\n\n// =======================================================================\n// Identity files\n// =======================================================================\n\nfunction getIdentityFiles(agentId: string) {\n const { def } = getAgentDef(agentId)\n return listAgentFiles(def.workspace)\n}\n\nfunction getIdentityFile(agentId: string, relPath: string) {\n if (!relPath) throw new Error(\"path query param required\")\n const { def } = getAgentDef(agentId)\n return readAgentFile(def.workspace, relPath)\n}\n\nfunction putIdentityFile(agentId: string, body: any) {\n const relPath = String(body?.path || \"\")\n if (!relPath) throw new Error(\"path required\")\n const { def } = getAgentDef(agentId)\n return writeAgentFile(def.workspace, relPath, String(body?.content ?? \"\"))\n}\n\n// =======================================================================\n// Skills — list / tree / file I/O / create / delete\n// =======================================================================\n\nfunction getSkills(agentId: string) {\n const { def } = getAgentDef(agentId)\n const overview = listAgentFiles(def.workspace)\n return { skills: overview.skills, workspace: overview.workspace }\n}\n\nfunction postCreateSkill(agentId: string, body: any) {\n const slug = String(body?.slug || \"\").trim()\n if (!slug) throw new Error(\"slug required\")\n const { def } = getAgentDef(agentId)\n return createAgentSkill(def.workspace, slug, {\n title: body?.title,\n content: body?.content,\n })\n}\n\nfunction deleteSkill(agentId: string, body: any) {\n const slug = String(body?.slug || \"\").trim()\n if (!slug) throw new Error(\"slug required\")\n const { def } = getAgentDef(agentId)\n return deleteAgentSkill(def.workspace, slug)\n}\n\nfunction getSkillTree(agentId: string, slug: string) {\n if (!slug) throw new Error(\"slug query param required\")\n const { def } = getAgentDef(agentId)\n const tree = listSkillTree(def.workspace, slug)\n const deps = detectSkillDeps(def.workspace, slug)\n return { slug, tree, deps }\n}\n\nfunction getSkillFile(agentId: string, slug: string, relPath: string) {\n if (!slug || !relPath) throw new Error(\"slug and path query params required\")\n const { def } = getAgentDef(agentId)\n return readSkillFile(def.workspace, slug, relPath)\n}\n\nfunction putSkillFile(agentId: string, body: any) {\n const slug = String(body?.slug || \"\")\n const relPath = String(body?.path || \"\")\n if (!slug || !relPath) throw new Error(\"slug and path required\")\n const { def } = getAgentDef(agentId)\n return writeSkillFile(def.workspace, slug, relPath, String(body?.content ?? \"\"))\n}\n\nfunction getDepsHint(agentId: string, slug: string) {\n if (!slug) throw new Error(\"slug query param required\")\n const { def } = getAgentDef(agentId)\n return detectSkillDeps(def.workspace, slug)\n}\n\n// =======================================================================\n// Dispatched actions — install package, install deps\n// =======================================================================\n\nasync function dispatchInstallPackage(\n agentId: string,\n body: any,\n req: IncomingMessage,\n): Promise<any> {\n const pkg = String(body?.package || \"\").trim()\n if (!pkg || !/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+(?:\\/[A-Za-z0-9_.-]+)?$/.test(pkg)) {\n throw new Error(\"package must be in 'owner/repo' or 'owner/repo/skill' form\")\n }\n const worker = pickWorker(body?.worker)\n const { def } = getAgentDef(agentId)\n const message = [\n `Install the skills.sh package \"${pkg}\" into agent \"${agentId}\".`,\n `Workspace: ${def.workspace}`,\n `Run: cd ${def.workspace} && npx -y skills add ${pkg}`,\n `If that fails, fall back to: agentx skill install ${pkg}`,\n `Skills should land under ${def.workspace}/.claude/skills/. Report what was installed and any errors.`,\n ].join(\"\\n\")\n return dispatchTask(req, worker, message, `install-${pkg}:${Date.now()}`)\n}\n\nasync function dispatchInstallDeps(\n agentId: string,\n body: any,\n req: IncomingMessage,\n): Promise<any> {\n const slug = String(body?.slug || \"\").trim()\n if (!slug) throw new Error(\"slug required\")\n const worker = pickWorker(body?.worker)\n const { def } = getAgentDef(agentId)\n const hint = detectSkillDeps(def.workspace, slug)\n if (!hint.manager) {\n throw new Error(\n `No dependency manifest detected in .claude/skills/${slug}. Expected one of: package.json, requirements.txt, Gemfile.`,\n )\n }\n const skillPath = resolve(def.workspace, \".claude\", \"skills\", slug)\n const message = [\n `Install dependencies for skill \"${slug}\" in agent \"${agentId}\".`,\n `cd to: ${skillPath}`,\n `Detected manifest: ${hint.file}`,\n `Run: ${hint.command}`,\n `Stream the install output. On success, summarise the packages installed.`,\n `On failure, report the exit code and the last 20 lines of output.`,\n ].join(\"\\n\")\n return dispatchTask(req, worker, message, `deps-${agentId}-${slug}:${Date.now()}`)\n}\n\nfunction pickWorker(explicit?: string): string {\n if (explicit && typeof explicit === \"string\") return explicit\n const cfg = loadDaemonConfig()\n return (\n cfg.dashboard?.draftAgent ||\n cfg.node?.defaultAgent ||\n // Fall back to any agent that looks like devops — we need SOMEONE to run shell\n Object.keys(cfg.agents).find((id) => id.includes(\"devops\")) ||\n Object.keys(cfg.agents)[0]\n )\n}\n\nasync function dispatchTask(\n req: IncomingMessage,\n workerAgent: string,\n message: string,\n chatId: string,\n): Promise<any> {\n const cfg = loadDaemonConfig()\n const daemonUrl = (cfg.dashboard.daemonUrl || \"http://localhost:18800\").replace(/\\/+$/, \"\")\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" }\n // Forward the operator's bearer token to the daemon so the task is\n // attributed + auth'd correctly. The admin auth gate already verified us.\n const auth = req.headers.authorization\n if (auth) headers.Authorization = String(auth)\n else if (cfg.dashboard.token) headers.Authorization = `Bearer ${cfg.dashboard.token}`\n\n const r = await fetch(`${daemonUrl}/task`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n agent: workerAgent,\n message,\n context: { channel: \"admin\", sender: \"admin-ui\", chatId },\n }),\n })\n const text = await r.text()\n let data: any\n try { data = JSON.parse(text) } catch { data = { content: text } }\n if (!r.ok) throw new Error(data?.error || `daemon /task HTTP ${r.status}`)\n return {\n worker: workerAgent,\n taskId: data?.taskId,\n content: data?.content,\n duration: data?.duration,\n error: data?.error,\n }\n}\n\n// =======================================================================\n// Channels & activity — read-only, best-effort from config\n// =======================================================================\n\nfunction getChannelsForAgent(agentId: string) {\n const cfg = readConfigRaw()\n const out: Array<{ channel: string; account?: string; detail: string }> = []\n const tg = cfg?.channels?.telegram?.accounts || {}\n for (const [acct, def] of Object.entries<any>(tg)) {\n if (def?.agentBinding === agentId) {\n out.push({ channel: \"telegram\", account: acct, detail: def?.botUsername || acct })\n }\n }\n if (cfg?.channels?.slack?.agentBinding === agentId) {\n out.push({ channel: \"slack\", detail: \"bound\" })\n }\n if (cfg?.channels?.discord?.agentBinding === agentId) {\n out.push({ channel: \"discord\", detail: \"bound\" })\n }\n const gitlab = cfg?.channels?.gitlab?.agentMappings || []\n for (const m of gitlab) {\n if (m?.agentId === agentId) {\n out.push({\n channel: \"gitlab\",\n detail: (m?.gitlabUsernames || []).join(\", \") || \"mapped\",\n })\n }\n }\n return { bindings: out }\n}\n\nasync function getRecentActivity(agentId: string, _req: IncomingMessage): Promise<any> {\n // Pull from the daemon's /agents snapshot so we don't duplicate task\n // history logic. Keep it best-effort; a failing daemon shouldn't 500 the UI.\n try {\n const cfg = loadDaemonConfig()\n const daemonUrl = (cfg.dashboard.daemonUrl || \"http://localhost:18800\").replace(/\\/+$/, \"\")\n const r = await fetch(`${daemonUrl}/agents`)\n if (!r.ok) throw new Error(`daemon HTTP ${r.status}`)\n const list: any[] = await r.json()\n const agent = list.find((a) => a.id === agentId)\n return {\n activeTasks: agent?.activeTasks ?? 0,\n totalTasks: agent?.totalTasks ?? 0,\n errors: agent?.errors ?? 0,\n lastActive: agent?.lastActive ?? null,\n runningTasks: agent?.runningTasks ?? [],\n lastSummary: agent?.lastSummary ?? null,\n }\n } catch (e: any) {\n return { error: e?.message, activeTasks: 0, totalTasks: 0, errors: 0, runningTasks: [] }\n }\n}\n\n// =======================================================================\n// Handovers\n// =======================================================================\n\nfunction handoverStore(): HandoverStore {\n return new HandoverStore()\n}\n\n/** List active handovers for this agent, split by direction. */\nfunction listHandovers(agentId: string) {\n const store = handoverStore()\n const { incoming, outgoing } = store.listByAgent(agentId)\n return { incoming, outgoing }\n}\n\n/** Create or replace a handover. Body:\n * { channel, chatId, accountId?, toAgent, summary?, expiresAt?, fromAgent? }\n * When `fromAgent` is omitted we default to the agent id in the URL — the\n * common case is \"I'm on agent X's page and handing this chat off from it\n * to agent Y\". */\nfunction createHandover(pageAgentId: string, body: any) {\n const channel = String(body?.channel || \"\").trim()\n const chatId = String(body?.chatId || \"\").trim()\n const toAgent = String(body?.toAgent || \"\").trim()\n if (!channel || !chatId || !toAgent) {\n throw new Error(\"channel, chatId, toAgent required\")\n }\n const fromAgent = String(body?.fromAgent || pageAgentId || \"\").trim() || pageAgentId\n const accountId = body?.accountId ? String(body.accountId) : undefined\n const summary = body?.summary ? String(body.summary) : undefined\n const expiresAt = body?.expiresAt ? String(body.expiresAt) : undefined\n const rec = handoverStore().set({\n channel, chatId, accountId, fromAgent, toAgent, summary, expiresAt,\n createdBy: \"admin-ui\",\n })\n return { summary: `Handed off ${channel}:${chatId} from ${fromAgent} to ${toAgent}`, record: rec }\n}\n\nfunction releaseHandover(body: any) {\n const channel = String(body?.channel || \"\").trim()\n const chatId = String(body?.chatId || \"\").trim()\n if (!channel || !chatId) throw new Error(\"channel and chatId required\")\n const accountId = body?.accountId ? String(body.accountId) : undefined\n const removed = handoverStore().remove(channel, chatId, accountId)\n return { summary: removed ? \"Released\" : \"No active handover for that chat\" }\n}\n\n// =======================================================================\n// Plumbing\n// =======================================================================\n\nfunction sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(data))\n}\n\nfunction readJsonBody(req: IncomingMessage): Promise<any> {\n return new Promise((ok, err) => {\n let raw = \"\"\n req.on(\"data\", (c) => { raw += c })\n req.on(\"end\", () => {\n try { ok(raw ? JSON.parse(raw) : {}) } catch (e) { err(e) }\n })\n req.on(\"error\", err)\n })\n}\n\n// =======================================================================\n// HTML\n// =======================================================================\n\n\n","// --- Agent page (/admin/agents/<id>) ---\n//\n// One dedicated page per agent. Tabs:\n// Overview — editable metadata + system prompt\n// Identity — CLAUDE.md / SOUL.md / IDENTITY.md etc via EasyMDE\n// Skills — per-skill directory tree, file editor, install actions\n// Channels — read-only list of bindings\n// Handovers — manual routing to/from this agent\n// Activity — recent tasks\n//\n// API is in agent-panel.ts (keeps file-ops + handover-store imports in one\n// place); this file owns ONLY the HTML/CSS/client JS.\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\n\nexport interface AgentPageOpts {\n agentId: string\n peers?: TopbarPeer[]\n currentPeerId?: string\n localToken?: string\n}\n\nexport function renderAgentPage(opts: AgentPageOpts): string {\n const agentId = opts.agentId\n const tokenScript = opts.localToken\n ? `<script>window.AX_LOCAL_TOKEN = ${JSON.stringify(opts.localToken)};</script>`\n : \"\"\n const agentIdScript = `<script>window.AGENT_ID = ${JSON.stringify(agentId)};</script>`\n\n // Body HTML — copied verbatim from the old renderAgentHtml, with the\n // ${escapeHtml(agentId)} interpolations re-wired to our esc() helper.\n const body = AGENT_PAGE_BODY.replace(/__AGENT_ID__/g, esc(agentId))\n\n return renderShell({\n title: `AgentX · ${esc(agentId)}`,\n activeTab: \"admin\",\n subtitle: `Agent · ${esc(agentId)}`,\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n // The extracted body already carries its own <main> tag plus the\n // confirm-modal <div> that sits alongside it at the same level.\n noMain: true,\n body,\n css: AGENT_PAGE_CSS,\n headExtras: `<link rel=\"stylesheet\" href=\"https://unpkg.com/easymde/dist/easymde.min.css\">`,\n scripts: `${agentIdScript}${tokenScript}<script src=\"https://unpkg.com/easymde/dist/easymde.min.js\"></script><script>${AGENT_PAGE_SCRIPT}</script>`,\n })\n}\n\nconst AGENT_PAGE_BODY = `\n<!-- Breadcrumb -->\n<div class=\"ax-crumb\">\n <a href=\"/admin\">Settings</a>\n <span class=\"ax-crumb__sep\">/</span>\n <a href=\"/admin#agents\">Agents</a>\n <span class=\"ax-crumb__sep\">/</span>\n <span class=\"ax-crumb__cur\" id=\"a-crumb-name\">__AGENT_ID__</span>\n</div>\n\n<!-- Hero -->\n<div class=\"ax-hero\">\n <div class=\"ax-hero__ava\" id=\"a-ava\">??<span class=\"ax-hero__on\"></span></div>\n <div class=\"ax-hero__title\">\n <h1><span id=\"a-name\">__AGENT_ID__</span></h1>\n <div class=\"ax-hero__sub\">\n <span class=\"mono muted\" id=\"a-id\">__AGENT_ID__</span>\n <span class=\"ax-hero__dot\"></span>\n <span id=\"a-short-desc\">Agent workspace &amp; behavior</span>\n <span class=\"ax-hero__dot\"></span>\n <span id=\"a-chips\"></span>\n </div>\n </div>\n <div class=\"ax-hero__actions\">\n <button class=\"ax-btn ax-btn--ghost\" id=\"btn-test-drive\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg>\n Test drive\n </button>\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-save-meta\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z\"/><polyline points=\"17 21 17 13 7 13 7 21\"/><polyline points=\"7 3 7 8 15 8\"/></svg>\n Save changes\n </button>\n </div>\n</div>\n\n<!-- Inline stats -->\n<div class=\"ax-stats-inline\" id=\"a-stats\">\n <div class=\"ax-stat-inline\">\n <div class=\"ax-stat-inline__icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/></svg></div>\n <div><div class=\"ax-stat-inline__v\" id=\"stat-today\">—</div><div class=\"ax-stat-inline__l\">messages today</div></div>\n </div>\n <div class=\"ax-stat-inline\">\n <div class=\"ax-stat-inline__icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/></svg></div>\n <div><div class=\"ax-stat-inline__v\" id=\"stat-avg\">—</div><div class=\"ax-stat-inline__l\">avg response</div></div>\n </div>\n <div class=\"ax-stat-inline\">\n <div class=\"ax-stat-inline__icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/></svg></div>\n <div><div class=\"ax-stat-inline__v\" id=\"stat-resolved\">—</div><div class=\"ax-stat-inline__l\">resolved today</div></div>\n </div>\n <div class=\"ax-stat-inline\">\n <div class=\"ax-stat-inline__icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"23 6 13.5 15.5 8.5 10.5 1 18\"/><polyline points=\"17 6 23 6 23 12\"/></svg></div>\n <div><div class=\"ax-stat-inline__v\" id=\"stat-handling\">—</div><div class=\"ax-stat-inline__l\">handling right now</div></div>\n </div>\n</div>\n\n<!-- Layout: rail + main + test-drive -->\n<div class=\"ax-layout\">\n\n <!-- LEFT RAIL -->\n <nav class=\"ax-rail\" id=\"a-rail\">\n <div class=\"ax-rail__label\">Configure</div>\n <a data-tab=\"overview\" class=\"is-active\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\"/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"/></svg>\n Overview\n </a>\n <a data-tab=\"identity\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg>\n Personality\n </a>\n <a data-tab=\"skills\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"/></svg>\n Skills\n <span class=\"ax-rail__count\" id=\"rail-skills-count\">0</span>\n </a>\n <a data-tab=\"channels\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\"/></svg>\n Channels\n <span class=\"ax-rail__count\" id=\"rail-channels-count\">0</span>\n </a>\n <a data-tab=\"handovers\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"17 1 21 5 17 9\"/><path d=\"M3 11V9a4 4 0 0 1 4-4h14\"/><polyline points=\"7 23 3 19 7 15\"/><path d=\"M21 13v2a4 4 0 0 1-4 4H3\"/></svg>\n Handovers\n <span class=\"ax-rail__count\" id=\"rail-handovers-count\">0</span>\n </a>\n <a data-tab=\"capability\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2L2 7l10 5 10-5-10-5z\"/><polyline points=\"2 17 12 22 22 17\"/><polyline points=\"2 12 12 17 22 12\"/></svg>\n Capability\n </a>\n\n <div class=\"ax-rail__sep\"></div>\n <div class=\"ax-rail__label\">Observe</div>\n <a data-tab=\"activity\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>\n Activity\n </a>\n\n <div class=\"ax-rail__sep\"></div>\n <a data-tab=\"danger\" style=\"color:var(--ax-text-2)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"color:var(--ax-err)\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>\n Danger zone\n </a>\n </nav>\n\n <!-- MAIN COLUMN -->\n <div>\n <div id=\"msg\" class=\"msg\"></div>\n\n <!-- Overview -->\n <section id=\"tab-overview\" class=\"ax-panel__tab is-active\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Basics</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Give your agent a name and describe what it does. This is how it'll show up in lists and the topbar.</p>\n </div>\n </div>\n <div class=\"ax-grid-2\">\n <div class=\"ax-field\"><label class=\"lbl\">Display name</label><input class=\"inp\" id=\"f-name\" /></div>\n <div class=\"ax-field\"><label class=\"lbl\">Short id <span class=\"opt\">used in URLs &amp; configs</span></label><input class=\"inp mono\" id=\"f-slug\" value=\"__AGENT_ID__\" readonly style=\"font-family:var(--ax-mono)\" /></div>\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Wake words <span class=\"opt\">what gets its attention in a chat</span></label>\n <div class=\"ax-triggers-edit\" id=\"f-triggers\" onclick=\"this.querySelector('input').focus()\">\n <input placeholder=\"Type a word and press Enter…\" onkeydown=\"addTrigger(event)\"/>\n </div>\n <!-- The canonical persisted value: comma-separated mentions. Kept\n hidden so existing saveAgent() wiring still reads it. -->\n <input id=\"f-mentions\" type=\"hidden\" />\n <div class=\"hint\">Messages containing any of these wake the agent. Other messages are ignored — unless the message is a direct reply to the agent.</div>\n </div>\n </div>\n\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Brains</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Which AI does the thinking? <b>Claude Code</b> is what most people want — it runs locally and has access to tools.</p>\n </div>\n </div>\n <div class=\"ax-grid-3\">\n <div class=\"ax-field\">\n <label class=\"lbl\">AI engine</label>\n <select class=\"inp\" id=\"f-tier\">\n <option value=\"claude-code\">Claude Code (recommended)</option>\n <option value=\"codex-cli\">Codex CLI</option>\n <option value=\"opencode\">OpenCode</option>\n <option value=\"sdk\">Anthropic API (SDK)</option>\n <option value=\"orchestrator\">Orchestrator (any LLM)</option>\n </select>\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Model</label>\n <input class=\"inp\" id=\"f-model\" placeholder=\"claude-sonnet-4-6\" />\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Visibility</label>\n <select class=\"inp\" id=\"f-access\">\n <option value=\"private\">Private (only this node)</option>\n <option value=\"public\">Public API</option>\n </select>\n <div id=\"public-api-hint\" style=\"display:none;margin-top:6px;padding:8px 10px;background:var(--ax-surface);border-radius:4px;font-size:11px;color:var(--ax-muted)\">\n Public API enabled. External callers POST to <code id=\"public-api-url\" style=\"font-family:'IBM Plex Mono',monospace\"></code> with a token scoped <code>agent:<span id=\"public-api-scope\"></span></code> or <code>agent:*</code>. Mint one in the <a href=\"/admin#tokens\">Tokens tab</a>.\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Limits &amp; safety</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Reasonable boundaries. Agents that go too long or too wide can burn tokens quickly.</p>\n </div>\n </div>\n <div class=\"ax-slider-row\">\n <label>Conversations at once</label>\n <span class=\"ax-hint\">How many people can this agent talk to in parallel? Lower = more focused, more waits.</span>\n <div class=\"ax-opts\" data-field=\"maxConcurrent\">\n <button data-v=\"1\">1</button><button data-v=\"3\">3</button>\n <button data-v=\"5\">5</button><button data-v=\"10\">10</button>\n </div>\n <input id=\"f-maxc\" type=\"hidden\" />\n </div>\n <div class=\"ax-slider-row\">\n <label>Max task length</label>\n <span class=\"ax-hint\">If a reply takes longer than this, the agent gives up. Protects against runaway loops.</span>\n <div class=\"ax-opts\" data-field=\"maxExecutionMinutes\">\n <button data-v=\"5\">5 min</button><button data-v=\"20\">20 min</button>\n <button data-v=\"60\">1 h</button><button data-v=\"0\">no limit</button>\n </div>\n <input id=\"f-maxt\" type=\"hidden\" />\n </div>\n <div class=\"ax-slider-row\">\n <label>Tool permissions</label>\n <span class=\"ax-hint\"><b>Ask first</b> = you approve each tool call. <b>Trusted</b> = it just goes.</span>\n <div class=\"ax-opts\" data-field=\"permissionMode\">\n <button data-v=\"default\">Ask first</button>\n <button data-v=\"acceptEdits\">Accept edits</button>\n <button data-v=\"plan\">Plan only</button>\n <button data-v=\"bypassPermissions\">Trusted</button>\n </div>\n <input id=\"f-pmode\" type=\"hidden\" />\n </div>\n </div>\n </section>\n\n <!-- Personality — left-docked vertical file list + MD editor -->\n <section id=\"tab-identity\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Personality &amp; instructions</h2>\n <p class=\"ax-lead\" style=\"margin:0\">This is the most important thing about your agent. It's how you tell it who it is, what it can and can't do, and how to speak.</p>\n </div>\n </div>\n\n <div class=\"ax-callout\">\n <span class=\"ax-callout__icon\">?</span>\n <div>These files live in the agent's workspace and are injected at the top of every conversation. Markdown is supported; changes hot-reload on the next turn.</div>\n </div>\n\n <div class=\"ax-identity\">\n <!-- Vertical file list -->\n <aside class=\"ax-identity__files\" id=\"identity-files-tabs\"></aside>\n\n <!-- MD editor for the active file -->\n <div class=\"ax-identity__editor\">\n <div class=\"ax-identity__head\">\n <span class=\"mono muted\" id=\"id-path\" style=\"font-size:11.5px\">—</span>\n <span class=\"ax-pbadge\" id=\"id-chars\">0 chars</span>\n <span style=\"flex:1\"></span>\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-save-id\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z\"/><polyline points=\"17 21 17 13 7 13 7 21\"/><polyline points=\"7 3 7 8 15 8\"/></svg>\n Save\n </button>\n </div>\n <textarea id=\"id-editor\"></textarea>\n <div style=\"margin-top:6px;display:flex;align-items:center;gap:8px\">\n <span class=\"mono muted\" id=\"id-saved-note\" style=\"font-size:11px\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Overview-style system-prompt summary (read + quick-edit) -->\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Quick system prompt</h2>\n <p class=\"ax-lead\" style=\"margin:0\">The <code>systemPrompt</code> field in agentx.json, a short one-liner injected before any CLAUDE.md file loads.</p>\n </div>\n </div>\n <textarea class=\"inp\" id=\"sp-editor\" style=\"min-height:120px;line-height:1.55\" placeholder=\"(Optional) A one-liner that sets the top of every prompt.\"></textarea>\n </div>\n </section>\n\n <!-- Skills -->\n <section id=\"tab-skills\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Skills</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Little plug-ins that give this agent new abilities — look up docs, create tickets, summarise PDFs, you name it.</p>\n </div>\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-install-pkg-open\">+ Install package</button>\n </div>\n\n <div class=\"ax-skill-toolbar\">\n <div class=\"ax-search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/></svg>\n <input class=\"inp\" id=\"skill-search\" placeholder=\"Search skills…\" />\n </div>\n <button class=\"ax-btn\" id=\"btn-new-skill\">+ New skill</button>\n </div>\n\n <div class=\"ax-sec-label-d\"><h3 id=\"skills-count-label\">Installed</h3></div>\n <div class=\"ax-skill-grid\" id=\"skills-list\"></div>\n\n <!-- Selected-skill file tree + editor (inline below the grid). -->\n <div id=\"skill-editor-wrap\" style=\"display:none;margin-top:16px\">\n <div class=\"ax-sec-label-d\">\n <h3 id=\"skill-editor-slug\">Skill files</h3>\n <div style=\"display:flex;gap:6px\">\n <button class=\"ax-btn\" id=\"btn-install-deps\" disabled>Install deps…</button>\n <button class=\"ax-btn ax-btn--danger\" id=\"btn-delete-skill\" disabled>Delete skill</button>\n </div>\n </div>\n <div class=\"ax-grid-2\">\n <div class=\"ax-panel\" style=\"padding:14px\">\n <div id=\"skill-tree\"></div>\n </div>\n <div class=\"ax-panel\" style=\"padding:14px\">\n <div id=\"sk-editor-wrap\" style=\"display:none\">\n <div style=\"display:flex;align-items:center;gap:6px;margin-bottom:8px;font-size:11.5px\">\n <span class=\"mono muted\" id=\"sk-path\">—</span>\n <span style=\"flex:1\"></span>\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-save-sk\" style=\"padding:4px 10px;font-size:11.5px\">Save file</button>\n </div>\n <textarea class=\"inp mono\" id=\"sk-editor\" style=\"min-height:300px;line-height:1.55;font-family:var(--ax-mono);font-size:12px\"></textarea>\n </div>\n <div id=\"sk-editor-empty\" style=\"color:var(--ax-muted);padding:20px;text-align:center;font-size:12px\">Select a file in the tree to edit.</div>\n </div>\n </div>\n </div>\n\n <!-- Install-package dispatcher output + dependencies. -->\n <details id=\"pkg-details\" style=\"margin-top:14px\">\n <summary style=\"cursor:pointer;color:var(--ax-muted);font-size:12px;padding:6px 0\">Install from package (advanced) ▾</summary>\n <div class=\"ax-panel\" style=\"margin-top:10px\">\n <div class=\"ax-grid-2\">\n <div class=\"ax-field\">\n <label class=\"lbl\">Package</label>\n <input class=\"inp mono\" id=\"pkg-input\" placeholder=\"owner/repo or owner/repo/skill\" />\n </div>\n <div class=\"ax-field\" style=\"display:flex;align-items:flex-end\">\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-install-pkg\" style=\"width:100%\">Install</button>\n </div>\n </div>\n <pre id=\"dispatch-output\" style=\"margin:10px 0 0;padding:10px;background:var(--ax-bg-elev);border:1px solid var(--ax-border);border-radius:5px;font-family:var(--ax-mono);font-size:11px;color:var(--ax-text-2);max-height:240px;overflow:auto;white-space:pre-wrap;display:none\"></pre>\n </div>\n </details>\n </div>\n </section>\n\n <!-- Channels -->\n <section id=\"tab-channels\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Where this agent lives</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Places this agent shows up. Connections are configured in the global <a href=\"/admin#channels\">Channels</a> tab — here you see which ones route to this agent.</p>\n </div>\n </div>\n <div id=\"channels-list\"></div>\n </div>\n </section>\n\n <!-- Handovers -->\n <section id=\"tab-handovers\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Route a conversation somewhere else</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Sometimes you'll want to pass a chat to another agent — say Support has been trying to explain pricing, and Sales should take over. <b>Every message from that chat will go to the new agent until you release the handover.</b></p>\n </div>\n </div>\n\n <div class=\"ax-handover-viz\">\n <div class=\"ax-ho-side ax-ho-side--from\">\n <div class=\"ax-ho-side__av\" id=\"ho-from-av\">??</div>\n <div class=\"ax-ho-side__nm\" id=\"ho-from-nm\">__AGENT_ID__</div>\n <div class=\"ax-ho-side__rl\">from</div>\n </div>\n <div class=\"ax-ho-wire\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"9 18 15 12 9 6\"/></svg>\n </div>\n <div class=\"ax-ho-side ax-ho-side--to\">\n <div class=\"ax-ho-side__av\" id=\"ho-to-av\">—</div>\n <div class=\"ax-ho-side__nm\" id=\"ho-to-nm\">pick one</div>\n <div class=\"ax-ho-side__rl\">to</div>\n </div>\n </div>\n\n <div class=\"ax-grid-3\">\n <div class=\"ax-field\">\n <label class=\"lbl\">Channel</label>\n <select class=\"inp\" id=\"ho-channel\">\n <option value=\"telegram\">Telegram</option>\n <option value=\"whatsapp\">WhatsApp</option>\n </select>\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Chat id <span class=\"opt\">who's the person?</span></label>\n <input class=\"inp mono\" id=\"ho-chatid\" placeholder=\"1816212449 or +10000000000\" />\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Hand off to</label>\n <select class=\"inp\" id=\"ho-target\"></select>\n </div>\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Account <span class=\"opt\">optional, e.g. \"default\" for a multi-bot setup</span></label>\n <input class=\"inp mono\" id=\"ho-account\" placeholder=\"default\" />\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Brief the receiving agent <span class=\"opt\">one-shot note</span></label>\n <textarea class=\"inp\" id=\"ho-summary\" style=\"min-height:100px\" placeholder=\"Customer is asking about enterprise pricing. They've already seen the standard tier...\"></textarea>\n <div class=\"hint\">The note is injected into the next reply's context. The agent will know <i>why</i> it received the handover.</div>\n </div>\n <div class=\"ax-grid-2\">\n <div class=\"ax-field\" style=\"margin-bottom:0\">\n <label class=\"lbl\">Expires <span class=\"opt\">ISO date; blank = never</span></label>\n <input class=\"inp mono\" id=\"ho-expires\" placeholder=\"2026-04-20T00:00:00Z\" />\n </div>\n <div style=\"display:flex;align-items:flex-end;justify-content:flex-end\">\n <button class=\"ax-btn ax-btn--primary lg\" id=\"btn-handover\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"/><polygon points=\"22 2 15 22 11 13 2 9 22 2\"/></svg>\n Hand off\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\"><div><h2>Currently routed elsewhere</h2><p class=\"ax-lead\" style=\"margin:0\">Chats this agent has handed to someone else.</p></div></div>\n <div id=\"ho-outgoing\"></div>\n </div>\n\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\"><div><h2>Taken over from somewhere</h2><p class=\"ax-lead\" style=\"margin:0\">Chats other agents routed <i>to</i> this agent.</p></div></div>\n <div id=\"ho-incoming\"></div>\n </div>\n </section>\n\n <!-- Capability — mirrors agentx agent capability -->\n <section id=\"tab-capability\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Capability flags</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Phase 5 typed dispatch, Phase 8 delegation depth, and per-agent context-engine overrides. Mirrors <code>agentx agent capability</code>. <b>Restart the daemon</b> after changes for them to take effect.</p>\n </div>\n </div>\n <div class=\"ax-panel__body\">\n <label class=\"ax-field\">\n <span class=\"ax-field__label\">Allowed intents <span class=\"ax-hint\">(comma-separated allow-list — leave blank for permissive default)</span></span>\n <input id=\"cap-intents\" type=\"text\" placeholder=\"issue.opened, merge_request.opened\" />\n <span class=\"ax-hint\">Set to limit this agent to specific intents. The org-chart canHandle() check rejects anything not on the list.</span>\n </label>\n <label class=\"ax-field\">\n <span class=\"ax-field__label\">Max delegation depth <span class=\"ax-hint\">(0..50; lower for agents at the bottom of a chain)</span></span>\n <input id=\"cap-mdd\" type=\"number\" min=\"0\" max=\"50\" />\n <span class=\"ax-hint\">Caps cascade chains where A → B → A. Default 5.</span>\n </label>\n <label class=\"ax-field\">\n <span class=\"ax-field__label\">Context references</span>\n <select id=\"cap-cref\">\n <option value=\"false\">off — skip the [Verified References] block</option>\n <option value=\"true\">on — render deterministic references in the prompt</option>\n </select>\n <span class=\"ax-hint\">Turn on for agents (PMs, devops) that need cited facts. Requires a <code>references/</code> registry in the workspace.</span>\n </label>\n <label class=\"ax-field\">\n <span class=\"ax-field__label\">Context strategy <span class=\"ax-hint\">(per-agent override; blank = global default)</span></span>\n <select id=\"cap-cstrat\">\n <option value=\"\">(global default)</option>\n <option value=\"layered\">layered — full context, larger prompt</option>\n <option value=\"planner\">planner — smaller prompt, more tool-driven exploration</option>\n </select>\n </label>\n <label class=\"ax-field\">\n <span class=\"ax-field__label\">Max execution minutes <span class=\"ax-hint\">(1..240; SIGTERM after this)</span></span>\n <input id=\"cap-mxm\" type=\"number\" min=\"1\" max=\"240\" />\n <span class=\"ax-hint\">Wall-clock cap on a single Claude Code invocation. Default 20m. Bump for devops/coder agents that do long investigations.</span>\n </label>\n <div class=\"ax-form-actions\" style=\"margin-top:14px\">\n <button class=\"ax-btn ax-btn--primary\" id=\"cap-save\">Save capability</button>\n <span id=\"cap-msg\" class=\"ax-hint\" style=\"margin-left:10px\"></span>\n </div>\n </div>\n </div>\n\n <!-- MCP servers — synced to <workspace>/.mcp.json by agent-mcp.ts at boot -->\n <div class=\"ax-panel\" style=\"margin-top:14px\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>MCP servers</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Each entry becomes a server in the agent\\'s <code>.mcp.json</code>. Operator-edited entries in that file are respected (the sync layer marks them operator-owned and skips them); this tab only writes to <code>agentx.json</code>.</p>\n </div>\n </div>\n <div class=\"ax-panel__body\">\n <div id=\"mcp-list\" style=\"display:flex;flex-direction:column;gap:6px;margin-bottom:12px\"></div>\n <details>\n <summary style=\"cursor:pointer;font-size:12px;color:var(--ax-accent,#3a7bd5)\">+ Add or update server</summary>\n <div style=\"margin-top:10px;display:flex;flex-direction:column;gap:8px\">\n <label class=\"ax-field\">\n <span class=\"ax-field__label\">Name <span class=\"ax-hint\">(identifier; will key the entry in agent.mcp)</span></span>\n <input id=\"mcp-name\" type=\"text\" placeholder=\"filesystem, gitlab, brave-search…\" />\n </label>\n <label class=\"ax-field\">\n <span class=\"ax-field__label\">Command</span>\n <input id=\"mcp-command\" type=\"text\" placeholder=\"npx\" />\n </label>\n <label class=\"ax-field\">\n <span class=\"ax-field__label\">Args <span class=\"ax-hint\">(comma-separated)</span></span>\n <input id=\"mcp-args\" type=\"text\" placeholder=\"-y, @modelcontextprotocol/server-filesystem, /Users/me/code\" />\n </label>\n <label class=\"ax-field\">\n <span class=\"ax-field__label\">Env <span class=\"ax-hint\">(KEY=value, comma-separated; optional)</span></span>\n <input id=\"mcp-env\" type=\"text\" placeholder=\"GITLAB_TOKEN=glpat-…, GITLAB_HOST=https://gitlab.example.com\" />\n </label>\n <div class=\"ax-form-actions\">\n <button class=\"ax-btn ax-btn--primary\" id=\"mcp-save\">Save MCP server</button>\n <span id=\"mcp-msg\" class=\"ax-hint\" style=\"margin-left:10px\"></span>\n </div>\n </div>\n </details>\n </div>\n </div>\n </section>\n\n <!-- Activity -->\n <section id=\"tab-activity\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>How it's doing</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Activity across every channel this agent answers from.</p>\n </div>\n </div>\n <div id=\"activity\"></div>\n </div>\n </section>\n\n <!-- Danger zone -->\n <section id=\"tab-danger\" class=\"ax-panel__tab\">\n <div class=\"ax-panel ax-panel--danger\">\n <div class=\"ax-panel__head\">\n <div>\n <h2 style=\"color:var(--ax-err)\">Danger zone</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Actions here are hard to undo. They affect this agent only — other agents are safe.</p>\n </div>\n </div>\n <div class=\"ax-danger-item\">\n <div class=\"ax-danger-item__info\">\n <div class=\"ax-danger-item__name\" style=\"color:var(--ax-err)\">Delete this agent</div>\n <div class=\"ax-danger-item__desc\">Permanently removes the agent from agentx.json. The workspace folder on disk is preserved; the config entry is gone. You'll need to recreate it from scratch to bring it back.</div>\n </div>\n <button class=\"ax-btn ax-btn--danger\" id=\"btn-delete-agent\">Delete agent…</button>\n </div>\n </div>\n </section>\n\n </div>\n\n\n</div>\n\n<!-- TEST DRIVE SIDE PANEL — docked to the right, toggled by the Test drive\n button in the hero. Closed by default; slides in when opened. -->\n<aside class=\"ax-td-panel\" id=\"td-panel\" aria-hidden=\"true\">\n <div class=\"ax-td-head\">\n <div class=\"ax-td-head__dot\"></div>\n <div>\n <div class=\"ax-td-head__t\">Test drive</div>\n <div class=\"ax-td-head__sub\">not recorded · sandbox</div>\n </div>\n <div style=\"flex:1\"></div>\n <div class=\"ax-td-head__seg\">\n <button class=\"is-active\">Live</button>\n <button>Dry</button>\n </div>\n <button class=\"ax-btn ax-btn--ghost\" id=\"td-close\" aria-label=\"Close\" style=\"padding:4px 9px;font-size:14px;line-height:1;margin-left:4px\">×</button>\n </div>\n\n <div class=\"ax-td-scenarios\">\n <div class=\"ax-td-scenarios__lbl\">Quick scenarios</div>\n <span class=\"ax-td-scenario\" onclick=\"sendScenario(this.textContent)\">Who are you?</span>\n <span class=\"ax-td-scenario\" onclick=\"sendScenario(this.textContent)\">What can you help with?</span>\n <span class=\"ax-td-scenario\" onclick=\"sendScenario(this.textContent)\">Show me your skills</span>\n </div>\n\n <div class=\"ax-td-chat\" id=\"td-chat\">\n <div class=\"ax-td-chat__msg-wrap is-bot\">\n <div class=\"ax-td-chat__bubble is-bot\" id=\"td-greeting\">Hey! I'm ready. Send a message to try me out.</div>\n <div class=\"ax-td-chat__meta\">Sandbox · nothing here is recorded</div>\n </div>\n </div>\n\n <form class=\"ax-td-input\" onsubmit=\"sendTestMessage(event)\">\n <input id=\"td-input-el\" placeholder=\"Test a message…\" autocomplete=\"off\"/>\n <button type=\"submit\">Send</button>\n </form>\n</aside>\n\n<!-- Confirm modal (legacy — used by install/delete flows) -->\n<div class=\"ax-modal-bd\" id=\"confirm-modal-bd\">\n <div class=\"ax-modal\" style=\"width:min(480px,94vw)\">\n <header>\n <h3 id=\"confirm-title\">Confirm</h3>\n <button class=\"ax-modal__close\" onclick=\"closeConfirm()\">×</button>\n </header>\n <div class=\"ax-modal__body\" id=\"confirm-detail\"></div>\n <div class=\"ax-modal__foot\">\n <button class=\"ax-btn\" id=\"confirm-cancel\" onclick=\"closeConfirm()\">Cancel</button>\n <button class=\"ax-btn ax-btn--primary\" id=\"confirm-ok\">Confirm</button>\n </div>\n </div>\n</div>\n`\nconst AGENT_PAGE_CSS = `\n/* Agent-specific layout + legacy styles for bits the migration left in\n * place (EasyMDE theming, skills tree, plain-editor). Shared chrome + tab\n * primitives live in components.css.ts. */\n\n/* Personality tab — vertical file list on the left, MD editor on the right */\n.ax-identity {\n display: grid; grid-template-columns: 220px 1fr; gap: 16px; align-items: flex-start;\n}\n.ax-identity__files {\n display: flex; flex-direction: column; gap: 2px;\n background: var(--ax-bg); border: 1px solid var(--ax-border);\n border-radius: var(--ax-radius-sm); padding: 6px;\n}\n.ax-identity__files button {\n background: transparent; border: 0; color: var(--ax-text-2);\n padding: 8px 10px; font: inherit; font-size: 12.5px; cursor: pointer;\n border-radius: 5px; text-align: left;\n display: flex; align-items: center; gap: 8px;\n font-family: var(--ax-mono);\n}\n.ax-identity__files button:hover { background: var(--ax-surface); color: var(--ax-text); }\n.ax-identity__files button.is-active {\n background: color-mix(in oklch, var(--ax-accent) 14%, var(--ax-surface));\n color: var(--ax-accent); font-weight: 500;\n}\n.ax-identity__files button .ax-f-glyph {\n color: var(--ax-muted); font-size: 11px; width: 14px; text-align: center;\n}\n.ax-identity__files button.is-active .ax-f-glyph { color: var(--ax-accent); }\n.ax-identity__files button .ax-f-new {\n margin-left: auto; font-size: 10px; color: var(--ax-muted);\n letter-spacing: 0.06em; text-transform: uppercase;\n font-family: var(--ax-mono);\n}\n.ax-identity__editor {\n background: var(--ax-bg); border: 1px solid var(--ax-border);\n border-radius: var(--ax-radius-sm); padding: 10px;\n}\n.ax-identity__head {\n display: flex; align-items: center; gap: 10px; margin-bottom: 8px;\n padding-bottom: 8px; border-bottom: 1px solid var(--ax-border);\n}\n\n/* EasyMDE theme tweaks to match AgentX tokens */\n.EasyMDEContainer .editor-toolbar {\n border-color: var(--ax-border) !important;\n background: var(--ax-surface); border-radius: 5px 5px 0 0;\n}\n.EasyMDEContainer .editor-toolbar button {\n color: var(--ax-text-2) !important;\n}\n.EasyMDEContainer .editor-toolbar button:hover,\n.EasyMDEContainer .editor-toolbar button.active {\n background: var(--ax-surface-2) !important; color: var(--ax-accent) !important;\n border-color: var(--ax-border-2) !important;\n}\n.EasyMDEContainer .editor-toolbar i.separator {\n border-color: var(--ax-border) !important;\n}\n.EasyMDEContainer .CodeMirror {\n border-color: var(--ax-border) !important;\n background: var(--ax-bg-elev) !important;\n color: var(--ax-text) !important;\n font-family: var(--ax-mono) !important;\n font-size: 12.5px;\n border-radius: 0 0 5px 5px;\n}\n.EasyMDEContainer .CodeMirror-cursor { border-left-color: var(--ax-accent) !important; }\n.EasyMDEContainer .CodeMirror-selected { background: color-mix(in oklch, var(--ax-accent) 25%, transparent) !important; }\n.EasyMDEContainer .editor-statusbar { color: var(--ax-muted) !important; border-color: var(--ax-border) !important; }\n.EasyMDEContainer .editor-preview,\n.EasyMDEContainer .editor-preview-side {\n background: var(--ax-bg-elev) !important; color: var(--ax-text) !important;\n border-color: var(--ax-border) !important;\n}\n.EasyMDEContainer.sided--no-fullscreen .editor-preview-side {\n border-color: var(--ax-border) !important;\n}\n.cm-s-easymde .cm-header, .cm-s-easymde .cm-hr { color: var(--ax-accent) !important; }\n.cm-s-easymde .cm-keyword, .cm-s-easymde .cm-link { color: var(--ax-info) !important; }\n.cm-s-easymde .cm-string, .cm-s-easymde .cm-strong { color: var(--ax-text) !important; }\n.cm-s-easymde .cm-comment { color: var(--ax-muted) !important; }\n\n@media (max-width: 900px) {\n .ax-identity { grid-template-columns: 1fr; }\n}\n\n.msg { font-size: 12px; padding: 8px 12px; border-radius: 4px; margin-bottom: 12px; display: none; }\n.msg.ok { background: color-mix(in oklch, var(--ax-accent) 12%, transparent); color: var(--ax-accent); display: block; }\n.msg.err { background: color-mix(in oklch, var(--ax-err) 12%, transparent); color: var(--ax-err); display: block; }\n\n/* Form labels + hints used inside panels */\nlabel.lbl, .lbl {\n display: block; font-size: 11px; color: var(--ax-muted);\n letter-spacing: 0.03em; text-transform: uppercase; margin: 0 0 6px;\n font-weight: 500;\n}\nlabel.lbl .opt, .lbl .opt {\n text-transform: none; letter-spacing: 0; font-weight: 400;\n opacity: 0.75; margin-left: 6px;\n}\ninput.inp, textarea.inp, select.inp {\n width: 100%; background: var(--ax-bg); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 5px;\n padding: 8px 11px; font: inherit; font-size: 13px;\n transition: all 120ms;\n}\ninput.inp:focus, textarea.inp:focus, select.inp:focus {\n outline: none; border-color: var(--ax-accent);\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ax-accent) 18%, transparent);\n}\ntextarea.inp { min-height: 100px; line-height: 1.55; resize: vertical; font-family: inherit; }\ntextarea.inp.mono { font-family: var(--ax-mono); }\n.ax-field { margin-bottom: 14px; }\n.hint { font-size: 11.5px; color: var(--ax-muted); margin-top: 5px; line-height: 1.5; }\n\n/* Agent-specific button variant used in rail context (no ax-btn wrapper) */\n.ax-btn.lg { padding: 9px 16px; font-size: 13px; }\n`\nconst AGENT_PAGE_SCRIPT = `\nconst $ = (id) => document.getElementById(id);\nconst state = { agent: null, activeSkill: null, activeSkillFile: null, identityPath: null };\n\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,c=>({\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\"}[c]));}\n\nasync function req(method, path, body) {\n const headers = { 'Content-Type': 'application/json' };\n if (window.AX_LOCAL_TOKEN) headers['Authorization'] = 'Bearer ' + window.AX_LOCAL_TOKEN;\n const r = await fetch(path, { method, headers, body: body ? JSON.stringify(body) : undefined });\n const data = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));\n return data;\n}\n\nfunction showMsg(kind, text){ const m = $('msg'); m.className='msg '+kind; m.textContent=text; setTimeout(()=>{m.className='msg';}, 4000); }\n\n// --- confirm modal ---\nfunction confirmAction(title, detail, onConfirm){\n $('confirm-title').textContent = title;\n $('confirm-detail').textContent = detail;\n const bd = $('confirm-modal-bd');\n if (bd) bd.classList.add('is-show');\n $('confirm-ok').onclick = () => { closeConfirm(); onConfirm(); };\n}\nfunction closeConfirm(){ const bd = $('confirm-modal-bd'); if (bd) bd.classList.remove('is-show'); }\nwindow.closeConfirm = closeConfirm;\n\n// --- Tabs (rail) ---\nfunction switchTab(name) {\n document.querySelectorAll('.ax-rail a').forEach(a => a.classList.toggle('is-active', a.dataset.tab === name));\n document.querySelectorAll('.ax-panel__tab').forEach(s => s.classList.toggle('is-active', s.id === 'tab-' + name));\n try { localStorage.setItem('ax-agent-tab', name); } catch(_) {}\n if (name === 'skills' && !state.skillsLoaded) { loadSkills(); state.skillsLoaded = true; }\n if (name === 'identity' && !state.identityLoaded) { loadIdentity(); state.identityLoaded = true; }\n if (name === 'channels') loadChannels();\n if (name === 'handovers') loadHandovers();\n if (name === 'activity') loadActivity();\n window.scrollTo({ top: 0, behavior: 'smooth' });\n}\ndocument.querySelectorAll('.ax-rail a[data-tab]').forEach(a => {\n a.addEventListener('click', (e) => { e.preventDefault(); switchTab(a.dataset.tab); });\n});\ntry {\n const s = localStorage.getItem('ax-agent-tab');\n if (s && document.querySelector('#tab-' + s)) switchTab(s);\n} catch(_) {}\n\n// --- Hero avatar initials/color mirror the Settings agents list helpers. ---\nfunction initialsOf(name, id) {\n const src = (name || id || '?').replace(/[^a-zA-Z ]/g, '').trim();\n const parts = src.split(/\\\\s+/).filter(Boolean);\n if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();\n return (parts[0] || id || '?').slice(0, 2).toUpperCase();\n}\n\n// --- Trigger-pill editing: render from a string array into #f-triggers.\n// Persists the canonical value as comma-separated into the hidden #f-mentions\n// input so saveAgent() doesn't need new wiring. ---\nfunction renderTriggers(mentions) {\n const host = $('f-triggers');\n if (!host) return;\n // Remove existing pills (keep the <input> at the end).\n host.querySelectorAll('.ax-trig-pill').forEach(n => n.remove());\n const input = host.querySelector('input');\n for (const m of (mentions || [])) {\n const pill = document.createElement('span');\n pill.className = 'ax-trig-pill';\n pill.innerHTML = esc(m) + ' <button type=\"button\">×</button>';\n pill.querySelector('button').addEventListener('click', (e) => {\n e.stopPropagation();\n pill.remove();\n syncTriggersHidden();\n });\n host.insertBefore(pill, input);\n }\n syncTriggersHidden();\n}\nfunction syncTriggersHidden() {\n const pills = [...document.querySelectorAll('#f-triggers .ax-trig-pill')]\n .map(p => p.firstChild?.textContent?.trim() || p.textContent.replace(/×$/, '').trim())\n .filter(Boolean);\n const hidden = $('f-mentions');\n if (hidden) hidden.value = pills.join(', ');\n}\nwindow.addTrigger = function(e) {\n if (e.key !== 'Enter') return;\n e.preventDefault();\n const v = e.target.value.trim();\n if (!v) return;\n const input = e.target;\n const pill = document.createElement('span');\n pill.className = 'ax-trig-pill';\n pill.innerHTML = esc(v) + ' <button type=\"button\">×</button>';\n pill.querySelector('button').addEventListener('click', (ev) => {\n ev.stopPropagation(); pill.remove(); syncTriggersHidden();\n });\n input.parentElement.insertBefore(pill, input);\n input.value = '';\n syncTriggersHidden();\n};\n\n// Slider-row segmented toggles: one button per value. Click writes the\n// value into the sibling hidden input so the existing saveAgent() flow reads\n// it. Call on page load to hydrate from state.\nfunction initOptsToggles() {\n document.querySelectorAll('.ax-opts').forEach((grp) => {\n grp.addEventListener('click', (e) => {\n const b = e.target.closest('button');\n if (!b || !grp.contains(b)) return;\n grp.querySelectorAll('button').forEach(x => x.classList.toggle('is-active', x === b));\n // Find the hidden input that the parent slider-row owns\n const row = grp.closest('.ax-slider-row');\n if (!row) return;\n const hidden = row.querySelector('input[type=hidden]');\n if (hidden) hidden.value = b.dataset.v || '';\n });\n });\n}\nfunction setOpt(field, value) {\n const v = String(value);\n document.querySelectorAll('.ax-opts[data-field=\"' + field + '\"] button').forEach((b) => {\n b.classList.toggle('is-active', String(b.dataset.v || '') === v);\n });\n // also populate the hidden input\n const row = document.querySelector('.ax-opts[data-field=\"' + field + '\"]')?.closest('.ax-slider-row');\n const hidden = row?.querySelector('input[type=hidden]');\n if (hidden) hidden.value = v;\n}\n\n// --- Overview ---\nasync function loadAgent(){\n try {\n const a = await req('GET', '/api/admin/agent/' + AGENT_ID);\n state.agent = a;\n\n // Hero\n const displayName = a.name || AGENT_ID;\n $('a-name').textContent = displayName;\n $('a-id').textContent = AGENT_ID;\n const crumb = $('a-crumb-name'); if (crumb) crumb.textContent = displayName;\n const ava = $('a-ava');\n if (ava) {\n // Strip the pulse dot, rewrite initials, keep the dot.\n ava.textContent = initialsOf(a.name, AGENT_ID);\n const dot = document.createElement('span');\n dot.className = 'ax-hero__on';\n ava.appendChild(dot);\n }\n // Hero chips (tier/access/public-API)\n const chipsParts = [];\n chipsParts.push('<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>online</span>');\n if (a.access === 'public') chipsParts.push('<span class=\"ax-pill ax-pill--info\">public API</span>');\n if (a.tier) chipsParts.push('<span class=\"ax-pill\">' + esc(a.tier) + '</span>');\n $('a-chips').innerHTML = chipsParts.join(' ');\n\n // Overview form\n $('f-name').value = a.name || '';\n $('f-tier').value = a.tier || 'claude-code';\n $('f-model').value = a.model || '';\n $('f-access').value = a.access || 'private';\n // Public-API hint — show endpoint + scope when access is public.\n const hintEl = document.getElementById('public-api-hint');\n if (hintEl) {\n const isPublic = a.access === 'public';\n hintEl.style.display = isPublic ? '' : 'none';\n if (isPublic) {\n const daemonUrl = (state && state.daemonUrl) || (location.origin || '').replace(':4202', ':18800');\n const urlEl = document.getElementById('public-api-url');\n const scopeEl = document.getElementById('public-api-scope');\n if (urlEl) urlEl.textContent = daemonUrl + '/api/public/agents/' + AGENT_ID + '/messages';\n if (scopeEl) scopeEl.textContent = AGENT_ID;\n }\n }\n // Also re-render when the operator flips the dropdown.\n $('f-access').addEventListener('change', () => {\n if (hintEl) hintEl.style.display = $('f-access').value === 'public' ? '' : 'none';\n }, { once: true });\n setOpt('maxConcurrent', a.maxConcurrent ?? 1);\n setOpt('maxExecutionMinutes', a.maxExecutionMinutes ?? 20);\n setOpt('permissionMode', a.permissionMode || 'default');\n renderTriggers(a.mentions || []);\n\n // Quick system prompt + personality editor share the systemPrompt field.\n const sp = $('sp-editor'); if (sp) sp.value = a.systemPrompt || '';\n\n // Stats placeholders; loadActivity() fills real numbers once the tab is visited.\n const handling = (a.runningTasks?.length) || 0;\n $('stat-handling').textContent = String(handling);\n\n // Capability tab — Phase 5/8 fields. Mirrors the agentx agent capability CLI.\n const intentsEl = $('cap-intents');\n if (intentsEl) {\n intentsEl.value = Array.isArray(a.intents) ? a.intents.join(', ') : '';\n $('cap-mdd').value = a.maxDelegationDepth ?? 5;\n $('cap-cref').value = a.contextReferences ? 'true' : 'false';\n $('cap-cstrat').value = a.contextStrategy || '';\n $('cap-mxm').value = a.maxExecutionMinutes ?? 20;\n }\n // MCP servers list\n const mcpListEl = document.getElementById('mcp-list');\n if (mcpListEl) {\n const mcp = a.mcp || {};\n const names = Object.keys(mcp);\n if (names.length === 0) {\n mcpListEl.innerHTML = '<div style=\"font-size:11px;color:var(--ax-muted);font-style:italic;padding:8px;border:1px dashed var(--ax-border);border-radius:4px\">no MCP servers configured</div>';\n } else {\n mcpListEl.innerHTML = names.map(function(n){\n const s = mcp[n];\n const args = (s.args || []).map(function(x){ return esc(x); }).join(' ');\n const envCount = s.env ? Object.keys(s.env).length : 0;\n return '<div style=\"display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--ax-border);border-radius:4px\">' +\n '<div style=\"flex:1;min-width:0\">' +\n '<div><b>' + esc(n) + '</b> <code style=\"font-size:11px;color:var(--ax-muted);margin-left:4px\">' + esc(s.command) + ' ' + args + '</code></div>' +\n (envCount ? '<div style=\"font-size:11px;color:var(--ax-muted);margin-top:2px\">env: ' + envCount + ' var(s)</div>' : '') +\n '</div>' +\n '<button class=\"ax-btn ax-btn--danger\" data-mcp-rm=\"' + esc(n) + '\">Remove</button>' +\n '</div>';\n }).join('');\n mcpListEl.querySelectorAll('[data-mcp-rm]').forEach(function(b){\n b.addEventListener('click', async function(){\n const n = b.getAttribute('data-mcp-rm');\n if (!confirm('Remove MCP server \"' + n + '\"?')) return;\n try {\n await req('DELETE', '/api/admin/agent/' + AGENT_ID + '/mcp', { name: n });\n await loadAgent();\n } catch (e) { showMsg('err', e.message); }\n });\n });\n }\n }\n } catch (e) { showMsg('err', e.message); }\n}\n\n// MCP save — adds or updates one server entry on the agent.\n(function(){\n const btn = document.getElementById('mcp-save');\n if (!btn) return;\n btn.addEventListener('click', async () => {\n const name = document.getElementById('mcp-name').value.trim();\n const command = document.getElementById('mcp-command').value.trim();\n const argsRaw = document.getElementById('mcp-args').value.trim();\n const envRaw = document.getElementById('mcp-env').value.trim();\n const args = argsRaw ? argsRaw.split(',').map(function(s){ return s.trim(); }).filter(Boolean) : [];\n const env = {};\n if (envRaw) {\n envRaw.split(',').map(function(s){ return s.trim(); }).filter(Boolean).forEach(function(pair){\n const eq = pair.indexOf('=');\n if (eq > 0) env[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();\n });\n }\n const body = { name, command, args };\n if (Object.keys(env).length > 0) body.env = env;\n const msg = document.getElementById('mcp-msg');\n try {\n await req('POST', '/api/admin/agent/' + AGENT_ID + '/mcp', body);\n if (msg) { msg.textContent = '✓ saved — agent picks it up at next daemon boot'; msg.style.color = 'var(--ax-ok)'; }\n ['mcp-name','mcp-command','mcp-args','mcp-env'].forEach(function(id){ document.getElementById(id).value = ''; });\n await loadAgent();\n } catch (e) {\n if (msg) { msg.textContent = e.message; msg.style.color = 'var(--ax-err)'; }\n }\n });\n})();\n\n// Capability save — POST to the same agent-config write path the\n// CLI uses, so both surfaces persist identically. (See agentx agent\n// capability in the CLI for the cli mirror.)\n(function(){\n const capSaveBtn = document.getElementById('cap-save');\n if (!capSaveBtn) return;\n capSaveBtn.addEventListener('click', async () => {\n const intentsRaw = document.getElementById('cap-intents').value.trim();\n const intents = intentsRaw ? intentsRaw.split(',').map(function(s){ return s.trim(); }).filter(Boolean) : [];\n const body = {\n intents: intents,\n maxDelegationDepth: parseInt(document.getElementById('cap-mdd').value, 10) || 5,\n contextReferences: document.getElementById('cap-cref').value === 'true',\n maxExecutionMinutes: parseInt(document.getElementById('cap-mxm').value, 10) || 20,\n };\n const cstrat = document.getElementById('cap-cstrat').value;\n if (cstrat) body.contextStrategy = cstrat;\n const msg = document.getElementById('cap-msg');\n try {\n await req('PATCH', '/api/admin/agent/' + AGENT_ID + '/capability', body);\n if (msg) { msg.textContent = '✓ saved — restart the daemon for changes to take effect'; msg.style.color = 'var(--ax-ok)'; }\n await loadAgent();\n } catch (e) {\n if (msg) { msg.textContent = e.message; msg.style.color = 'var(--ax-err)'; }\n }\n });\n})();\n\n$('btn-save-meta').addEventListener('click', async () => {\n const body = {\n name: $('f-name').value.trim(),\n tier: $('f-tier').value,\n model: $('f-model').value.trim(),\n access: $('f-access').value,\n maxConcurrent: Number($('f-maxc').value) || 1,\n maxExecutionMinutes: Number($('f-maxt').value) || 20,\n permissionMode: $('f-pmode').value,\n mentions: $('f-mentions').value.split(',').map(s=>s.trim()).filter(Boolean),\n systemPrompt: $('sp-editor').value,\n };\n try {\n await req('PATCH', '/api/admin/agent/' + AGENT_ID, body);\n showMsg('ok', 'Saved. Restart the daemon for tier/model changes to take effect.');\n } catch (e) { showMsg('err', e.message); }\n});\n\n// --- Identity (Personality tab) ---\n// Vertical file list + EasyMDE editor. The file list stays docked on the\n// left; clicking a file swaps the editor's contents. Unsaved edits are\n// persisted per-file in state.idDrafts so switching doesn't lose them.\nlet idMde = null;\nstate.idDrafts = {};\n\nasync function loadIdentity(){\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/identity');\n const host = $('identity-files-tabs');\n host.innerHTML = '';\n for (const f of r.identity) {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.dataset.path = f.path;\n // Derive a small 2-3 char glyph from the filename (CLAUDE.md → CL, etc.).\n const base = (f.path.split('/').pop() || f.path).replace(/\\\\.md$/i, '');\n const glyph = base.slice(0, 2).toUpperCase();\n btn.innerHTML =\n '<span class=\"ax-f-glyph\">' + esc(glyph) + '</span>' +\n '<span>' + esc(f.title) + '</span>' +\n (f.exists ? '' : '<span class=\"ax-f-new\">new</span>');\n btn.addEventListener('click', () => {\n host.querySelectorAll('button').forEach(x => x.classList.toggle('is-active', x === btn));\n loadIdentityFile(f.path);\n });\n host.appendChild(btn);\n }\n if (r.identity.length) {\n host.firstElementChild.classList.add('is-active');\n await loadIdentityFile(r.identity[0].path);\n }\n } catch (e) { showMsg('err', e.message); }\n}\n\nfunction ensureIdMde() {\n if (idMde) return idMde;\n if (typeof EasyMDE === 'undefined') return null;\n const el = document.getElementById('id-editor');\n if (!el) return null;\n idMde = new EasyMDE({\n element: el, spellChecker: false, autoDownloadFontAwesome: true,\n status: ['lines','words'], autofocus: false,\n minHeight: '360px',\n toolbar: ['bold','italic','heading','|','quote','code','unordered-list','ordered-list','|','link','table','|','preview','side-by-side','fullscreen','|','guide'],\n });\n idMde.codemirror.on('change', () => {\n updateIdCharCount();\n if (state.identityPath) state.idDrafts[state.identityPath] = idMde.value();\n });\n return idMde;\n}\n\nasync function loadIdentityFile(path){\n // Stash current draft before switching\n if (state.identityPath && idMde) state.idDrafts[state.identityPath] = idMde.value();\n state.identityPath = path;\n $('id-path').textContent = path;\n try {\n const f = await req('GET', '/api/admin/agent/' + AGENT_ID + '/identity/file?path=' + encodeURIComponent(path));\n const mde = ensureIdMde();\n const initial = state.idDrafts[path] !== undefined ? state.idDrafts[path] : (f.content || '');\n if (mde) mde.value(initial);\n else $('id-editor').value = initial;\n updateIdCharCount();\n } catch (e) {\n showMsg('err', e.message);\n if (idMde) idMde.value(''); else $('id-editor').value = '';\n }\n}\n\nfunction updateIdCharCount() {\n const val = idMde ? idMde.value() : ($('id-editor')?.value || '');\n const chars = $('id-chars');\n if (chars) chars.textContent = val.length.toLocaleString() + ' chars';\n}\n\n$('btn-save-id').addEventListener('click', async () => {\n if (!state.identityPath) return;\n const content = idMde ? idMde.value() : $('id-editor').value;\n try {\n await req('PUT', '/api/admin/agent/' + AGENT_ID + '/identity/file', {\n path: state.identityPath, content,\n });\n showMsg('ok', 'Saved ' + state.identityPath);\n const note = $('id-saved-note');\n if (note) note.textContent = 'saved ' + new Date().toLocaleTimeString();\n // Clear the draft now that it's persisted\n delete state.idDrafts[state.identityPath];\n } catch (e) { showMsg('err', e.message); }\n});\n\n// --- Skills ---\nasync function loadSkills(){\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/skills');\n const list = $('skills-list');\n const countLabel = $('skills-count-label');\n const railCount = $('rail-skills-count');\n if (!r.skills.length) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"grid-column:1/-1;text-align:center;padding:32px;background:var(--ax-bg-elev);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><p style=\"margin:0;font-size:13px\">No skills installed yet. Install a package below or create a new one.</p></div>';\n if (countLabel) countLabel.textContent = 'Installed';\n if (railCount) railCount.textContent = '0';\n } else {\n list.innerHTML = r.skills.map(s =>\n '<div class=\"ax-skill-card is-on\" data-slug=\"' + esc(s.slug) + '\">' +\n '<div class=\"ax-skill-card__top\">' +\n '<div class=\"ax-skill-card__icon\">' +\n '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"/></svg>' +\n '</div>' +\n '<div><div class=\"ax-skill-card__name\">' + esc(s.title || s.slug) + '</div><div class=\"ax-skill-card__slug\">' + esc(s.slug) + '</div></div>' +\n '</div>' +\n '<div class=\"ax-skill-card__desc\">' + esc(s.description || 'No description.') + '</div>' +\n '<div class=\"ax-skill-card__foot\"><span>' + esc(s.source || 'installed') + '</span><span>' + esc(s.version || '') + '</span></div>' +\n '</div>'\n ).join('');\n list.querySelectorAll('[data-slug]').forEach(el =>\n el.addEventListener('click', () => selectSkill(el.dataset.slug))\n );\n if (countLabel) countLabel.textContent = 'Installed — ' + r.skills.length + ' active';\n if (railCount) railCount.textContent = String(r.skills.length);\n }\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function selectSkill(slug){\n state.activeSkill = slug;\n state.activeSkillFile = null;\n document.querySelectorAll('#skills-list [data-slug]').forEach(r=>{\n r.classList.toggle('ax-skill-card--active', r.dataset.slug === slug);\n });\n // Reveal the tree + editor area and set the section label\n const wrap = $('skill-editor-wrap');\n if (wrap) wrap.style.display = 'block';\n const slugLabel = $('skill-editor-slug');\n if (slugLabel) slugLabel.textContent = slug;\n $('btn-delete-skill').disabled = false;\n $('btn-install-deps').disabled = true;\n $('sk-editor-wrap').style.display = 'none';\n $('sk-editor-empty').style.display = 'block';\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/skills/tree?slug=' + encodeURIComponent(slug));\n $('btn-install-deps').disabled = !r.deps?.manager;\n $('btn-install-deps').title = r.deps?.command || 'No package manifest detected';\n const host = $('skill-tree');\n host.innerHTML = r.tree.map(e => {\n const depth = (e.path.match(/\\\\//g) || []).length;\n const pad = 'padding-left:' + (depth * 10) + 'px';\n const icon = e.kind === 'dir' ? '▸' : '·';\n const cls = 'tree-entry ' + e.kind + (e.editable ? '' : ' readonly');\n return '<div class=\"'+cls+'\" style=\"'+pad+'\" data-path=\"'+esc(e.path)+'\" data-editable=\"'+(e.editable?'1':'0')+'\">'\n + '<span>'+icon+' '+esc(e.path.split('/').filter(Boolean).pop() || e.path)+'</span>'\n + '<span style=\"color:var(--ax-muted);font-size:10px\">'+(e.kind==='file'?formatBytes(e.size):'')+'</span>'\n + '</div>';\n }).join('');\n host.querySelectorAll('[data-path]').forEach(r=>r.addEventListener('click', ()=>{\n if (r.dataset.editable === '1') openSkillFile(r.dataset.path);\n }));\n // Auto-open SKILL.md\n const skillMd = r.tree.find(e => e.path === 'SKILL.md' && e.editable);\n if (skillMd) openSkillFile('SKILL.md');\n } catch (e) { showMsg('err', e.message); }\n}\n\nfunction formatBytes(n){ if (!n) return ''; if (n < 1024) return n+'b'; if (n<1048576) return (n/1024).toFixed(1)+'k'; return (n/1048576).toFixed(1)+'M'; }\n\nasync function openSkillFile(path){\n state.activeSkillFile = path;\n $('sk-path').textContent = state.activeSkill + '/' + path;\n $('sk-editor-empty').style.display = 'none';\n $('sk-editor-wrap').style.display = 'block';\n document.querySelectorAll('#skill-tree [data-path]').forEach(r=>r.classList.toggle('active', r.dataset.path === path));\n try {\n const f = await req('GET', '/api/admin/agent/' + AGENT_ID + '/skills/file?slug=' + encodeURIComponent(state.activeSkill) + '&path=' + encodeURIComponent(path));\n $('sk-editor').value = f.content || '';\n } catch (e) { showMsg('err', e.message); }\n}\n\n$('btn-save-sk').addEventListener('click', async () => {\n if (!state.activeSkill || !state.activeSkillFile) return;\n const content = $('sk-editor').value;\n try {\n await req('PUT', '/api/admin/agent/' + AGENT_ID + '/skills/file', {\n slug: state.activeSkill, path: state.activeSkillFile, content,\n });\n showMsg('ok', 'Saved ' + state.activeSkill + '/' + state.activeSkillFile);\n } catch (e) { showMsg('err', e.message); }\n});\n\n$('btn-new-skill').addEventListener('click', async () => {\n const slug = prompt('New skill slug (lowercase, letters/digits/-/_):');\n if (!slug) return;\n try {\n await req('POST', '/api/admin/agent/' + AGENT_ID + '/skills', { slug });\n showMsg('ok', 'Skill created: ' + slug);\n await loadSkills();\n selectSkill(slug);\n } catch (e) { showMsg('err', e.message); }\n});\n\n$('btn-delete-skill').addEventListener('click', () => {\n if (!state.activeSkill) return;\n const slug = state.activeSkill;\n confirmAction(\n 'Delete skill \"' + slug + '\"?',\n 'This removes .claude/skills/' + slug + ' and all files inside it. This cannot be undone.',\n async () => {\n try {\n await req('DELETE', '/api/admin/agent/' + AGENT_ID + '/skills', { slug });\n state.activeSkill = null;\n state.activeSkillFile = null;\n $('skill-tree').innerHTML = '';\n $('sk-editor-wrap').style.display = 'none';\n $('sk-editor-empty').style.display = 'block';\n $('btn-delete-skill').disabled = true;\n $('btn-install-deps').disabled = true;\n showMsg('ok', 'Deleted ' + slug);\n loadSkills();\n } catch (e) { showMsg('err', e.message); }\n }\n );\n});\n\n$('btn-install-pkg').addEventListener('click', () => {\n const pkg = $('pkg-input').value.trim();\n if (!pkg) { showMsg('err', 'Enter a package like \"owner/repo\".'); return; }\n const worker = state.agent?.draftAgent || state.agent?.defaultWorker || 'devops-agent';\n confirmAction(\n 'Install skills.sh package?',\n 'Package: ' + pkg + '\\\\n\\\\nWorker agent: ' + worker + '\\\\n\\\\nThe worker will run: npx skills add ' + pkg + '\\\\n\\\\nInside: ' + (state.agent?.workspace || '(agent workspace)') + '\\\\n\\\\nOutput will appear below.',\n async () => {\n $('dispatch-output').style.display = 'block';\n $('dispatch-output').textContent = 'Dispatching to ' + worker + '…';\n try {\n const r = await req('POST', '/api/admin/agent/' + AGENT_ID + '/skills/install', { package: pkg });\n $('dispatch-output').textContent = (r.content || '(no output)') + '\\\\n\\\\n-- done --';\n loadSkills();\n } catch (e) { $('dispatch-output').textContent = 'Error: ' + e.message; }\n }\n );\n});\n\n$('btn-install-deps').addEventListener('click', async () => {\n if (!state.activeSkill) return;\n const slug = state.activeSkill;\n try {\n const hint = await req('GET', '/api/admin/agent/' + AGENT_ID + '/skills/deps?slug=' + encodeURIComponent(slug));\n if (!hint.manager) { showMsg('err', 'No package manifest in skill dir.'); return; }\n const worker = state.agent?.draftAgent || state.agent?.defaultWorker || 'devops-agent';\n confirmAction(\n 'Install dependencies?',\n 'Skill: ' + slug + '\\\\nManifest: ' + hint.file + '\\\\nCommand: ' + hint.command + '\\\\n\\\\nWorker agent: ' + worker + '\\\\n\\\\nRunning install scripts executes third-party code. Confirm only if you trust this skill\\\\'s source.',\n async () => {\n $('dispatch-output').style.display = 'block';\n $('dispatch-output').textContent = 'Dispatching to ' + worker + '…';\n try {\n const r = await req('POST', '/api/admin/agent/' + AGENT_ID + '/skills/deps', { slug });\n $('dispatch-output').textContent = (r.content || '(no output)') + '\\\\n\\\\n-- done --';\n } catch (e) { $('dispatch-output').textContent = 'Error: ' + e.message; }\n }\n );\n } catch (e) { showMsg('err', e.message); }\n});\n\n// --- Channels + Activity ---\nconst CH_LOGO = {\n telegram: { bg: '#229ED9', init: 'TG' },\n whatsapp: { bg: '#25D366', init: 'WA' },\n slack: { bg: '#4A154B', init: 'SL' },\n discord: { bg: '#5865F2', init: 'DC' },\n gitlab: { bg: '#FC6D26', init: 'GL' },\n github: { bg: '#24292e', init: 'GH' },\n webhook: { bg: 'var(--ax-surface-3)', init: 'WH' },\n};\n\nasync function loadChannels(){\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/channels');\n const host = $('channels-list');\n const railCount = $('rail-channels-count');\n if (railCount) railCount.textContent = String(r.bindings.length);\n if (!r.bindings.length) {\n host.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:32px;background:var(--ax-bg-elev);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><p style=\"margin:0;font-size:13px\">No channels route to this agent yet. <a href=\"/admin#channels\">Wire one up in Settings → Channels</a>.</p></div>';\n return;\n }\n host.innerHTML = r.bindings.map((b) => {\n const meta = CH_LOGO[b.channel] || { bg: 'var(--ax-surface-3)', init: b.channel.slice(0, 2).toUpperCase() };\n return '<div class=\"ax-chan-row\">' +\n '<div class=\"ax-chan-row__logo\" style=\"background:' + meta.bg + '\">' + meta.init + '</div>' +\n '<div class=\"ax-chan-row__info\">' +\n '<div class=\"ax-chan-row__name\">' + esc(b.channel) + (b.account ? ' · ' + esc(b.account) : '') + '</div>' +\n '<div class=\"ax-chan-row__desc\">' + esc(b.detail || '—') + '</div>' +\n '</div>' +\n '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>bound</span>' +\n '</div>';\n }).join('');\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function loadHandovers(){\n try {\n // Populate the \"Transfer to\" dropdown with every local agent except self.\n const targets = $('ho-target');\n if (!targets.options.length) {\n const raw = await fetch('/api/admin/state', { headers: window.AX_LOCAL_TOKEN ? { Authorization: 'Bearer ' + window.AX_LOCAL_TOKEN } : {} });\n const st = await raw.json();\n targets.innerHTML = '<option value=\"\">Pick a target…</option>' + (st.agents || [])\n .filter(a => a.id !== AGENT_ID)\n .map(a => '<option value=\"'+esc(a.id)+'\" data-name=\"'+esc(a.name || a.id)+'\">'+esc(a.name || a.id)+'</option>')\n .join('');\n targets.addEventListener('change', () => {\n const sel = targets.options[targets.selectedIndex];\n const name = sel?.dataset?.name || sel?.textContent || '—';\n const id = targets.value;\n $('ho-to-nm').textContent = name;\n $('ho-to-av').textContent = id ? initialsOf(name, id) : '—';\n });\n }\n // Hydrate the \"from\" side from the current agent\n const a = state.agent;\n if (a) {\n $('ho-from-av').textContent = initialsOf(a.name, AGENT_ID);\n $('ho-from-nm').textContent = a.name || AGENT_ID;\n }\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/handovers');\n const railCount = $('rail-handovers-count');\n if (railCount) railCount.textContent = String((r.incoming?.length || 0) + (r.outgoing?.length || 0));\n renderHandoverList($('ho-incoming'), r.incoming, 'incoming');\n renderHandoverList($('ho-outgoing'), r.outgoing, 'outgoing');\n } catch (e) { showMsg('err', e.message); }\n}\n\nfunction renderHandoverList(host, list, direction){\n if (!list.length) {\n host.innerHTML = '<div style=\"text-align:center;padding:20px;color:var(--ax-muted);font-size:12.5px;font-style:italic\">' + (direction === 'incoming'\n ? 'No chats are currently routed to this agent.'\n : 'No chats are currently routed away from this agent.') + '</div>';\n return;\n }\n host.innerHTML = list.map(o => {\n const who = direction === 'incoming'\n ? '<b>' + esc(o.chatId) + '</b> from <b>' + esc(o.fromAgent) + '</b>'\n : '<b>' + esc(o.chatId) + '</b> → handed to <b>' + esc(o.toAgent) + '</b>';\n const expires = o.expiresAt ? 'expires ' + esc(o.expiresAt) : 'never expires';\n const consumed = o.summaryConsumedAt ? ' · briefing delivered' : (o.summary ? ' · briefing pending' : '');\n return '<div class=\"ax-ho-list-row\">' +\n '<span class=\"ax-pill ax-pill--info\">' + esc(o.channel) + '</span>' +\n '<span class=\"ax-who\">' + who + consumed + '</span>' +\n '<span class=\"ax-when\">' + esc(o.createdAt) + ' · ' + expires + '</span>' +\n '<button class=\"ax-btn ax-btn--danger\" data-release=\"' + esc(o.channel) + '::' + esc(o.chatId) + '::' + esc(o.accountId || '') + '\">Release</button>' +\n '</div>';\n }).join('');\n host.querySelectorAll('[data-release]').forEach(btn => btn.addEventListener('click', () => {\n const [ch, cid, acct] = btn.dataset.release.split('::');\n releaseHandover(ch, cid, acct || undefined);\n }));\n}\n\nasync function releaseHandover(channel, chatId, accountId){\n if (!confirm('Release the handover for ' + channel + ':' + chatId + '? Routing will revert to the config default on the next message.')) return;\n try {\n await req('DELETE', '/api/admin/agent/' + AGENT_ID + '/handovers', { channel, chatId, accountId });\n showMsg('ok', 'Handover released.');\n loadHandovers();\n } catch (e) { showMsg('err', e.message); }\n}\n\n$('btn-handover').addEventListener('click', () => {\n const channel = $('ho-channel').value;\n const chatId = $('ho-chatid').value.trim();\n const accountId = $('ho-account').value.trim() || undefined;\n const toAgent = $('ho-target').value;\n const summary = $('ho-summary').value.trim() || undefined;\n const expiresAt = $('ho-expires').value.trim() || undefined;\n if (!chatId || !toAgent) { showMsg('err', 'chat id and target agent are required'); return; }\n confirmAction(\n 'Hand off this chat?',\n channel + ':' + chatId + (accountId ? ':'+accountId : '') + '\\\\n\\\\n'\n + 'From: ' + AGENT_ID + '\\\\nTo: ' + toAgent + '\\\\n\\\\n'\n + 'Every next message from this chat will be routed to ' + toAgent + ' until you release the handover. '\n + (summary ? 'The target agent will see your briefing note on its first reply.' : 'No briefing note set — the target agent will not know why.'),\n async () => {\n try {\n await req('POST', '/api/admin/agent/' + AGENT_ID + '/handovers',\n { channel, chatId, accountId, toAgent, summary, expiresAt });\n $('ho-summary').value = '';\n showMsg('ok', 'Handed off.');\n loadHandovers();\n } catch (e) { showMsg('err', e.message); }\n }\n );\n});\n\nasync function loadActivity(){\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/activity');\n const host = $('activity');\n host.innerHTML =\n '<div class=\"ax-activity-grid\">' +\n '<div class=\"ax-activity-cell\"><div class=\"ax-v\">' + (r.totalTasks ?? 0) + '</div><div class=\"ax-l\">Total tasks</div></div>' +\n '<div class=\"ax-activity-cell\"><div class=\"ax-v\">' + (r.activeTasks ?? 0) + '</div><div class=\"ax-l\">Handling now</div></div>' +\n '<div class=\"ax-activity-cell\"><div class=\"ax-v\">' + (r.errors ?? 0) + '</div><div class=\"ax-l\">Errors</div></div>' +\n '<div class=\"ax-activity-cell\"><div class=\"ax-v\" style=\"font-size:13px;font-family:var(--ax-mono)\">' + esc(r.lastActive || '—') + '</div><div class=\"ax-l\">Last active</div></div>' +\n '</div>' +\n (r.lastSummary\n ? '<div class=\"ax-sec-label-d\"><h3>Last reply</h3></div>' +\n '<div style=\"background:var(--ax-bg-elev);border:1px solid var(--ax-border);border-radius:var(--ax-radius-sm);padding:14px;font-size:12.5px;color:var(--ax-text-2);line-height:1.6\">' +\n esc((r.lastSummary.text || '').slice(0, 400)) +\n '</div>'\n : '');\n // Update hero stats from the activity payload.\n const sToday = $('stat-today'); if (sToday) sToday.textContent = String(r.totalTasks ?? '—');\n const sHand = $('stat-handling'); if (sHand) sHand.textContent = String(r.activeTasks ?? 0);\n } catch (e) { showMsg('err', e.message); }\n}\n\n/* ===== Test drive (sandbox chat) ===== */\nlet __tdBusy = false;\n// Stable conversation id for the test-drive session (page load scoped)\nconst __tdConvId = 'td-' + Math.random().toString(36).slice(2, 10);\nfunction appendTdBubble(kind, text, meta, queuedTask) {\n const wrap = document.createElement('div');\n wrap.className = 'ax-td-chat__msg-wrap is-' + kind;\n let chipHtml = '';\n if (queuedTask) {\n const isActive = queuedTask.status === 'active';\n const label = isActive ? 'QUEUED · running now' : 'QUEUED · #' + queuedTask.position + ' in line';\n chipHtml =\n '<div class=\"ax-td-task-chip' + (isActive ? '' : ' ax-td-task-chip--queued') + '\">' +\n '<span class=\"ax-td-task-chip__dot\"></span>' +\n label +\n ' <span class=\"ax-td-task-chip__id\">' + esc(queuedTask.id) + '</span>' +\n '</div>';\n }\n wrap.innerHTML =\n '<div class=\"ax-td-chat__bubble is-' + kind + '\">' + esc(text) + '</div>' +\n chipHtml +\n '<div class=\"ax-td-chat__meta\">' + esc(meta || '') + '</div>';\n const chat = $('td-chat');\n chat.appendChild(wrap);\n chat.scrollTop = chat.scrollHeight;\n return wrap;\n}\nfunction appendTdTyping() {\n const el = document.createElement('div');\n el.className = 'ax-td-chat__typing';\n el.id = 'td-typing';\n el.innerHTML = '<span></span><span></span><span></span>';\n $('td-chat').appendChild(el);\n $('td-chat').scrollTop = $('td-chat').scrollHeight;\n}\nfunction removeTdTyping() {\n const el = document.getElementById('td-typing');\n if (el) el.remove();\n}\n\nwindow.sendScenario = function(text) {\n const inp = $('td-input-el');\n if (inp) inp.value = text;\n dispatchTd(text);\n};\nwindow.sendTestMessage = function(e) {\n e.preventDefault();\n const inp = $('td-input-el');\n const text = inp.value.trim();\n if (!text) return;\n inp.value = '';\n dispatchTd(text);\n};\nasync function dispatchTd(text) {\n if (__tdBusy) return;\n __tdBusy = true;\n const now = new Date().toLocaleTimeString();\n appendTdBubble('user', text, now + ' · you');\n appendTdTyping();\n const t0 = Date.now();\n try {\n // Use /chat endpoint so test-drive exercises the same path as example.com.\n // Falls back to /task reply shape if /chat isn't reachable (no auth configured).\n let reply, queuedTask;\n try {\n const r = await req('POST', '/chat', {\n agentId: AGENT_ID,\n user_id: 'admin-ui',\n conversation_id: __tdConvId,\n message: text,\n });\n reply = r.reply || r.content || '(empty reply)';\n queuedTask = r.queued_task || null;\n } catch (_chatErr) {\n // /chat may reject with 401 if AGENTX_CHAT_SECRET is set — fall back to /task\n const r = await req('POST', '/task', { agent: AGENT_ID, message: text, context: { channel: 'test-drive', sender: 'admin-ui' } });\n reply = r.content || '(empty reply)';\n }\n removeTdTyping();\n const elapsed = ((Date.now() - t0) / 1000).toFixed(1) + 's';\n appendTdBubble('bot', reply, new Date().toLocaleTimeString() + ' · ' + elapsed, queuedTask);\n } catch (e) {\n removeTdTyping();\n appendTdBubble('bot', '[error] ' + e.message, 'failed');\n } finally {\n __tdBusy = false;\n }\n}\n\n/* ===== Delete agent ===== */\n$('btn-delete-agent')?.addEventListener('click', () => {\n confirmAction(\n 'Delete this agent?',\n 'Agent \"' + AGENT_ID + '\" will be removed from agentx.json. The workspace folder on disk is preserved; the config entry is gone. You will need to recreate it from scratch to bring it back.',\n async () => {\n try {\n await req('DELETE', '/api/admin/agents', { id: AGENT_ID });\n window.location.href = '/admin';\n } catch (e) { showMsg('err', e.message); }\n }\n );\n});\n\n/* ===== Test-drive side panel: open/close ===== */\nfunction openTestDrive() {\n const p = $('td-panel');\n if (!p) return;\n p.classList.add('is-open');\n p.setAttribute('aria-hidden', 'false');\n setTimeout(() => $('td-input-el')?.focus(), 250);\n}\nfunction closeTestDrive() {\n const p = $('td-panel');\n if (!p) return;\n p.classList.remove('is-open');\n p.setAttribute('aria-hidden', 'true');\n}\n$('btn-test-drive')?.addEventListener('click', openTestDrive);\n$('td-close')?.addEventListener('click', closeTestDrive);\ndocument.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && $('td-panel')?.classList.contains('is-open')) closeTestDrive();\n});\n\n/* ===== Boot ===== */\ninitOptsToggles();\nloadAgent();\n`\n","// Single source of truth for user-facing labels in the dashboard web UIs.\n//\n// AgentX keeps its JSON schema keys (tier, mention, workspace, mesh, heartbeat)\n// because third-party configs depend on them. But the dashboard is for operators\n// who don't care about the schema — they want plain English. This module maps\n// the internal vocabulary to the labels shown on screen and to the glossary.\n//\n// Changes here are SAFE: they only affect what the browser renders. They do NOT\n// change agentx.json, the CLI, or any external API surface.\n\nexport const UI_LABELS = {\n brand: \"AgentX\",\n subtitle: \"Team AI ops\",\n\n // Header summary\n nodes: \"machines\",\n agentsCount: \"agents\",\n activeNow: \"handling now\",\n errorsCount: \"failed\",\n\n // Per-agent card\n stats: {\n active: \"Handling\",\n total: \"Today\",\n errors: \"Failed\",\n },\n idle: \"idle\",\n runningNoPreview: \"working · preparing reply\",\n recentActivities: \"Recent activities →\",\n noAgentsOnNode: \"No agents set up on this machine.\",\n unreachable: \"This machine is unreachable.\",\n neverRan: \"not used yet\",\n\n // Friendly names per tier\n tierLabels: {\n \"claude-code\": \"Claude Code\",\n \"codex-cli\": \"Codex CLI\",\n \"opencode\": \"OpenCode\",\n \"sdk\": \"SDK\",\n \"orchestrator\": \"Orchestrator\",\n } as Record<string, string>,\n\n // Task history / modal\n taskModalTitle: \"Task activity\",\n taskModalConnecting: \"connecting…\",\n taskModalLive: \"live\",\n taskModalFinished: \"finished\",\n taskModalArchived: \"archived\",\n taskModalLoadFailed: \"couldn't load\",\n taskModalFinalResponse: \"Final reply\",\n historyPanelTitle: \"Recent activities\",\n historyEmpty: \"No activity yet.\",\n historyLoading: \"loading…\",\n}\n\n/**\n * Rename map used by a small tooltip helper in the UI. Lets us hover over any\n * badge/label and see both the friendly name and the original schema key so\n * operators who do know the config aren't confused.\n */\nexport const SCHEMA_ALIASES: Record<string, string> = {\n tier: \"AI engine\",\n model: \"model\",\n mention: \"trigger word\",\n mentions: \"trigger words\",\n workspace: \"agent folder\",\n mesh: \"team network\",\n heartbeat: \"status check-in\",\n channel: \"channel\",\n cron: \"schedule\",\n primaryToolLabel: \"board filter label\",\n scopedLabel: \"status label\",\n}\n\n/**\n * Friendly glossary shown at /glossary. Keep entries short, definition-style,\n * no jargon — written for a business operator, not an engineer.\n */\nexport const GLOSSARY: Array<{ term: string; alias?: string; definition: string }> = [\n {\n term: \"Agent\",\n definition: \"An AI worker with its own role, instructions, and channels it responds on. Think of each agent as a job title — \\\"support\\\", \\\"devops\\\", \\\"sales\\\".\",\n },\n {\n term: \"Trigger word\",\n alias: \"mention\",\n definition: \"A word that activates an agent. When the trigger appears in a message (e.g. @support), AgentX routes the message to that agent.\",\n },\n {\n term: \"Channel\",\n definition: \"Where messages come from — Telegram, WhatsApp, Discord, GitLab comments, a scheduled cron, or an HTTP webhook.\",\n },\n {\n term: \"AI engine\",\n alias: \"tier\",\n definition: \"Which backend answers: Claude Code, Codex CLI, Anthropic Agent SDK, or AgentX's provider-agnostic orchestrator.\",\n },\n {\n term: \"Agent folder\",\n alias: \"workspace\",\n definition: \"The directory on disk where the agent lives. It holds the agent's personality (CLAUDE.md), its knowledge (wiki), and the tools it can use.\",\n },\n {\n term: \"Team network\",\n alias: \"mesh\",\n definition: \"When you run AgentX on more than one machine, the machines find each other and share work. Each machine is a \\\"node\\\"; together they are your team network.\",\n },\n {\n term: \"Status check-in\",\n alias: \"heartbeat\",\n definition: \"A lightweight ping between machines so each one knows the others are alive and responsive.\",\n },\n {\n term: \"Board\",\n definition: \"A Kanban view of tasks (issues, tickets) pulled from GitLab. Drag cards between columns to move work forward — the labels update upstream automatically.\",\n },\n {\n term: \"Task\",\n definition: \"A single conversation or job an agent handles, from trigger message to final reply. Every task is logged and replayable.\",\n },\n {\n term: \"Schedule\",\n alias: \"cron\",\n definition: \"A recurring job — e.g. \\\"every Monday at 9am\\\" — that nudges an agent without a human trigger.\",\n },\n {\n term: \"Status label\",\n alias: \"scopedLabel\",\n definition: \"A GitLab label that moves an issue between Kanban columns (e.g. Status::Doing). Labels are mutually exclusive by prefix.\",\n },\n]\n\nexport function tierFriendly(tier: string | undefined): string {\n if (!tier) return \"\"\n return UI_LABELS.tierLabels[tier] || tier\n}\n","// --- Live dashboard page ---\n//\n// Real-time view of every reachable daemon's agents. The HTML here is a\n// skeleton; everything interesting is painted by LIVE_PAGE_SCRIPT after an\n// SSE snapshot lands from /api/live/stream.\n//\n// Ownership split vs the old board-dashboard.ts monolith:\n// ui/tokens.ts — :root / theme tokens (was at the top of LIVE_CSS)\n// ui/components.css.ts — .ax-dot, .ax-badge, .ax-statstrip, .ax-chip,\n// .ax-card (was also at the top of LIVE_CSS)\n// topbar.ts — .ax-topbar, .ax-theme-switch, .ax-mesh-sel\n// HERE — layout + agent cards + history panel + task modal\n//\n// That's ~300 lines of CSS reclaimed from LIVE_CSS onto shared modules.\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\nimport { UI_LABELS } from \"../../ui-labels\"\n\nexport interface LivePageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderLivePage(opts: LivePageOpts = {}): string {\n const labelsScript = `<script>window.UI_LABELS = ${JSON.stringify(UI_LABELS)};</script>`\n const rightExtras = `<span id=\"ts\" class=\"ax-mono\" title=\"Last update\">—</span>\n <span id=\"conn-dot\" class=\"ax-dot ax-dot--ok ax-dot--pulse\" title=\"connected\"></span>\n <span id=\"conn-label\">live</span>`\n\n const body = `<div class=\"ax-app-live\">\n <section id=\"statstrip\" class=\"ax-statstrip\"></section>\n <main id=\"grid\" class=\"ax-live__body\"></main>\n</div>\n`\n\n return renderShell({\n title: `${UI_LABELS.brand} · ${UI_LABELS.subtitle}`,\n activeTab: \"live\",\n subtitle: UI_LABELS.subtitle,\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n // Paste rightExtras manually since renderShell doesn't forward them to\n // renderTopbar — the Live page is the only one that uses them (ticking\n // timestamp + connection dot), not worth a config knob.\n customHeader: undefined, // let shell emit the standard topbar\n noMain: true,\n body,\n css: LIVE_PAGE_CSS,\n scripts: labelsScript + `<script>${LIVE_PAGE_SCRIPT}</script>`,\n headExtras: `<script>\n // rightExtras for the topbar are injected at runtime — they reference\n // IDs (#ts, #conn-dot) that the script below addresses directly.\n document.addEventListener('DOMContentLoaded', () => {\n const right = document.querySelector('.ax-topbar__right');\n if (!right) return;\n const extras = document.createElement('div');\n extras.style.cssText = 'display:flex;gap:10px;align-items:center;font-size:var(--ax-fs-xs);color:var(--ax-muted)';\n extras.innerHTML = ${JSON.stringify(rightExtras)};\n right.insertBefore(extras, right.firstChild);\n });\n </script>`,\n })\n}\n\n/** CSS specific to the Live page. Tokens + .ax-badge/.ax-dot/.ax-statstrip/\n * .ax-chip/.ax-card / topbar all live in shared modules — this file owns\n * only layout (app flex column, node sections, agent grid) + the agent\n * card internals + history panel + task modal + event timeline. */\nconst LIVE_PAGE_CSS = `\n/* --- Page frame --- */\n.ax-app-live { min-height: 100vh; display: flex; flex-direction: column; }\n\n/* --- Live body (list of node sections) --- */\n.ax-live__body { display: flex; flex-direction: column; gap: var(--ax-gap); padding: var(--ax-pad); }\n.ax-node {\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 8px; overflow: hidden;\n}\n.ax-node > header {\n background: transparent; padding: 12px 16px;\n border-bottom: 1px solid var(--ax-border);\n display: flex; align-items: center; gap: 10px;\n}\n.ax-node__name { font-weight: 600; font-size: 14px; }\n.ax-node__url { color: var(--ax-muted); font-family: var(--ax-mono); font-size: 11px; }\n.ax-node__tag {\n font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;\n padding: 2px 7px; border-radius: 3px; border: 1px solid var(--ax-border-2);\n color: var(--ax-text-2); font-family: var(--ax-mono);\n}\n.ax-node__tag--up { color: var(--ax-accent); border-color: color-mix(in oklch, var(--ax-accent) 50%, transparent); }\n.ax-node__tag--down { color: var(--ax-err); border-color: color-mix(in oklch, var(--ax-err) 50%, transparent); }\n.ax-grid--agents {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));\n gap: var(--ax-gap); padding: 12px 14px;\n}\n\n/* --- Agent cards --- */\n.ax-agent {\n display: flex; flex-direction: column; gap: 10px;\n transition: border-color 200ms ease, box-shadow 200ms ease, background 200ms ease;\n}\n/* Idle agents are context, not content: tighter padding, muted, no shadow.\n A working agent should be visibly heavier than a resting one. */\n.ax-agent.is-collapsed {\n gap: 6px; padding: 12px 14px; box-shadow: none;\n background: var(--ax-surface-2); opacity: 0.78;\n}\n.ax-agent.is-collapsed:hover { opacity: 1; }\n.ax-agent.is-collapsed .ax-agent__foot { border-top: none; padding-top: 0; }\n.ax-agent__spark { color: var(--ax-accent); }\n.ax-agent__spark svg { display: block; width: 100%; height: 22px; }\n.ax-agent__spark-caption {\n display: flex; justify-content: space-between; font-size: 10px;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ax-muted); margin-top: 1px;\n}\n.ax-agent.is-collapsed .ax-agent__spark { opacity: 0.75; }\n\n/* Active agents inside the \"running now\" tile — each row opens that\n conversation. */\n.ax-stat__running { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }\n.ax-stat__running-row { display: block; text-decoration: none; color: inherit; border-radius: 8px; padding: 3px 6px; margin: 0 -6px; }\n.ax-stat__running-row:hover { background: var(--ax-surface-2); text-decoration: none; }\n.ax-stat__running-line { display: flex; align-items: center; gap: 8px; font-size: var(--ax-fs-xs); min-width: 0; }\n.ax-stat__running-ch { color: var(--ax-muted); text-transform: uppercase; letter-spacing: 0.05em; font-size: 10px; }\n.ax-stat__running-el { margin-left: auto; color: var(--ax-muted); }\n.ax-agent.is-handling {\n border-color: color-mix(in oklch, var(--ax-accent) 75%, var(--ax-border));\n background: linear-gradient(180deg,\n color-mix(in oklch, var(--ax-accent) 6%, var(--ax-surface)) 0%,\n var(--ax-surface) 38%);\n animation: ax-agent-breathe 2.2s ease-in-out infinite;\n}\n.ax-agent.is-errored {\n border-color: color-mix(in oklch, var(--ax-err) 55%, var(--ax-border));\n box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--ax-err) 25%, transparent);\n}\n@keyframes ax-agent-breathe {\n 0%, 100% {\n box-shadow:\n 0 0 0 1px color-mix(in oklch, var(--ax-accent) 45%, transparent),\n 0 0 20px -6px color-mix(in oklch, var(--ax-accent) 30%, transparent);\n }\n 50% {\n box-shadow:\n 0 0 0 1px color-mix(in oklch, var(--ax-accent) 70%, transparent),\n 0 0 32px -2px color-mix(in oklch, var(--ax-accent) 55%, transparent);\n }\n}\n.ax-agent__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }\n.ax-agent__id { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex: 1; }\n.ax-mention {\n color: var(--ax-accent); font-size: 15px; font-weight: 500;\n font-family: var(--ax-mono); letter-spacing: -0.01em;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.ax-agent__name {\n font-size: var(--ax-fs-sm); color: var(--ax-muted);\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.ax-agent__tier { display: flex; gap: 4px; flex-shrink: 0; }\n.ax-agent__model {\n font-size: var(--ax-fs-xs); color: var(--ax-muted);\n font-family: var(--ax-mono); margin-top: -4px;\n}\n.ax-agent__running { display: flex; flex-direction: column; gap: 6px; }\n.ax-agent__task {\n text-align: left; background: var(--ax-bg);\n border: 1px solid color-mix(in oklch, var(--ax-accent) 30%, var(--ax-border));\n padding: 8px 10px; border-radius: 4px; cursor: pointer;\n font: inherit; color: var(--ax-text);\n}\n.ax-agent__task:hover { border-color: var(--ax-accent); }\n.ax-agent__task-head {\n display: flex; align-items: center; gap: 6px;\n font-size: var(--ax-fs-xs); color: var(--ax-accent);\n}\n.ax-agent__task-head .elapsed { margin-left: auto; font-family: var(--ax-mono); color: var(--ax-muted); }\n.ax-agent__task-body {\n font-size: var(--ax-fs-sm); margin-top: 4px;\n display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;\n overflow: hidden; line-height: 1.35;\n}\n.ax-agent__task-actions {\n display: flex; gap: 6px; margin-top: 6px; justify-content: flex-end;\n}\n.ax-task-action {\n font: inherit; font-size: 11px; line-height: 1;\n padding: 4px 8px; border-radius: 3px; cursor: pointer;\n border: 1px solid var(--ax-border); background: transparent; color: var(--ax-muted);\n}\n.ax-task-action:hover { border-color: var(--ax-accent); color: var(--ax-text); }\n.ax-task-action--stop:hover { border-color: #d33; color: #d33; }\n.ax-agent__summary { border-top: 1px dashed var(--ax-border); padding-top: 8px; }\n.ax-agent__summary-caption {\n font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--ax-muted);\n}\n.ax-agent__summary-text {\n font-size: var(--ax-fs-sm); color: var(--ax-text-2);\n line-height: 1.45; margin-top: 4px; text-wrap: pretty;\n display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;\n}\n.ax-agent__summary.is-fail .ax-agent__summary-caption { color: var(--ax-err); }\n.ax-agent__foot {\n display: flex; justify-content: space-between; align-items: center;\n font-size: var(--ax-fs-xs); color: var(--ax-muted);\n border-top: 1px solid var(--ax-border); padding-top: 8px;\n}\n.ax-linkbtn {\n background: transparent; border: none; color: var(--ax-text-2);\n padding: 2px 6px; font: inherit; cursor: pointer;\n font-size: var(--ax-fs-sm); text-decoration: none;\n}\n.ax-linkbtn:hover { color: var(--ax-accent); }\n\n.ax-empty {\n padding: 32px 10px; text-align: center; color: var(--ax-muted);\n font-size: var(--ax-fs-sm); border: 1px dashed var(--ax-border);\n border-radius: 4px; margin: 12px 14px;\n}\n\n/* --- History panel (right-docked sheet) --- */\n.ax-ev { border-left: 2px solid var(--ax-border-2); padding: 2px 0 2px 12px; }\n.ax-ev--tool { border-color: var(--ax-info); }\n.ax-ev--tool-result { border-color: var(--ax-border-2); }\n.ax-ev--tool-result.is-err { border-color: var(--ax-err); }\n.ax-ev--thought { border-color: var(--ax-muted); }\n.ax-ev--text { border-color: var(--ax-accent); }\n.ax-ev--system {\n border-color: var(--ax-border-2); color: var(--ax-muted); font-size: var(--ax-fs-xs);\n font-family: var(--ax-mono); padding-top: 4px; padding-bottom: 4px;\n}\n.ax-ev--system.is-done { border-color: var(--ax-accent); }\n.ax-ev--error { border-color: var(--ax-err); }\n.ax-ev__head { display: flex; align-items: center; gap: 8px; font-size: var(--ax-fs-xs); color: var(--ax-text-2); }\n.ax-ev__label { text-transform: uppercase; letter-spacing: 0.06em; font-size: 10px; font-family: var(--ax-mono); }\n.ax-ev__label--soft { color: var(--ax-muted); }\n.ax-ev__label--tool { color: var(--ax-info); }\n.ax-ev__label--result { color: var(--ax-muted); }\n.ax-ev__label--text { color: var(--ax-accent); }\n.ax-ev__label--error { color: var(--ax-err); }\n.ax-ev__time { margin-left: auto; font-family: var(--ax-mono); color: var(--ax-muted); }\n.ax-ev__tool { font-family: var(--ax-mono); color: var(--ax-info); font-size: var(--ax-fs-xs); }\n.ax-ev__code {\n margin: 4px 0 0; padding: 8px 10px; background: var(--ax-bg-elev);\n border: 1px solid var(--ax-border); border-radius: 3px; font-family: var(--ax-mono);\n font-size: var(--ax-fs-xs); white-space: pre-wrap; word-break: break-word;\n line-height: 1.5; color: var(--ax-text-2); max-height: 200px; overflow: auto;\n}\n.ax-ev__code--muted { color: var(--ax-muted); }\n.ax-ev__code--err { color: var(--ax-err); border-color: color-mix(in oklch, var(--ax-err) 35%, var(--ax-border)); }\n.ax-ev__thought {\n margin-top: 4px; color: var(--ax-text-2); font-style: italic;\n font-size: var(--ax-fs-sm); line-height: 1.55; text-wrap: pretty;\n}\n.ax-ev__text {\n margin-top: 4px; color: var(--ax-text); font-size: var(--ax-fs-sm);\n line-height: 1.55; text-wrap: pretty; white-space: pre-wrap; word-break: break-word;\n}\n.ax-ev--thinking {\n display: flex; align-items: center; gap: 8px; padding: 8px 0 0 12px;\n color: var(--ax-muted); font-size: var(--ax-fs-sm);\n}\n.ax-ev__spinner {\n width: 10px; height: 10px; border: 1.5px solid var(--ax-border-2);\n border-top-color: var(--ax-accent); border-radius: 50%;\n animation: ax-spin 800ms linear infinite; display: inline-block;\n}\n@keyframes ax-spin { to { transform: rotate(360deg); } }`\n\n/** Live-page client script — SSE snapshot handler, agent card renderer,\n * task modal + history panel. 650-ish lines of straight JS; moving it\n * here doesn't change it. */\nconst LIVE_PAGE_SCRIPT = `\n'use strict';\n\nconst ui = { nodes: new Map(), summary: { nodes: 0, agents: 0, busy: 0, errors: 0 } };\n\nfunction escapeHtml(s) {\n return String(s).replace(/[&<>\"']/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '\"':'&quot;', \"'\":'&#39;' }[c]));\n}\nfunction fmtElapsed(ms) {\n if (ms < 1000) return ms + 'ms';\n const s = Math.floor(ms / 1000); if (s < 60) return s + 's';\n const m = Math.floor(s / 60); const rs = s % 60;\n if (m < 60) return m + 'm ' + (rs < 10 ? '0' : '') + rs + 's';\n const h = Math.floor(m / 60); const rm = m % 60;\n return h + 'h ' + (rm < 10 ? '0' : '') + rm + 'm';\n}\n\nfunction render(snapshot) {\n const grid = document.getElementById('grid');\n const nowTs = new Date(snapshot.ts).toLocaleTimeString();\n document.getElementById('ts').textContent = nowTs;\n const summary = { nodes: snapshot.nodes.length, reachable: 0, agents: 0, busy: 0, errors: 0 };\n grid.innerHTML = '';\n for (const node of snapshot.nodes) {\n if (node.reachable) summary.reachable++;\n summary.agents += node.agents.length;\n for (const a of node.agents) {\n const busy = (a.runningTasks && a.runningTasks.length > 0) || (a.active || 0) > 0;\n if (busy) summary.busy++;\n summary.errors += (a.errors || 0);\n }\n grid.appendChild(renderNode(node));\n }\n const L = window.UI_LABELS || {};\n renderStatStrip(snapshot, summary);\n // The connection dot in the topbar gets pulsed only when tasks are running.\n const connDot = document.getElementById('conn-dot');\n const connLabel = document.getElementById('conn-label');\n if (connDot) connDot.classList.toggle('ax-dot--pulse', summary.busy > 0);\n if (connLabel) connLabel.textContent = summary.busy > 0 ? 'running ' + summary.busy : 'idle';\n}\n\n/**\n * Paint the stat strip: agents online, running tasks, tasks today, tokens, errors.\n * The legacy \"today-strip\" logic is absorbed here — same underlying data.\n */\nfunction renderStatStrip(snapshot, summary) {\n const strip = document.getElementById('statstrip');\n if (!strip) return;\n const L = window.UI_LABELS || {};\n // Only what the two tiles need: the error count, and which channels are\n // carrying the currently-running work. Token and duration totals moved out\n // with the cost tiles.\n let errors = 0;\n const byChannel = {};\n for (const node of snapshot.nodes) {\n if (!node.usage || !node.usage.agents) continue;\n for (const agentId of Object.keys(node.usage.agents)) {\n const u = node.usage.agents[agentId];\n errors += u.errors || 0;\n if (u.byChannel) for (const ch of Object.keys(u.byChannel)) {\n byChannel[ch] = (byChannel[ch] || 0) + (u.byChannel[ch].tasks || 0);\n }\n }\n }\n\n // Two tiles, because this page answers one question: who is alive, and what\n // are they doing right now. \"tasks today\" and \"tokens today\" answer a cost\n // question that /admin/cost owns; \"failed\" answers a health question that\n // /admin/health owns. Five tiles made the page look like a status report\n // and buried the two numbers someone opening Live actually came for.\n const failing = errors + summary.errors;\n const runningRows = collectRunning(snapshot);\n strip.innerHTML =\n stat({ label: 'agents online', value: summary.reachable + '/' + summary.nodes + ' machines',\n sub: summary.agents + ' agents', variant: 'live', pulse: summary.reachable > 0 }) +\n // The busy COUNT and the running-task LIST come from different parts of\n // the snapshot and can lag each other by a poll: an agent flips to active\n // before its task appears, and the task disappears before the count drops.\n // Without a fallback the tile then renders a number and nothing else,\n // which reads as broken. Never let this tile be blank.\n stat({ label: 'running now', value: summary.busy,\n sub: runningRows.length ? '' :\n (summary.busy > 0 ? 'starting…' : 'nothing active'),\n bodyHtml: runningRows.length ? runningBodyHtml(runningRows) : '',\n variant: summary.busy > 0 ? 'live' : '' }) +\n // Failures are the one exception: they earn a tile only when non-zero,\n // because a silent failure is the thing you most need pulled forward.\n (failing > 0\n ? stat({ label: L.errorsCount || 'failed', value: failing,\n sub: 'see health →', variant: 'err' })\n : '');\n}\n\nfunction stat({ label, value, sub, variant, pulse, bodyHtml }) {\n const cls = 'ax-stat' + (variant ? ' ax-stat--' + variant : '');\n const dot = pulse ? '<span class=\"ax-dot ax-dot--live ax-dot--pulse\"></span>' : '';\n return '<div class=\"' + cls + '\">' +\n '<div class=\"ax-stat__label\">' + dot + escapeHtml(String(label)) + '</div>' +\n '<div class=\"ax-stat__value\">' + escapeHtml(String(value)) + '</div>' +\n (bodyHtml || (sub ? '<div class=\"ax-stat__sub\">' + escapeHtml(String(sub)) + '</div>' : '')) +\n '</div>';\n}\n\n/** The \"running now\" tile, when something IS running, lists who — each a link\n * straight into that conversation.\n *\n * A count alone made you hunt: read \"2\", then scan 31 cards for the two that\n * are lit. The tile already knows which agents they are, so it should hand\n * them over. This is the page's one shortcut — everything else is a roster. */\nfunction collectRunning(snapshot) {\n const rows = [];\n for (const node of (snapshot && snapshot.nodes) || []) {\n for (const a of (node.agents || [])) {\n for (const t of (a.runningTasks || [])) {\n rows.push({\n agentId: a.id,\n agentName: a.name || a.id,\n nodeUrl: (node && node.url) || '',\n taskId: t.id,\n channel: t.channel || '',\n preview: t.messagePreview || '',\n startedAt: t.startedAt,\n });\n }\n }\n }\n return rows;\n}\n\nfunction runningBodyHtml(rows) {\n if (!rows.length) return '';\n return '<div class=\"ax-stat__running\">' + rows.slice(0, 4).map(function (r) {\n const href = r.taskId ? taskPageUrl(r) : ''; // r carries preview + startedAt\n const elapsed = r.startedAt ? fmtElapsed(Date.now() - new Date(r.startedAt).getTime()) : '';\n const inner =\n '<span class=\"ax-mention\">@' + escapeHtml(r.agentId) + '</span>' +\n (r.channel ? '<span class=\"ax-stat__running-ch\">' + escapeHtml(r.channel) + '</span>' : '') +\n '<span class=\"ax-stat__running-el ax-mono\">' + escapeHtml(elapsed) + '</span>';\n const line = '<div class=\"ax-stat__running-line\" title=\"' + escapeHtml(r.preview) + '\">' + inner + '</div>';\n return href ? '<a class=\"ax-stat__running-row\" href=\"' + escapeHtml(href) + '\">' + line + '</a>'\n : '<div class=\"ax-stat__running-row\">' + line + '</div>';\n }).join('') +\n (rows.length > 4 ? '<div class=\"ax-stat__sub\">+' + (rows.length - 4) + ' more</div>' : '') +\n '</div>';\n}\n\n\n\nfunction renderNode(node) {\n const sec = document.createElement('section');\n sec.className = 'ax-node';\n const tag = node.reachable\n ? '<span class=\"ax-node__tag ax-node__tag--up\">online · ' + (node.uptimeSec ? Math.round(node.uptimeSec / 60) + 'm' : '—') + '</span>'\n : '<span class=\"ax-node__tag ax-node__tag--down\">offline — ' + escapeHtml(node.error || 'unreachable') + '</span>';\n sec.innerHTML = '<header>' +\n '<span class=\"ax-node__name\">' + escapeHtml(node.name) + '</span>' +\n '<span class=\"ax-node__url\">' + escapeHtml(node.url) + '</span>' +\n tag + '</header><div class=\"ax-grid--agents\"></div>';\n const g = sec.querySelector('.ax-grid--agents');\n if (!node.reachable || node.agents.length === 0) {\n const empty = document.createElement('div');\n empty.className = 'ax-empty';\n const L = window.UI_LABELS || {};\n empty.textContent = node.reachable ? (L.noAgentsOnNode || 'No agents on this node.') : (L.unreachable || 'Unreachable.');\n g.appendChild(empty);\n } else {\n for (const a of node.agents) g.appendChild(renderAgent(a, node));\n }\n return sec;\n}\n\nfunction renderAgent(a, node) {\n const card = document.createElement('div');\n const busy = (a.runningTasks && a.runningTasks.length > 0) || (a.active || 0) > 0;\n // Error border reflects the MOST RECENT task's status, not the cumulative\n // error count. A red border drops the moment a new task succeeds — it should\n // not persist forever because of a failure an hour ago. The cumulative\n // a.errors count stays visible in the \"failed\" ministat below as an\n // informational value.\n const errored = a.lastSummary && a.lastSummary.ok === false;\n card.className = 'ax-card ax-agent' + (busy ? ' is-handling' : '') + (errored ? ' is-errored' : '');\n const nodeUrl = (node && node.url) || '';\n const L = window.UI_LABELS || {};\n const tierLabels = (L.tierLabels) || {};\n const tierDisplay = tierLabels[a.tier] || a.tier || '';\n\n // Running task card (clickable → opens live stream modal). Wrapper is a\n // <div role=\"button\"> so we can nest the Stop / Update action buttons\n // inside (button-in-button is invalid HTML); click handler is delegated\n // via the .ax-agent__task class.\n const taskHtml = (a.runningTasks || []).map(t => {\n const elapsed = fmtElapsed(Date.now() - new Date(t.startedAt).getTime());\n const dataAttrs = t.id\n ? ' data-task-id=\"' + escapeHtml(t.id) + '\" data-agent-id=\"' + escapeHtml(a.id) + '\" data-node-url=\"' + escapeHtml(nodeUrl) + '\" data-channel=\"' + escapeHtml(t.channel || '') + '\" data-agent-name=\"' + escapeHtml(a.name || a.id) + '\"'\n : '';\n const actions = t.id\n ? '<div class=\"ax-agent__task-actions\">' +\n '<button type=\"button\" class=\"ax-task-action ax-task-action--update\" data-action=\"followup\" data-task-id=\"' + escapeHtml(t.id) + '\" data-node-url=\"' + escapeHtml(nodeUrl) + '\" title=\"Add a message to this chat — the current turn keeps running, your message dispatches as the next turn\">✎ update</button>' +\n '<button type=\"button\" class=\"ax-task-action ax-task-action--stop\" data-action=\"cancel\" data-task-id=\"' + escapeHtml(t.id) + '\" data-node-url=\"' + escapeHtml(nodeUrl) + '\" title=\"Stop this running task\">✕ stop</button>' +\n '</div>'\n : '';\n return '<div class=\"ax-agent__task\" role=\"button\" tabindex=\"0\"' + dataAttrs + ' data-started-at=\"' + escapeHtml(t.startedAt || '') + '\" title=\"' + escapeHtml(t.messagePreview || '') + '\">' +\n '<div class=\"ax-agent__task-head\">' +\n '<span class=\"ax-dot ax-dot--live ax-dot--pulse\"></span>' +\n '<span>running · ' + escapeHtml(t.channel || '') + '</span>' +\n '<span class=\"elapsed\">' + elapsed + '</span>' +\n '</div>' +\n '<div class=\"ax-agent__task-body\">' + escapeHtml(t.messagePreview || '(no preview)') + '</div>' +\n actions +\n '</div>';\n }).join('');\n const runningBlock = taskHtml\n ? '<div class=\"ax-agent__running\">' + taskHtml + '</div>'\n : (busy ? '<div class=\"ax-agent__running\"><div class=\"ax-agent__task\"><div class=\"ax-agent__task-head\"><span class=\"ax-dot ax-dot--live ax-dot--pulse\"></span>' + escapeHtml(L.runningNoPreview || 'working · preparing reply') + '</div></div></div>' : '');\n\n // Last-reply summary (shown when idle)\n const summaryBlock = (!busy && a.lastSummary && a.lastSummary.text)\n ? '<div class=\"ax-agent__summary' + (a.lastSummary.ok === false ? ' is-fail' : '') + '\">' +\n '<div class=\"ax-agent__summary-caption\">last reply' + (a.lastSummary.at ? ' · ' + escapeHtml(fmtAgo(a.lastSummary.at)) : '') + '</div>' +\n '<div class=\"ax-agent__summary-text\" title=\"' + escapeHtml(a.lastSummary.text) + '\">' + escapeHtml(a.lastSummary.text) + '</div>' +\n '</div>'\n : (busy ? '' : '<div class=\"ax-agent__summary\"><div class=\"ax-agent__summary-caption\">' + escapeHtml(L.idle || 'idle') + '</div><div class=\"ax-agent__summary-text\" style=\"font-style:italic;color:var(--ax-muted)\">' + escapeHtml(L.neverRan || 'awaiting first task') + '</div></div>');\n\n const lastActiveText = a.lastActive ? 'last active ' + fmtAgo(a.lastActive) : (L.neverRan || 'not used yet');\n const lastActiveAttr = a.lastActive ? ' data-last-active=\"' + escapeHtml(a.lastActive) + '\"' : '';\n const recentLink = nodeUrl\n ? '<button class=\"ax-linkbtn\" data-agent-id=\"' + escapeHtml(a.id) + '\" data-agent-name=\"' + escapeHtml(a.name || a.id) + '\" data-node-url=\"' + escapeHtml(nodeUrl) + '\" data-recent=\"1\">history →</button>'\n : '';\n\n // Head: mention (trigger) + human name + tier badge + live/idle badge\n const mention = (a.mentions && a.mentions.length) ? '@' + a.mentions[0].replace(/^@/, '') : '@' + a.id;\n const liveBadge = busy\n ? '<span class=\"ax-badge ax-badge--mono ax-badge--live\"><span class=\"ax-dot ax-dot--live ax-dot--pulse\"></span> live</span>'\n : (errored ? '<span class=\"ax-badge ax-badge--mono ax-badge--warn\">errored</span>' : '<span class=\"ax-badge ax-badge--mono ax-badge--ghost\">idle</span>');\n const tierBadge = tierDisplay ? '<span class=\"ax-badge ax-badge--mono ax-badge--ghost\" title=\"AI engine\">' + escapeHtml(tierDisplay) + '</span>' : '';\n\n const head =\n '<div class=\"ax-agent__head\">' +\n '<div class=\"ax-agent__id\">' +\n '<span class=\"ax-mention\">' + escapeHtml(mention) + '</span>' +\n '<span class=\"ax-agent__name\">' + escapeHtml(a.name || a.id) + '</span>' +\n '</div>' +\n '<div class=\"ax-agent__tier\">' + tierBadge + liveBadge + '</div>' +\n '</div>';\n\n // An idle agent collapses to one line.\n //\n // The page's job is \"who is alive and what are they doing RIGHT NOW\". Giving\n // an idle agent the same real estate as a working one — model, last-reply\n // excerpt, footer — is exactly backwards: it makes the answer harder to see\n // the more agents you run. On peer (22 agents) the busy ones were lost in a\n // wall of identical idle cards.\n //\n // Collapsed still carries what the question needs: who, engine, and when it\n // was last active. Everything else is one click away in history.\n if (!busy) {\n card.className += ' is-collapsed';\n card.innerHTML = head + sparkBlockFor(a) +\n '<div class=\"ax-agent__foot\"' + lastActiveAttr + '><span class=\"last-active\">' +\n escapeHtml(lastActiveText) + '</span>' + recentLink + '</div>';\n return card;\n }\n\n card.innerHTML =\n head +\n (a.model ? '<div class=\"ax-agent__model\">' + escapeHtml(shortenModel(a.model)) + '</div>' : '') +\n sparkBlockFor(a) +\n runningBlock +\n summaryBlock +\n '<div class=\"ax-agent__foot\"' + lastActiveAttr + '><span class=\"last-active\">' + escapeHtml(lastActiveText) + '</span>' + recentLink + '</div>';\n return card;\n}\n\n/** Inline SVG sparkline of the last 24 hourly task counts.\n *\n * This came out with the per-agent stat boxes and shouldn't have. The stat\n * boxes were three numbers restating what the row already said; the chart is\n * the one thing on the card that carries information nothing else does — the\n * SHAPE of an agent's day. A flat line beside a busy one is a real signal,\n * and it costs 22px. */\nfunction renderSpark(data) {\n const w = 280, h = 22;\n const max = Math.max.apply(null, data.concat([1]));\n const step = w / Math.max(data.length - 1, 1);\n let pts = '';\n for (let i = 0; i < data.length; i++) {\n const x = (i * step).toFixed(1);\n const y = (h - (data[i] / max) * (h - 4) - 2).toFixed(1);\n pts += (i ? ' ' : '') + x + ',' + y;\n }\n const lastX = ((data.length - 1) * step).toFixed(1);\n const lastY = (h - (data[data.length - 1] / max) * (h - 4) - 2).toFixed(1);\n return '<svg viewBox=\"0 0 ' + w + ' ' + h + '\" preserveAspectRatio=\"none\">' +\n '<polyline points=\"' + pts + '\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.25\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />' +\n '<circle cx=\"' + lastX + '\" cy=\"' + lastY + '\" r=\"2\" fill=\"currentColor\" />' +\n '</svg>';\n}\n\nfunction sparkBlockFor(a) {\n if (!Array.isArray(a.hourlyTasks) || !a.hourlyTasks.length) return '';\n const total = a.hourlyTasks.reduce(function (s, v) { return s + v; }, 0);\n if (!total) return '';\n return '<div class=\"ax-agent__spark\">' + renderSpark(a.hourlyTasks) +\n '<div class=\"ax-agent__spark-caption\"><span>last 24h</span>' +\n '<span class=\"ax-mono\">' + total + '</span></div></div>';\n}\n\nfunction shortenModel(m) {\n if (!m) return '';\n return String(m)\n .replace(/^claude-/, '')\n .replace(/-\\\\d{8}$/, '')\n .replace(/\\\\[1m\\\\]$/, ' · 1M');\n}\n\nfunction fmtAgo(iso) {\n const t = new Date(iso).getTime();\n if (!t) return '—';\n const s = Math.floor((Date.now() - t) / 1000);\n if (s < 60) return s + 's ago';\n const m = Math.floor(s / 60); if (m < 60) return m + 'm ago';\n const h = Math.floor(m / 60); if (h < 24) return h + 'h ago';\n const d = Math.floor(h / 24); return d + 'd ago';\n}\n\nfunction connect() {\n // #conn exists on the Kanban page; the Live page only has #conn-dot/#conn-label\n // in the topbar. Guard it or the snapshot handler throws before render runs,\n // which silently freezes the live grid between page reloads.\n const conn = document.getElementById('conn');\n let es;\n const open = () => {\n try { es = new EventSource('/api/live/stream'); } catch (e) { setTimeout(open, 2000); return; }\n es.addEventListener('snapshot', (ev) => {\n if (conn) { conn.className = 'conn ok'; conn.title = 'connected'; }\n try { render(JSON.parse(ev.data)); } catch (e) { console.error('live render failed', e); }\n });\n es.addEventListener('error', () => {\n if (conn) { conn.className = 'conn err'; conn.title = 'reconnecting…'; }\n es.close(); setTimeout(open, 2000);\n });\n };\n open();\n}\n\n// Tick elapsed / last-active counters every second from DOM data attributes,\n// so they advance smoothly between 2s server snapshots instead of jumping.\nlet lastSnap = null;\nfetch('/api/live').then(r => r.json()).then(s => { lastSnap = s; render(s); }).catch(() => {});\nsetInterval(() => {\n const now = Date.now();\n // Running-task elapsed counters.\n document.querySelectorAll('.ax-agent__task[data-started-at]').forEach((btn) => {\n const startedAt = btn.getAttribute('data-started-at');\n if (!startedAt) return;\n const t = new Date(startedAt).getTime();\n if (!t) return;\n const el = btn.querySelector('.elapsed');\n if (el) el.textContent = fmtElapsed(now - t);\n });\n // Idle-card \"last active\" text — smooths from \"5s ago\" to \"6s ago\".\n document.querySelectorAll('.ax-agent__foot[data-last-active]').forEach((foot) => {\n const iso = foot.getAttribute('data-last-active');\n if (!iso) return;\n const el = foot.querySelector('.last-active');\n if (el) el.textContent = 'last active ' + fmtAgo(iso);\n });\n}, 1000);\nconnect();\n\n/** Navigate to a task's own page. Watching an agent work is a place — it gets\n * a URL you can share, reload and keep open beside other things. */\nfunction taskPageUrl(d) {\n // Carry the request preview across. A LIVE task's SSE stream only sends the\n // agent's output — the daemon never replays what was asked — so without this\n // the request card stays hidden for exactly the tasks you're most likely to\n // be watching. Capped so a long GitLab comment can't blow the URL.\n const ask = (d.preview || '').slice(0, 400);\n return '/tasks/' + encodeURIComponent(d.taskId)\n + '?agent=' + encodeURIComponent(d.agentId || '')\n + '&node=' + encodeURIComponent(d.nodeUrl || '')\n + (d.channel ? '&channel=' + encodeURIComponent(d.channel) : '')\n + (d.agentName ? '&name=' + encodeURIComponent(d.agentName) : '')\n + (ask ? '&ask=' + encodeURIComponent(ask) : '')\n + (d.startedAt ? '&at=' + encodeURIComponent(d.startedAt) : '');\n}\n\n// Click delegation on the agent grid — opens the modal for any task card,\n// or the history panel for the \"history →\" link. Task action buttons\n// (stop / update) are intercepted FIRST so they don't bubble into the\n// modal-open path.\ndocument.getElementById('grid').addEventListener('click', (e) => {\n const actionEl = e.target.closest('.ax-task-action[data-action]');\n if (actionEl) {\n e.preventDefault();\n e.stopPropagation();\n const action = actionEl.dataset.action;\n const taskId = actionEl.dataset.taskId;\n const nodeUrl = actionEl.dataset.nodeUrl || '';\n if (!taskId) return;\n if (action === 'cancel') {\n if (!confirm('Stop this running task?')) return;\n taskAction(nodeUrl, taskId, 'cancel', {});\n } else if (action === 'followup') {\n // Both actions land on the task's page, which hosts the compose box\n // and the Stop control alongside the live stream.\n const card = actionEl.closest('.ax-agent__task[data-task-id]');\n location.href = taskPageUrl({\n taskId,\n agentId: card && card.dataset.agentId,\n nodeUrl,\n channel: card && card.dataset.channel,\n agentName: (card && (card.dataset.agentName || card.dataset.agentId)) || '',\n });\n }\n return;\n }\n const taskEl = e.target.closest('.ax-agent__task[data-task-id]');\n if (taskEl) {\n e.preventDefault();\n location.href = taskPageUrl({\n taskId: taskEl.dataset.taskId,\n agentId: taskEl.dataset.agentId,\n nodeUrl: taskEl.dataset.nodeUrl,\n channel: taskEl.dataset.channel,\n agentName: taskEl.dataset.agentName || taskEl.dataset.agentId,\n preview: taskEl.getAttribute('title') || '',\n startedAt: taskEl.dataset.startedAt,\n });\n return;\n }\n const recentEl = e.target.closest('[data-recent]');\n if (recentEl) {\n e.preventDefault();\n location.href = '/agents/' + encodeURIComponent(recentEl.dataset.agentId || '')\n + '/history?node=' + encodeURIComponent(recentEl.dataset.nodeUrl || '')\n + '&name=' + encodeURIComponent(recentEl.dataset.agentName || '');\n }\n});\n\nfunction taskAction(nodeUrl, taskId, kind, body, hooks) {\n // Browser → dashboard origin → originating daemon. Mirrors the proxy\n // pattern used by openTaskModal / openHistoryPanel.\n hooks = hooks || {};\n if (!nodeUrl) {\n const msg = 'no daemon URL on this task';\n if (hooks.onErr) hooks.onErr(msg); else alert('Task ' + kind + ' failed: ' + msg);\n return;\n }\n if (hooks.onStart) hooks.onStart();\n const url = '/api/task/action?node=' + encodeURIComponent(nodeUrl)\n + '&task=' + encodeURIComponent(taskId)\n + '&kind=' + encodeURIComponent(kind);\n fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body || {}),\n })\n .then(async r => {\n const txt = await r.text();\n let parsed = null; try { parsed = JSON.parse(txt); } catch {}\n if (!r.ok) throw new Error((parsed && parsed.error) || txt || ('HTTP ' + r.status));\n console.log('[task ' + kind + ']', parsed || txt);\n if (hooks.onOk) hooks.onOk(parsed);\n })\n .catch(err => {\n const msg = err && err.message || String(err);\n if (hooks.onErr) hooks.onErr(msg); else alert('Task ' + kind + ' failed: ' + msg);\n });\n}\n\n`\n","// Page CSS for /mesh. Kept out of mesh.ts so the page module stays a\n// readable assembly of sections rather than a wall of declarations.\n//\n// Chart colours live here as page-scoped tokens, defined for BOTH themes.\n// Two families, deliberately separated:\n// --mx-lane-* one hue per activity lane. Each lane is its own small\n// multiple with a single series, so no categorical palette\n// is in play and none needs validating.\n// --mx-good/warn/serious/crit the reserved STATUS ramp, used only for\n// state (a job's verdict, a failed run). Never for identity.\n// Verdicts also carry a distinct marker shape and a direct label, so the\n// status ramp is never the only thing distinguishing them.\nexport const MESH_CSS = `\n.mx-content{max-width:1180px;margin:0 auto;padding:0 24px 56px}\n.mx-updated{margin:0;color:var(--ax-muted);font-size:11px;font-family:var(--ax-mono)}\n.mx-section{margin-bottom:28px}.mx-section .ax-section-head{margin-bottom:14px}\n.mx-section-actions{display:flex;align-items:center;gap:8px;margin-left:auto}\n.mx-show-all{padding:4px 9px;font-size:11px;border-radius:var(--ax-radius-sm)}\n.mx-columns{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr);gap:24px}\n.mx-two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}\n.mx-agent-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}\n\n/* --- chart tokens ------------------------------------------------- */\n/* Scoped to the document root, not .mx-content: the details drawer is a\n sibling of <main> and needs the same status ramp for its run cells.\n Matches how tokens.ts carries the theme (data-theme on <html>). */\n:root{\n --mx-lane-cron:#3B82F6;--mx-lane-workflow:#C2255C;--mx-lane-direct:#0E9594;\n --mx-good:#22B573;--mx-warn:#FFB300;--mx-serious:#E8590C;--mx-crit:#F23A3A;\n --mx-grid:var(--ax-border);--mx-track:var(--ax-surface-3);\n}\n[data-theme=\"dark\"]{\n --mx-lane-cron:#4A86E8;--mx-lane-workflow:#DB3E77;--mx-lane-direct:#159B8B;\n --mx-good:#4ED89B;--mx-warn:#FFC948;--mx-serious:#FF922B;--mx-crit:#FF6B6B;\n}\n\n/* --- toolbar ------------------------------------------------------- */\n.mx-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:0 0 22px}\n.mx-tabs{display:flex;gap:0;border:var(--ax-border-w) solid var(--ax-border-2);border-radius:var(--ax-radius-pill);padding:3px;background:var(--ax-surface)}\n.mx-tab{border:0;background:transparent;color:var(--ax-text-2);font:600 12px var(--ax-font);padding:6px 14px;border-radius:var(--ax-radius-pill);cursor:pointer}\n.mx-tab[aria-selected=\"true\"]{background:var(--ax-accent);color:#fff}\n.mx-tab:focus-visible{outline:2px solid var(--ax-accent);outline-offset:2px}\n.mx-range{display:flex;gap:6px;margin-left:auto}\n.mx-range button{border:var(--ax-border-w) solid var(--ax-border-2);background:var(--ax-surface);color:var(--ax-text-2);font:600 11px var(--ax-mono);padding:5px 10px;border-radius:var(--ax-radius-sm);cursor:pointer}\n.mx-range button[aria-pressed=\"true\"]{border-color:var(--ax-accent);color:var(--ax-accent)}\n.mx-view[hidden]{display:none}\n\n/* --- activity lanes ------------------------------------------------ */\n.mx-lane{display:grid;grid-template-columns:132px minmax(0,1fr) 92px;align-items:center;gap:14px;padding:10px 0;border-bottom:1px solid var(--ax-border)}\n.mx-lane:last-child{border-bottom:0}\n.mx-lane__name{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600}\n.mx-lane__swatch{width:10px;height:10px;border-radius:3px;flex:0 0 auto}\n.mx-lane__peak{font:10px var(--ax-mono);color:var(--ax-muted);margin-top:2px}\n.mx-lane__tot{text-align:right;font:11px var(--ax-mono);color:var(--ax-text-2)}\n.mx-lane__tot b{display:block;font-size:15px;font-family:var(--ax-font);color:var(--ax-text)}\n.mx-cols{display:flex;align-items:flex-end;gap:2px;height:56px}\n.mx-col{flex:1 1 0;min-width:2px;height:100%;display:flex;flex-direction:column;justify-content:flex-end;gap:2px;background:none;border:0;padding:0;border-radius:3px;cursor:pointer}\n.mx-col:hover,.mx-col:focus-visible{outline:2px solid var(--ax-accent);outline-offset:2px}\n.mx-col i{display:block;font-style:normal;border-radius:3px 3px 0 0}\n.mx-col i.is-base{border-radius:0 0 3px 3px}\n.mx-col i.is-fail{background:var(--mx-crit)}\n.mx-col i.is-empty{height:2px;background:var(--ax-border);border-radius:1px}\n.mx-axis{display:flex;justify-content:space-between;font:10px var(--ax-mono);color:var(--ax-muted);padding:6px 0 0 146px}\n.mx-note{margin:12px 0 0;font-size:11px;color:var(--ax-muted);line-height:1.55}\n\n/* --- horizontal bar lists ------------------------------------------ */\n.mx-bars{display:grid;gap:9px}\n.mx-bar{display:grid;grid-template-columns:minmax(96px,150px) minmax(0,1fr) auto;align-items:center;gap:10px;width:100%;background:none;border:0;padding:2px 0;color:inherit;font:inherit;text-align:left;cursor:pointer;border-radius:var(--ax-radius-sm)}\n.mx-bar:hover .mx-bar__label,.mx-bar:focus-visible .mx-bar__label{color:var(--ax-accent)}\n.mx-bar:focus-visible{outline:2px solid var(--ax-accent);outline-offset:3px}\n.mx-bar__label{font-size:12px;font-weight:600;line-height:1.25;overflow-wrap:anywhere}\n.mx-bar__track{position:relative;height:14px;border-radius:4px;background:var(--mx-track);overflow:hidden}\n.mx-bar__fill{position:absolute;inset:0 auto 0 0;border-radius:4px}\n.mx-bar__fail{position:absolute;top:0;bottom:0;border-radius:4px;background:var(--mx-crit);box-shadow:-2px 0 0 var(--ax-surface)}\n.mx-bar__val{font:11px var(--ax-mono);color:var(--ax-text-2);white-space:nowrap}\n.mx-bar__val em{font-style:normal;color:var(--ax-muted)}\n\n/* --- scatter -------------------------------------------------------- */\n.mx-scatter{position:relative;overflow-x:auto}\n/* Below ~700px the plot would shrink its 10px axis labels into noise, so\n it keeps a legible minimum and scrolls inside its own container rather\n than forcing the page to scroll sideways. */\n.mx-scatter svg{display:block;width:100%;min-width:660px;height:340px;overflow:visible}\n.mx-scatter text{font:10px var(--ax-mono);fill:var(--ax-muted)}\n.mx-scatter .mx-pt{cursor:pointer}\n.mx-scatter .mx-pt:hover circle,.mx-scatter .mx-pt:hover path,.mx-scatter .mx-pt:hover rect.mx-mark{stroke:var(--ax-text);stroke-width:2}\n.mx-scatter .mx-lbl{fill:var(--ax-text-2);font-size:10px}\n.mx-legend{display:flex;flex-wrap:wrap;gap:6px 16px;margin-top:12px}\n.mx-legend span{display:inline-flex;align-items:center;gap:7px;font-size:11px;color:var(--ax-text-2)}\n.mx-legend svg{width:12px;height:12px;overflow:visible}\n.mx-tip{position:absolute;z-index:5;pointer-events:none;min-width:170px;padding:9px 11px;border:var(--ax-border-w) solid var(--ax-border-2);border-radius:var(--ax-radius-sm);background:var(--ax-surface);box-shadow:var(--ax-shadow-lg);font-size:11px;line-height:1.5}\n.mx-tip b{display:block;font-size:12px;margin-bottom:3px}\n.mx-tip dl{display:grid;grid-template-columns:auto auto;gap:2px 10px;margin:4px 0 0;font-family:var(--ax-mono);font-size:10px}\n.mx-tip dt{color:var(--ax-muted)}.mx-tip dd{margin:0;text-align:right}\n\n/* --- lifetime ------------------------------------------------------- */\n.mx-life{display:grid;grid-template-columns:minmax(150px,200px) minmax(0,1fr) 158px;align-items:center;gap:14px;width:100%;padding:9px 8px;background:none;border:0;border-radius:var(--ax-radius-sm);color:inherit;font:inherit;text-align:left;cursor:pointer}\n.mx-life:hover,.mx-life:focus-visible{background:var(--ax-surface-2);outline:none}\n.mx-life__id{min-width:0}\n.mx-life__id b{display:block;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.mx-life__id span{display:block;font:10px var(--ax-mono);color:var(--ax-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.mx-life__span{position:relative;height:20px}\n.mx-life__track{position:absolute;top:8px;height:4px;border-radius:2px;background:var(--ax-accent);opacity:.55}\n.mx-life__tick{position:absolute;top:2px;width:2px;height:16px;border-radius:1px}\n.mx-life__meta{text-align:right;font:10px var(--ax-mono);color:var(--ax-muted)}\n.mx-life__meta b{font-family:var(--ax-font);font-size:13px;color:var(--ax-text)}\n.mx-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}\n.mx-chip{border:var(--ax-border-w) solid var(--ax-border);background:var(--ax-surface);color:var(--ax-text-2);font:600 11px var(--ax-font);padding:4px 11px;border-radius:var(--ax-radius-pill);cursor:pointer}\n.mx-chip[aria-pressed=\"true\"]{border-color:var(--ax-accent);color:var(--ax-accent)}\n\n/* --- drawer: stat tiles, conversation list ------------------------- */\n.mx-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));gap:8px}\n.mx-stat{padding:9px 11px;border:1px solid var(--ax-border);border-radius:var(--ax-radius-sm);background:var(--ax-surface-2)}\n.mx-stat b{display:block;font:10px var(--ax-mono);text-transform:uppercase;letter-spacing:.05em;color:var(--ax-muted);font-weight:400}\n.mx-stat span{display:block;margin-top:3px;font-size:15px;font-weight:600;font-variant-numeric:tabular-nums}\n.mx-convs{display:grid;gap:4px;max-height:300px;overflow:auto}\n.mx-conv{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;padding:8px 10px;border:1px solid var(--ax-border);border-radius:var(--ax-radius-sm);background:var(--ax-surface);color:inherit;font:inherit;text-align:left;cursor:pointer}\n.mx-conv:hover,.mx-conv:focus-visible{border-color:var(--ax-accent);outline:none}\n.mx-conv__id{min-width:0}\n.mx-conv__id b{display:block;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.mx-conv__id span{display:block;font:10px var(--ax-mono);color:var(--ax-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.mx-conv__meta{flex:0 0 auto;text-align:right;font:10px var(--ax-mono);color:var(--ax-text-2);line-height:1.5}\n.mx-tip__hint{margin:6px 0 0;padding-top:5px;border-top:1px solid var(--ax-border);font-size:10px;color:var(--ax-muted)}\n\n/* --- run grid + step list (drawer) --------------------------------- */\n#mx-drill,#mx-day,#mx-conv{display:grid;gap:16px}\n.mx-runs{display:flex;flex-wrap:wrap;gap:3px;margin:6px 0 4px}\n.mx-run{width:15px;height:15px;border-radius:3px;border:0;padding:0;cursor:pointer}\n.mx-run:focus-visible{outline:2px solid var(--ax-accent);outline-offset:2px}\n.mx-run[aria-pressed=\"true\"]{box-shadow:0 0 0 2px var(--ax-surface),0 0 0 4px var(--ax-text)}\n.mx-touch{display:flex;flex-wrap:wrap;gap:6px;margin:4px 0 0}\n.mx-touch span{border:1px solid var(--ax-border-2);border-radius:var(--ax-radius-pill);padding:2px 9px;font:10px var(--ax-mono);color:var(--ax-text-2)}\n.mx-touch span.is-zero{color:var(--ax-muted);opacity:.7}\n.mx-steps{margin:0;padding:0;list-style:none;max-height:260px;overflow:auto;border:1px solid var(--ax-border);border-radius:var(--ax-radius-sm)}\n.mx-steps li{display:grid;grid-template-columns:30px 1fr auto;gap:8px;padding:5px 9px;font:10px var(--ax-mono);border-bottom:1px solid var(--ax-border)}\n.mx-steps li:last-child{border-bottom:0}\n.mx-steps li[data-err=\"1\"]{background:var(--ax-red-t)}\n.mx-steps .mx-seq{color:var(--ax-muted)}\n\n/* --- shared bits ---------------------------------------------------- */\n.mx-item{display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;text-align:left;color:var(--ax-text);font:inherit;text-decoration:none;cursor:pointer}\n.mx-item:hover,.mx-item:focus-visible{border-color:var(--ax-accent);text-decoration:none;outline:none}\n.mx-item[hidden]{display:none}.mx-item .ax-avatar{width:34px;height:34px;border-radius:var(--ax-radius-sm)}\n.mx-item .ax-row-card__actions{flex-shrink:0}.mx-item .ax-sub{min-width:0}\n.mx-item .ax-sub code{font:11px var(--ax-mono);color:var(--ax-muted)}\n.mx-summary{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ax-muted)}\n.mx-node-meta{font:11px var(--ax-mono);color:var(--ax-muted)}\n.mx-empty{padding:28px 18px;text-align:center;color:var(--ax-muted);font-size:12px;border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);background:var(--ax-surface)}\n.mx-link{display:inline-block;margin-top:14px;color:var(--ax-accent);font-size:12px;font-weight:600;text-decoration:none}\n.mx-link:hover{text-decoration:underline}\n.mx-scrim{position:fixed;inset:0;background:color-mix(in oklch,var(--ax-bg) 55%,black);z-index:29}\n.mx-drawer{position:fixed;z-index:30;top:0;right:0;width:min(520px,calc(100vw - 20px));height:100vh;box-sizing:border-box;background:var(--ax-surface);border-left:var(--ax-border-w) solid var(--ax-border-2);transform:translateX(102%);transition:transform 180ms ease;overflow:auto;box-shadow:-12px 0 36px rgba(0,0,0,.2)}\n.mx-drawer.is-open{transform:translateX(0)}\n.mx-drawer__head{position:sticky;top:0;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:18px 20px;background:var(--ax-surface);border-bottom:var(--ax-border-w) solid var(--ax-border);z-index:1}\n.mx-drawer__head .ax-kicker{font:10px var(--ax-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ax-muted);margin-bottom:4px}\n.mx-drawer__head h2{margin:0;font-size:18px;font-weight:600;letter-spacing:-.01em}\n.mx-drawer__head .ax-btn{padding:6px 10px;font-size:11px;box-shadow:none}\n.mx-drawer__body{padding:20px}\n.mx-detail{display:grid;gap:16px}\n.mx-detail h3{margin:0 0 8px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:var(--ax-muted);font-family:var(--ax-mono)}\n.mx-detail dl{display:grid;grid-template-columns:118px 1fr;gap:9px 12px;margin:0;font-size:12px}\n.mx-detail dt{color:var(--ax-muted);font-family:var(--ax-mono);font-size:10px;text-transform:uppercase;letter-spacing:.05em}\n.mx-detail dd{margin:0;word-break:break-word}\n.mx-detail pre{margin:0;padding:14px;background:var(--ax-bg);border:var(--ax-border-w) solid var(--ax-border);border-radius:var(--ax-radius-sm);white-space:pre-wrap;font:11px/1.55 var(--ax-mono)}\n.mx-verdict{display:inline-flex;align-items:center;gap:6px;font-weight:600;font-size:12px}\n.mx-verdict i{width:10px;height:10px;border-radius:2px;font-style:normal}\n\n@media(max-width:860px){\n .mx-content{padding:0 16px 40px}\n .mx-columns,.mx-agent-grid,.mx-two{grid-template-columns:1fr}\n .mx-section{margin-bottom:24px}\n .mx-lane{grid-template-columns:1fr;gap:6px}\n .mx-lane__tot{text-align:left}\n .mx-axis{padding-left:0}\n .mx-bar{grid-template-columns:96px minmax(0,1fr)}\n .mx-bar__val{grid-column:1 / -1;text-align:right}\n .mx-life{grid-template-columns:1fr;gap:8px}\n .mx-life__meta{text-align:left}\n .mx-section .ax-section-head__text .ax-lead{display:none}\n .mx-summary{max-width:56vw}\n}\n@media(prefers-reduced-motion:reduce){.mx-drawer{transition:none}}\n`\n","// Browser helpers shared by the mesh page's three views. Loaded once,\n// before the view scripts, and exposed as `window.MX`.\n//\n// No build step (CLAUDE.md: zero-build dashboard stack), so this is a\n// plain string of ES5-compatible browser JS. Avoid backslash escapes —\n// inside a TS template literal they are eaten before reaching the browser.\nexport const MESH_SHARED_SCRIPT = `<script>\nwindow.MX=(function(){\n var ENT={'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'};\n function esc(v){return String(v==null?'':v).replace(/[&<>\"']/g,function(c){return ENT[c]})}\n function num(v){return Number(v||0).toLocaleString()}\n /** \"1 turn\" / \"2 turns\" — a count reads as sloppy without it. */\n function plural(n,one,many){return num(n)+' '+(Number(n)===1?one:(many||one+'s'))}\n function pct(part,total){return total?Math.round(part/total*1000)/10+'%':'0%'}\n function age(v){if(!v)return 'unknown';var s=Math.max(0,(Date.now()-new Date(v).getTime())/1000);\n if(s<60)return Math.floor(s)+'s ago';if(s<3600)return Math.floor(s/60)+'m ago';\n if(s<86400)return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago'}\n function dur(ms){if(!Number.isFinite(ms)||ms==null)return '-';\n if(ms<1000)return ms+'ms';if(ms<60000)return (ms/1000).toFixed(1)+'s';\n if(ms<3600000)return (ms/60000).toFixed(1)+'m';return (ms/3600000).toFixed(1)+'h'}\n function hours(h){return h>=1?h.toFixed(1)+'h':Math.round(h*60)+'m'}\n function day(iso){var p=String(iso||'').split('-');return p.length===3?p[2]+'/'+p[1]:iso}\n function when(ms){return ms?new Date(ms).toLocaleString():'-'}\n\n var drawer,scrim,closeBtn,lastFocus=null;\n function mount(){\n drawer=document.getElementById('mx-drawer');\n scrim=document.getElementById('mx-scrim');\n closeBtn=document.getElementById('mx-close');\n if(!drawer)return;\n closeBtn.addEventListener('click',close);\n scrim.addEventListener('click',close);\n document.addEventListener('keydown',function(e){if(e.key==='Escape'&&drawer.classList.contains('is-open'))close()});\n }\n function open(kicker,title,html){\n lastFocus=document.activeElement;\n document.getElementById('mx-drawer-kicker').textContent=kicker;\n document.getElementById('mx-drawer-title').textContent=title;\n document.getElementById('mx-drawer-body').innerHTML=html;\n drawer.classList.add('is-open');drawer.setAttribute('aria-hidden','false');\n scrim.hidden=false;closeBtn.focus();\n }\n function close(){\n if(!drawer)return;\n drawer.classList.remove('is-open');drawer.setAttribute('aria-hidden','true');\n scrim.hidden=true;\n if(lastFocus&&lastFocus.focus)lastFocus.focus();\n }\n function body(){return document.getElementById('mx-drawer-body')}\n\n /** Definition list from [label, value] pairs. Values are escaped. */\n function fields(pairs){\n return '<dl>'+pairs.map(function(f){\n return '<dt>'+esc(f[0])+'</dt><dd>'+esc(f[1])+'</dd>'}).join('')+'</dl>';\n }\n function section(title,html){return '<div><h3>'+esc(title)+'</h3>'+html+'</div>'}\n\n /** Verdicts carry a colour AND a shape AND a label — never colour alone. */\n var VERDICT={\n 'healthy':{label:'Healthy',color:'var(--mx-good)',shape:'circle',\n why:'Output tracks the runtime it spends.'},\n 'fragile':{label:'Fragile',color:'var(--mx-warn)',shape:'circle',\n why:'At least 30% of attempts fail.'},\n 'zombie':{label:'No real output',color:'var(--mx-serious)',shape:'square',\n why:'Succeeds, runs a minute or more, returns under 25 tokens.'},\n 'never-succeeded':{label:'Never succeeded',color:'var(--mx-crit)',shape:'triangle',\n why:'Every recorded attempt failed. Still scheduled.'}\n };\n function verdict(v){return VERDICT[v]||VERDICT.healthy}\n function mark(v,x,y,r){\n var c=verdict(v).color;\n if(verdict(v).shape==='square')\n return '<rect class=\"mx-mark\" x=\"'+(x-r)+'\" y=\"'+(y-r)+'\" width=\"'+(r*2)+'\" height=\"'+(r*2)+'\" rx=\"2\" fill=\"'+c+'\"/>';\n if(verdict(v).shape==='triangle')\n return '<path d=\"M '+x+' '+(y-r*1.15)+' L '+(x+r*1.1)+' '+(y+r*0.8)+' L '+(x-r*1.1)+' '+(y+r*0.8)+' Z\" fill=\"'+c+'\"/>';\n return '<circle cx=\"'+x+'\" cy=\"'+y+'\" r=\"'+r+'\" fill=\"'+c+'\"/>';\n }\n function verdictChip(v){\n var d=verdict(v);\n return '<span class=\"mx-verdict\"><i style=\"background:'+d.color+'\"></i>'+esc(d.label)+'</span>';\n }\n\n var CAUSE={\n auth:'Authentication / entitlement',\n quota:'Usage limit reached',\n connectivity:'Network or DNS',\n overload:'Provider overloaded',\n timeout:'Ran past its time limit',\n crash:'Runtime crashed',\n misconfig:'Configuration or missing binary',\n other:'Unclassified'\n };\n function causeLabel(id){return CAUSE[id]||id}\n\n /** Health-strip cards are written by whichever view owns the number, so\n * the setter lives here rather than in one of them. */\n function setHealth(index,value,kind,label){\n var card=document.querySelectorAll('#mx-health .ax-health-card')[index];\n if(!card)return;\n card.querySelector('.ax-hc-num').textContent=String(value);\n card.querySelector('.ax-hc-dot').className='ax-hc-dot ax-hc-dot--'+kind;\n if(label){var l=card.querySelector('.ax-hc-lbl');if(l)l.textContent=label}\n }\n\n function get(url){\n return fetch(url,{headers:{Accept:'application/json'}}).then(function(r){\n if(!r.ok)throw new Error('HTTP '+r.status);return r.json()});\n }\n\n mount();\n return {esc:esc,num:num,plural:plural,pct:pct,age:age,dur:dur,hours:hours,day:day,when:when,\n open:open,close:close,body:body,fields:fields,section:section,\n verdict:verdict,verdictChip:verdictChip,mark:mark,causeLabel:causeLabel,\n setHealth:setHealth,get:get};\n})();\n</script>`\n","// Drill-down drawers for /mesh, exposed as window.MXD.\n//\n// Four entry points, all landing in the same drawer and all ending at the\n// same place — one attempt and the tools it touched:\n//\n// openDay an activity column → what ran that day → a conversation\n// openJob a scatter bubble → the job's attempt history\n// openConversation a thread row → the whole life of one conversation\n// openRun one attempt → its step ledger\n//\n// Kept out of mesh-analytics.client.ts because that file is about drawing\n// the overview; this one is about what happens after a click.\nexport const MESH_DRILL_SCRIPT = `<script>\nwindow.MXD=(function(){\n function el(id){return document.getElementById(id)}\n function tz(){return -new Date().getTimezoneOffset()}\n function tiles(items){\n return '<div class=\"mx-stats\">'+items.map(function(t){\n return '<div class=\"mx-stat\"><b>'+MX.esc(t[0])+'</b><span>'+MX.esc(t[1])+'</span></div>';\n }).join('')+'</div>';\n }\n function miniBars(rows,color){\n if(!rows.length)return '<p class=\"mx-note\">None recorded.</p>';\n var max=Math.max.apply(null,rows.map(function(r){return r[1]}))||1;\n return '<div class=\"mx-bars\">'+rows.map(function(r){\n return '<div class=\"mx-bar\"><span class=\"mx-bar__label\">'+MX.esc(r[0])+'</span>'\n +'<span class=\"mx-bar__track\"><span class=\"mx-bar__fill\" style=\"width:'\n +Math.max(2,r[1]/max*100)+'%;background:'+color+';opacity:.6\"></span></span>'\n +'<span class=\"mx-bar__val\">'+MX.num(r[1])+'</span></div>';\n }).join('')+'</div>';\n }\n\n // --- One activity column ------------------------------------------\n //\n // The column the operator clicked belongs to ONE lane, so the drawer is\n // scoped to that lane by default. Opening the whole day from a Scheduled\n // column would answer a question nobody asked, and the numbers would not\n // match the bar that was clicked.\n var LANE_NAME={cron:'Scheduled',workflow:'Workflow',direct:'Direct'};\n function laneOf(channel){\n return channel==='cron'?'cron':channel==='workflow'?'workflow':'direct';\n }\n function openDay(day,lane){\n MX.open('Activity',day+(lane?' · '+(LANE_NAME[lane]||lane):''),\n '<div class=\"mx-detail\"><div id=\"mx-day\">'\n +'<div class=\"mx-empty\">Loading '+MX.esc(day)+'...</div></div></div>');\n MX.get('/api/mesh/day?day='+encodeURIComponent(day)+'&tzOffset='+tz()+'&limit=60')\n .then(function(d){renderDay(d,lane)})\n .catch(function(e){var h=el('mx-day');if(h)h.innerHTML='<div class=\"mx-empty\">'+MX.esc(e.message)+'</div>'});\n }\n\n /** Narrow a fleet day to one lane, using the same origin rule the lanes\n * themselves are built with. Causes cannot be split this way — they are\n * aggregated per day, not per channel — so they stay fleet-wide and say so. */\n function scopeDay(d,lane){\n if(!lane)return {scoped:d,all:d,lane:null};\n var origins=d.origins.filter(function(o){return laneOf(o.channel)===lane});\n var conversations=d.conversations.filter(function(c){return laneOf(c.channel)===lane});\n var totals={runs:0,errors:0,ms:0,inputTokens:0,outputTokens:0};\n origins.forEach(function(o){totals.runs+=o.runs;totals.errors+=o.errors;totals.ms+=o.ms});\n conversations.forEach(function(c){totals.inputTokens+=c.inputTokens;totals.outputTokens+=c.outputTokens});\n return {scoped:{day:d.day,nodes:d.nodes,totals:totals,origins:origins,causes:[],conversations:conversations},all:d,lane:lane};\n }\n function renderDay(full,lane){\n var host=el('mx-day');if(!host)return;\n var view=scopeDay(full,lane),d=view.scoped;\n if(!d.totals.runs){\n host.innerHTML='<div class=\"mx-empty\">No '+MX.esc(lane?(LANE_NAME[lane]||lane).toLowerCase()+' ':'')\n +'work ran on '+MX.esc(d.day)+'.</div>';\n return;\n }\n var down=d.nodes.filter(function(n){return !n.ok});\n var html=tiles([\n ['Runs',MX.num(d.totals.runs)],\n ['Failed',MX.num(d.totals.errors)+' ('+MX.pct(d.totals.errors,d.totals.runs)+')'],\n ['Runtime',MX.dur(d.totals.ms)],\n ['Output',MX.num(d.totals.outputTokens)+' tok']\n ]);\n if(down.length)html+='<p class=\"mx-note\">Missing '+MX.esc(down.map(function(n){return n.name}).join(', '))+' — these totals are partial.</p>';\n if(lane)html+='<p class=\"mx-note\">Scoped to the '+MX.esc((LANE_NAME[lane]||lane).toLowerCase())\n +' lane you selected. The whole day ran '+MX.num(view.all.totals.runs)+' across every origin.</p>';\n html+=MX.section(lane?'Channels in this lane':'By origin',miniBars(d.origins.map(function(o){\n return [o.channel+(o.errors?' ('+o.errors+' failed)':''),o.runs]}),'var(--ax-accent)'));\n // Causes are grouped per day, not per channel, so they cannot be split\n // by lane without inventing an attribution. Show the day's, labelled.\n if(view.all.causes.length)html+=MX.section('Failures'+(lane?' (whole day)':''),\n miniBars(view.all.causes.map(function(c){return [MX.causeLabel(c.cause),c.count]}),'var(--mx-crit)'));\n // 60 is the fan-out cap per node, so a full list may be truncated —\n // label it rather than implying the day had exactly this many.\n var convCap=d.conversations.length>=60*Math.max(1,d.nodes.filter(function(n){return n.ok}).length);\n html+=MX.section('Conversations that day ('+(convCap?'busiest '+d.conversations.length:d.conversations.length)+')',\n '<div class=\"mx-convs\">'+d.conversations.map(function(c,i){\n return '<button class=\"mx-conv\" type=\"button\" data-conv=\"'+i+'\">'\n +'<span class=\"mx-conv__id\"><b>'+MX.esc(c.agent)+'</b><span>'+MX.esc(c.channel)+' · '+MX.esc(c.chatId)+'</span></span>'\n +'<span class=\"mx-conv__meta\">'+MX.plural(c.turns,'turn')+(c.errors?' · '+MX.num(c.errors)+' failed':'')\n +'<br>'+MX.dur(c.ms)+' · '+MX.num(c.outputTokens)+' tok out</span></button>';\n }).join('')+'</div><p class=\"mx-note\">Ordered by runtime. Select one for its full history.</p>');\n host.innerHTML=html;\n host.querySelectorAll('[data-conv]').forEach(function(b){\n b.addEventListener('click',function(){openConversation(d.conversations[Number(b.dataset.conv)])});\n });\n }\n\n // --- One recurring job --------------------------------------------\n function openJob(j){\n var m=MX.verdict(j.verdict);\n MX.open('Recurring job',j.label,'<div class=\"mx-detail\">'\n +'<div>'+MX.verdictChip(j.verdict)+'<p class=\"mx-note\">'+MX.esc(m.why)+'</p></div>'\n +MX.fields([['Node',j.node],['Agent',j.agent],['Kind',j.kind],\n ['Runs',MX.num(j.runs)],['Succeeded',MX.num(j.okRuns)],['Failed',MX.num(j.errors)],\n ['Runtime',MX.hours(j.hours)],['Avg run',j.avgMinutes+' min'],\n ['Avg output',MX.num(Math.round(j.avgOutput))+' tokens'],['Last run',MX.when(j.lastAt)]])\n +'<div id=\"mx-drill\">'+MX.section('Attempts','<div class=\"mx-empty\">Loading attempts...</div>')+'</div></div>');\n MX.get('/api/mesh/job?node='+encodeURIComponent(j.nodeUrl)+'&kind='+encodeURIComponent(j.kind)\n +'&key='+encodeURIComponent(j.label)+'&limit=150')\n .then(function(r){renderRunGrid(j.nodeUrl,r.runs||[])})\n .catch(function(e){var t=el('mx-drill');if(t)t.innerHTML=MX.section('Attempts','<div class=\"mx-empty\">'+MX.esc(e.message)+'</div>')});\n }\n\n // --- One conversation, whole life ---------------------------------\n function openConversation(c){\n MX.open('Conversation',c.agent,'<div class=\"mx-detail\">'\n +MX.fields([['Node',c.node],['Channel',c.channel],['Conversation',c.chatId]])\n +'<div id=\"mx-conv\"><div class=\"mx-empty\">Loading summary...</div></div>'\n +'<div id=\"mx-drill\">'+MX.section('Attempts','<div class=\"mx-empty\">Loading attempts...</div>')+'</div></div>');\n var q='node='+encodeURIComponent(c.nodeUrl)+'&agent='+encodeURIComponent(c.agent)\n +'&channel='+encodeURIComponent(c.channel)+'&chat='+encodeURIComponent(c.chatId);\n MX.get('/api/mesh/conversation?'+q+'&tzOffset='+tz())\n .then(renderConversation)\n .catch(function(e){var h=el('mx-conv');if(h)h.innerHTML='<div class=\"mx-empty\">'+MX.esc(e.message)+'</div>'});\n MX.get('/api/mesh/thread?'+q+'&limit=150')\n .then(function(r){renderRunGrid(c.nodeUrl,r.runs||[])})\n .catch(function(e){var t=el('mx-drill');if(t)t.innerHTML=MX.section('Attempts','<div class=\"mx-empty\">'+MX.esc(e.message)+'</div>')});\n }\n function renderConversation(s){\n var host=el('mx-conv');if(!host)return;\n if(!s.found){host.innerHTML='<div class=\"mx-empty\">No history for this conversation.</div>';return}\n // Cache reads are shown apart from fresh input on purpose: a long\n // thread is mostly cache, and one \"tokens\" number would make every\n // conversation look equally heavy.\n var html=tiles([\n ['Turns',MX.num(s.turns)],\n ['Failed',MX.num(s.errors)+(s.turns?' ('+MX.pct(s.errors,s.turns)+')':'')],\n ['Total time',MX.dur(s.totalMs)],\n ['Avg turn',MX.dur(s.avgMs)],\n ['Longest turn',MX.dur(s.maxMs)],\n ['Alive',MX.num(s.spanDays)+' days'],\n ['Used on',MX.num(s.activeDays)+' days'],\n ['Session cuts',MX.num(s.cuts.total)]\n ]);\n html+=MX.section('Tokens',tiles([\n ['Input',MX.num(s.inputTokens)],\n ['Output',MX.num(s.outputTokens)],\n ['Cache read',MX.num(s.cacheReadTokens)],\n ['Cache write',MX.num(s.cacheCreateTokens)]\n ])+'<p class=\"mx-note\">Summed over every recorded turn. Cache reads are kept separate from fresh input.</p>');\n html+=MX.section('Span',MX.fields([\n ['First turn',MX.when(s.firstAt)],['Last turn',MX.when(s.lastAt)],\n ['Busiest day',s.busiestDay?s.busiestDay.day+' ('+MX.plural(s.busiestDay.turns,'turn')+')':'-'],\n ['Models',s.models.map(function(m){return m.model+' x'+m.turns}).join(', ')||'-']]));\n if(s.cuts.total)html+=MX.section('Session cuts',miniBars(s.cuts.byReason.map(function(r){\n return [r.reason,r.count]}),'var(--mx-warn)'));\n if(s.causes.length)html+=MX.section('Failure causes',miniBars(s.causes.map(function(c){\n return [MX.causeLabel(c.cause),c.count]}),'var(--mx-crit)'));\n html+=MX.section('Tools used',\n (s.tools.length?miniBars(s.tools.map(function(t){return [t.tool,t.used]}),'var(--ax-accent)')\n :'<p class=\"mx-note\">No step ledger survived retention for this conversation.</p>')\n +'<p class=\"mx-note\">Counted across the '+MX.num(s.toolRunsCounted)+' of '+MX.num(s.turns)\n +' turns whose steps are still retained.</p>');\n host.innerHTML=html;\n }\n\n // --- Attempts, then one attempt -----------------------------------\n function renderRunGrid(nodeUrl,runs){\n var host=el('mx-drill');if(!host)return;\n if(!runs.length){host.innerHTML=MX.section('Attempts','<div class=\"mx-empty\">No individual attempts retained.</div>');return}\n var cells=runs.map(function(r,i){\n var c=r.status==='ok'?'var(--mx-good)':r.status==='timeout'?'var(--mx-warn)':'var(--mx-crit)';\n return '<button class=\"mx-run\" type=\"button\" data-run=\"'+i+'\" aria-pressed=\"false\" style=\"background:'+c+'\"'\n +' title=\"'+MX.esc(new Date(r.startedAt).toLocaleString()+' · '+r.status+' · '+MX.dur(r.durationMs))+'\"'\n +' aria-label=\"'+MX.esc(new Date(r.startedAt).toLocaleString()+', '+r.status)+'\"></button>';\n }).join('');\n // Deliberately NOT windowed: \"has this ever succeeded?\" is a question\n // about all of history. The counters above it ARE windowed, so say so.\n var capped=runs.length>=150;\n host.innerHTML=MX.section('Attempts ('+(capped?'latest 150':runs.length)+', newest first)',\n '<div class=\"mx-runs\">'+cells+'</div>'\n +'<p class=\"mx-note\">Full recorded history, not limited to the selected window. Select an attempt to see what it touched.</p>')\n +'<div id=\"mx-run-detail\"></div>';\n host.querySelectorAll('[data-run]').forEach(function(b){\n b.addEventListener('click',function(){\n host.querySelectorAll('[data-run]').forEach(function(o){o.setAttribute('aria-pressed','false')});\n b.setAttribute('aria-pressed','true');\n openRun(nodeUrl,runs[Number(b.dataset.run)]);\n });\n });\n }\n function openRun(nodeUrl,run){\n var host=el('mx-run-detail');if(!host)return;\n host.innerHTML=MX.section('Attempt','<div class=\"mx-empty\">Loading steps...</div>');\n MX.get('/api/mesh/run?node='+encodeURIComponent(nodeUrl)+'&task='+encodeURIComponent(run.taskId))\n .then(function(s){\n var touched='<div class=\"mx-touch\">'\n +'<span'+(s.reads?'':' class=\"is-zero\"')+'>'+s.reads+' read</span>'\n +'<span'+(s.writes?'':' class=\"is-zero\"')+'>'+s.writes+' write</span>'\n +'<span'+(s.sends?'':' class=\"is-zero\"')+'>'+s.sends+' send</span>'\n +s.tools.map(function(t){return '<span>'+MX.esc(t.tool)+' x'+t.used+(t.failed?' ('+t.failed+' failed)':'')+'</span>'}).join('')\n +'</div>';\n var steps=s.steps.length\n ? '<ul class=\"mx-steps\">'+s.steps.map(function(st){\n return '<li data-err=\"'+(st.status==='error'?1:0)+'\"><span class=\"mx-seq\">'+st.seq+'</span>'\n +'<span>'+MX.esc(st.action||st.name)+'</span><span>'+(st.ms==null?'':MX.dur(st.ms))+'</span></li>'}).join('')+'</ul>'\n : '<div class=\"mx-empty\">Steps for this attempt were pruned by retention. Its totals above are still exact.</div>';\n host.innerHTML=MX.section('Attempt '+run.taskId.slice(-8),\n MX.fields([['Started',MX.when(s.startedAt)],['Duration',MX.dur(s.durationMs)],\n ['Status',s.status],['Model',s.model||'default'],\n ['Output',MX.num(s.outputTokens||0)+' tokens'],\n ['Cause',s.cause?MX.causeLabel(s.cause):'-']])\n +'<p class=\"mx-note\">What this attempt touched</p>'+touched)\n +MX.section('Steps',steps);\n })\n .catch(function(e){host.innerHTML=MX.section('Attempt','<div class=\"mx-empty\">'+MX.esc(e.message)+'</div>')});\n }\n\n return {openDay:openDay,openJob:openJob,openConversation:openConversation};\n})();\n</script>`\n","// Activity + Lifetime views for /mesh. Reads GET /api/mesh/analytics\n// (fleet-merged) and drills into a single node through the /api/mesh/{job,\n// thread,run} proxies.\n//\n// Every number rendered here comes from the operational SQLite of some\n// node. Nothing is modelled, smoothed, or extrapolated: when a series is\n// empty the panel says so instead of drawing a plausible shape.\nexport const MESH_ANALYTICS_SCRIPT = `<script>\n(function(){\n var LANES=[\n {id:'cron',name:'Scheduled',hint:'cron jobs'},\n {id:'workflow',name:'Workflow',hint:'dataflow runs'},\n {id:'direct',name:'Direct',hint:'chat, API, git hosts'}\n ];\n var state={days:30,data:null,channel:'all',threadLimit:25};\n var tip=null;\n\n function el(id){return document.getElementById(id)}\n function showTip(host,html,ev){\n if(!tip){tip=document.createElement('div');tip.className='mx-tip';document.body.appendChild(tip)}\n tip.innerHTML=html;tip.style.display='block';\n var pad=14,w=tip.offsetWidth,h=tip.offsetHeight;\n var x=Math.min(window.innerWidth-w-8,Math.max(8,ev.clientX+pad));\n var y=Math.max(8,ev.clientY-h-pad);\n tip.style.position='fixed';tip.style.left=x+'px';tip.style.top=y+'px';\n }\n function hideTip(){if(tip)tip.style.display='none'}\n\n // --- Activity lanes: one small multiple per origin. Each lane is a\n // single series scaled to ITS OWN peak, so a 2,400-run workflow day\n // cannot flatten a 60-run cron day. The peak is printed next to the\n // lane name — without it a self-scaled lane would be misleading.\n function renderLanes(d){\n var root=el('mx-lanes');\n if(!d.days.length){root.innerHTML='<div class=\"mx-empty\">No runs recorded in this window.</div>';el('mx-axis').innerHTML='';return}\n root.innerHTML=LANES.map(function(lane){\n var ok=[],fail=[],peak=0,tot=0,totFail=0;\n d.days.forEach(function(day){\n var b=day[lane.id]||[0,0];\n ok.push(b[0]);fail.push(b[1]);\n peak=Math.max(peak,b[0]+b[1]);tot+=b[0]+b[1];totFail+=b[1];\n });\n var cols=d.days.map(function(day,i){\n var total=ok[i]+fail[i];\n if(!total)return '<button class=\"mx-col\" type=\"button\" data-lane=\"'+lane.id+'\" data-i=\"'+i+'\" aria-label=\"'+MX.esc(day.day)+': no runs. Open this day\"><i class=\"is-empty\"></i></button>';\n var hOk=Math.max(total?2:0,Math.round(ok[i]/peak*54));\n var hFail=fail[i]?Math.max(2,Math.round(fail[i]/peak*54)):0;\n return '<button class=\"mx-col\" type=\"button\" data-lane=\"'+lane.id+'\" data-i=\"'+i+'\"'\n +' aria-label=\"'+MX.esc(day.day)+': '+total+' runs, '+fail[i]+' failed. Open this day\">'\n +(hFail?'<i class=\"is-fail\" style=\"height:'+hFail+'px\"></i>':'')\n +'<i class=\"is-base\" style=\"height:'+hOk+'px;background:var(--mx-lane-'+lane.id+')\"></i>'\n +'</button>';\n }).join('');\n return '<div class=\"mx-lane\">'\n +'<div class=\"mx-lane__name\"><span class=\"mx-lane__swatch\" style=\"background:var(--mx-lane-'+lane.id+')\"></span>'\n +'<div>'+MX.esc(lane.name)+'<div class=\"mx-lane__peak\">peak '+MX.num(peak)+'/day</div></div></div>'\n +'<div class=\"mx-cols\">'+cols+'</div>'\n +'<div class=\"mx-lane__tot\"><b>'+MX.num(tot)+'</b>'+MX.num(totFail)+' failed</div></div>';\n }).join('');\n el('mx-axis').innerHTML='<span>'+MX.esc(d.days[0].day)+'</span><span>'+MX.esc(d.days[d.days.length-1].day)+'</span>';\n root.querySelectorAll('.mx-col').forEach(function(b){\n function show(ev){\n var lane=b.dataset.lane,day=d.days[Number(b.dataset.i)],v=day[lane]||[0,0];\n showTip(b,'<b>'+MX.esc(day.day)+'</b><dl><dt>Ran</dt><dd>'+MX.num(v[0]+v[1])+'</dd>'\n +'<dt>Failed</dt><dd>'+MX.num(v[1])+'</dd><dt>Origin</dt><dd>'+MX.esc(lane)+'</dd></dl>'\n +'<p class=\"mx-tip__hint\">Select for the full day</p>',ev);\n }\n b.addEventListener('mousemove',show);\n b.addEventListener('focus',function(){var r=b.getBoundingClientRect();show({clientX:r.left,clientY:r.top})});\n b.addEventListener('mouseleave',hideTip);b.addEventListener('blur',hideTip);\n b.addEventListener('click',function(){hideTip();MXD.openDay(d.days[Number(b.dataset.i)].day,b.dataset.lane)});\n });\n }\n\n // --- Where it breaks: one bar per origin channel, total runs with the\n // failed share drawn on top of the same track. One measure, one hue.\n function renderOrigins(d){\n var root=el('mx-origins');\n if(!d.origins.length){root.innerHTML='<div class=\"mx-empty\">No runs in this window.</div>';return}\n var max=d.origins[0].runs||1;\n root.innerHTML=d.origins.map(function(o,i){\n var w=Math.max(2,o.runs/max*100),fw=o.runs?o.errors/o.runs*w:0;\n return '<button class=\"mx-bar\" type=\"button\" data-origin=\"'+i+'\">'\n +'<span class=\"mx-bar__label\">'+MX.esc(o.channel)+'</span>'\n +'<span class=\"mx-bar__track\"><span class=\"mx-bar__fill\" style=\"width:'+w+'%;background:var(--ax-accent);opacity:.35\"></span>'\n +(fw?'<span class=\"mx-bar__fail\" style=\"left:'+(w-fw)+'%;width:'+fw+'%\"></span>':'')+'</span>'\n +'<span class=\"mx-bar__val\">'+MX.num(o.runs)+' <em>'+MX.pct(o.errors,o.runs)+' failed</em></span></button>';\n }).join('');\n root.querySelectorAll('[data-origin]').forEach(function(b){\n b.addEventListener('click',function(){\n var o=d.origins[Number(b.dataset.origin)];\n MX.open('Origin',o.channel,'<div class=\"mx-detail\">'+MX.fields([\n ['Runs',MX.num(o.runs)],['Failed',MX.num(o.errors)],\n ['Failure rate',MX.pct(o.errors,o.runs)],['Runtime',MX.hours(o.hours)],\n ['Window',d.windowDays===0?'today so far':'last '+d.windowDays+' days']\n ])+'<p class=\"mx-note\">Failure rate is computed over every recorded attempt on this channel, retries included. Runtime is the sum of measured task durations, not wall-clock.</p></div>');\n });\n });\n }\n\n // --- Why it fails: counts per cause class. Single series, single hue.\n function renderCauses(d){\n var root=el('mx-causes');\n if(!d.causes.length){root.innerHTML='<div class=\"mx-empty\">No failures in this window.</div>';return}\n var max=d.causes[0].count||1,total=d.causes.reduce(function(v,c){return v+c.count},0);\n root.innerHTML=d.causes.map(function(c,i){\n return '<button class=\"mx-bar\" type=\"button\" data-cause=\"'+i+'\">'\n +'<span class=\"mx-bar__label\">'+MX.esc(MX.causeLabel(c.cause))+'</span>'\n +'<span class=\"mx-bar__track\"><span class=\"mx-bar__fill\" style=\"width:'+Math.max(2,c.count/max*100)+'%;background:var(--mx-crit);opacity:.7\"></span></span>'\n +'<span class=\"mx-bar__val\">'+MX.num(c.count)+' <em>'+MX.pct(c.count,total)+'</em></span></button>';\n }).join('');\n var known=d.causes.filter(function(c){return c.cause!=='other'}).reduce(function(v,c){return v+c.count},0);\n el('mx-cause-note').textContent=(known===total\n ? 'All '+MX.num(total)+' failures matched a known cause class.'\n : MX.pct(known,total)+' of '+MX.num(total)+' failures matched a known cause class.')\n +' None of them are the agent reasoning badly — they are the environment the agent runs in.';\n root.querySelectorAll('[data-cause]').forEach(function(b){\n b.addEventListener('click',function(){\n var c=d.causes[Number(b.dataset.cause)];\n MX.open('Failure cause',MX.causeLabel(c.cause),'<div class=\"mx-detail\">'+MX.fields([\n ['Occurrences',MX.num(c.count)],['Share of failures',MX.pct(c.count,total)],\n ['Agents hit',c.agents.join(', ')||'-']\n ])+(c.example?MX.section('Representative error','<pre>'+MX.esc(c.example)+'</pre>'):'')+'</div>');\n });\n });\n }\n\n // --- Effort vs output. x = total runtime, y = mean output tokens on\n // SUCCESSFUL runs. Both log-scaled because both span four orders of\n // magnitude. The shaded band is the literal zombie rule, drawn so the\n // classification is visible rather than asserted.\n function renderScatter(d){\n var root=el('mx-scatter'),jobs=d.jobs.filter(function(j){return j.runs>0});\n if(!jobs.length){root.innerHTML='<div class=\"mx-empty\">No recurring jobs ran in this window.</div>';return}\n // R is a deliberate right gutter, not just padding: points near the\n // maximum runtime are exactly the ones that most need a direct label.\n var W=820,H=340,L=54,R=74,T=18,B=38;\n var mins=function(j){return Math.max(j.hours*60,0.5)};\n var lx=function(m){return Math.log10(m)};\n var ly=function(t){return Math.log10(Math.max(t,0)+1)};\n var x0=lx(0.5),x1=Math.max(lx(60),Math.max.apply(null,jobs.map(function(j){return lx(mins(j))})));\n var y1=Math.max(ly(100),Math.max.apply(null,jobs.map(function(j){return ly(j.avgOutput)})));\n var px=function(m){return L+(lx(m)-x0)/(x1-x0)*(W-L-R)};\n var py=function(t){return H-B-ly(t)/y1*(H-T-B)};\n var g='';\n [1,10,60,600,6000].forEach(function(m){\n if(lx(m)<x0||lx(m)>x1)return;\n var gx=px(m),lbl=m<60?m+'m':(m/60)+'h';\n g+='<line x1=\"'+gx+'\" y1=\"'+T+'\" x2=\"'+gx+'\" y2=\"'+(H-B)+'\" stroke=\"var(--mx-grid)\" stroke-width=\"1\"/>'\n +'<text x=\"'+gx+'\" y=\"'+(H-B+16)+'\" text-anchor=\"middle\">'+lbl+'</text>';\n });\n [0,10,100,1000,10000].forEach(function(t){\n if(ly(t)>y1)return;\n var gy=py(t);\n g+='<line x1=\"'+L+'\" y1=\"'+gy+'\" x2=\"'+(W-R)+'\" y2=\"'+gy+'\" stroke=\"var(--mx-grid)\" stroke-width=\"1\"/>'\n +'<text x=\"'+(L-8)+'\" y=\"'+(gy+3)+'\" text-anchor=\"end\">'+MX.num(t)+'</text>';\n });\n var bandTop=py(25);\n var band='<rect x=\"'+L+'\" y=\"'+bandTop+'\" width=\"'+(W-L-R)+'\" height=\"'+(H-B-bandTop)+'\" fill=\"var(--mx-serious)\" opacity=\".08\"/>'\n +'<text x=\"'+(W-R-4)+'\" y=\"'+(bandTop+13)+'\" text-anchor=\"end\">under 25 tokens returned</text>';\n\n // Direct labels for every job that is not healthy — but only where one\n // fits. Candidates are tried right, left, above, below; a label that\n // still collides is dropped rather than stacked into an unreadable\n // smear. Hover and keyboard focus still name every point.\n var taken=[],marks='',labels='';\n function fits(b){\n for(var i=0;i<taken.length;i++){var t=taken[i];\n if(b.x<t.x+t.w&&t.x<b.x+b.w&&b.y<t.y+t.h&&t.y<b.y+b.h)return false}\n return true;\n }\n // Pass 1: every mark, and every mark's footprint, so a label placed in\n // pass 2 can never be dropped behind a point drawn after it.\n var placed=jobs.map(function(j,i){\n var r=Math.max(4,Math.min(15,Math.sqrt(j.runs)*1.1));\n return {j:j,i:i,r:r,cx:px(mins(j)),cy:py(j.avgOutput)};\n });\n placed.forEach(function(o){\n taken.push({x:o.cx-o.r,y:o.cy-o.r,w:o.r*2,h:o.r*2});\n marks+='<g class=\"mx-pt\" tabindex=\"0\" role=\"button\" data-job=\"'+o.i+'\"'\n +' aria-label=\"'+MX.esc(o.j.label+', '+MX.verdict(o.j.verdict).label+', '+o.j.runs+' runs, '+Math.round(o.j.avgOutput)+' tokens average output')+'\">'\n +MX.mark(o.j.verdict,o.cx,o.cy,o.r)+'</g>';\n });\n // Pass 2: label the jobs that need acting on, worst runtime first.\n placed.filter(function(o){return o.j.verdict!=='healthy'})\n .sort(function(a,b){return b.j.hours-a.j.hours})\n .forEach(function(o){\n // Roboto Mono at 10px advances ~6.1px per character; the estimate\n // must over- rather than under-shoot or two labels touch.\n var w=o.j.label.length*6.2+10,h=13;\n var spots=[[o.cx+o.r+5,o.cy-h/2],[o.cx-o.r-5-w,o.cy-h/2],[o.cx-w/2,o.cy-o.r-4-h],[o.cx-w/2,o.cy+o.r+4]];\n for(var k=0;k<spots.length;k++){\n var b={x:spots[k][0],y:spots[k][1],w:w,h:h};\n if(b.x<L||b.x+w>W-2||b.y<T-6||b.y+h>H-B+2)continue;\n if(!fits(b))continue;\n taken.push(b);\n labels+='<text class=\"mx-lbl\" x=\"'+b.x+'\" y=\"'+(b.y+10)+'\">'+MX.esc(o.j.label)+'</text>';\n break;\n }\n });\n root.innerHTML='<svg viewBox=\"0 0 '+W+' '+H+'\" role=\"img\" aria-label=\"Runtime against output for every recurring job\">'\n +band+g\n +'<line x1=\"'+L+'\" y1=\"'+(H-B)+'\" x2=\"'+(W-R)+'\" y2=\"'+(H-B)+'\" stroke=\"var(--ax-border-2)\" stroke-width=\"2\"/>'\n +'<text x=\"'+L+'\" y=\"'+(H-6)+'\">total runtime in window</text>'\n +'<text x=\"'+(L-8)+'\" y=\"'+(T-5)+'\" text-anchor=\"end\">tokens</text>'\n +marks+labels+'</svg>';\n el('mx-scatter-legend').innerHTML=['healthy','fragile','zombie','never-succeeded'].map(function(v){\n var m=MX.verdict(v);\n return '<span><svg viewBox=\"-8 -8 16 16\">'+MX.mark(v,0,0,6)+'</svg>'+MX.esc(m.label)+' — '+MX.esc(m.why)+'</span>';\n }).join('');\n root.querySelectorAll('[data-job]').forEach(function(gEl){\n var j=jobs[Number(gEl.dataset.job)];\n function show(ev){showTip(gEl,'<b>'+MX.esc(j.label)+'</b>'+MX.esc(j.agent)+' on '+MX.esc(j.node)\n +'<dl><dt>Verdict</dt><dd>'+MX.esc(MX.verdict(j.verdict).label)+'</dd>'\n +'<dt>Runs</dt><dd>'+MX.num(j.runs)+'</dd><dt>Failed</dt><dd>'+MX.num(j.errors)+'</dd>'\n +'<dt>Runtime</dt><dd>'+MX.hours(j.hours)+'</dd><dt>Avg run</dt><dd>'+j.avgMinutes+'m</dd>'\n +'<dt>Avg output</dt><dd>'+MX.num(Math.round(j.avgOutput))+' tok</dd></dl>',ev)}\n gEl.addEventListener('mousemove',show);\n gEl.addEventListener('mouseleave',hideTip);gEl.addEventListener('blur',hideTip);\n gEl.addEventListener('focus',function(){var r=gEl.getBoundingClientRect();show({clientX:r.left,clientY:r.top})});\n gEl.addEventListener('click',function(){MXD.openJob(j)});\n gEl.addEventListener('keydown',function(ev){if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();MXD.openJob(j)}});\n });\n }\n\n function renderThreads(d){\n var root=el('mx-threads');\n var chans=['all'].concat(d.origins.map(function(o){return o.channel}));\n el('mx-thread-chips').innerHTML=chans.map(function(c){\n return '<button class=\"mx-chip\" type=\"button\" data-chan=\"'+MX.esc(c)+'\" aria-pressed=\"'+(state.channel===c)+'\">'+MX.esc(c==='all'?'All channels':c)+'</button>';\n }).join('');\n el('mx-thread-chips').querySelectorAll('[data-chan]').forEach(function(b){\n b.addEventListener('click',function(){state.channel=b.dataset.chan;state.threadLimit=25;renderThreads(d)});\n });\n var rows=d.threads.filter(function(t){return state.channel==='all'||t.channel===state.channel});\n if(!rows.length){root.innerHTML='<div class=\"mx-empty\">No threads on this channel in the window.</div>';return}\n var t0=d.windowStart,t1=d.generatedAt,span=Math.max(1,t1-t0);\n var CUT={'tier-2':'var(--mx-warn)','max-turns':'var(--ax-accent)','stale':'var(--ax-muted)'};\n var shown=rows.slice(0,state.threadLimit);\n root.innerHTML=shown.map(function(t,i){\n var left=Math.max(0,(t.firstAt-t0)/span*100);\n var width=Math.min(100-left,Math.max(0.6,(t.lastAt-t.firstAt)/span*100));\n // Ticks sit at the real timestamp of each recorded cut. When a thread\n // has more cuts than the payload carries, the count says so.\n var ticks=(t.cuts||[]).map(function(c){\n var pos=(c.at-t0)/span*100;\n if(pos<0||pos>100)return '';\n return '<span class=\"mx-life__tick\" style=\"left:'+pos+'%;background:'+(CUT[c.reason]||'var(--ax-muted)')+'\"></span>';\n }).join('');\n var cutLabel=t.rotations>t.cutsShown\n ? MX.num(t.cutsShown)+' of '+MX.num(t.rotations)+' cuts'\n : MX.num(t.rotations)+' cuts';\n return '<button class=\"mx-life\" type=\"button\" data-thread=\"'+i+'\">'\n +'<span class=\"mx-life__id\"><b>'+MX.esc(t.agent)+'</b><span>'+MX.esc(t.channel)+' · '+MX.esc(t.chatId)+'</span></span>'\n +'<span class=\"mx-life__span\"><span class=\"mx-life__track\" style=\"left:'+left+'%;width:'+width+'%\"></span>'+ticks+'</span>'\n +'<span class=\"mx-life__meta\"><b>'+MX.num(t.runs)+'</b> runs · '+MX.num(t.errors)+' failed<br>'\n +MX.esc(cutLabel)+' · '+MX.age(t.lastAt)+'</span></button>';\n }).join('');\n if(rows.length>shown.length){\n root.innerHTML+='<button class=\"ax-btn ax-btn--ghost mx-show-all\" id=\"mx-more-threads\" type=\"button\">Show '\n +Math.min(25,rows.length-shown.length)+' more of '+rows.length+'</button>';\n root.querySelector('#mx-more-threads').addEventListener('click',function(){\n state.threadLimit+=25;renderThreads(d);\n });\n }\n root.querySelectorAll('[data-thread]').forEach(function(b){\n b.addEventListener('click',function(){MXD.openConversation(shown[Number(b.dataset.thread)])});\n });\n }\n\n function renderRotations(d){\n var r=d.rotations,root=el('mx-rotations');\n if(!r.total){root.innerHTML='<div class=\"mx-empty\">No session cuts recorded in this window.</div>';return}\n var max=r.byReason[0]?r.byReason[0].count:1;\n var avg=r.tokenSamples?Math.round(r.tokenSum/r.tokenSamples):0;\n root.innerHTML=r.byReason.map(function(x){\n return '<div class=\"mx-bar\"><span class=\"mx-bar__label\">'+MX.esc(x.reason)+'</span>'\n +'<span class=\"mx-bar__track\"><span class=\"mx-bar__fill\" style=\"width:'+Math.max(2,x.count/max*100)+'%;background:var(--ax-accent);opacity:.45\"></span></span>'\n +'<span class=\"mx-bar__val\">'+MX.num(x.count)+' <em>'+MX.pct(x.count,r.total)+'</em></span></div>';\n }).join('');\n el('mx-rot-note').textContent=MX.num(r.total)+' session cuts. Where a token count was recorded, the average thread carried '\n +MX.num(avg)+' input tokens at the cut; the largest carried '+MX.num(r.maxTokens)+'.';\n }\n\n function renderNodes(d){\n var down=d.nodes.filter(function(n){return !n.ok});\n var txt='Merged from '+d.nodes.filter(function(n){return n.ok}).length+' of '+d.nodes.length+' nodes'\n +' · '+(d.windowDays===0?'today so far':'last '+d.windowDays+' days')\n +' · '+MX.num(d.totals.runs)+' runs, '+MX.num(d.totals.errors)+' failed'\n +' · steps retained for '+MX.num(d.retention.tracesWithSteps)+' of '+MX.num(d.retention.traces)+' runs';\n if(down.length)txt+=' · UNREACHABLE: '+down.map(function(n){return n.name+' ('+(n.error||'no answer')+')'}).join(', ');\n el('mx-analytics-updated').textContent=txt;\n var attention=d.jobs.filter(function(j){return j.verdict!=='healthy'}).length;\n MX.setHealth(3,attention,attention?'warn':'ok','Jobs needing review');\n }\n\n function render(d){\n state.data=d;\n // The server knows where the window really starts; deriving it from\n // windowDays here breaks the Today range, whose start is local\n // midnight rather than a whole number of days back.\n d.windowStart=Number.isFinite(d.since)?d.since:(d.generatedAt-d.windowDays*86400000);\n renderNodes(d);renderLanes(d);renderOrigins(d);renderCauses(d);\n renderScatter(d);renderThreads(d);renderRotations(d);\n }\n\n function load(){\n el('mx-analytics-updated').textContent='Reading node histories...';\n MX.get('/api/mesh/analytics?days='+state.days+'&tzOffset='+(-new Date().getTimezoneOffset())+'&limit=80')\n .then(render)\n .catch(function(e){el('mx-analytics-updated').textContent='Analytics unavailable: '+e.message});\n }\n\n document.querySelectorAll('#mx-range button').forEach(function(b){\n b.addEventListener('click',function(){\n state.days=Number(b.dataset.days);\n document.querySelectorAll('#mx-range button').forEach(function(o){o.setAttribute('aria-pressed',String(o===b))});\n load();\n });\n });\n window.addEventListener('scroll',hideTip,{passive:true});\n load();\n})();\n</script>`\n","// Operations view for /mesh — the live fleet snapshot that predates the\n// analytics views: today's scheduled attempts, what is running right now,\n// node reachability, and the agent/skill inventory.\n//\n// Unchanged in substance; it now shares the drawer and formatters with the\n// other views through window.MX instead of carrying its own copies.\nexport const MESH_OPS_SCRIPT = `<script>\n(function(){\n var showAll=document.getElementById('mx-show-all'),expanded=false;\n var esc=MX.esc;\n function initials(v){return String(v||'?').split(/[-_ ]+/).slice(0,2).map(function(x){return x.charAt(0)}).join('').toUpperCase()}\n function badge(status){\n var kind=(status==='success'||status==='active'||status==='online')?'ax-badge--live'\n :(status==='failed'||status==='timeout'||status==='offline')?'ax-badge--err'\n :status==='retrying'?'ax-badge--warn':'ax-badge--ghost';\n return '<span class=\"ax-badge '+kind+'\">'+esc(status)+'</span>';\n }\n function avatar(text,variant){return '<span class=\"ax-avatar '+(variant?'ax-avatar--'+variant:'')+'\">'+esc(initials(text))+'</span>'}\n function item(info,actions,attrs,variant){\n var tag=info.href?'a':'button';\n return '<'+tag+' class=\"ax-row-card mx-item\" '+(info.href?'href=\"'+info.href+'\"':'type=\"button\"')+' '+(attrs||'')+'>'\n +avatar(info.avatar||info.title,variant)\n +'<span class=\"ax-row-card__info\"><span class=\"ax-name\">'+esc(info.title)\n +(info.slug?' <span class=\"ax-slug\">'+esc(info.slug)+'</span>':'')+'</span>'\n +'<span class=\"ax-sub\">'+info.sub+'</span></span>'\n +'<span class=\"ax-row-card__actions\">'+(actions||'')+'</span></'+tag+'>';\n }\n function detail(data){\n return '<div class=\"mx-detail\">'+MX.fields(data.fields)+(data.note?'<pre>'+esc(data.note)+'</pre>':'')+'</div>';\n }\n showAll.addEventListener('click',function(){\n expanded=!expanded;\n document.querySelectorAll('#mx-runs .mx-item').forEach(function(el,i){el.hidden=!expanded&&i>=8});\n showAll.textContent=expanded?'Show less':'Show all';\n });\n\n function render(s){\n var nodes=s.nodes||[],reachable=nodes.filter(function(n){return n.reachable});\n var agents=reachable.flatMap(function(n){return (n.agents||[]).map(function(a){return {node:n,agent:a}})});\n var tasks=agents.flatMap(function(x){return (x.agent.runningTasks||[]).map(function(t){return {node:x.node,agent:x.agent,task:t}})});\n var runs=reachable.flatMap(function(n){return (n.cronRuns||[]).map(function(r){return {node:n,run:r}})});\n var failed=runs.filter(function(x){return x.run.status!=='success'}).length;\n MX.setHealth(0,reachable.length+'/'+nodes.length,reachable.length===nodes.length?'ok':'warn');\n MX.setHealth(1,tasks.length,tasks.length?'ok':'off');\n MX.setHealth(2,failed,failed?'warn':'off');\n document.getElementById('mx-updated').textContent='Fleet snapshot '+new Date(s.ts).toLocaleTimeString()\n +' · '+Intl.DateTimeFormat().resolvedOptions().timeZone;\n document.getElementById('mx-active-count').textContent=tasks.length+' active';\n document.getElementById('mx-node-count').textContent=nodes.length+' nodes';\n document.getElementById('mx-agent-count').textContent=agents.length+' agents';\n renderRuns(reachable);renderTasks(tasks);renderNodes(nodes);renderAgents(agents);\n }\n function renderRuns(nodes){\n var cards=[];\n nodes.forEach(function(n){(n.crons||[]).forEach(function(job){\n var history=(n.cronRuns||[]).filter(function(r){return r.jobId===job.id}),latest=history[0];\n var status=!job.enabled?'disabled':latest?latest.status:job.retryPending?'retrying':'waiting';\n var summary=latest?(latest.errorSummary||latest.responseSummary||'No summary')\n :(job.enabled?'No attempt persisted today':'Schedule disabled');\n var agent=(n.agents||[]).find(function(a){return a.id===job.agent});\n cards.push({node:n,job:job,run:latest,status:status,summary:summary,agent:agent});\n })});\n var root=document.getElementById('mx-runs');\n document.getElementById('mx-run-count').textContent=cards.length+' schedules';\n showAll.hidden=cards.length<=8;\n if(!cards.length){root.innerHTML='<div class=\"mx-empty\">No schedules reported by reachable nodes.</div>';return}\n root.innerHTML=cards.map(function(x,i){\n var sub='<code>'+esc(x.job.agent)+'</code><span>'+esc(x.job.schedule)+'</span>'\n +'<span class=\"mx-summary\">'+esc(x.summary)+'</span>';\n var variant=x.status==='success'?'teal':(x.status==='failed'||x.status==='timeout')?'coral':'plain';\n return item({title:x.job.id,slug:x.node.name,sub:sub},badge(x.status),\n 'data-run=\"'+i+'\"'+(!expanded&&i>=8?' hidden':''),variant);\n }).join('');\n root.querySelectorAll('[data-run]').forEach(function(el){\n el.addEventListener('click',function(){\n var x=cards[Number(el.dataset.run)],runtime=(x.agent&&x.agent.tier)||'unknown';\n var model=x.job.model||(x.agent&&x.agent.model);\n MX.open('Automation',x.job.id,detail({fields:[\n ['Node',x.node.name],['Agent',x.job.agent],['Runtime',runtime],\n ['Model',x.job.model?model:(model?model+' (inherited)':'default')],\n ['Status',x.status],['Schedule',x.job.schedule],['Timezone',x.job.timezone||'local'],\n ['Started',x.run?new Date(x.run.startedAt).toLocaleString():'Not today'],\n ['Duration',x.run?MX.dur(x.run.duration):'-'],\n ['Retry',x.run&&x.run.isRetry?'Attempt '+x.run.retryAttempt:'No'],\n ['Task ID',(x.run&&x.run.taskId)||'-'],['Session ID',(x.run&&x.run.sessionId)||'-']\n ],note:x.summary}));\n });\n });\n }\n function renderTasks(tasks){\n var root=document.getElementById('mx-active');\n if(!tasks.length){root.innerHTML='<div class=\"mx-empty\">The mesh is idle.</div>';return}\n root.innerHTML=tasks.map(function(x){\n var href='/tasks/'+encodeURIComponent(x.task.id)+'?node='+encodeURIComponent(x.node.url)\n +'&agent='+encodeURIComponent(x.agent.id)+'&name='+encodeURIComponent(x.agent.name)\n +'&channel='+encodeURIComponent(x.task.channel||'unknown');\n var sub='<code>'+esc(x.node.name)+' / '+esc(x.task.channel||'unknown')+'</code>'\n +'<span class=\"mx-summary\">'+esc(x.task.messagePreview||'Working')+'</span>';\n return item({href:href,title:x.agent.name,slug:x.task.sender?'by '+x.task.sender:'',sub:sub},\n '<span class=\"mx-node-meta\">'+esc(MX.age(x.task.startedAt))+'</span>','','blue');\n }).join('');\n }\n function renderNodes(nodes){\n var root=document.getElementById('mx-nodes');\n root.innerHTML=nodes.map(function(n,i){\n var active=(n.agents||[]).reduce(function(v,a){return v+(a.active||0)},0);\n var sub='<span>'+esc(n.reachable?n.agents.length+' agents · '+active+' active':n.error||'unreachable')+'</span>';\n var status=n.reachable?(active?'active':'online'):'offline';\n return item({title:n.name,slug:n.id,sub:sub},badge(status),'data-node=\"'+i+'\"',n.reachable?'teal':'coral');\n }).join('');\n root.querySelectorAll('[data-node]').forEach(function(el){\n el.addEventListener('click',function(){\n var n=nodes[Number(el.dataset.node)];\n MX.open('Fleet node',n.name,detail({fields:[\n ['Status',n.reachable?'Reachable':'Unreachable'],['URL',n.url],\n ['Agents',(n.agents||[]).length],['Schedules',(n.crons||[]).length],\n ['Uptime',n.uptimeSec?Math.floor(n.uptimeSec/60)+' minutes':'-']\n ],note:n.error||''}));\n });\n });\n }\n function renderAgents(items){\n var root=document.getElementById('mx-agents');\n if(!items.length){root.innerHTML='<div class=\"mx-empty\">No agent inventory available.</div>';return}\n root.innerHTML=items.map(function(x,i){\n var a=x.agent;\n var sub='<code>'+esc(a.tier)+' · '+esc(a.model||'default model')+'</code>'\n +'<span>'+esc(a.skillCount==null?'skills unavailable':a.skillCount+' skills')+' · '+esc(a.total||0)+' tasks</span>';\n return item({title:a.name,slug:x.node.name,sub:sub},badge(a.active?'active':'idle'),'data-agent=\"'+i+'\"',a.active?'blue':'plain');\n }).join('');\n root.querySelectorAll('[data-agent]').forEach(function(el){\n el.addEventListener('click',function(){\n var x=items[Number(el.dataset.agent)],a=x.agent;\n var history='/agents/'+encodeURIComponent(a.id)+'/history?node='+encodeURIComponent(x.node.url)\n +'&name='+encodeURIComponent(a.name);\n MX.open('Agent inventory',a.name,detail({fields:[\n ['Node',x.node.name],['Agent ID',a.id],['Runtime',a.tier],['Model',a.model||'default'],\n ['Skills',a.skillCount==null?'Unavailable until node upgrade':a.skillCount],\n ['Active tasks',a.active||0],['Total tasks',a.total||0],['Errors',a.errors||0]\n ],note:(a.lastSummary&&a.lastSummary.text)||''})\n +'<a class=\"mx-link\" href=\"'+history+'\">Open activity history</a>');\n });\n });\n }\n function load(){\n var date=new Date().toLocaleDateString('en-CA');\n var tz=Intl.DateTimeFormat().resolvedOptions().timeZone||'UTC';\n MX.get('/api/mesh?date='+encodeURIComponent(date)+'&timezone='+encodeURIComponent(tz))\n .then(render)\n .catch(function(e){document.getElementById('mx-updated').textContent='Fleet snapshot unavailable: '+e.message});\n }\n load();setInterval(load,5000);\n})();\n</script>`\n","import {\n healthStrip,\n pageHead,\n renderShell,\n sectionHead,\n type TopbarPeer,\n} from \"../index\"\nimport { MESH_CSS } from \"./mesh.css\"\nimport { MESH_SHARED_SCRIPT } from \"./mesh-shared.client\"\nimport { MESH_DRILL_SCRIPT } from \"./mesh-drill.client\"\nimport { MESH_ANALYTICS_SCRIPT } from \"./mesh-analytics.client\"\nimport { MESH_OPS_SCRIPT } from \"./mesh-ops.client\"\n\n// --- /mesh — three views over one fleet -------------------------------\n//\n// Activity what ran, where it breaks, and which recurring jobs burn\n// runtime without producing anything.\n// Lifetime how long a thread lives, where its sessions were cut, and\n// the thread → run → step drill behind any of it.\n// Operations the live snapshot: today's schedules, current work, node\n// reachability, agent inventory.\n//\n// The split exists because those are three different questions asked at\n// three different moments, and answering them on one scroll was the thing\n// that made the old page a wall of reading. Only the Operations view\n// polls; the analytics views load once per selected window.\n\nconst ICONS = {\n activity: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12h3l2-6 4 12 2-6h5\"/></svg>`,\n breakage: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3v6l-3 3 3 3v6\"/><path d=\"M5 5l14 14\"/></svg>`,\n effort: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 20V4M4 20h16\"/><circle cx=\"9\" cy=\"15\" r=\"2\"/><circle cx=\"15\" cy=\"8\" r=\"2\"/></svg>`,\n lifetime: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12h18\"/><path d=\"M7 8v8M13 8v8M19 8v8\"/></svg>`,\n cuts: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"2.5\"/><circle cx=\"6\" cy=\"18\" r=\"2.5\"/><path d=\"M8 8l12 10M8 16L20 6\"/></svg>`,\n schedules: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>`,\n nodes: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"2\"/><circle cx=\"18\" cy=\"6\" r=\"2\"/><circle cx=\"12\" cy=\"18\" r=\"2\"/><path d=\"M8 6h8M7 8l4 8M17 8l-4 8\"/></svg>`,\n agents: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"10\" rx=\"2\"/><circle cx=\"12\" cy=\"5\" r=\"2\"/><path d=\"M12 7v4M8 16h.01M16 16h.01\"/></svg>`,\n}\n\nfunction head(icon: string, title: string, lead: string, id: string, action = \"\"): string {\n return sectionHead({ icon, title, lead, actionHtml: action }).replace(\"<h2>\", `<h2 id=\"${id}\">`)\n}\n\nconst TABS: Array<{ id: string; label: string }> = [\n { id: \"activity\", label: \"Activity\" },\n { id: \"lifetime\", label: \"Lifetime\" },\n { id: \"ops\", label: \"Operations\" },\n]\n\n// \"Today\" is a range like any other, but it is the one the page gets opened\n// for — a 30-day default answers \"is this fleet healthy\", not \"what just ran\".\nconst RANGES: Array<{ days: number; label: string }> = [\n { days: 0, label: \"Today\" },\n { days: 7, label: \"7d\" },\n { days: 30, label: \"30d\" },\n { days: 90, label: \"90d\" },\n]\n\nexport function renderMeshPage(opts: { peers?: TopbarPeer[] }): string {\n const chrome =\n pageHead({\n kicker: \"Operations\",\n title: \"Mesh operations\",\n lead: `What the fleet actually did, how long it lived, and which of it was worth running.`,\n }) +\n healthStrip([\n { kind: \"off\", num: \"-\", label: \"Nodes online\" },\n { kind: \"off\", num: \"-\", label: \"Tasks running\" },\n { kind: \"off\", num: \"-\", label: \"Failed today\" },\n { kind: \"off\", num: \"-\", label: \"Jobs needing review\" },\n ]).replace('<div class=\"ax-health-strip\">', `<div class=\"ax-health-strip\" id=\"mx-health\">`)\n\n const toolbar = `<div class=\"mx-toolbar\">\n <div class=\"mx-tabs\" role=\"tablist\" aria-label=\"Mesh views\">\n ${TABS.map((t, i) => `<button class=\"mx-tab\" type=\"button\" role=\"tab\" id=\"mx-tab-${t.id}\" aria-controls=\"mx-view-${t.id}\" aria-selected=\"${i === 0}\" data-view=\"${t.id}\">${t.label}</button>`).join(\"\")}\n </div>\n <div class=\"mx-range\" id=\"mx-range\" role=\"group\" aria-label=\"History window\">\n ${RANGES.map((r) => `<button type=\"button\" data-days=\"${r.days}\" aria-pressed=\"${r.days === 30}\">${r.label}</button>`).join(\"\")}\n </div>\n </div>\n <p id=\"mx-analytics-updated\" class=\"mx-updated\" aria-live=\"polite\">Reading node histories...</p>`\n\n const activityView = `<div class=\"mx-view\" id=\"mx-view-activity\" role=\"tabpanel\" aria-labelledby=\"mx-tab-activity\">\n <section class=\"mx-section\" aria-labelledby=\"mx-lanes-title\">\n ${head(ICONS.activity, \"What ran\", \"One lane per origin. Each lane is scaled to its own peak so a quiet channel stays readable next to a busy one. Select a column for the day behind it.\", \"mx-lanes-title\")}\n <div id=\"mx-lanes\"><div class=\"mx-empty\">Loading activity...</div></div>\n <div class=\"mx-axis\" id=\"mx-axis\"></div>\n <p class=\"mx-note\">Coloured bars are runs that finished; the red cap on a column is the failures inside that day. Select any column for what ran that day, down to a single conversation.</p>\n </section>\n\n <div class=\"mx-two\">\n <section class=\"mx-section\" aria-labelledby=\"mx-origins-title\">\n ${head(ICONS.breakage, \"Where it breaks\", \"Attempts per origin, with the failed share marked on the same bar.\", \"mx-origins-title\")}\n <div class=\"mx-bars\" id=\"mx-origins\"><div class=\"mx-empty\">Loading...</div></div>\n </section>\n <section class=\"mx-section\" aria-labelledby=\"mx-causes-title\">\n ${head(ICONS.breakage, \"Why it fails\", \"Failures grouped by cause class, matched from the recorded error text.\", \"mx-causes-title\")}\n <div class=\"mx-bars\" id=\"mx-causes\"><div class=\"mx-empty\">Loading...</div></div>\n <p class=\"mx-note\" id=\"mx-cause-note\"></p>\n </section>\n </div>\n\n <section class=\"mx-section\" aria-labelledby=\"mx-scatter-title\">\n ${head(ICONS.effort, \"Effort against output\", \"Every recurring job, plotted as runtime spent against tokens produced on its successful runs. Both axes are logarithmic.\", \"mx-scatter-title\")}\n <div class=\"mx-scatter\" id=\"mx-scatter\"><div class=\"mx-empty\">Loading jobs...</div></div>\n <div class=\"mx-legend\" id=\"mx-scatter-legend\"></div>\n <p class=\"mx-note\">Bubble size is the number of runs. Anything not healthy is labelled on the chart; select it for the attempt history behind the verdict.</p>\n </section>\n </div>`\n\n const lifetimeView = `<div class=\"mx-view\" id=\"mx-view-lifetime\" role=\"tabpanel\" aria-labelledby=\"mx-tab-lifetime\" hidden>\n <section class=\"mx-section\" aria-labelledby=\"mx-threads-title\">\n ${head(ICONS.lifetime, \"Thread lifetimes\", \"A thread is one agent talking on one channel in one conversation. The bar is its real first-to-last span; each tick is a session cut. Select one for its turns, tokens and time.\", \"mx-threads-title\")}\n <div class=\"mx-chips\" id=\"mx-thread-chips\"></div>\n <div id=\"mx-threads\"><div class=\"mx-empty\">Loading threads...</div></div>\n <p class=\"mx-note\">Select a thread to walk its runs, then a run to see which tools it touched. Grey ticks are stale cuts, amber ticks are context-full cuts.</p>\n </section>\n\n <section class=\"mx-section\" aria-labelledby=\"mx-rot-title\">\n ${head(ICONS.cuts, \"Session cuts\", \"Why threads lost their session, across the fleet.\", \"mx-rot-title\")}\n <div class=\"mx-bars\" id=\"mx-rotations\"><div class=\"mx-empty\">Loading...</div></div>\n <p class=\"mx-note\" id=\"mx-rot-note\"></p>\n </section>\n </div>`\n\n const opsView = `<div class=\"mx-view\" id=\"mx-view-ops\" role=\"tabpanel\" aria-labelledby=\"mx-tab-ops\" hidden>\n <p id=\"mx-updated\" class=\"mx-updated\" aria-live=\"polite\">Connecting to the fleet...</p>\n\n <section class=\"mx-section\" aria-labelledby=\"mx-auto-title\">\n ${head(ICONS.schedules, \"Today's automations\", \"Persisted attempts are authoritative. Select a schedule to inspect its latest result and runtime.\", \"mx-auto-title\", `<div class=\"mx-section-actions\"><span class=\"ax-tab-count\" id=\"mx-run-count\">0 schedules</span><button class=\"ax-btn ax-btn--ghost mx-show-all\" id=\"mx-show-all\" type=\"button\" hidden>Show all</button></div>`)}\n <div class=\"ax-stack\" id=\"mx-runs\"><div class=\"mx-empty\">Loading schedules...</div></div>\n </section>\n\n <div class=\"mx-columns\">\n <section class=\"mx-section\" aria-labelledby=\"mx-active-title\">\n ${head(ICONS.activity, \"Activity provenance\", \"Who initiated current work and where it is running.\", \"mx-active-title\", `<span class=\"ax-tab-count\" id=\"mx-active-count\">0 active</span>`)}\n <div class=\"ax-stack\" id=\"mx-active\"><div class=\"mx-empty\">No activity loaded.</div></div>\n </section>\n <section class=\"mx-section\" aria-labelledby=\"mx-nodes-title\">\n ${head(ICONS.nodes, \"Nodes\", \"Reachability and capacity across the fleet.\", \"mx-nodes-title\", `<span class=\"ax-tab-count\" id=\"mx-node-count\">0 nodes</span>`)}\n <div class=\"ax-stack\" id=\"mx-nodes\"><div class=\"mx-empty\">No nodes loaded.</div></div>\n </section>\n </div>\n\n <section class=\"mx-section\" aria-labelledby=\"mx-agents-title\">\n ${head(ICONS.agents, \"Agents and skills\", \"Runtime, model, installed skills, and task totals by node.\", \"mx-agents-title\", `<span class=\"ax-tab-count\" id=\"mx-agent-count\">0 agents</span>`)}\n <div class=\"mx-agent-grid\" id=\"mx-agents\"><div class=\"mx-empty\">No agents loaded.</div></div>\n </section>\n </div>`\n\n const body = `${chrome}<main class=\"mx-content\">\n ${toolbar}\n ${activityView}\n ${lifetimeView}\n ${opsView}\n</main>\n<div class=\"mx-scrim\" id=\"mx-scrim\" hidden></div>\n<aside class=\"mx-drawer\" id=\"mx-drawer\" aria-labelledby=\"mx-drawer-title\" aria-hidden=\"true\">\n <header class=\"mx-drawer__head\">\n <div><div class=\"ax-kicker\" id=\"mx-drawer-kicker\">Details</div><h2 id=\"mx-drawer-title\">Selection</h2></div>\n <button class=\"ax-btn ax-btn--ghost\" id=\"mx-close\" type=\"button\" aria-label=\"Close details\">Close</button>\n </header>\n <div class=\"mx-drawer__body\" id=\"mx-drawer-body\"></div>\n</aside>`\n\n return renderShell({\n title: \"AgentX · Mesh Operations\",\n activeTab: \"mesh\",\n subtitle: \"Operations\",\n peers: opts.peers,\n noMain: true,\n body,\n css: MESH_CSS,\n scripts: MESH_SHARED_SCRIPT + MESH_TABS_SCRIPT + MESH_DRILL_SCRIPT + MESH_ANALYTICS_SCRIPT + MESH_OPS_SCRIPT,\n })\n}\n\n// The history window only affects the two analytics views, so it is hidden\n// while Operations is showing rather than sitting there doing nothing.\nconst MESH_TABS_SCRIPT = `<script>\n(function(){\n var tabs=Array.prototype.slice.call(document.querySelectorAll('.mx-tab'));\n var range=document.getElementById('mx-range');\n var stamp=document.getElementById('mx-analytics-updated');\n function select(id){\n tabs.forEach(function(t){\n var on=t.dataset.view===id;\n t.setAttribute('aria-selected',String(on));\n document.getElementById('mx-view-'+t.dataset.view).hidden=!on;\n });\n var analytics=id!=='ops';\n range.hidden=!analytics;stamp.hidden=!analytics;\n try{localStorage.setItem('mx-view',id)}catch(e){}\n }\n tabs.forEach(function(t,i){\n t.addEventListener('click',function(){select(t.dataset.view)});\n t.addEventListener('keydown',function(e){\n var d=e.key==='ArrowRight'?1:e.key==='ArrowLeft'?-1:0;\n if(!d)return;\n e.preventDefault();\n var next=tabs[(i+d+tabs.length)%tabs.length];\n next.focus();select(next.dataset.view);\n });\n });\n var saved=null;try{saved=localStorage.getItem('mx-view')}catch(e){}\n select(tabs.some(function(t){return t.dataset.view===saved})?saved:'activity');\n})();\n</script>`\n","import type { ServerResponse } from \"http\"\nimport type { MeshAnalytics, CauseId } from \"@/storage/mesh-analytics\"\nimport type { DayActivity, DayConversation } from \"@/storage/mesh-drill\"\n\n// --- Mesh-wide analytics fan-out + merge ------------------------------\n//\n// Each node answers GET /analytics/mesh for its OWN SQLite. This module\n// asks every reachable node in parallel and folds the answers into one\n// fleet view. Two rules keep the merge honest:\n//\n// 1. Aggregates that are genuinely fleet-wide (day columns, failure\n// causes, origin reliability, rotations) are summed.\n// 2. Anything an operator would act on — a job, a thread — stays\n// attributed to the node that owns it, because \"disable this cron\"\n// is a per-node action. Merging those would invent a fleet object\n// that nobody can actually operate.\n//\n// A node that fails to answer is reported, never silently dropped: a\n// partial fleet must look partial (CLAUDE.md — \"support unreachable\n// nodes and partial fleet failures\").\n\nexport interface NodeTarget { name: string; url: string; token?: string }\n\nexport interface MergedAnalytics {\n generatedAt: number\n windowDays: number\n /** Actual window start in epoch ms, taken from the nodes rather than\n * recomputed by the client. `days` alone cannot express it: days:0\n * means \"since local midnight\", and generatedAt - 0 is not that. */\n since: number\n nodes: Array<{ name: string; url: string; ok: boolean; error?: string; runs: number }>\n totals: MeshAnalytics[\"totals\"]\n days: MeshAnalytics[\"days\"]\n origins: MeshAnalytics[\"origins\"]\n causes: MeshAnalytics[\"causes\"]\n jobs: Array<MeshAnalytics[\"jobs\"][number] & { node: string; nodeUrl: string }>\n threads: Array<MeshAnalytics[\"threads\"][number] & { node: string; nodeUrl: string }>\n rotations: MeshAnalytics[\"rotations\"]\n retention: MeshAnalytics[\"retention\"]\n}\n\nexport interface AnalyticsQuery { days: number; tzOffsetMinutes: number; limit: number }\n\nexport async function fetchMeshAnalytics(\n nodes: NodeTarget[],\n q: AnalyticsQuery,\n timeoutMs = 6000,\n): Promise<MergedAnalytics> {\n const ac = new AbortController()\n const timer = setTimeout(() => ac.abort(), timeoutMs)\n const qs = `?days=${q.days}&tzOffset=${q.tzOffsetMinutes}&limit=${q.limit}`\n let results: Array<{ node: NodeTarget; data?: MeshAnalytics; error?: string }>\n try {\n results = await Promise.all(nodes.map(async (node) => {\n const headers: Record<string, string> = { Accept: \"application/json\" }\n if (node.token) headers[\"Authorization\"] = `Bearer ${node.token}`\n try {\n const r = await fetch(node.url + \"/analytics/mesh\" + qs, { headers, signal: ac.signal })\n if (!r.ok) return { node, error: `HTTP ${r.status}` }\n return { node, data: (await r.json()) as MeshAnalytics }\n } catch (e: any) {\n return { node, error: e?.name === \"AbortError\" ? \"timeout\" : (e?.message || \"unreachable\") }\n }\n }))\n } finally { clearTimeout(timer) }\n return mergeMeshAnalytics(results, q.days)\n}\n\nexport function mergeMeshAnalytics(\n results: Array<{ node: NodeTarget; data?: MeshAnalytics; error?: string }>,\n windowDays: number,\n): MergedAnalytics {\n const totals = { runs: 0, errors: 0, hours: 0, agents: 0, threads: 0 }\n const days = new Map<string, MeshAnalytics[\"days\"][number]>()\n const origins = new Map<string, { channel: string; runs: number; errors: number; hours: number }>()\n const causes = new Map<CauseId, { cause: CauseId; count: number; agents: string[]; example: string }>()\n const rotReasons = new Map<string, number>()\n const jobs: MergedAnalytics[\"jobs\"] = []\n const threads: MergedAnalytics[\"threads\"] = []\n let rotTotal = 0, rotTokSum = 0, rotTokN = 0, rotMax = 0\n let traces = 0, withSteps = 0\n\n for (const { node, data } of results) {\n if (!data) continue\n totals.runs += data.totals.runs\n totals.errors += data.totals.errors\n totals.hours += data.totals.hours\n totals.agents += data.totals.agents\n totals.threads += data.totals.threads\n\n for (const d of data.days) {\n const cur = days.get(d.day) || { day: d.day, cron: [0, 0], workflow: [0, 0], direct: [0, 0] }\n for (const k of [\"cron\", \"workflow\", \"direct\"] as const) {\n cur[k][0] += d[k]?.[0] || 0\n cur[k][1] += d[k]?.[1] || 0\n }\n days.set(d.day, cur)\n }\n for (const o of data.origins) {\n const cur = origins.get(o.channel) || { channel: o.channel, runs: 0, errors: 0, hours: 0 }\n cur.runs += o.runs; cur.errors += o.errors; cur.hours += o.hours\n origins.set(o.channel, cur)\n }\n for (const c of data.causes) {\n const cur = causes.get(c.cause) || { cause: c.cause, count: 0, agents: [], example: \"\" }\n cur.count += c.count\n for (const a of c.agents) if (!cur.agents.includes(a)) cur.agents.push(a)\n if (!cur.example) cur.example = c.example\n causes.set(c.cause, cur)\n }\n for (const r of data.rotations.byReason) rotReasons.set(r.reason, (rotReasons.get(r.reason) || 0) + r.count)\n rotTotal += data.rotations.total\n rotTokSum += data.rotations.tokenSum || 0\n rotTokN += data.rotations.tokenSamples || 0\n rotMax = Math.max(rotMax, data.rotations.maxTokens)\n traces += data.retention.traces\n withSteps += data.retention.tracesWithSteps\n\n for (const j of data.jobs) jobs.push({ ...j, node: node.name, nodeUrl: node.url })\n for (const t of data.threads) threads.push({ ...t, node: node.name, nodeUrl: node.url })\n }\n\n const sinces = results.filter((r) => r.data).map((r) => r.data!.since).filter((n) => Number.isFinite(n))\n return {\n generatedAt: Date.now(),\n windowDays,\n since: sinces.length ? Math.min(...sinces) : Date.now() - windowDays * 86_400_000,\n nodes: results.map((r) => ({\n name: r.node.name, url: r.node.url, ok: !!r.data,\n error: r.error, runs: r.data?.totals.runs || 0,\n })),\n totals: { ...totals, hours: Math.round(totals.hours * 100) / 100 },\n days: [...days.values()].sort((a, b) => a.day.localeCompare(b.day)),\n origins: [...origins.values()]\n .map((o) => ({ ...o, hours: Math.round(o.hours * 100) / 100 }))\n .sort((a, b) => b.runs - a.runs),\n causes: [...causes.values()].sort((a, b) => b.count - a.count),\n jobs: jobs.sort((a, b) => b.hours - a.hours),\n // Same ordering the node applies: longest-lived first. Sorting the\n // merged list by runs instead would re-rank a fleet view differently\n // from every node view that feeds it.\n threads: threads.sort((a, b) =>\n (b.lastAt - b.firstAt) - (a.lastAt - a.firstAt) || b.runs - a.runs),\n rotations: {\n total: rotTotal,\n byReason: [...rotReasons.entries()].map(([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count),\n tokenSum: rotTokSum, tokenSamples: rotTokN, maxTokens: rotMax,\n },\n retention: { traces, tracesWithSteps: withSteps },\n }\n}\n\n/** Forward one drill-down read to the node that owns the record. The\n * dashboard never guesses which node holds a task — the caller passes the\n * node URL it got from the merged payload, and we check it against the\n * same allowlist the task proxies use. */\nexport async function proxyNodeAnalytics(\n res: ServerResponse,\n nodes: NodeTarget[],\n targetUrl: string,\n upstreamPath: string,\n): Promise<void> {\n const target = targetUrl.replace(/\\/+$/, \"\")\n const node = nodes.find((n) => n.url.replace(/\\/+$/, \"\") === target)\n if (!node) {\n res.writeHead(403, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify({ error: \"node not in dashboard allowlist\", target }))\n return\n }\n const headers: Record<string, string> = { Accept: \"application/json\" }\n if (node.token) headers[\"Authorization\"] = `Bearer ${node.token}`\n try {\n const r = await fetch(target + upstreamPath, { headers })\n const body = await r.text()\n res.writeHead(r.status, { \"Content-Type\": \"application/json; charset=utf-8\" })\n res.end(body)\n } catch (e: any) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify({ error: e?.message || \"upstream fetch failed\" }))\n }\n}\n\n\n// --- One day, across the fleet ----------------------------------------\n//\n// Clicking an activity column asks a fleet question, so this fans out the\n// same way the window query does. Conversations keep their node, for the\n// same reason jobs do: you act on them per node.\n\nexport interface MergedDay {\n day: string\n nodes: Array<{ name: string; url: string; ok: boolean; error?: string; runs: number }>\n totals: DayActivity[\"totals\"]\n origins: DayActivity[\"origins\"]\n causes: DayActivity[\"causes\"]\n conversations: Array<DayConversation & { node: string; nodeUrl: string }>\n}\n\nexport async function fetchMeshDay(\n nodes: NodeTarget[],\n q: { day: string; tzOffsetMinutes: number; limit: number },\n timeoutMs = 6000,\n): Promise<MergedDay> {\n const ac = new AbortController()\n const timer = setTimeout(() => ac.abort(), timeoutMs)\n const qs = `?day=${encodeURIComponent(q.day)}&tzOffset=${q.tzOffsetMinutes}&limit=${q.limit}`\n let results: Array<{ node: NodeTarget; data?: DayActivity; error?: string }>\n try {\n results = await Promise.all(nodes.map(async (node) => {\n const headers: Record<string, string> = { Accept: \"application/json\" }\n if (node.token) headers[\"Authorization\"] = `Bearer ${node.token}`\n try {\n const r = await fetch(node.url + \"/analytics/day\" + qs, { headers, signal: ac.signal })\n if (!r.ok) return { node, error: `HTTP ${r.status}` }\n return { node, data: (await r.json()) as DayActivity }\n } catch (e: any) {\n return { node, error: e?.name === \"AbortError\" ? \"timeout\" : (e?.message || \"unreachable\") }\n }\n }))\n } finally { clearTimeout(timer) }\n return mergeMeshDay(results, q.day)\n}\n\nexport function mergeMeshDay(\n results: Array<{ node: NodeTarget; data?: DayActivity; error?: string }>,\n day: string,\n): MergedDay {\n const totals = { runs: 0, errors: 0, ms: 0, inputTokens: 0, outputTokens: 0 }\n const origins = new Map<string, { channel: string; runs: number; errors: number; ms: number }>()\n const causes = new Map<CauseId, number>()\n const conversations: MergedDay[\"conversations\"] = []\n\n for (const { node, data } of results) {\n if (!data) continue\n totals.runs += data.totals.runs\n totals.errors += data.totals.errors\n totals.ms += data.totals.ms\n totals.inputTokens += data.totals.inputTokens\n totals.outputTokens += data.totals.outputTokens\n for (const o of data.origins) {\n const cur = origins.get(o.channel) || { channel: o.channel, runs: 0, errors: 0, ms: 0 }\n cur.runs += o.runs; cur.errors += o.errors; cur.ms += o.ms\n origins.set(o.channel, cur)\n }\n for (const c of data.causes) causes.set(c.cause, (causes.get(c.cause) || 0) + c.count)\n for (const c of data.conversations) conversations.push({ ...c, node: node.name, nodeUrl: node.url })\n }\n\n return {\n day,\n nodes: results.map((r) => ({\n name: r.node.name, url: r.node.url, ok: !!r.data,\n error: r.error, runs: r.data?.totals.runs || 0,\n })),\n totals,\n origins: [...origins.values()].sort((a, b) => b.runs - a.runs),\n causes: [...causes.entries()].map(([cause, count]) => ({ cause, count })).sort((a, b) => b.count - a.count),\n conversations: conversations.sort((a, b) => b.ms - a.ms || b.turns - a.turns),\n }\n}\n","// --- Boards (Kanban) page ---\n//\n// Column-based issue/task view over configured boards (GitLab/GitHub/local).\n// Subheader owns board picker + search + filters; body holds the kanban\n// columns + issue modal + agent activity drawer. Client JS drives sortable\n// drag-drop, live updates, and optimistic writes.\n//\n// Deduplicated from the old board-dashboard.ts monolith:\n// ui/tokens.ts — :root, theme overrides, font/color vars\n// ui/components.css.ts — .ax-badge / .ax-card / .ax-field reusable bits\n// topbar.ts — .ax-topbar / .ax-subheader / .ax-theme-switch\n// HERE — subheader inputs + kanban columns + issue modal +\n// activity drawer + client JS\n//\n// The page accepts renderBoardPage({ peers }) — daemon-config awareness\n// stays in board-dashboard.ts where the routing happens.\n\nimport { renderShell, type TopbarPeer } from \"..\"\n\nexport interface BoardsPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderBoardsPage(opts: BoardsPageOpts = {}): string {\n const subheader = `\n <select id=\"boardPicker\" title=\"Switch board\"></select>\n <div class=\"search\">\n <span class=\"icon\">⌕</span>\n <input id=\"filterSearch\" type=\"search\" placeholder=\"Search or filter…\" autocomplete=\"off\" />\n </div>\n <select id=\"filterAssignee\" title=\"Assignee\"><option value=\"\">All assignees</option></select>\n <select id=\"filterMilestone\" title=\"Milestone\"><option value=\"\">Any milestone</option></select>\n <div id=\"filterLabels\" class=\"chips\"></div>\n <div class=\"spacer\"></div>\n <button id=\"activityBtn\" title=\"Agent activity (a)\">◉ Activity</button>\n <button id=\"newIssueBtn\" title=\"New issue\">+ New</button>\n <button id=\"refreshBtn\" title=\"Refresh (r)\">↻</button>`\n\n const body = `<aside id=\"activity-panel\" class=\"activity-panel hidden\" aria-hidden=\"true\">\n <header>\n <h2>Agent Activity</h2>\n <span id=\"activity-source\" class=\"source\"></span>\n <button id=\"activity-close\" class=\"x\" title=\"Close\">✕</button>\n </header>\n <div id=\"activity-body\">Loading…</div>\n <footer id=\"activity-events\" class=\"events\"></footer>\n</aside>\n<main id=\"columns\"></main>\n<div id=\"toast\"></div>\n<div id=\"modal\" class=\"modal\" aria-hidden=\"true\">\n <div class=\"modal-backdrop\"></div>\n <div class=\"modal-panel\" role=\"dialog\" aria-modal=\"true\">\n <header class=\"modal-h\">\n <div class=\"modal-title-row\">\n <span id=\"m-iid\" class=\"iid\"></span>\n <input id=\"m-title\" type=\"text\" placeholder=\"Issue title\" />\n </div>\n <button id=\"m-close\" class=\"x\" title=\"Close (esc)\">✕</button>\n </header>\n <div class=\"modal-body\">\n <div class=\"m-main\">\n <div id=\"m-draft\" class=\"draft-box\" hidden>\n <label class=\"fld\">\n <span>✨ Describe the issue (the agent will draft a proper title + description)</span>\n <textarea id=\"m-rough\" rows=\"3\" placeholder=\"e.g. when I upload a CSV over 500 rows in Receiving Records the import silently fails\"></textarea>\n </label>\n <div class=\"draft-row\">\n <select id=\"m-draft-agent\" title=\"Drafting agent\"><option value=\"\">Agent…</option></select>\n <button id=\"m-draft-go\" class=\"btn-primary\" type=\"button\">✨ Draft</button>\n <span id=\"m-draft-hint\" class=\"hint\"></span>\n </div>\n </div>\n <div class=\"fld\">\n <div class=\"desc-head\">\n <span>Description</span>\n <div class=\"desc-tabs\" role=\"tablist\">\n <button type=\"button\" class=\"desc-tab\" data-tab=\"preview\" role=\"tab\">Preview</button>\n <button type=\"button\" class=\"desc-tab active\" data-tab=\"write\" role=\"tab\">Write</button>\n </div>\n </div>\n <div id=\"m-desc-preview\" class=\"desc-preview\" hidden></div>\n <textarea id=\"m-desc\" rows=\"10\" placeholder=\"Markdown supported (GitLab flavored)\"></textarea>\n </div>\n <div class=\"fld\">\n <span>Labels</span>\n <div id=\"m-labels\" class=\"chip-picker\"></div>\n </div>\n </div>\n <aside class=\"m-side\">\n <div class=\"fld\">\n <span>State</span>\n <div id=\"m-state\" class=\"state-toggle\">\n <button data-state=\"opened\">Open</button>\n <button data-state=\"closed\">Closed</button>\n </div>\n </div>\n <div class=\"fld\">\n <span>Assignees</span>\n <div id=\"m-assignees\" class=\"chip-picker\"></div>\n </div>\n <div class=\"fld\">\n <span>Milestone</span>\n <select id=\"m-milestone\"><option value=\"\">None</option></select>\n </div>\n <div class=\"fld muted\">\n <span>Author</span>\n <div id=\"m-author\"></div>\n </div>\n <div class=\"fld muted\">\n <span>Created</span>\n <div id=\"m-created\"></div>\n </div>\n <div class=\"fld muted\">\n <span>Updated</span>\n <div id=\"m-updated\"></div>\n </div>\n <a id=\"m-link\" class=\"ext-link\" href=\"#\" target=\"_blank\" rel=\"noopener\">Open in GitLab ↗</a>\n </aside>\n </div>\n <footer class=\"modal-f\">\n <div id=\"m-hint\" class=\"hint\"></div>\n <div class=\"spacer\"></div>\n <button id=\"m-cancel\" class=\"btn-ghost\">Cancel</button>\n <button id=\"m-save\" class=\"btn-primary\">Save</button>\n </footer>\n </div>\n</div>\n<script src=\"/sortable.min.js\"></script>`\n\n return renderShell({\n title: \"AgentX · Boards\",\n activeTab: \"boards\",\n subtitle: \"Boards\",\n subheader,\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n noMain: true,\n body,\n css: BOARDS_PAGE_CSS,\n scripts: `<script>${BOARDS_PAGE_SCRIPT}</script>`,\n headExtras: `<script>\n // The boards page has its own connection dot + wants body height:100%\n // for the full-viewport kanban layout. Inject the dot into the topbar\n // and override body height at runtime so the shared shell doesn't\n // need a dedicated opt-in.\n document.addEventListener('DOMContentLoaded', () => {\n document.documentElement.style.height = '100%';\n document.body.style.height = '100%';\n const right = document.querySelector('.ax-topbar__right');\n if (right && !right.querySelector('.conn')) {\n const dot = document.createElement('div');\n dot.id = 'conn'; dot.className = 'conn ok'; dot.title = 'ready';\n dot.textContent = '●';\n right.insertBefore(dot, right.firstChild);\n }\n });\n </script>`,\n })\n}\n\nconst BOARDS_PAGE_CSS = `\n/* Kanban subheader — replaces the legacy toolbar that used to live in <header>. */\n.ax-subheader select, .ax-subheader input, .ax-subheader button {\n background: var(--col); color: var(--text); border: 1px solid var(--border);\n padding: 6px 10px; border-radius: 6px; font: inherit; outline: none; }\n.ax-subheader select:focus, .ax-subheader input:focus { border-color: var(--accent); }\n.ax-subheader .search { position: relative; }\n.ax-subheader .search .icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%);\n color: var(--muted); pointer-events: none; font-size: 14px; }\n.ax-subheader .search input { padding-left: 28px; width: 260px; }\n.ax-subheader button { cursor: pointer; }\n.ax-subheader button:hover { border-color: var(--accent); }\n.ax-subheader .chips { display: flex; gap: 4px; flex-wrap: wrap; }\n.ax-subheader #newIssueBtn { background: var(--accent); color: #fff; border-color: var(--accent); }\n.ax-subheader #newIssueBtn:hover { filter: brightness(1.1); }\n.ax-topbar__right .conn { font-size: 14px; line-height: 1; }\n.ax-topbar__right .conn.ok { color: var(--green); }\n.ax-topbar__right .conn.warn { color: var(--yellow); }\n.ax-topbar__right .conn.err { color: var(--red); }\n\nmain#columns { display: flex; gap: 10px; padding: 12px; overflow-x: auto; align-items: stretch;\n min-height: calc(100vh - 60px); }\n.col { background: var(--col); border: 1px solid var(--border); border-radius: 8px;\n min-width: 280px; max-width: 320px; flex: 1 0 280px; display: flex; flex-direction: column;\n max-height: calc(100vh - 84px); }\n.col .accent { height: 3px; border-top-left-radius: 7px; border-top-right-radius: 7px; background: var(--gray); }\n.col header.col-h { margin: 0; padding: 10px 12px; background: var(--col-hdr);\n border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px;\n position: sticky; top: 0; z-index: 1; border-radius: 0; }\n.col header.col-h h3 { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); flex: 1; }\n.col header.col-h .count { background: rgba(255,255,255,0.08); color: var(--muted);\n font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }\n.stack { padding: 8px; display: flex; flex-direction: column; gap: 8px; overflow-y: auto;\n flex: 1; min-height: 40px; }\n\n.card { background: var(--card); border: 1px solid var(--border); border-radius: 6px;\n padding: 10px 12px; cursor: grab; transition: border-color .15s, transform .15s;\n display: flex; flex-direction: column; gap: 6px; }\n.card:hover { border-color: var(--accent); }\n.card.closed { opacity: 0.75; }\n.card .title { font-weight: 500; font-size: 13px; line-height: 1.4; color: var(--text);\n overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; word-break: break-word; }\n.card .labels { display: flex; gap: 4px; flex-wrap: wrap; }\n.card .label { font-size: 10px; padding: 2px 8px; border-radius: 10px; font-weight: 500;\n background: rgba(99, 102, 241, 0.25); color: #e6e8ef; white-space: nowrap; line-height: 1.4; }\n.card .foot { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted);\n flex-wrap: wrap; }\n.card .foot .iid { font-family: ui-monospace, monospace; }\n.card .foot .milestone { background: rgba(99,102,241,0.15); color: var(--accent);\n padding: 1px 6px; border-radius: 3px; font-size: 10px; }\n.card .foot .est { background: rgba(255,255,255,0.08); padding: 1px 6px; border-radius: 3px; font-size: 10px; }\n.card .foot .date { margin-left: auto; }\n.card .foot a.link { color: var(--muted); text-decoration: none; }\n.card .foot a.link:hover { color: var(--accent); }\n\n.avatar { display: inline-flex; align-items: center; justify-content: center;\n width: 22px; height: 22px; border-radius: 50%; font-size: 10px; font-weight: 600;\n color: white; flex-shrink: 0; }\n.avatar.unassigned { background: var(--gray); }\n\n.sortable-ghost { opacity: 0.4; }\n.sortable-drag { cursor: grabbing; }\n.sortable-chosen { border-color: var(--accent); }\n.empty { color: var(--muted); font-style: italic; text-align: center; padding: 16px; font-size: 12px; }\n\n#toast { position: fixed; right: 16px; bottom: 16px; padding: 10px 14px;\n background: var(--red); color: white; border-radius: 6px; font-size: 13px;\n max-width: 360px; display: none; z-index: 200; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }\n#toast.show { display: block; }\n\n/* Multi-assignee stack on cards */\n.avatar-stack { display: inline-flex; }\n.avatar-stack .avatar { margin-left: -6px; border: 2px solid var(--card); }\n.avatar-stack .avatar:first-child { margin-left: 0; }\n.avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }\n\n/* Column add button */\n.col header.col-h .add { background: transparent; color: var(--muted); border: 1px solid var(--border);\n padding: 0 8px; line-height: 22px; border-radius: 4px; cursor: pointer; font-size: 14px; }\n.col header.col-h .add:hover { color: var(--accent); border-color: var(--accent); }\n\n/* --- Modal --- */\n.modal { position: fixed; inset: 0; z-index: 150; display: none; }\n.modal.open { display: block; }\n.modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }\n.modal-panel { position: relative; max-width: 940px; width: calc(100% - 40px); height: calc(100vh - 60px);\n margin: 30px auto; background: var(--card); border: 1px solid var(--border); border-radius: 10px;\n display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,0.55); overflow: hidden; }\n.modal-h { display: flex; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--border); align-items: center; }\n.modal-h .modal-title-row { flex: 1; display: flex; gap: 10px; align-items: center; }\n.modal-h .iid { font-family: ui-monospace, monospace; color: var(--muted); font-size: 13px; }\n.modal-h input[type=\"text\"] { flex: 1; background: var(--col); border: 1px solid var(--border); color: var(--text);\n padding: 8px 10px; border-radius: 6px; font-size: 15px; font-weight: 500; outline: none; }\n.modal-h input[type=\"text\"]:focus { border-color: var(--accent); }\n.modal-h .x { background: transparent; color: var(--muted); border: none; font-size: 18px; cursor: pointer; padding: 6px 8px; }\n.modal-h .x:hover { color: var(--text); }\n\n.modal-body { flex: 1; display: grid; grid-template-columns: 1fr 300px; gap: 0;\n overflow: hidden; min-height: 0; }\n.modal-body .m-main { padding: 16px; overflow-y: auto; min-height: 0; }\n.modal-body .m-side { padding: 16px; border-left: 1px solid var(--border); background: var(--col);\n overflow-y: auto; min-height: 0; display: flex; flex-direction: column; gap: 14px; }\n\n.fld { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }\n.fld > span { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }\n.fld.muted > div { font-size: 12px; color: var(--muted); }\n.fld textarea { background: var(--col); border: 1px solid var(--border); color: var(--text);\n padding: 10px; border-radius: 6px; font: 13px/1.5 ui-monospace, \"SF Mono\", monospace; outline: none; resize: vertical; }\n.fld textarea:focus { border-color: var(--accent); }\n.fld select { background: var(--col); border: 1px solid var(--border); color: var(--text);\n padding: 6px 10px; border-radius: 6px; font: inherit; outline: none; }\n\n/* Description edit/preview tabs */\n.desc-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.desc-tabs { display: inline-flex; background: var(--col); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }\n.desc-tabs button { background: transparent; border: none; color: var(--muted); padding: 4px 10px; cursor: pointer; font: inherit; font-size: 11px; }\n.desc-tabs button.active { background: var(--accent); color: white; }\n.desc-preview { background: var(--col); border: 1px solid var(--border); border-radius: 6px;\n padding: 12px 14px; min-height: 120px; max-height: 420px; overflow-y: auto; line-height: 1.55; }\n.desc-preview.empty { color: var(--muted); font-style: italic; }\n.desc-preview h1 { font-size: 18px; margin: 16px 0 8px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }\n.desc-preview h2 { font-size: 16px; margin: 14px 0 6px; }\n.desc-preview h3 { font-size: 14px; margin: 12px 0 4px; color: var(--text); }\n.desc-preview p { margin: 6px 0; }\n.desc-preview ul, .desc-preview ol { margin: 6px 0; padding-left: 20px; }\n.desc-preview li { margin: 2px 0; }\n.desc-preview li.task { list-style: none; margin-left: -16px; }\n.desc-preview li.task input { margin-right: 6px; vertical-align: middle; }\n.desc-preview code { background: rgba(255,255,255,0.08); padding: 1px 5px; border-radius: 3px;\n font: 12px ui-monospace, \"SF Mono\", monospace; color: #e6e8ef; }\n.desc-preview pre { background: #0a0c11; border: 1px solid var(--border); border-radius: 6px;\n padding: 10px; overflow-x: auto; margin: 8px 0; }\n.desc-preview pre code { background: transparent; padding: 0; font-size: 12px; line-height: 1.45; color: #d8dce6; }\n.desc-preview blockquote { border-left: 3px solid var(--accent); padding: 2px 12px; margin: 8px 0;\n color: var(--muted); background: rgba(99,102,241,0.08); border-radius: 0 4px 4px 0; }\n.desc-preview a { color: var(--accent); text-decoration: none; }\n.desc-preview a:hover { text-decoration: underline; }\n.desc-preview hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; }\n.desc-preview table { border-collapse: collapse; margin: 8px 0; font-size: 12px; }\n.desc-preview th, .desc-preview td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }\n.desc-preview th { background: var(--col-hdr); font-weight: 600; }\n.desc-preview img { max-width: 100%; border-radius: 4px; margin: 6px 0; }\n.desc-preview del { color: var(--muted); }\n.desc-preview .ref { background: rgba(99,102,241,0.15); padding: 0 4px; border-radius: 3px; font-family: ui-monospace, monospace; font-size: 12px; }\n\n.chip-picker { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 6px;\n background: var(--col); border: 1px solid var(--border); border-radius: 6px; min-height: 36px; }\n.chip-picker .chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px;\n border-radius: 10px; font-size: 11px; background: rgba(99,102,241,0.25); color: #e6e8ef; font-weight: 500; }\n.chip-picker .chip .remove { cursor: pointer; color: rgba(255,255,255,0.7); margin-left: 2px; }\n.chip-picker .chip .remove:hover { color: white; }\n.chip-picker .add-chip { background: transparent; border: 1px dashed var(--border); color: var(--muted);\n padding: 3px 10px; border-radius: 10px; cursor: pointer; font: inherit; font-size: 11px; }\n.chip-picker .add-chip:hover { border-color: var(--accent); color: var(--accent); }\n\n.chip-picker .dropdown { position: relative; }\n.chip-picker .dropdown .menu { position: absolute; top: 100%; left: 0; margin-top: 4px;\n background: var(--card); border: 1px solid var(--border); border-radius: 6px; min-width: 240px;\n max-height: 280px; overflow-y: auto; box-shadow: 0 8px 20px rgba(0,0,0,0.4); z-index: 10; padding: 4px; }\n.chip-picker .dropdown .menu input.search { width: 100%; background: var(--col); border: 1px solid var(--border);\n color: var(--text); padding: 6px 10px; border-radius: 4px; font: inherit; margin-bottom: 4px; outline: none; }\n.chip-picker .dropdown .menu .opt { padding: 6px 10px; cursor: pointer; border-radius: 4px;\n font-size: 12px; display: flex; align-items: center; gap: 8px; }\n.chip-picker .dropdown .menu .opt:hover { background: var(--col); }\n.chip-picker .dropdown .menu .opt.selected { background: rgba(99,102,241,0.15); color: var(--accent); }\n.chip-picker .dropdown .menu .empty { padding: 10px; color: var(--muted); font-size: 12px; text-align: center; }\n\n.state-toggle { display: inline-flex; background: var(--col); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }\n.state-toggle button { background: transparent; border: none; color: var(--muted); padding: 6px 12px;\n cursor: pointer; font: inherit; font-size: 12px; }\n.state-toggle button.active[data-state=\"opened\"] { background: var(--green); color: white; }\n.state-toggle button.active[data-state=\"closed\"] { background: var(--gray); color: white; }\n\n.ext-link { color: var(--muted); text-decoration: none; font-size: 12px; margin-top: auto; padding-top: 10px;\n border-top: 1px solid var(--border); }\n.ext-link:hover { color: var(--accent); }\n\n.modal-f { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--border); }\n.modal-f .spacer { flex: 1; }\n.modal-f .hint { color: var(--muted); font-size: 12px; }\n.btn-primary { background: var(--accent); color: white; border: 1px solid var(--accent);\n padding: 7px 16px; border-radius: 6px; cursor: pointer; font: inherit; font-weight: 500; }\n.btn-primary:hover { filter: brightness(1.1); }\n.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }\n.btn-ghost { background: transparent; color: var(--text); border: 1px solid var(--border);\n padding: 7px 14px; border-radius: 6px; cursor: pointer; font: inherit; }\n.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }\n\n@media (max-width: 720px) {\n .modal-body { grid-template-columns: 1fr; }\n .modal-body .m-side { border-left: none; border-top: 1px solid var(--border); }\n}\n\n/* --- Activity panel --- */\n.activity-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 340px;\n background: var(--card); border-left: 1px solid var(--border); z-index: 140;\n display: flex; flex-direction: column; box-shadow: -8px 0 24px rgba(0,0,0,0.35);\n transition: transform 0.2s ease; }\n.activity-panel.hidden { transform: translateX(100%); pointer-events: none; }\n.activity-panel > header { display: flex; align-items: center; gap: 8px; padding: 12px 14px;\n border-bottom: 1px solid var(--border); background: var(--col-hdr); }\n.activity-panel > header h2 { margin: 0; font-size: 13px; font-weight: 600; flex: 1; }\n.activity-panel > header .source { font-size: 10px; color: var(--muted); font-family: ui-monospace, monospace; }\n.activity-panel > header .x { background: transparent; border: none; color: var(--muted); font-size: 16px;\n cursor: pointer; padding: 4px 8px; }\n.activity-panel > header .x:hover { color: var(--text); }\n#activity-body { flex: 1; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 8px; }\n.agent-card { background: var(--col); border: 1px solid var(--border); border-radius: 6px;\n padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }\n.agent-card .top { display: flex; align-items: center; gap: 8px; }\n.agent-card .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--gray); flex-shrink: 0; }\n.agent-card .dot.active { background: var(--green); box-shadow: 0 0 6px var(--green); animation: pulse 1.2s ease-in-out infinite; }\n.agent-card .dot.errored { background: var(--red); }\n.agent-card .name { font-weight: 600; font-size: 13px; flex: 1; }\n.agent-card .tier { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }\n.agent-card .meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.agent-card .model { font-family: ui-monospace, monospace; font-size: 10px; color: var(--accent);\n background: rgba(99,102,241,0.12); padding: 1px 7px; border-radius: 3px; letter-spacing: 0.2px; }\n.agent-card .last { font-size: 10px; color: var(--muted); }\n.agent-card .last.muted { font-style: italic; opacity: 0.7; }\n.agent-card .stats { display: flex; gap: 10px; font-size: 11px; color: var(--muted); }\n.agent-card .stats b { color: var(--text); font-weight: 600; }\n.agent-card .stats .errors b { color: var(--red); }\n.agent-card .current { font-size: 11px; color: var(--muted); padding-top: 4px; border-top: 1px dashed var(--border);\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.agent-card .current b { color: var(--text); font-weight: 500; }\n@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.4 } }\n\n.activity-panel .events { padding: 10px 14px; max-height: 200px; overflow-y: auto;\n border-top: 1px solid var(--border); background: #0a0c11; font: 11px/1.5 ui-monospace, monospace; }\n.activity-panel .events .evt { color: var(--muted); margin-bottom: 4px; }\n.activity-panel .events .evt .ts { color: #4a4e5a; }\n.activity-panel .events .evt .who { color: var(--accent); }\n.activity-panel .events .evt .what { color: var(--text); }\n.activity-panel .events .evt.err .what { color: var(--red); }\n\n/* Shift the main grid left when activity panel is open so cards aren't hidden */\nbody.panel-open main#columns { padding-right: 352px; }\nbody.panel-open header { padding-right: 356px; }\n\nheader #activityBtn.on { background: var(--accent); color: white; border-color: var(--accent); }\nheader a.link { color: var(--text); text-decoration: none; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px; }\nheader a.link:hover { color: var(--accent); border-color: var(--accent); }\n\n/* AI draft row inside the create modal */\n.draft-box { background: var(--col); border: 1px dashed var(--accent); border-radius: 8px;\n padding: 10px 12px; margin-bottom: 14px; }\n.draft-box .fld { margin-bottom: 8px; }\n.draft-box .draft-row { display: flex; gap: 8px; align-items: center; }\n.draft-box .draft-row select { flex: 0 0 160px; }\n.draft-box .draft-row .hint { color: var(--muted); font-size: 12px; flex: 1; }\n.draft-box .draft-row .hint.err { color: var(--red); }\n.draft-box .draft-row .hint.ok { color: var(--green); }\n`\nconst BOARDS_PAGE_SCRIPT = `\n'use strict';\nconst state = {\n boards: [], current: null, items: {},\n filter: { q: '', assignee: '', label: '', milestone: '' },\n // Per-board caches — members + milestones + known labels.\n meta: { members: [], milestones: [], labels: [] },\n modal: { mode: null, id: null, original: null, draft: null }\n};\n\nfunction csrfHeaders(extra) {\n return Object.assign({ 'Content-Type': 'application/json', 'X-Requested-With': 'agentx-board' }, extra || {});\n}\nfunction toast(msg, kind) {\n const el = document.getElementById('toast');\n el.textContent = msg;\n el.style.background = kind === 'ok' ? 'var(--green)' : 'var(--red)';\n el.classList.add('show');\n clearTimeout(toast._t);\n toast._t = setTimeout(() => el.classList.remove('show'), 4000);\n}\n\nasync function loadBoards() {\n const r = await fetch('/api/boards');\n if (!r.ok) throw new Error('failed to load boards');\n state.boards = await r.json();\n const picker = document.getElementById('boardPicker');\n picker.innerHTML = '';\n for (const b of state.boards) {\n const opt = document.createElement('option');\n opt.value = b.id; opt.textContent = b.name || b.id;\n picker.appendChild(opt);\n }\n picker.onchange = () => switchBoard(picker.value);\n if (state.boards.length) switchBoard(state.boards[0].id);\n}\n\nasync function switchBoard(id) {\n state.current = state.boards.find(b => b.id === id);\n state.meta = { members: [], milestones: [], labels: [] };\n renderColumns();\n populateFilters();\n await loadItems();\n // Fetch members + milestones in the background for pickers.\n loadMeta();\n}\n\nasync function loadMeta() {\n if (!state.current) return;\n try {\n const [mem, ms] = await Promise.all([\n fetch(\\`/api/boards/\\${state.current.id}/members\\`).then(r => r.ok ? r.json() : []),\n fetch(\\`/api/boards/\\${state.current.id}/milestones\\`).then(r => r.ok ? r.json() : []),\n ]);\n state.meta.members = mem;\n state.meta.milestones = ms;\n state.meta.labels = collectKnownLabels();\n } catch (e) { /* non-fatal */ }\n}\n\nfunction collectKnownLabels() {\n const set = new Set();\n for (const list of Object.values(state.items)) for (const i of list) {\n for (const l of (i.labels || [])) set.add(l);\n }\n return [...set].sort();\n}\n\nfunction populateFilters() {\n if (!state.current) return;\n const labelsEl = document.getElementById('filterLabels');\n labelsEl.innerHTML = '';\n for (const lbl of state.current.labels || []) {\n const chip = document.createElement('span');\n chip.className = 'label';\n chip.style.cursor = 'pointer';\n chip.style.background = lbl.color || labelColor(lbl.name);\n chip.style.color = pickTextColor(lbl.color);\n chip.textContent = lbl.name;\n chip.onclick = () => {\n state.filter.label = state.filter.label === lbl.name ? '' : lbl.name;\n renderChipStates(); loadItems();\n };\n labelsEl.appendChild(chip);\n }\n}\nfunction renderChipStates() {\n document.querySelectorAll('#filterLabels .label').forEach(chip => {\n chip.style.opacity = (state.filter.label && chip.textContent !== state.filter.label) ? '0.35' : '1';\n });\n}\n\nfunction renderColumns() {\n const main = document.getElementById('columns');\n main.innerHTML = '';\n if (!state.current) return;\n for (const col of state.current.columns) {\n const section = document.createElement('section');\n section.className = 'col';\n section.dataset.colId = col.id;\n const canAdd = col.kind !== 'closed';\n section.innerHTML = \\`\n <div class=\"accent\" style=\"background:\\${col.accent || 'var(--gray)'}\"></div>\n <header class=\"col-h\">\n <h3>\\${escapeHtml(col.title)}</h3>\n <span class=\"count\">0</span>\n \\${canAdd ? \\`<button class=\"add\" data-col-id=\"\\${col.id}\" title=\"New issue in this column\">+</button>\\` : ''}\n </header>\n <div class=\"stack\" data-col-id=\"\\${col.id}\"></div>\n \\`;\n main.appendChild(section);\n }\n document.querySelectorAll('.col .add').forEach(btn => {\n btn.onclick = () => openCreateModal(btn.dataset.colId);\n });\n document.querySelectorAll('.stack').forEach(el => {\n Sortable.create(el, {\n group: 'board', animation: 150,\n onEnd: async (ev) => {\n const from = ev.from.dataset.colId;\n const to = ev.to.dataset.colId;\n if (from === to) return;\n const itemId = ev.item.dataset.id;\n try {\n const r = await fetch(\\`/api/boards/\\${state.current.id}/items/\\${encodeURIComponent(itemId)}/move\\`, {\n method: 'PATCH', headers: csrfHeaders(), body: JSON.stringify({ from, to })\n });\n if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || r.statusText);\n toast('Moved ✓', 'ok');\n updateCounts();\n } catch (e) {\n ev.from.insertBefore(ev.item, ev.from.children[ev.oldIndex] || null);\n updateCounts();\n toast('Move failed: ' + (e.message || 'unknown'));\n }\n }\n });\n });\n}\n\nfunction updateCounts() {\n document.querySelectorAll('.col').forEach(col => {\n const n = col.querySelector('.stack').children.length;\n const empty = col.querySelector('.stack .empty');\n const real = empty ? 0 : n;\n col.querySelector('.count').textContent = real;\n });\n}\n\nasync function loadItems() {\n const conn = document.getElementById('conn');\n conn.className = 'conn warn'; conn.title = 'loading…';\n if (!state.current) return;\n const params = new URLSearchParams();\n if (state.filter.q) params.set('q', state.filter.q);\n if (state.filter.assignee) params.set('assignee', state.filter.assignee);\n if (state.filter.label) params.set('label', state.filter.label);\n if (state.filter.milestone) params.set('milestone', state.filter.milestone);\n try {\n const r = await fetch(\\`/api/boards/\\${state.current.id}/items?\\${params.toString()}\\`);\n if (!r.ok) throw new Error(await r.text());\n const data = await r.json();\n state.items = data.columns;\n paintItems();\n populateAssigneeFilter();\n populateMilestoneFilter();\n conn.className = 'conn ok'; conn.title = 'ok';\n } catch (e) {\n conn.className = 'conn err'; conn.title = 'load failed';\n toast('Load failed: ' + (e.message || 'unknown'));\n }\n}\n\nfunction populateAssigneeFilter() {\n const set = new Set();\n for (const list of Object.values(state.items)) for (const i of list) if (i.assignee) set.add(i.assignee);\n const sel = document.getElementById('filterAssignee');\n const current = sel.value;\n sel.innerHTML = '<option value=\"\">All assignees</option>';\n for (const a of [...set].sort()) {\n const o = document.createElement('option'); o.value = a; o.textContent = a;\n sel.appendChild(o);\n }\n sel.value = current;\n}\nfunction populateMilestoneFilter() {\n const set = new Set();\n for (const list of Object.values(state.items)) for (const i of list) if (i.milestone) set.add(i.milestone);\n const sel = document.getElementById('filterMilestone');\n const current = sel.value;\n sel.innerHTML = '<option value=\"\">Any milestone</option>';\n for (const m of [...set].sort()) {\n const o = document.createElement('option'); o.value = m; o.textContent = m;\n sel.appendChild(o);\n }\n sel.value = current;\n}\n\nfunction paintItems() {\n if (!state.current) return;\n for (const col of state.current.columns) {\n const stack = document.querySelector(\\`.stack[data-col-id=\"\\${col.id}\"]\\`);\n if (!stack) continue;\n stack.innerHTML = '';\n const list = state.items[col.id] || [];\n if (list.length === 0) {\n const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'Empty';\n stack.appendChild(empty);\n } else {\n for (const item of list) stack.appendChild(renderCard(item, col));\n }\n }\n updateCounts();\n}\n\nfunction renderCard(item, col) {\n const card = document.createElement('div');\n card.className = 'card' + (item.state === 'closed' ? ' closed' : '');\n card.dataset.id = item.id;\n const iid = item.id.split(':').slice(-1)[0];\n\n // Filter out Status::* scoped labels from display (represented by the column).\n const details = (item.labelDetails || (item.labels || []).map(n => ({ name: n })))\n .filter(l => !/^Status::/i.test(l.name));\n const labelChips = details.slice(0, 5).map(l => {\n const bg = l.color || labelColor(l.name);\n const fg = l.text_color || pickTextColor(bg);\n return \\`<span class=\"label\" title=\"\\${escapeHtml(l.name)}\" style=\"background:\\${bg};color:\\${fg}\">\\${escapeHtml(l.name)}</span>\\`;\n }).join('');\n\n const assignees = (item.assignees && item.assignees.length)\n ? item.assignees\n : (item.assignee ? [{ username: item.assignee }] : []);\n const avatarStack = assignees.length\n ? \\`<span class=\"avatar-stack\">\\${assignees.slice(0, 3).map(renderAvatar).join('')}</span>\\`\n : \\`<span class=\"avatar unassigned\" title=\"unassigned\">?</span>\\`;\n\n const milestone = item.milestone ? \\`<span class=\"milestone\" title=\"Milestone\">🏁 \\${escapeHtml(item.milestone)}</span>\\` : '';\n const est = item.estimatedSeconds ? \\`<span class=\"est\" title=\"Estimate\">⏱ \\${fmtDuration(item.estimatedSeconds)}</span>\\` : '';\n const date = item.updatedAt ? \\`<span class=\"date\" title=\"Updated \\${escapeHtml(item.updatedAt)}\">\\${fmtDate(item.updatedAt)}</span>\\` : '';\n const link = item.url ? \\`<a class=\"link\" href=\"\\${escapeHtml(item.url)}\" target=\"_blank\" rel=\"noopener\" title=\"Open in GitLab\" onclick=\"event.stopPropagation()\">↗</a>\\` : '';\n\n card.innerHTML = \\`\n <div class=\"title\">\\${escapeHtml(item.title || '(untitled)')}</div>\n \\${labelChips ? \\`<div class=\"labels\">\\${labelChips}</div>\\` : ''}\n <div class=\"foot\">\n \\${avatarStack}\n <span class=\"iid\">#\\${escapeHtml(iid)}</span>\n \\${milestone}\n \\${est}\n \\${date}\n \\${link}\n </div>\n \\`;\n card.addEventListener('click', (e) => {\n if (e.target.closest('a,button')) return;\n openDetailModal(item.id);\n });\n return card;\n}\n\nfunction renderAvatar(a) {\n const name = a.name || a.username || '?';\n const img = a.avatarUrl ? \\`<img src=\"\\${escapeHtml(a.avatarUrl)}\" alt=\"\">\\` : '';\n const bg = labelColor(a.username || name);\n return \\`<span class=\"avatar\" title=\"\\${escapeHtml(name)}\" style=\"background:\\${bg}\">\\${img || escapeHtml(initials(name))}</span>\\`;\n}\n\nfunction escapeHtml(s) {\n return String(s).replace(/[&<>\"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]));\n}\nfunction initials(s) {\n const parts = String(s).replace(/[_\\\\-\\\\.]+/g, ' ').trim().split(/\\\\s+/);\n if (!parts.length) return '?';\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();\n return (parts[0][0] + parts[1][0]).toUpperCase();\n}\nfunction labelColor(s) {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;\n return 'hsl(' + (h % 360) + ', 55%, 45%)';\n}\nfunction pickTextColor(bg) {\n if (!bg) return '#fff';\n if (bg.startsWith('#') && bg.length >= 7) {\n const r = parseInt(bg.slice(1,3),16), g = parseInt(bg.slice(3,5),16), b = parseInt(bg.slice(5,7),16);\n return (r*299 + g*587 + b*114) / 1000 >= 160 ? '#111' : '#fff';\n }\n return '#fff';\n}\nfunction fmtDuration(sec) {\n if (!sec) return '';\n const m = Math.round(sec / 60);\n if (m < 60) return m + 'm';\n const h = Math.floor(m / 60); const rem = m % 60;\n return rem ? \\`\\${h}h\\${rem}m\\` : h + 'h';\n}\nfunction fmtDate(iso) {\n const d = new Date(iso); if (Number.isNaN(d.getTime())) return '';\n return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });\n}\nfunction debounce(fn, ms) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }\n\ndocument.getElementById('filterSearch').oninput = debounce(e => { state.filter.q = e.target.value; loadItems(); }, 250);\ndocument.getElementById('filterAssignee').onchange = e => { state.filter.assignee = e.target.value; loadItems(); };\ndocument.getElementById('filterMilestone').onchange = e => { state.filter.milestone = e.target.value; loadItems(); };\ndocument.getElementById('refreshBtn').onclick = () => loadItems();\ndocument.getElementById('newIssueBtn').onclick = () => openCreateModal(null);\ndocument.getElementById('activityBtn').onclick = () => toggleActivity();\ndocument.getElementById('activity-close').onclick = () => toggleActivity(false);\nwindow.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && document.getElementById('modal').classList.contains('open')) { closeModal(); return; }\n if (e.target.matches('input,select,textarea')) return;\n if (e.key === 'r') loadItems();\n if (e.key === 'a') toggleActivity();\n});\n\n// --- Modal (detail view / create) ---\n\nasync function openDetailModal(itemId) {\n openModal('edit', itemId);\n const hint = document.getElementById('m-hint');\n hint.textContent = 'Loading…';\n try {\n const r = await fetch('/api/boards/' + state.current.id + '/items/' + encodeURIComponent(itemId));\n if (!r.ok) throw new Error((await r.json().catch(()=>({}))).error || r.statusText);\n const detail = await r.json();\n state.modal.original = detail;\n state.modal.draft = cloneForDraft(detail);\n paintModal(state.modal.draft);\n hint.textContent = '';\n } catch (e) {\n hint.textContent = 'Load failed: ' + e.message;\n }\n}\n\nfunction openCreateModal(columnId) {\n state.modal.targetColumnId = columnId || null;\n const blank = {\n id: null, title: '', description: '',\n labels: [], labelDetails: [], assignees: [], milestone: '', state: 'opened',\n };\n // Pre-apply the target column's scoped label so the new issue lands in that column.\n if (columnId && state.current) {\n const col = state.current.columns.find(c => c.id === columnId);\n if (col && col.kind === 'scoped-label' && col.scopedLabel) {\n blank.labels.push(col.scopedLabel);\n blank.labelDetails.push({ name: col.scopedLabel, color: col.accent });\n } else if (col && col.kind === 'label' && col.mapsToLabel) {\n blank.labels.push(col.mapsToLabel);\n blank.labelDetails.push({ name: col.mapsToLabel, color: col.accent });\n }\n }\n // Carry the board's primary label (the team filter) so created issues show up here.\n if (state.current && state.current.primaryToolLabel && !blank.labels.includes(state.current.primaryToolLabel)) {\n blank.labels.push(state.current.primaryToolLabel);\n blank.labelDetails.push({ name: state.current.primaryToolLabel });\n }\n openModal('create', null);\n state.modal.original = null;\n state.modal.draft = blank;\n paintModal(blank);\n}\n\nfunction openModal(mode, id) {\n state.modal.mode = mode;\n state.modal.id = id;\n const m = document.getElementById('modal');\n m.classList.add('open');\n m.setAttribute('aria-hidden', 'false');\n document.body.style.overflow = 'hidden';\n document.getElementById('m-save').textContent = mode === 'create' ? 'Create' : 'Save';\n document.getElementById('m-iid').textContent = id ? ('#' + id.split(':').slice(-1)[0]) : 'NEW';\n // Default description tab: Preview when reading an existing issue (focus on\n // rendered content), Write when creating (focus on typing).\n setDescTab(mode === 'create' ? 'write' : 'preview');\n // Draft box only in create mode.\n const draftBox = document.getElementById('m-draft');\n if (draftBox) {\n draftBox.hidden = mode !== 'create';\n if (mode === 'create') {\n document.getElementById('m-rough').value = '';\n document.getElementById('m-draft-hint').textContent = '';\n document.getElementById('m-draft-hint').className = 'hint';\n populateDraftAgents();\n }\n }\n // Pre-focus: rough for create, description for edit.\n setTimeout(() => {\n const el = document.getElementById(mode === 'create' ? 'm-rough' : 'm-desc');\n if (el) el.focus();\n }, 50);\n}\n\n/** Toggle between rendered preview and the raw textarea for the description. */\nfunction setDescTab(tab) {\n const preview = document.getElementById('m-desc-preview');\n const textarea = document.getElementById('m-desc');\n if (!preview || !textarea) return;\n const showPreview = tab === 'preview';\n preview.hidden = !showPreview;\n textarea.hidden = showPreview;\n document.querySelectorAll('.desc-tabs .desc-tab').forEach((b) => {\n b.classList.toggle('active', b.dataset.tab === tab);\n });\n if (showPreview) {\n // Re-render with the current draft content so previews stay in sync with\n // in-progress edits (e.g. after an ✨ Draft).\n paintDescriptionPreview(state.modal.draft || {});\n }\n}\n\nfunction paintDescriptionPreview(d) {\n const preview = document.getElementById('m-desc-preview');\n if (!preview) return;\n const text = (state.modal.draft && state.modal.draft.description) || d.description || '';\n if (!text.trim()) {\n preview.classList.add('empty');\n preview.textContent = 'No description yet.';\n return;\n }\n preview.classList.remove('empty');\n // Derive GitLab host+project from the item URL so #123 / !45 / @user become\n // real links. Falls back to plain text rendering when URL isn't available.\n const url = (state.modal.original && state.modal.original.url) || d.url || '';\n const ctx = parseGitLabContext(url);\n preview.innerHTML = renderGitLabMarkdown(text, ctx);\n}\n\n/** Extract {host, project} from a GitLab issue/MR URL. Returns null on failure. */\nfunction parseGitLabContext(url) {\n if (!url) return null;\n try {\n const u = new URL(url);\n // \".../:namespace/:project/-/issues/:iid\" or \".../-/merge_requests/...\"\n const m = u.pathname.match(/^\\\\/(.+?)\\\\/-\\\\//);\n if (!m) return { host: u.origin, project: '' };\n return { host: u.origin, project: m[1] };\n } catch { return null; }\n}\n\n// Alias escapeHtml so the markdown renderer reads concisely.\nconst esc = escapeHtml;\n\n/* ------------------------------------------------------------------ */\n/* Minimal GitLab-flavored Markdown renderer */\n/* ------------------------------------------------------------------ */\n/* Handles: headings, bold, italic, strikethrough, inline + fenced */\n/* code, links, images, blockquotes, HR, ordered/unordered lists, */\n/* task lists (- [ ] / - [x]), tables, paragraphs. Post-process for */\n/* GitLab refs (#NNN, !NNN, @user). */\n/* Not a full CommonMark implementation — covers the 95% used in */\n/* issue bodies. Output is assembled from escaped + linkified tokens */\n/* so no user content lands in the DOM unescaped. */\n/* ------------------------------------------------------------------ */\n\nfunction renderGitLabMarkdown(md, ctx) {\n const src = String(md || '').replace(/\\\\r\\\\n?/g, '\\\\n');\n // Protect fenced code blocks first — they must pass through untouched.\n // Regex built via RegExp constructor because we're inside a TS template\n // literal and backtick regex literals confuse the bundler.\n const fences = [];\n const fenceRe = new RegExp('\\\\\\\\u0060\\\\\\\\u0060\\\\\\\\u0060(\\\\\\\\w*)\\\\\\\\n([\\\\\\\\s\\\\\\\\S]*?)\\\\\\\\n\\\\\\\\u0060\\\\\\\\u0060\\\\\\\\u0060', 'g');\n const withoutFences = src.replace(fenceRe, (_m, lang, code) => {\n const idx = fences.length;\n fences.push({ lang, code });\n return '\\\\u0000FENCE' + idx + '\\\\u0000';\n });\n\n const lines = withoutFences.split('\\\\n');\n let html = '';\n let i = 0;\n while (i < lines.length) {\n const line = lines[i];\n\n // Fence placeholder — emit <pre><code>.\n const fm = line.match(/^\\\\u0000FENCE(\\\\d+)\\\\u0000\\$/);\n if (fm) {\n const f = fences[+fm[1]];\n const cls = f.lang ? ' class=\"lang-' + esc(f.lang) + '\"' : '';\n html += '<pre><code' + cls + '>' + esc(f.code) + '</code></pre>';\n i++;\n continue;\n }\n\n // Blank line — paragraph break.\n if (!line.trim()) { i++; continue; }\n\n // Heading.\n const hm = line.match(/^(#{1,6})\\\\s+(.*)/);\n if (hm) {\n const level = hm[1].length;\n html += '<h' + level + '>' + inlineMd(hm[2], ctx) + '</h' + level + '>';\n i++;\n continue;\n }\n\n // Horizontal rule.\n if (/^(---+|\\\\*\\\\*\\\\*+|___+)\\$/.test(line.trim())) {\n html += '<hr>';\n i++;\n continue;\n }\n\n // Blockquote (consecutive > lines).\n if (/^>\\\\s?/.test(line)) {\n const buf = [];\n while (i < lines.length && /^>\\\\s?/.test(lines[i])) { buf.push(lines[i].replace(/^>\\\\s?/, '')); i++; }\n html += '<blockquote>' + inlineMd(buf.join(' '), ctx) + '</blockquote>';\n continue;\n }\n\n // Table (GFM) — header row + alignment row + body rows.\n if (line.includes('|') && i + 1 < lines.length && /^\\\\s*\\\\|?\\\\s*(:?-+:?\\\\s*\\\\|?)+\\\\s*\\$/.test(lines[i+1])) {\n const headerCells = splitTableRow(line);\n let t = '<table><thead><tr>' + headerCells.map(c => '<th>' + inlineMd(c, ctx) + '</th>').join('') + '</tr></thead><tbody>';\n i += 2;\n while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {\n const cells = splitTableRow(lines[i]);\n t += '<tr>' + cells.map(c => '<td>' + inlineMd(c, ctx) + '</td>').join('') + '</tr>';\n i++;\n }\n html += t + '</tbody></table>';\n continue;\n }\n\n // Lists (ordered, unordered, task).\n const ulMatch = line.match(/^(\\\\s*)[-*+]\\\\s+(.*)/);\n const olMatch = line.match(/^(\\\\s*)(\\\\d+)\\\\.\\\\s+(.*)/);\n if (ulMatch || olMatch) {\n const ordered = !!olMatch;\n const tag = ordered ? 'ol' : 'ul';\n let items = '';\n while (i < lines.length) {\n const cur = lines[i];\n const um = cur.match(/^(\\\\s*)[-*+]\\\\s+(.*)/);\n const om = cur.match(/^(\\\\s*)(\\\\d+)\\\\.\\\\s+(.*)/);\n if (!um && !om) break;\n const content = (um ? um[2] : om[3]);\n // Task list item: \"- [ ] label\" or \"- [x] label\"\n const tm = content.match(/^\\\\[( |x|X)\\\\]\\\\s+(.*)/);\n if (tm) {\n const checked = tm[1].toLowerCase() === 'x' ? ' checked' : '';\n items += '<li class=\"task\"><input type=\"checkbox\" disabled' + checked + '>' + inlineMd(tm[2], ctx) + '</li>';\n } else {\n items += '<li>' + inlineMd(content, ctx) + '</li>';\n }\n i++;\n }\n html += '<' + tag + '>' + items + '</' + tag + '>';\n continue;\n }\n\n // Paragraph — absorb consecutive non-empty, non-special lines.\n const para = [line];\n i++;\n while (i < lines.length && lines[i].trim() && !/^(\\\\u0000FENCE|#|>|\\\\s*[-*+]\\\\s|\\\\s*\\\\d+\\\\.\\\\s|---+|===+)/.test(lines[i])) {\n para.push(lines[i]);\n i++;\n }\n html += '<p>' + inlineMd(para.join(' '), ctx) + '</p>';\n }\n return html;\n}\n\nfunction splitTableRow(line) {\n // Strip leading/trailing pipe and split by unescaped pipes.\n const s = line.trim().replace(/^\\\\||\\\\|\\$/g, '');\n return s.split(/(?<!\\\\\\\\)\\\\|/).map(c => c.trim());\n}\n\nfunction inlineMd(text, ctx) {\n // Inline code — protect first with placeholders. Regex built via RegExp\n // constructor so a TS template literal doesn't choke on backtick escapes.\n const codes = [];\n const inlineCodeRe = new RegExp('\\\\\\\\u0060([^\\\\\\\\u0060]+)\\\\\\\\u0060', 'g');\n let out = text.replace(inlineCodeRe, (_m, c) => {\n const idx = codes.length;\n codes.push(c);\n return '\\\\u0001CODE' + idx + '\\\\u0001';\n });\n out = esc(out);\n // Images first so their URLs aren't caught by the link rule.\n out = out.replace(/!\\\\[([^\\\\]]*)\\\\]\\\\(([^)\\\\s]+)(?:\\\\s+\\\"([^\\\"]*)\\\")?\\\\)/g, (_m, alt, src, title) => {\n return '<img src=\"' + sanitizeUrl(src) + '\" alt=\"' + esc(alt) + '\"' + (title ? ' title=\"' + esc(title) + '\"' : '') + '>';\n });\n // Links [text](url).\n out = out.replace(/\\\\[([^\\\\]]+)\\\\]\\\\(([^)\\\\s]+)(?:\\\\s+\\\"([^\\\"]*)\\\")?\\\\)/g, (_m, label, href, title) => {\n return '<a href=\"' + sanitizeUrl(href) + '\"' + (title ? ' title=\"' + esc(title) + '\"' : '') + ' target=\"_blank\" rel=\"noopener\">' + label + '</a>';\n });\n // Bold, italic, strikethrough.\n out = out.replace(/\\\\*\\\\*([^*\\\\n]+)\\\\*\\\\*/g, '<strong>\\$1</strong>');\n out = out.replace(/(?<![\\\\w*])\\\\*([^*\\\\n]+)\\\\*(?![\\\\w])/g, '<em>\\$1</em>');\n out = out.replace(/(?<![\\\\w_])_([^_\\\\n]+)_(?![\\\\w])/g, '<em>\\$1</em>');\n out = out.replace(/~~([^~\\\\n]+)~~/g, '<del>\\$1</del>');\n // Autolink raw URLs.\n out = out.replace(/(^|[\\\\s(])((?:https?:\\\\/\\\\/)[^\\\\s<)]+)/g, (_m, pre, url) => pre + '<a href=\"' + sanitizeUrl(url) + '\" target=\"_blank\" rel=\"noopener\">' + esc(url) + '</a>');\n // GitLab refs: #NNN (issue), !NNN (MR), @user. Only when we have context.\n if (ctx && ctx.host && ctx.project) {\n out = out.replace(/(^|[\\\\s(])#(\\\\d+)\\\\b/g, (_m, pre, n) => pre + '<a class=\"ref\" href=\"' + ctx.host + '/' + ctx.project + '/-/issues/' + n + '\" target=\"_blank\" rel=\"noopener\">#' + n + '</a>');\n out = out.replace(/(^|[\\\\s(])!(\\\\d+)\\\\b/g, (_m, pre, n) => pre + '<a class=\"ref\" href=\"' + ctx.host + '/' + ctx.project + '/-/merge_requests/' + n + '\" target=\"_blank\" rel=\"noopener\">!' + n + '</a>');\n out = out.replace(/(^|[\\\\s(])@([A-Za-z0-9_.\\\\-]+)/g, (_m, pre, u) => pre + '<a class=\"ref\" href=\"' + ctx.host + '/' + u + '\" target=\"_blank\" rel=\"noopener\">@' + u + '</a>');\n }\n // Restore inline code.\n out = out.replace(/\\\\u0001CODE(\\\\d+)\\\\u0001/g, (_m, idx) => '<code>' + esc(codes[+idx]) + '</code>');\n return out;\n}\n\nfunction sanitizeUrl(u) {\n const s = String(u || '').trim();\n // Block javascript: and data: (except safe images) URLs.\n if (/^javascript:/i.test(s)) return '#';\n return esc(s);\n}\n\nasync function populateDraftAgents() {\n const sel = document.getElementById('m-draft-agent');\n if (!sel) return;\n if (sel.options.length > 1) {\n // Already populated — just sync selection.\n const saved = localStorage.getItem('agentx.draftAgent');\n if (saved) sel.value = saved;\n return;\n }\n try {\n const r = await fetch('/api/agents');\n if (!r.ok) return;\n const list = await r.json();\n sel.innerHTML = '<option value=\"\">Agent…</option>';\n for (const a of list) {\n const o = document.createElement('option');\n o.value = a.id; o.textContent = a.name + ' (' + a.tier + ')';\n sel.appendChild(o);\n }\n const saved = localStorage.getItem('agentx.draftAgent');\n if (saved && list.some(a => a.id === saved)) sel.value = saved;\n sel.onchange = () => localStorage.setItem('agentx.draftAgent', sel.value);\n } catch {}\n}\n\nasync function draftWithAgent() {\n const rough = document.getElementById('m-rough').value.trim();\n const agentId = document.getElementById('m-draft-agent').value;\n const hint = document.getElementById('m-draft-hint');\n const goBtn = document.getElementById('m-draft-go');\n if (!rough) { hint.textContent = 'Describe the issue first.'; hint.className = 'hint err'; return; }\n if (!agentId) { hint.textContent = 'Pick a drafting agent.'; hint.className = 'hint err'; return; }\n goBtn.disabled = true;\n hint.textContent = 'Drafting with ' + agentId + '…'; hint.className = 'hint';\n\n // Build board context for convention-aware prompting.\n const board = state.current;\n const col = state.modal.targetColumnId ? board.columns.find(c => c.id === state.modal.targetColumnId) : null;\n const columnLabel = col && (col.scopedLabel || col.mapsToLabel);\n const ctx = {\n boardName: board.name,\n primaryLabel: board.primaryToolLabel,\n project: board.source && board.source.projects && board.source.projects[0],\n knownLabels: state.meta.labels || [],\n members: (state.meta.members || []).map(m => m.username),\n columnLabel: columnLabel,\n };\n\n try {\n const r = await fetch('/api/draft', {\n method: 'POST', headers: csrfHeaders(),\n body: JSON.stringify({ rough, agent: agentId, context: ctx }),\n });\n if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || r.statusText);\n const drafted = await r.json();\n // Populate the existing form fields.\n document.getElementById('m-title').value = drafted.title || '';\n document.getElementById('m-desc').value = drafted.description || '';\n state.modal.draft.title = drafted.title || '';\n state.modal.draft.description = drafted.description || '';\n state.modal.draft.labels = Array.isArray(drafted.labels) ? drafted.labels.slice() : state.modal.draft.labels;\n if (drafted.assigneeUsername) {\n const m = state.meta.members.find(x => x.username === drafted.assigneeUsername);\n if (!state.modal.draft.assignees.some(a => a.username === drafted.assigneeUsername)) {\n state.modal.draft.assignees.push({\n username: drafted.assigneeUsername, name: m && m.name, avatarUrl: m && m.avatarUrl,\n });\n }\n }\n paintModal(state.modal.draft);\n hint.textContent = 'Draft ready — review and edit before creating.';\n hint.className = 'hint ok';\n } catch (e) {\n hint.textContent = 'Draft failed: ' + e.message;\n hint.className = 'hint err';\n } finally {\n goBtn.disabled = false;\n }\n}\nfunction closeModal() {\n const m = document.getElementById('modal');\n m.classList.remove('open');\n m.setAttribute('aria-hidden', 'true');\n document.body.style.overflow = '';\n state.modal = { mode: null, id: null, original: null, draft: null };\n}\nfunction cloneForDraft(d) {\n return {\n id: d.id, title: d.title || '', description: d.description || '',\n labels: (d.labels || []).slice(), labelDetails: (d.labelDetails || []).slice(),\n assignees: (d.assignees || []).slice(),\n milestone: d.milestone || '',\n state: d.state || 'opened',\n stage: d.stage, url: d.url, createdAt: d.createdAt, updatedAt: d.updatedAt, author: d.author,\n };\n}\n\nfunction paintModal(d) {\n document.getElementById('m-title').value = d.title || '';\n document.getElementById('m-desc').value = d.description || '';\n paintDescriptionPreview(d);\n\n document.querySelectorAll('#m-state button').forEach(b => {\n b.classList.toggle('active', b.dataset.state === (d.state || 'opened'));\n b.onclick = () => { state.modal.draft.state = b.dataset.state; paintModal(state.modal.draft); };\n });\n\n renderChipPicker('m-labels', d.labels || [], state.meta.labels, {\n chipStyle: (name) => {\n const match = (d.labelDetails || []).find(l => l.name === name);\n const bg = match && match.color ? match.color : labelColor(name);\n return { background: bg, color: pickTextColor(bg) };\n },\n onAdd: (name) => { if (!state.modal.draft.labels.includes(name)) { state.modal.draft.labels.push(name); paintModal(state.modal.draft); } },\n onRemove: (name) => { state.modal.draft.labels = state.modal.draft.labels.filter(l => l !== name); paintModal(state.modal.draft); },\n placeholder: 'Add label…',\n allowCustom: true,\n });\n\n const aNames = (d.assignees || []).map(a => a.username);\n renderChipPicker('m-assignees', aNames, state.meta.members.map(m => m.username), {\n chipLabel: (u) => {\n const m = state.meta.members.find(x => x.username === u) || (d.assignees || []).find(x => x.username === u);\n return (m && m.name) || u;\n },\n onAdd: (u) => {\n if (state.modal.draft.assignees.some(a => a.username === u)) return;\n const m = state.meta.members.find(x => x.username === u);\n state.modal.draft.assignees.push({ username: u, name: m && m.name, avatarUrl: m && m.avatarUrl });\n paintModal(state.modal.draft);\n },\n onRemove: (u) => { state.modal.draft.assignees = state.modal.draft.assignees.filter(a => a.username !== u); paintModal(state.modal.draft); },\n placeholder: 'Assign…',\n });\n\n const ms = document.getElementById('m-milestone');\n ms.innerHTML = '<option value=\"\">None</option>';\n for (const m of state.meta.milestones) {\n const o = document.createElement('option');\n o.value = m.title; o.textContent = m.title + (m.state === 'closed' ? ' (closed)' : '');\n if (m.title === d.milestone) o.selected = true;\n ms.appendChild(o);\n }\n if (d.milestone && !state.meta.milestones.some(m => m.title === d.milestone)) {\n const o = document.createElement('option'); o.value = d.milestone; o.textContent = d.milestone; o.selected = true;\n ms.appendChild(o);\n }\n ms.onchange = () => { state.modal.draft.milestone = ms.value; };\n\n document.getElementById('m-author').textContent = (d.author && (d.author.name || d.author.username)) || '—';\n document.getElementById('m-created').textContent = d.createdAt ? fmtDate(d.createdAt) : '—';\n document.getElementById('m-updated').textContent = d.updatedAt ? fmtDate(d.updatedAt) : '—';\n const link = document.getElementById('m-link');\n if (d.url) { link.href = d.url; link.style.display = ''; } else { link.style.display = 'none'; }\n}\n\nfunction renderChipPicker(elId, selected, options, opts) {\n const el = document.getElementById(elId);\n el.innerHTML = '';\n for (const name of selected) {\n const chip = document.createElement('span');\n chip.className = 'chip';\n chip.textContent = opts.chipLabel ? opts.chipLabel(name) : name;\n if (opts.chipStyle) {\n const st = opts.chipStyle(name);\n if (st.background) chip.style.background = st.background;\n if (st.color) chip.style.color = st.color;\n }\n const x = document.createElement('span');\n x.className = 'remove'; x.textContent = '✕'; x.title = 'Remove';\n x.onclick = (e) => { e.stopPropagation(); opts.onRemove(name); };\n chip.appendChild(x);\n el.appendChild(chip);\n }\n const dd = document.createElement('span');\n dd.className = 'dropdown';\n const btn = document.createElement('button');\n btn.className = 'add-chip'; btn.textContent = '+ ' + (opts.placeholder || 'Add');\n btn.type = 'button';\n btn.onclick = (e) => {\n e.stopPropagation();\n let menu = dd.querySelector('.menu');\n if (menu) { menu.remove(); return; }\n menu = document.createElement('div');\n menu.className = 'menu';\n const search = document.createElement('input');\n search.className = 'search'; search.type = 'search'; search.placeholder = opts.placeholder || 'Search…';\n menu.appendChild(search);\n const list = document.createElement('div');\n menu.appendChild(list);\n const paintList = (q) => {\n list.innerHTML = '';\n const ql = (q || '').toLowerCase();\n const filtered = options.filter(o => o.toLowerCase().includes(ql) && !selected.includes(o));\n if (filtered.length === 0 && !(opts.allowCustom && q)) {\n const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'No matches';\n list.appendChild(empty);\n }\n for (const o of filtered.slice(0, 50)) {\n const opt = document.createElement('div');\n opt.className = 'opt'; opt.textContent = opts.chipLabel ? opts.chipLabel(o) : o;\n opt.onclick = () => { opts.onAdd(o); menu.remove(); };\n list.appendChild(opt);\n }\n if (opts.allowCustom && q && !options.includes(q) && !selected.includes(q)) {\n const opt = document.createElement('div');\n opt.className = 'opt'; opt.textContent = 'Create \"' + q + '\"';\n opt.onclick = () => { opts.onAdd(q); menu.remove(); };\n list.appendChild(opt);\n }\n };\n search.oninput = (ev) => paintList(ev.target.value);\n paintList('');\n dd.appendChild(menu);\n setTimeout(() => search.focus(), 0);\n const away = (ev) => {\n if (!dd.contains(ev.target)) { menu.remove(); document.removeEventListener('click', away); }\n };\n setTimeout(() => document.addEventListener('click', away), 0);\n };\n dd.appendChild(btn);\n el.appendChild(dd);\n}\n\nasync function saveModal() {\n const saveBtn = document.getElementById('m-save');\n const hint = document.getElementById('m-hint');\n saveBtn.disabled = true;\n hint.textContent = 'Saving…';\n const d = state.modal.draft;\n d.title = document.getElementById('m-title').value.trim();\n d.description = document.getElementById('m-desc').value;\n if (!d.title) { hint.textContent = 'Title required'; saveBtn.disabled = false; return; }\n\n try {\n if (state.modal.mode === 'create') {\n const r = await fetch('/api/boards/' + state.current.id + '/items', {\n method: 'POST', headers: csrfHeaders(),\n body: JSON.stringify({\n title: d.title, description: d.description, labels: d.labels,\n assigneeUsernames: (d.assignees || []).map(a => a.username),\n milestoneTitle: d.milestone || undefined,\n }),\n });\n if (!r.ok) throw new Error((await r.json().catch(()=>({}))).error || r.statusText);\n toast('Created ✓', 'ok');\n } else {\n const orig = state.modal.original || {};\n const patch = {};\n if (d.title !== (orig.title || '')) patch.title = d.title;\n if (d.description !== (orig.description || '')) patch.description = d.description;\n const origLabels = (orig.labels || []).slice().sort().join('|');\n const newLabels = (d.labels || []).slice().sort().join('|');\n if (origLabels !== newLabels) patch.labels = d.labels;\n const origA = (orig.assignees || []).map(a => a.username).sort().join('|');\n const newA = (d.assignees || []).map(a => a.username).sort().join('|');\n if (origA !== newA) patch.assigneeUsernames = (d.assignees || []).map(a => a.username);\n if (d.milestone !== (orig.milestone || '')) patch.milestoneTitle = d.milestone || null;\n\n if (Object.keys(patch).length > 0) {\n const r = await fetch('/api/boards/' + state.current.id + '/items/' + encodeURIComponent(state.modal.id), {\n method: 'PATCH', headers: csrfHeaders(), body: JSON.stringify(patch),\n });\n if (!r.ok) throw new Error((await r.json().catch(()=>({}))).error || r.statusText);\n }\n // State change → move to the appropriate column (reuses transition logic on server).\n if (d.state !== (orig.state || 'opened')) {\n const fromCol = state.current.columns.find(c => c.id === orig.stage);\n const toCol = d.state === 'closed'\n ? state.current.columns.find(c => c.kind === 'closed')\n : state.current.columns.find(c => c.kind === 'open-backlog');\n if (toCol) {\n await fetch('/api/boards/' + state.current.id + '/items/' + encodeURIComponent(state.modal.id) + '/move', {\n method: 'PATCH', headers: csrfHeaders(),\n body: JSON.stringify({ from: fromCol && fromCol.id, to: toCol.id }),\n });\n }\n }\n toast('Saved ✓', 'ok');\n }\n closeModal();\n await loadItems();\n state.meta.labels = collectKnownLabels();\n } catch (e) {\n hint.textContent = 'Save failed: ' + e.message;\n } finally {\n saveBtn.disabled = false;\n }\n}\n\ndocument.getElementById('m-close').onclick = closeModal;\ndocument.getElementById('m-cancel').onclick = closeModal;\ndocument.getElementById('m-save').onclick = saveModal;\ndocument.querySelector('#modal .modal-backdrop').onclick = closeModal;\ndocument.getElementById('m-title').addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) saveModal();\n});\nconst draftGo = document.getElementById('m-draft-go');\nif (draftGo) draftGo.onclick = draftWithAgent;\nconst roughEl = document.getElementById('m-rough');\nif (roughEl) roughEl.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) draftWithAgent();\n});\n\n// Description Preview/Write tabs. Textarea edits sync to draft so the\n// preview re-renders the CURRENT text when the user flips to it.\ndocument.querySelectorAll('.desc-tabs .desc-tab').forEach((b) => {\n b.addEventListener('click', () => setDescTab(b.dataset.tab));\n});\nconst descEl = document.getElementById('m-desc');\nif (descEl) descEl.addEventListener('input', () => {\n if (state.modal.draft) state.modal.draft.description = descEl.value;\n});\n\n// --- Agent activity panel (live polling of the daemon API) ---\n\nconst activity = {\n open: false, timer: null, daemonUrl: null, lastEventAt: 0, eventsRing: [],\n};\n\nfunction resolveDaemonUrl() {\n // Allow ?daemon=http://host:port override; fall back to localhost:18800 (MacBook) then 19900 (peer).\n const q = new URLSearchParams(window.location.search).get('daemon');\n if (q) return q.replace(/\\\\/+\\$/, '');\n return localStorage.getItem('agentx.daemon') || 'http://localhost:18800';\n}\n\nfunction toggleActivity(force) {\n activity.open = typeof force === 'boolean' ? force : !activity.open;\n const panel = document.getElementById('activity-panel');\n const btn = document.getElementById('activityBtn');\n panel.classList.toggle('hidden', !activity.open);\n panel.setAttribute('aria-hidden', activity.open ? 'false' : 'true');\n btn.classList.toggle('on', activity.open);\n document.body.classList.toggle('panel-open', activity.open);\n if (activity.open) startActivity(); else stopActivity();\n}\n\nasync function startActivity() {\n activity.daemonUrl = resolveDaemonUrl();\n document.getElementById('activity-source').textContent = activity.daemonUrl.replace(/^https?:\\\\/\\\\//, '');\n await tickActivity();\n activity.timer = setInterval(tickActivity, 3000);\n}\nfunction stopActivity() {\n if (activity.timer) { clearInterval(activity.timer); activity.timer = null; }\n}\n\nasync function tickActivity() {\n const body = document.getElementById('activity-body');\n try {\n // Fetch /agents (always present) + /business/status (optional — 404 tolerated)\n const [agentsRes, bizRes] = await Promise.all([\n fetch(activity.daemonUrl + '/agents').catch(() => null),\n fetch(activity.daemonUrl + '/business/status').catch(() => null),\n ]);\n if (!agentsRes || !agentsRes.ok) throw new Error('daemon unreachable at ' + activity.daemonUrl);\n const agents = await agentsRes.json();\n const biz = bizRes && bizRes.ok ? await bizRes.json() : null;\n\n // Merge business data (currentItem, onClock, lastReport) per-agent when available.\n const bizByAgent = {};\n if (biz && Array.isArray(biz.employees)) for (const e of biz.employees) bizByAgent[e.agentId || e.id] = e;\n\n body.innerHTML = '';\n for (const a of agents) {\n const b = bizByAgent[a.id];\n body.appendChild(renderAgentCard(a, b));\n }\n if (agents.length === 0) {\n body.innerHTML = '<div class=\"empty\">No agents on this daemon.</div>';\n }\n // Events ring\n if (biz && biz.recentEvents) appendEvents(biz.recentEvents);\n } catch (e) {\n body.innerHTML = '<div class=\"empty\" style=\"color:var(--red)\">' + escapeHtml(e.message) + '</div>';\n }\n}\n\nfunction renderAgentCard(a, biz) {\n const card = document.createElement('div');\n card.className = 'agent-card';\n const active = (a.active || 0) > 0 || (biz && biz.onClock && biz.busy);\n const dotClass = (a.errors || 0) > 0 ? 'errored' : (active ? 'active' : '');\n const current = biz && biz.currentItem\n ? '<div class=\"current\">Working on <b>' + escapeHtml(biz.currentItem.title || biz.currentItem.id) + '</b></div>'\n : (active ? '<div class=\"current\"><b>' + a.active + ' active task' + (a.active === 1 ? '' : 's') + '</b></div>' : '');\n const modelLabel = a.model ? '<span class=\"model\" title=\"Model\">' + escapeHtml(shortenModelName(a.model)) + '</span>' : '';\n const lastLabel = a.lastActive\n ? '<span class=\"last\" title=\"' + escapeHtml(new Date(a.lastActive).toLocaleString()) + '\">last ' + escapeHtml(fmtAgoBoard(a.lastActive)) + '</span>'\n : '<span class=\"last muted\">never ran</span>';\n card.innerHTML =\n '<div class=\"top\">' +\n '<span class=\"dot ' + dotClass + '\"></span>' +\n '<span class=\"name\">' + escapeHtml(a.name || a.id) + '</span>' +\n '<span class=\"tier\">' + escapeHtml(a.tier || '') + '</span>' +\n '</div>' +\n '<div class=\"meta\">' + modelLabel + lastLabel + '</div>' +\n '<div class=\"stats\">' +\n '<span>Active <b>' + (a.active || 0) + '</b></span>' +\n '<span>Total <b>' + (a.total || 0) + '</b></span>' +\n '<span class=\"errors\">Errors <b>' + (a.errors || 0) + '</b></span>' +\n '</div>' + current;\n return card;\n}\n\nfunction shortenModelName(m) {\n if (!m) return '';\n return String(m)\n .replace(/^claude-/, '')\n .replace(/-\\\\d{8}$/, '')\n .replace(/\\\\[1m\\\\]$/, ' · 1M');\n}\n\nfunction fmtAgoBoard(iso) {\n const t = new Date(iso).getTime();\n if (!t) return '—';\n const s = Math.floor((Date.now() - t) / 1000);\n if (s < 60) return s + 's ago';\n const m = Math.floor(s / 60); if (m < 60) return m + 'm ago';\n const h = Math.floor(m / 60); if (h < 24) return h + 'h ago';\n const d = Math.floor(h / 24); return d + 'd ago';\n}\n\nfunction appendEvents(events) {\n const holder = document.getElementById('activity-events');\n const existingKeys = new Set([...holder.querySelectorAll('.evt')].map(e => e.dataset.key));\n for (const ev of events.slice(-50)) {\n const key = (ev.ts || '') + '|' + (ev.type || '') + '|' + (ev.agentId || '');\n if (existingKeys.has(key)) continue;\n const div = document.createElement('div');\n div.className = 'evt' + (ev.type && ev.type.includes('error') ? ' err' : '');\n div.dataset.key = key;\n div.innerHTML = '<span class=\"ts\">' + (ev.ts ? new Date(ev.ts).toLocaleTimeString() : '') + '</span> ' +\n '<span class=\"who\">' + escapeHtml(ev.agentId || '—') + '</span> ' +\n '<span class=\"what\">' + escapeHtml(ev.summary || ev.type || '') + '</span>';\n holder.appendChild(div);\n }\n // Keep last 50 entries\n while (holder.children.length > 50) holder.removeChild(holder.firstChild);\n holder.scrollTop = holder.scrollHeight;\n}\n\nloadBoards().catch(e => toast('Init failed: ' + e.message));\n`\n","// --- Glossary page ---\n//\n// Plain-English definitions of the terms that appear on every dashboard\n// surface. Static; sourced from ui-labels.GLOSSARY.\n//\n// Small surface — could have lived inline in board-dashboard.ts, but\n// having it as its own file keeps the \"one route, one page module\"\n// convention and lets the `esc` helper come from ui/util instead of a\n// local duplicate.\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\nimport { UI_LABELS, GLOSSARY } from \"../../ui-labels\"\n\nexport interface GlossaryPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderGlossaryPage(opts: GlossaryPageOpts = {}): string {\n const items = GLOSSARY.map((g) => {\n const alias = g.alias\n ? `<span class=\"alias\" title=\"Schema key\">${esc(g.alias)}</span>`\n : \"\"\n return `<article class=\"term\"><h3>${esc(g.term)}${alias}</h3><p>${esc(g.definition)}</p></article>`\n }).join(\"\")\n\n const body = `<h1>Plain-English glossary</h1>\n <p class=\"lead\">What the terms on the dashboard mean. Schema keys (shown in the pill on the right of each term) are what you'd write in <code>agentx.json</code> — the dashboard just relabels them for readability.</p>\n ${items}`\n\n return renderShell({\n title: `${UI_LABELS.brand} · Glossary`,\n activeTab: \"glossary\",\n subtitle: \"Glossary\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: GLOSSARY_PAGE_CSS,\n })\n}\n\nconst GLOSSARY_PAGE_CSS = `\nmain { max-width: 760px; margin: 0 auto; padding: 32px 24px 80px; }\nh1 { font-size: 22px; margin: 0 0 6px; font-weight: 600; letter-spacing: -0.01em; }\n.lead {\n color: var(--ax-muted); margin: 0 0 28px;\n font-size: 13px; line-height: 1.6;\n}\n.lead code {\n font-family: var(--ax-mono); font-size: 12px;\n background: var(--ax-surface); padding: 1px 6px; border-radius: 3px;\n border: 1px solid var(--ax-border);\n}\narticle.term {\n background: var(--ax-surface); border: 1px solid var(--ax-border);\n border-radius: 6px; padding: 16px 20px; margin: 10px 0;\n}\narticle.term h3 {\n font-size: 14px; margin: 0 0 6px;\n display: flex; align-items: center; gap: 10px;\n font-weight: 600; letter-spacing: -0.005em;\n}\narticle.term .alias {\n font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--ax-muted); background: var(--ax-bg);\n padding: 2px 7px; border-radius: 3px;\n font-weight: 500; font-family: var(--ax-mono);\n border: 1px solid var(--ax-border);\n}\narticle.term p {\n margin: 0; color: var(--ax-text-2); font-size: 13px; line-height: 1.6;\n}`\n","// --- Workflows page (Phase 2: observability) ---\n//\n// Read-only view over the workflow definitions + runs under .agentx/workflows/.\n// Scope is deliberately tight: list workflows, show per-workflow status,\n// inspect recent runs. No visual editor, no manual-run button — those land\n// in later phases once the observability surface has proven useful.\n//\n// Renders a server skeleton; the client JS fetches /api/workflows and\n// /api/workflows/runs, paints cards, and streams the selected run's history\n// via SSE at /api/workflows/runs/<runId>/stream.\n//\n// Tab highlight note: the TopbarTab enum does not yet contain \"workflows\".\n// Using \"admin\" as placeholder — the integration-seam commit adds\n// \"workflows\" to src/daemon/topbar.ts:TopbarTab and this switches over.\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\nimport { injectFns } from \"../inject\"\nimport { workflowHealth } from \"../../workflow-health\"\n\nexport interface WorkflowsPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderWorkflowsPage(opts: WorkflowsPageOpts = {}): string {\n const body = `<div class=\"ax-wf__root\">\n <aside class=\"ax-wf__list\">\n <header>\n <h2>Workflows</h2>\n <div class=\"ax-wf__header-actions\">\n <a id=\"wf-new\" class=\"ax-wf__icon\" href=\"/workflows/editor?new=1\" title=\"Build a new workflow\">+</a>\n <button id=\"wf-refresh\" class=\"ax-wf__icon\" title=\"Refresh (r)\">↻</button>\n </div>\n </header>\n <div class=\"ax-wf__search\">\n <input id=\"wf-filter\" type=\"search\" placeholder=\"Filter by id or title…\" autocomplete=\"off\" />\n <select id=\"wf-project-filter\" class=\"ax-wf__project-filter\" title=\"Filter by project\">\n <option value=\"\">All projects</option>\n </select>\n </div>\n <ul id=\"wf-list\" class=\"ax-wf__cards\" aria-live=\"polite\"></ul>\n <div id=\"wf-empty\" class=\"ax-wf__empty\" hidden>\n <p><b>No workflows yet.</b></p>\n <p class=\"hint\">A workflow watches for something &mdash; a merge request, a message, a time of day &mdash; and puts an agent on it. Ask an agent to draft one for you, or connect n8n below and let it hand work over.</p>\n </div>\n <section class=\"ax-wf__n8n\">\n <header><h3>Hand work over from n8n</h3></header>\n <p class=\"hint\">n8n already talks to hundreds of services. Let it keep doing that, and have it call a workflow here when it needs an agent.</p>\n\n <ol class=\"ax-wf__n8n-steps\">\n <li>\n <b>Name the job.</b> Pick a short word for what n8n is handing over &mdash;\n <code>invoice-received</code>, <code>lead-signed-up</code>. That word is the\n <i>topic</i>, and it is the last part of the address below.\n </li>\n <li>\n <b>In n8n</b>, add an <b>HTTP Request</b> node: method <code>POST</code>, this address\n with your topic on the end, and send whatever JSON the workflow should read.\n <div class=\"ax-wf__n8n-url\" id=\"wf-n8n-in\">&hellip;</div>\n <button class=\"ax-wf__n8n-copy\" id=\"wf-n8n-copy\" type=\"button\">Copy address</button>\n <span class=\"hint\" id=\"wf-n8n-auth\"></span>\n </li>\n <li>\n <b>Here</b>, build a workflow that starts with <b>n8n hands work over</b> and set its\n topic to the same word. Without that, every workflow listening for n8n runs on\n every call.\n </li>\n </ol>\n\n <p class=\"hint\">Going the other way is one step: add <b>Call a web address</b> and point it at an n8n webhook &mdash; or connect n8n in Settings and pick one of your n8n workflows straight from the palette.</p>\n </section>\n\n </aside>\n\n <section class=\"ax-wf__detail\">\n <header id=\"wf-detail-head\">\n <span class=\"hint\">Select a workflow to inspect.</span>\n </header>\n <div id=\"wf-detail-body\" class=\"ax-wf__detail-body\"></div>\n </section>\n\n <aside id=\"wf-run-panel\" class=\"ax-wf__run-panel hidden\" aria-hidden=\"true\">\n <header>\n <div>\n <h3 id=\"wf-run-title\">Run</h3>\n <span id=\"wf-run-meta\" class=\"hint\"></span>\n </div>\n <button id=\"wf-run-close\" class=\"ax-wf__icon\" title=\"Close (esc)\">×</button>\n </header>\n <div id=\"wf-run-timeline\" class=\"ax-wf__timeline\"></div>\n <footer>\n <span id=\"wf-run-status\" class=\"ax-wf__status-pill\">—</span>\n <span id=\"wf-run-live\" class=\"hint\">—</span>\n <span id=\"wf-run-actions\" class=\"ax-wf__run-actions\">\n <button data-run-action=\"pause\" type=\"button\" title=\"Pause this run\">Pause</button>\n <button data-run-action=\"resume\" type=\"button\" title=\"Resume a paused run\">Resume</button>\n <button data-run-action=\"cancel\" type=\"button\" title=\"Cancel this run\" class=\"danger\">Cancel</button>\n </span>\n </footer>\n </aside>\n</div>\n\n<div id=\"wf-toast\" class=\"ax-wf__toast\" hidden></div>`\n\n return renderShell({\n title: \"AgentX · Workflows\",\n activeTab: \"workflows\",\n subtitle: \"Workflows\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: WORKFLOWS_PAGE_CSS,\n scripts: `<script>${injectFns({ workflowHealth })}${WORKFLOWS_PAGE_SCRIPT}</script>`,\n })\n}\n\n// ───────────────────────── CSS ─────────────────────────\n\nconst WORKFLOWS_PAGE_CSS = `\n.ax-wf__root {\n display: grid;\n grid-template-columns: 340px 1fr;\n grid-template-rows: 100%;\n height: calc(100vh - var(--ax-topbar-h, 48px));\n background: var(--ax-bg);\n}\n.ax-wf__list {\n border-right: 1px solid var(--ax-border);\n display: flex; flex-direction: column;\n background: var(--ax-bg-elev);\n}\n.ax-wf__list > header {\n display: flex; align-items: center; justify-content: space-between;\n padding: 14px 18px; border-bottom: 1px solid var(--ax-border);\n}\n.ax-wf__header-actions {\n display: flex; gap: 2px; align-items: center;\n}\n.ax-wf__header-actions a.ax-wf__icon {\n text-decoration: none;\n}\n.ax-wf__detail-title { flex: 1; }\n.ax-wf__detail-actions { display: flex; gap: 6px; }\n.ax-wf__btn {\n font-family: inherit; font-size: 12px;\n background: var(--ax-bg); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 4px;\n padding: 5px 10px; cursor: pointer;\n text-decoration: none; display: inline-flex; gap: 4px; align-items: center;\n transition: background 120ms ease, border-color 120ms ease;\n}\n.ax-wf__btn:hover { background: var(--ax-surface); border-color: var(--ax-border-2); }\n.ax-wf__list > header h2 {\n font-size: 14px; font-weight: 600; margin: 0; letter-spacing: -0.005em;\n}\n.ax-wf__search { padding: 10px 14px; border-bottom: 1px solid var(--ax-border); }\n.ax-wf__search input {\n width: 100%; padding: 7px 10px; font-size: 12px;\n background: var(--ax-bg); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 4px;\n font-family: inherit;\n}\n.ax-wf__state {\n font-size: 11.5px; font-weight: 600; margin-top: 4px;\n display: inline-flex; align-items: center; gap: 5px;\n}\n.ax-wf__state::before { content: \"\"; width: 7px; height: 7px; border-radius: 999px; background: currentColor; }\n.ax-wf__state--dormant { color: var(--ax-red-ink); }\n.ax-wf__state--failing { color: var(--ax-amber-ink); }\n.ax-wf__state--active { color: var(--ax-green-d); }\n.ax-wf__state--quiet,\n.ax-wf__state--never { color: var(--ax-text-2); }\n\n.ax-wf__n8n {\n border-top: var(--ax-border-w) solid var(--ax-border);\n padding: 14px 16px; display: flex; flex-direction: column; gap: 8px;\n}\n.ax-wf__n8n h3 { font-size: 13px; font-weight: 600; margin: 0; }\n.ax-wf__n8n-lbl {\n font-size: 10px; font-family: var(--ax-mono); text-transform: uppercase;\n letter-spacing: 0.06em; color: var(--ax-text-2); font-weight: 600;\n}\n.ax-wf__n8n-steps {\n margin: 4px 0 0; padding-left: 18px; display: flex; flex-direction: column; gap: 10px;\n font-size: 12px; line-height: 1.5; color: var(--ax-text-2);\n}\n.ax-wf__n8n-steps b { color: var(--ax-text); font-weight: 600; }\n.ax-wf__n8n-steps i { font-style: normal; color: var(--ax-text); font-weight: 600; }\n.ax-wf__n8n-steps code {\n font-family: var(--ax-mono); font-size: 11px; background: var(--ax-surface-2);\n border: 1px solid var(--ax-border); border-radius: 4px; padding: 0 4px;\n}\n.ax-wf__n8n-steps > li { padding-left: 2px; }\n.ax-wf__n8n-steps .ax-wf__n8n-url { margin: 7px 0 6px; }\n\n.ax-wf__n8n-url {\n font-family: var(--ax-mono); font-size: 11px; word-break: break-all;\n background: var(--ax-surface-2); border: 1px solid var(--ax-border);\n border-radius: var(--ax-radius-sm); padding: 8px 10px; color: var(--ax-text);\n}\n.ax-wf__n8n-copy {\n align-self: flex-start; font: inherit; font-size: 12px; font-weight: 600;\n padding: 5px 12px; border-radius: var(--ax-radius-sm);\n border: var(--ax-border-w) solid var(--ax-border-2);\n background: var(--ax-surface); color: var(--ax-text); cursor: pointer;\n}\n.ax-wf__n8n-copy:hover { background: var(--ax-surface-2); }\n\n.ax-wf__cards {\n list-style: none; margin: 0; padding: 8px 0;\n overflow-y: auto; flex: 1 1 auto; min-height: 140px;\n}\n.ax-wf__card {\n padding: 10px 18px; border-bottom: 1px solid var(--ax-border);\n cursor: pointer; transition: background 120ms ease;\n}\n.ax-wf__card:hover { background: var(--ax-surface); }\n.ax-wf__card.is-active { background: var(--ax-surface); box-shadow: inset 3px 0 0 var(--ax-accent); }\n.ax-wf__card-title {\n font-size: 13px; font-weight: 600; letter-spacing: -0.005em;\n color: var(--ax-text); margin-bottom: 3px;\n}\n.ax-wf__card-id {\n font-family: var(--ax-mono); font-size: 10px; color: var(--ax-muted);\n}\n.ax-wf__card-meta {\n display: flex; gap: 8px; flex-wrap: wrap;\n margin-top: 6px; font-size: 11px; color: var(--ax-text-2);\n}\n.ax-wf__card-meta .tag {\n padding: 1px 7px; border-radius: 3px; background: var(--ax-bg);\n border: 1px solid var(--ax-border); font-size: 10px;\n font-family: var(--ax-mono);\n}\n.ax-wf__card-meta .live {\n color: var(--ax-accent);\n border-color: color-mix(in oklch, var(--ax-accent) 50%, transparent);\n}\n.ax-wf__btn.danger:hover { background: color-mix(in oklch, var(--ax-err) 13%, transparent); border-color: var(--ax-err); color: var(--ax-err); }\n.ax-wf__btn.primary { border-color: color-mix(in oklch, var(--ax-accent) 55%, var(--ax-border)); color: var(--ax-accent); }\n\n.ax-wf__detail {\n display: flex; flex-direction: column;\n overflow: hidden;\n}\n.ax-wf__detail > header {\n padding: 16px 22px; border-bottom: 1px solid var(--ax-border);\n display: flex; align-items: center; gap: 12px;\n background: var(--ax-bg-elev);\n}\n.ax-wf__detail > header h2 {\n font-size: 15px; font-weight: 600; margin: 0; letter-spacing: -0.01em;\n}\n.ax-wf__detail-body {\n padding: 22px; overflow: auto;\n display: grid; gap: 18px;\n grid-template-columns: 1fr 1fr;\n align-content: start;\n}\n.ax-wf__panel {\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 6px; padding: 14px 16px;\n}\n.ax-wf__panel h3 {\n font-size: 11px; font-weight: 600; letter-spacing: 0.04em;\n text-transform: uppercase; color: var(--ax-muted);\n margin: 0 0 10px;\n}\n.ax-wf__panel--full { grid-column: 1 / -1; }\n.ax-wf__states {\n display: flex; flex-wrap: wrap; gap: 6px;\n font-size: 11px;\n}\n.ax-wf__state {\n font-family: var(--ax-mono); padding: 2px 8px;\n border-radius: 3px; background: var(--ax-surface);\n border: 1px solid var(--ax-border);\n}\n.ax-wf__state.is-terminal { color: var(--ax-muted); opacity: 0.75; }\n.ax-wf__trans {\n font-family: var(--ax-mono); font-size: 11px;\n color: var(--ax-text-2); line-height: 1.8;\n}\n.ax-wf__trans .arr { color: var(--ax-muted); }\n.ax-wf__trans .cond { color: var(--ax-accent); font-size: 10px; }\n.ax-wf__runs {\n display: flex; flex-direction: column; gap: 4px;\n}\n.ax-wf__run-row {\n display: grid;\n grid-template-columns: 76px 1fr 110px 110px 24px;\n gap: 10px; align-items: center;\n padding: 7px 8px; border-radius: 4px;\n font-size: 12px; cursor: pointer;\n}\n.ax-wf__run-row:hover { background: var(--ax-surface); }\n.ax-wf__run-row .id { font-family: var(--ax-mono); color: var(--ax-muted); font-size: 11px; }\n.ax-wf__run-row .status { font-size: 10px; text-align: center; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; font-family: var(--ax-mono); }\n.ax-wf__run-row .status.running { background: color-mix(in oklch, var(--ax-accent) 15%, transparent); color: var(--ax-accent); }\n.ax-wf__run-row .status.completed { color: var(--ax-muted); }\n.ax-wf__run-row .status.failed { color: var(--ax-err); }\n.ax-wf__run-row .status.paused { color: var(--ax-warn); }\n.ax-wf__run-row .status.canceled { color: var(--ax-muted); opacity: 0.7; }\n.ax-wf__json {\n font-family: var(--ax-mono); font-size: 11px;\n background: var(--ax-bg); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 4px;\n padding: 10px 12px; max-height: 280px; overflow: auto;\n white-space: pre-wrap; word-break: break-word;\n}\n/* Editable draft textarea — same look as the JSON pre, just typeable.\n * tab-size keeps indentation legible and resize: vertical lets reviewers\n * grow the panel without leaving the page. */\n.ax-wf__editor {\n display: block; width: 100%;\n font-family: var(--ax-mono); font-size: 12px;\n line-height: 1.45; tab-size: 2;\n background: var(--ax-bg); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 4px;\n padding: 10px 12px;\n min-height: 320px; max-height: 70vh;\n resize: vertical; overflow: auto;\n white-space: pre; word-break: normal;\n outline: none;\n}\n.ax-wf__editor:focus {\n border-color: var(--ax-accent, #6366f1);\n}\n.ax-wf__editor--small {\n min-height: 110px; max-height: 220px;\n}\n/* Run form lives in the detail header, below the row of buttons */\n.ax-wf__run-form {\n grid-column: 1 / -1;\n margin-top: 10px; padding: 12px;\n background: var(--ax-panel, var(--ax-bg)); border: 1px solid var(--ax-border);\n border-radius: 6px;\n display: flex; flex-direction: column; gap: 8px;\n}\n.ax-wf__run-form label { font-size: 11px; color: var(--ax-muted); }\n.ax-wf__run-form code { font-family: var(--ax-mono); font-size: 11px; }\n.ax-wf__inline { display: flex; align-items: flex-start; gap: 6px; }\n.ax-wf__inline input[type=\"checkbox\"] { margin-top: 3px; }\n.ax-wf__run-form-actions { display: flex; justify-content: flex-end; gap: 8px; }\n.ax-wf__run-fields-grid {\n display: grid; grid-template-columns: 1fr; gap: 10px;\n}\n.ax-wf__run-fld {\n display: flex; flex-direction: column; gap: 4px;\n font-size: 12px;\n}\n.ax-wf__run-label {\n display: flex; align-items: center; gap: 6px;\n font-weight: 500; color: var(--ax-text);\n}\n.ax-wf__run-label code {\n background: var(--ax-panel, var(--ax-bg));\n padding: 1px 5px; border-radius: 3px;\n}\n.ax-wf__run-req { color: var(--ax-err, #ef4444); font-weight: bold; }\n.ax-wf__run-fld input[type=\"text\"],\n.ax-wf__run-fld input[type=\"number\"],\n.ax-wf__run-fld select,\n.ax-wf__run-fld textarea {\n font: inherit; font-size: 12px;\n padding: 5px 8px; border: 1px solid var(--ax-border);\n border-radius: 4px; background: var(--ax-bg); color: var(--ax-text);\n}\n.ax-wf__run-fld textarea {\n font-family: var(--ax-mono); resize: vertical;\n}\n.ax-wf__run-hint { font-size: 11px; color: var(--ax-muted); }\n.ax-wf__run-raw { margin-top: 4px; }\n.ax-wf__run-raw summary {\n font-size: 11px; color: var(--ax-muted);\n cursor: pointer; padding: 4px 0;\n}\n.ax-wf__run-raw[open] summary { color: var(--ax-text); }\n/* Tiny re-run button on the run rows */\n.ax-wf__btn--xs {\n padding: 2px 6px; font-size: 12px; line-height: 1;\n background: transparent; border: 1px solid var(--ax-border);\n border-radius: 3px; cursor: pointer; color: var(--ax-text);\n}\n.ax-wf__btn--xs:hover { background: var(--ax-panel, var(--ax-bg)); border-color: var(--ax-accent, #6366f1); }\n/* Last-run status colors on the workflow card */\n.ax-wf__card-meta .tag.tag--completed { color: var(--ax-ok, #4ade80); }\n.ax-wf__card-meta .tag.tag--failed { color: var(--ax-err, #ef4444); }\n.ax-wf__card-meta .tag.tag--running { color: var(--ax-accent, #6366f1); }\n.ax-wf__card-meta .tag.tag--paused { color: var(--ax-warn, #facc15); }\n.ax-wf__card-meta .tag.tag--canceled { color: var(--ax-muted); opacity: 0.7; }\n.ax-wf__card-meta .tag.tag--project {\n color: var(--ax-accent, #6366f1);\n background: color-mix(in srgb, var(--ax-accent, #6366f1) 12%, transparent);\n border: 1px solid color-mix(in srgb, var(--ax-accent, #6366f1) 30%, transparent);\n font-weight: 500;\n}\n.ax-wf__project-filter {\n font: inherit;\n font-size: 12px;\n padding: 4px 6px;\n border: 1px solid var(--ax-border);\n border-radius: 4px;\n background: var(--ax-bg);\n color: var(--ax-text);\n margin-top: 6px;\n width: 100%;\n}\n/* Runbook framing — tables in WHEN / IN / OUT panels */\n.ax-wf__io { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 4px; }\n.ax-wf__io th { text-align: left; font-weight: 500; color: var(--ax-muted); font-size: 10px;\n letter-spacing: 0.04em; text-transform: uppercase; padding: 4px 8px;\n border-bottom: 1px solid var(--ax-border); }\n.ax-wf__io td { padding: 6px 8px; border-bottom: 1px solid var(--ax-border); vertical-align: top; }\n.ax-wf__io td.ax-wf__io-name { font-family: var(--ax-mono); color: var(--ax-text); font-weight: 500; white-space: nowrap; }\n.ax-wf__io td.ax-wf__io-type { font-family: var(--ax-mono); font-size: 11px; color: var(--ax-text-2); }\n.ax-wf__pill { display: inline-block; padding: 1px 7px; font-size: 10px; font-family: var(--ax-mono);\n border: 1px solid var(--ax-border); border-radius: 3px; color: var(--ax-text-2); background: var(--ax-surface); }\n.ax-wf__pill--req { color: var(--ax-warn, #f59e0b); border-color: color-mix(in oklch, var(--ax-warn, #f59e0b) 40%, var(--ax-border)); }\n.ax-wf__hkbd { font-size: 10px; font-family: var(--ax-mono); color: var(--ax-muted);\n font-weight: 400; letter-spacing: 0.04em; padding-left: 8px; }\ndetails.ax-wf__panel { padding: 0; }\ndetails.ax-wf__panel summary { padding: 12px 14px; cursor: pointer; user-select: none; font-weight: 500; }\ndetails.ax-wf__panel[open] summary { border-bottom: 1px solid var(--ax-border); }\ndetails.ax-wf__panel pre { margin: 0; padding: 10px 14px; }\n\n/* run detail drawer */\n.ax-wf__run-panel {\n position: fixed; top: var(--ax-topbar-h, 48px); right: 0; bottom: 0;\n width: 420px; background: var(--ax-bg-elev); border-left: 1px solid var(--ax-border);\n transform: translateX(100%); transition: transform 180ms ease;\n display: flex; flex-direction: column; z-index: 40;\n}\n.ax-wf__run-panel:not(.hidden) { transform: translateX(0); }\n.ax-wf__run-panel > header {\n display: flex; justify-content: space-between; align-items: center;\n padding: 14px 18px; border-bottom: 1px solid var(--ax-border);\n}\n.ax-wf__run-panel h3 {\n font-size: 13px; font-weight: 600; margin: 0 0 2px;\n}\n.ax-wf__timeline {\n flex: 1; overflow-y: auto; padding: 12px 18px;\n display: flex; flex-direction: column; gap: 8px;\n}\n.ax-wf__timeline-entry {\n border-left: 2px solid var(--ax-border);\n padding: 6px 0 10px 12px; position: relative;\n}\n.ax-wf__timeline-entry::before {\n content: \"\"; position: absolute; left: -5px; top: 10px;\n width: 8px; height: 8px; background: var(--ax-accent);\n border-radius: 50%;\n}\n.ax-wf__timeline-entry.result-failed::before { background: var(--ax-err); }\n.ax-wf__timeline-entry .time {\n font-family: var(--ax-mono); font-size: 10px; color: var(--ax-muted);\n}\n.ax-wf__timeline-entry .move {\n font-size: 12px; margin: 2px 0;\n}\n.ax-wf__timeline-entry .agent {\n font-family: var(--ax-mono); font-size: 10px; color: var(--ax-text-2);\n}\n.ax-wf__run-panel > footer {\n padding: 12px 18px; border-top: 1px solid var(--ax-border);\n display: flex; justify-content: space-between; align-items: center;\n}\n.ax-wf__status-pill {\n font-family: var(--ax-mono); font-size: 10px; text-transform: uppercase;\n padding: 2px 8px; border-radius: 3px;\n}\n.ax-wf__status-pill.running { background: color-mix(in oklch, var(--ax-accent) 15%, transparent); color: var(--ax-accent); }\n.ax-wf__status-pill.completed { color: var(--ax-muted); background: var(--ax-surface); }\n.ax-wf__status-pill.failed { color: var(--ax-err); background: color-mix(in oklch, var(--ax-err) 15%, transparent); }\n.ax-wf__run-actions { display: flex; gap: 4px; margin-left: auto; }\n.ax-wf__run-actions button { font-size: 11px; padding: 3px 9px; border-radius: 4px; border: 1px solid var(--ax-border); background: var(--ax-bg); color: var(--ax-text); cursor: pointer; }\n.ax-wf__run-actions button:hover { background: var(--ax-surface); }\n.ax-wf__run-actions button.danger:hover { background: color-mix(in oklch, var(--ax-err) 15%, transparent); border-color: var(--ax-err); color: var(--ax-err); }\n\n.ax-wf__icon {\n background: none; border: none; color: var(--ax-muted);\n cursor: pointer; font-size: 18px; padding: 4px 8px;\n}\n.ax-wf__icon:hover { color: var(--ax-text); }\n.ax-wf__empty {\n padding: 40px 22px; color: var(--ax-muted); font-size: 13px;\n text-align: center;\n}\n.ax-wf__empty .hint { font-size: 11px; margin-top: 6px; }\n.ax-wf__empty code {\n font-family: var(--ax-mono); background: var(--ax-surface);\n padding: 1px 6px; border-radius: 3px;\n border: 1px solid var(--ax-border);\n}\n.ax-wf__toast {\n position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);\n padding: 8px 16px; background: var(--ax-bg-elev);\n border: 1px solid var(--ax-border); border-radius: 4px;\n font-size: 12px; z-index: 50;\n}\n.hint { color: var(--ax-muted); font-size: 11px; }\n`\n\n// ───────────────────────── CLIENT JS ─────────────────────────\n//\n// Kept inline in the module per the existing page convention. All endpoints\n// assumed by this script are wired by daemon/workflows-api.ts.\n\nexport const WORKFLOWS_PAGE_SCRIPT = `\n(() => {\n const $ = (sel) => document.querySelector(sel)\n const $$ = (sel) => Array.from(document.querySelectorAll(sel))\n\n const state = {\n workflows: [],\n runs: [],\n selectedId: null,\n filter: \"\",\n /** Project filter — \"\" = all (incl. workflows without project),\n * \"__none__\" = only workflows missing the project field,\n * \"<org/repo>\" = only that project's workflows.\n * Populated dynamically from the loaded workflows. */\n projectFilter: \"\",\n sse: null,\n }\n\n const token = (typeof localStorage !== \"undefined\" && localStorage.getItem(\"ax_token\")) || \"\"\n const headers = () => token ? { \"Authorization\": \"Bearer \" + token } : {}\n\n async function fetchJSON(url) {\n const res = await fetch(url, { headers: headers() })\n if (!res.ok) throw new Error(url + \": \" + res.status)\n return res.json()\n }\n\n async function postJSON(url, body) {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-Requested-With\": \"agentx-board\", ...headers() },\n credentials: \"same-origin\",\n body: JSON.stringify(body || {}),\n })\n const data = await res.json().catch(() => ({}))\n if (!res.ok) throw new Error(data.error || (url + \": \" + res.status))\n return data\n }\n\n // --- Rendering -------------------------------------------------------\n\n function renderProjectFilter() {\n const sel = $(\"#wf-project-filter\")\n if (!sel) return\n // Collect distinct projects from currently loaded workflows. Sorted\n // alphabetically; \"(no project)\" entry only shown when at least one\n // workflow lacks the field, so the dropdown stays clean for fully-\n // tagged setups.\n const projects = new Set()\n let hasGlobal = false\n for (const wf of state.workflows) {\n if (wf.project) projects.add(wf.project)\n else hasGlobal = true\n }\n const sorted = Array.from(projects).sort()\n const current = state.projectFilter || \"\"\n sel.innerHTML = '<option value=\"\">All projects</option>'\n + sorted.map(p => '<option value=\"' + esc(p) + '\"' + (p === current ? ' selected' : '') + '>' + esc(p) + '</option>').join(\"\")\n + (hasGlobal ? '<option value=\"__none__\"' + (current === \"__none__\" ? ' selected' : '') + '>(no project)</option>' : \"\")\n }\n\n const FRIENDLY_TRIGGER = { gitlab: \"on GitLab activity\", github: \"on GitHub activity\", cron: \"on a schedule\",\n manual: \"when you run it\", hook: \"when something calls in\", channel: \"on a message\", form: \"from a form\", n8n: \"when n8n calls\" }\n // Plain words for what the operator needs to know, in the order they care:\n // has it stopped, is it breaking, is anyone blocked, is it fine.\n const STATE_LABEL = { dormant: \"stopped running\", failing: \"failing\", active: \"running normally\",\n quiet: \"idle\", never: \"never run yet\" }\n const STATE_RANK = { dormant: 0, failing: 1, active: 2, quiet: 3, never: 4 }\n function healthOf(id, myRuns) {\n const rows = (myRuns || []).map(r => ({ workflowId: id, status: r.status, at: new Date(r.updatedAt).getTime() }))\n const h = workflowHealth([{ id: id }], rows)[0]\n return { state: h.state, label: STATE_LABEL[h.state] || h.state, rank: STATE_RANK[h.state] }\n }\n\n // The address n8n posts to. Shown, and copyable with a real topic already\n // in it — a placeholder like <topic> is not something you can paste.\n (function n8nPanel() {\n const el = document.getElementById(\"wf-n8n-in\")\n if (!el) return\n const base = location.origin + \"/webhook/n8n/\"\n el.textContent = base + \"your-topic\"\n // Loopback is exempt from mesh auth; anything else needs the token, and\n // saying so here saves a silent 401 inside n8n.\n const local = /^(localhost|127\\.0\\.0\\.1|\\[::1\\])$/.test(location.hostname)\n const auth = document.getElementById(\"wf-n8n-auth\")\n if (auth) auth.textContent = local\n ? \"n8n on this machine needs no key.\"\n : \"n8n is off-box, so add the header Authorization: Bearer <your MESH_TOKEN>.\"\n const btn = document.getElementById(\"wf-n8n-copy\")\n if (btn) btn.onclick = async () => {\n try { await navigator.clipboard.writeText(el.textContent); btn.textContent = \"Copied\" }\n catch { btn.textContent = \"Copy failed\" }\n setTimeout(() => { btn.textContent = \"Copy address\" }, 1600)\n }\n })()\n\n function renderList() {\n renderProjectFilter()\n const needle = state.filter.toLowerCase()\n const projectFilter = state.projectFilter || \"\"\n let filtered = state.workflows.filter(wf => {\n const matchesText = !needle || wf.id.toLowerCase().includes(needle) || (wf.title || \"\").toLowerCase().includes(needle)\n // projectFilter values:\n // \"\" — no filter (all projects + global)\n // \"__none__\" — only workflows without a project field\n // \"<org/repo>\" — only workflows whose project field equals this\n let matchesProject = true\n if (projectFilter === \"__none__\") matchesProject = !wf.project\n else if (projectFilter) matchesProject = wf.project === projectFilter\n return matchesText && matchesProject\n })\n\n const runsByWorkflow = state.runs.reduce((acc, r) => {\n (acc[r.workflowId] ||= []).push(r); return acc\n }, {})\n\n if (filtered.length === 0) {\n $(\"#wf-list\").innerHTML = \"\"\n $(\"#wf-empty\").hidden = state.workflows.length > 0\n return\n }\n $(\"#wf-empty\").hidden = true\n\n // Anything that stopped or is failing comes first: a list sorted by name\n // hides the one workflow you needed to know about.\n filtered = filtered.slice().sort((a, b) =>\n healthOf(a.id, runsByWorkflow[a.id]).rank - healthOf(b.id, runsByWorkflow[b.id]).rank\n || String(a.title || a.id).localeCompare(String(b.title || b.id)))\n\n $(\"#wf-list\").innerHTML = filtered.map(wf => {\n const myRuns = runsByWorkflow[wf.id] || []\n const live = myRuns.filter(r => r.status === \"running\").length\n const lastRun = myRuns[0]\n const isActive = state.selectedId === wf.id\n // V2: trigger is a node inside wf.nodes, not a workflow-level field.\n const triggerNode = (wf.nodes || []).find(n => n && n.type && n.type.indexOf(\"trigger.\") === 0)\n const triggerSource = triggerNode ? (triggerNode.config && triggerNode.config.source) || triggerNode.type.replace(\"trigger.\", \"\") : \"?\"\n const health = healthOf(wf.id, myRuns)\n return \\`\n <li class=\"ax-wf__card \\${isActive ? \"is-active\" : \"\"}\" data-id=\"\\${wf.id}\">\n <div class=\"ax-wf__card-title\">\\${esc(wf.title || wf.id)}</div>\n <div class=\"ax-wf__card-id\">\\${esc(wf.id)}</div>\n <div class=\"ax-wf__state ax-wf__state--\\${health.state}\">\\${esc(health.label)}</div>\n <div class=\"ax-wf__card-meta\">\n \\${wf.project ? \\`<span class=\"tag tag--project\" title=\"Project\">\\${esc(wf.project)}</span>\\` : \"\"}\n <span class=\"tag\" title=\"What starts it\">\\${esc(FRIENDLY_TRIGGER[triggerSource] || triggerSource)}</span>\n \\${live > 0 ? \\`<span class=\"tag live\">\\${live} running</span>\\` : \"\"}\n \\${lastRun\n ? \\`<span class=\"tag tag--\\${esc(lastRun.status)}\" title=\"\\${esc(lastRun.id)} · \\${esc(new Date(lastRun.updatedAt).toLocaleString())}\">last: \\${esc(lastRun.status)} · \\${relTime(lastRun.updatedAt)}</span>\\`\n : \\`<span class=\"tag\">no runs</span>\\`}\n </div>\n </li>\\`\n }).join(\"\")\n\n $$(\".ax-wf__card\").forEach(el => {\n el.addEventListener(\"click\", () => select(el.dataset.id))\n })\n }\n\n function renderDetail() {\n const wf = state.workflows.find(w => w.id === state.selectedId)\n if (!wf) {\n $(\"#wf-detail-head\").innerHTML = \\`<span class=\"hint\">Select a workflow to inspect.</span>\\`\n $(\"#wf-detail-body\").innerHTML = \"\"\n return\n }\n // V2: trigger is a node. Pull source + filter from its config.\n const triggerNode = (wf.nodes || []).find(n => n && n.type && n.type.indexOf(\"trigger.\") === 0)\n const triggerCfg = (triggerNode && triggerNode.config) || {}\n const filter = triggerCfg.filter || {}\n const trigFilter = [filter.project, filter.repo, filter.chat].filter(Boolean).join(\" / \")\n const triggerSource = triggerNode ? (triggerCfg.source || triggerNode.type.replace(\"trigger.\", \"\")) : \"?\"\n\n const isManual = triggerNode && triggerNode.type === \"trigger.manual\"\n $(\"#wf-detail-head\").innerHTML = \\`\n <div class=\"ax-wf__detail-title\">\n <h2>\\${esc(wf.title || wf.id)}</h2>\n <a class=\"ax-wf__btn\" href=\"/workflows/editor?id=\\${encodeURIComponent(wf.id)}\" title=\"Open on the canvas\">Edit on canvas</a>\n <span class=\"hint\">\\${esc(wf.id)} · v\\${wf.version} · trigger: \\${esc(triggerSource)}\\${trigFilter ? \" · \" + esc(trigFilter) : \"\"}</span>\n </div>\n <div class=\"ax-wf__detail-actions\">\n <button class=\"ax-wf__btn primary\" id=\"wf-run-toggle\" type=\"button\" title=\"Trigger this workflow with a JSON payload\">▶ Run</button>\n </div>\n <div id=\"wf-run-form\" class=\"ax-wf__run-form\" hidden>\n <div id=\"wf-run-fields\"></div>\n <details class=\"ax-wf__run-raw\">\n <summary>Or edit the raw JSON payload</summary>\n <textarea id=\"wf-run-payload\" class=\"ax-wf__editor ax-wf__editor--small\" spellcheck=\"false\">{}</textarea>\n </details>\n <label class=\"ax-wf__inline\">\n <input type=\"checkbox\" id=\"wf-run-force\" \\${isManual ? \"\" : \"checked\"} />\n <span>Force (synthesize trigger event — required when trigger is not <code>trigger.manual</code>)</span>\n </label>\n <div class=\"ax-wf__run-form-actions\">\n <button class=\"ax-wf__btn\" id=\"wf-run-cancel\" type=\"button\">Cancel</button>\n <button class=\"ax-wf__btn primary\" id=\"wf-run-go\" type=\"button\">Run now</button>\n </div>\n </div>\\`\n\n // V2: list nodes (with brief type + any agent id) and edges. No more\n // states/transitions — the DAG structure lives in nodes + edges.\n const nodeCount = (wf.nodes || []).length\n const edgeCount = (wf.edges || []).length\n const nodeEntries = (wf.nodes || []).map((n) => {\n const cfg = n.config || {}\n const badge = n.type === \"agent\" ? (cfg.agentId || \"(no agent)\")\n : n.type.indexOf(\"action.\") === 0 ? n.type.replace(\"action.\", \"\")\n : n.type.indexOf(\"trigger.\") === 0 ? (cfg.source || n.type.replace(\"trigger.\", \"\"))\n : n.type\n return \\`<span class=\"ax-wf__state\" title=\"\\${esc(n.type)}\">\\${esc(n.id)} <span class=\"hint\" style=\"margin-left:4px\">\\${esc(badge)}</span></span>\\`\n }).join(\" \")\n\n const edgesHtml = (wf.edges || []).map((e) => {\n const label = e.label ? \\` <span class=\"cond\">\\${esc(e.label)}</span>\\` : \"\"\n const port = e.fromPort ? \\` :\\${esc(e.fromPort)}\\` : \"\"\n return \\`<div><span>\\${esc(e.from)}\\${port}</span> <span class=\"arr\">→</span> <span>\\${esc(e.to)}</span>\\${label}</div>\\`\n }).join(\"\") || \\`<span class=\"hint\">(no edges)</span>\\`\n\n const runs = state.runs.filter(r => r.workflowId === wf.id).slice(0, 50)\n const runsHtml = runs.length ? runs.map(r => {\n // V2: no more run.state. Show the next pending node (if running) or\n // the last executed node (if terminal).\n const last = (r.history && r.history.length) ? r.history[r.history.length - 1] : null\n const position = r.status === \"running\" && r.pending && r.pending.length\n ? r.pending[0] + \" (next)\"\n : last ? last.nodeId + \" (\" + last.status + \")\"\n : \"—\"\n // Re-run button shows for terminal statuses only (running/paused\n // re-runs would race with the in-flight execution). The button\n // refetches the run's stored payload from /api/workflows/runs/:id\n // (slim listing strips context, full detail keeps it) and POSTs\n // the same payload back to /workflows/<id>/run.\n const canRerun = r.status === \"completed\" || r.status === \"failed\" || r.status === \"canceled\"\n const rerunBtn = canRerun\n ? \\`<button class=\"ax-wf__btn ax-wf__btn--xs\" data-rerun=\"\\${esc(r.id)}\" title=\"Re-run with the same payload\">↻</button>\\`\n : \\`<span style=\"width:18px\"></span>\\`\n return \\`<div class=\"ax-wf__run-row\" data-runid=\"\\${r.id}\">\n <span class=\"id\">\\${r.id.slice(0,8)}</span>\n <span>\\${esc(position)} <span class=\"hint\">· \\${esc(r.entityRef.id)}</span></span>\n <span class=\"hint\">\\${relTime(r.updatedAt)}</span>\n <span class=\"status \\${r.status}\">\\${r.status}</span>\n \\${rerunBtn}\n <span>›</span>\n </div>\\`\n }).join(\"\") : \\`<span class=\"hint\">No runs yet for this workflow.</span>\\`\n\n // --- Runbook framing (see docs/architecture/three-tier.md) -------------\n // Each workflow renders as a runbook: WHEN, WHAT comes in, HOW, WHAT\n // comes out, WHO owns it. Nodes/Edges/Definition stay visible but as\n // collapsed details — the operator doesn't need to read JSON to\n // understand what the workflow does.\n const inputProps = (triggerCfg.inputSchema && triggerCfg.inputSchema.properties) || {}\n const inputRequired = new Set(Array.isArray(triggerCfg.inputSchema && triggerCfg.inputSchema.required) ? triggerCfg.inputSchema.required : [])\n const inputRows = Object.keys(inputProps).map(k => {\n const p = inputProps[k] || {}\n const enumStr = Array.isArray(p.enum) ? \\` <span class=\"hint\">[\\${p.enum.map(esc).join(\" | \")}]</span>\\` : \"\"\n const def = p.default !== undefined ? \\` <span class=\"hint\">default=\\${esc(JSON.stringify(p.default))}</span>\\` : \"\"\n const req = inputRequired.has(k) ? \\`<span class=\"ax-wf__pill ax-wf__pill--req\">required</span>\\` : \\`<span class=\"ax-wf__pill\">optional</span>\\`\n return \\`<tr>\n <td class=\"ax-wf__io-name\">\\${esc(k)}</td>\n <td class=\"ax-wf__io-type\">\\${esc(String(p.type || \"any\"))}\\${enumStr}\\${def}</td>\n <td>\\${req}</td>\n <td class=\"hint\">\\${esc(p.description || \"\")}</td>\n </tr>\\`\n }).join(\"\")\n const inputsBlock = inputRows\n ? \\`<table class=\"ax-wf__io\"><thead><tr><th>field</th><th>type</th><th></th><th>description</th></tr></thead><tbody>\\${inputRows}</tbody></table>\\`\n : \\`<span class=\"hint\">No declared inputSchema. The matcher's payload (message, channel, chatId, agentId) is passed through as-is.</span>\\`\n\n // WHAT comes out — read the end node's output template. Surface the\n // keys + values so operators see what the run resolves to.\n const endNode = (wf.nodes || []).find(n => n && n.type === \"end\")\n const endOutput = (endNode && endNode.config && endNode.config.output) || null\n let outputBlock\n if (endOutput && typeof endOutput === \"object\") {\n const outRows = Object.keys(endOutput).map(k => \\`<tr><td class=\"ax-wf__io-name\">\\${esc(k)}</td><td class=\"ax-wf__io-type\">\\${esc(String(endOutput[k]))}</td></tr>\\`).join(\"\")\n outputBlock = \\`<table class=\"ax-wf__io\"><tbody>\\${outRows}</tbody></table>\\`\n } else if (typeof endOutput === \"string\") {\n outputBlock = \\`<div class=\"hint\">\\${esc(endOutput)}</div>\\`\n } else {\n outputBlock = \\`<span class=\"hint\">No structured output declared on the end node.</span>\\`\n }\n\n // WHEN — trigger summary + last-fired info from the runs we already have.\n const lastRunForWhen = state.runs.find(r => r.workflowId === wf.id)\n const whenLine = triggerNode\n ? \\`<strong>\\${esc(triggerNode.type)}</strong>\\${triggerCfg.source ? \\` · source: <code>\\${esc(triggerCfg.source)}</code>\\` : \"\"}\\${trigFilter ? \\` · filter: <code>\\${esc(trigFilter)}</code>\\` : \"\"}\\`\n : \\`<span class=\"hint\">No trigger node — workflow is unreachable.</span>\\`\n const lastFiredLine = lastRunForWhen\n ? \\`<span class=\"hint\">last fired \\${relTime(lastRunForWhen.updatedAt)} (\\${esc(lastRunForWhen.status)}) — \\${runs.length} run\\${runs.length === 1 ? \"\" : \"s\"} on record</span>\\`\n : \\`<span class=\"hint\">No runs on record yet.</span>\\`\n\n // WHO — ownership + provenance metadata.\n const tagsHtml = (wf.tags || []).map(t => \\`<span class=\"ax-wf__pill\">\\${esc(t)}</span>\\`).join(\" \") || \\`<span class=\"hint\">(no tags)</span>\\`\n const sources = Array.isArray(wf.sourceTaskIds) ? wf.sourceTaskIds : []\n\n $(\"#wf-detail-body\").innerHTML = \\`\n <div class=\"ax-wf__panel ax-wf__panel--full\">\n <h3>WHEN <span class=\"ax-wf__hkbd\">trigger</span></h3>\n <div>\\${whenLine}</div>\n <div style=\"margin-top:6px\">\\${lastFiredLine}</div>\n </div>\n <div class=\"ax-wf__panel ax-wf__panel--full\">\n <h3>WHAT comes in <span class=\"ax-wf__hkbd\">inputSchema</span></h3>\n \\${inputsBlock}\n </div>\n <div class=\"ax-wf__panel\">\n <h3>HOW <span class=\"ax-wf__hkbd\">\\${nodeCount} nodes · \\${edgeCount} edges</span></h3>\n <div class=\"ax-wf__states\">\\${nodeEntries}</div>\n <div class=\"ax-wf__trans\" style=\"margin-top:8px\">\\${edgesHtml}</div>\n </div>\n <div class=\"ax-wf__panel\">\n <h3>WHAT comes out <span class=\"ax-wf__hkbd\">end.output</span></h3>\n \\${outputBlock}\n </div>\n <div class=\"ax-wf__panel ax-wf__panel--full\">\n <h3>WHO <span class=\"ax-wf__hkbd\">ownership</span></h3>\n <div>owner agent: <strong>\\${esc(wf.ownerAgent || \"(unowned)\")}</strong>\\${wf.generatedFrom ? \\` · generated from: <code>\\${esc(wf.generatedFrom)}</code>\\` : \"\"}\\${wf.confidence != null ? \\` · confidence: \\${Number(wf.confidence).toFixed(2)}\\` : \"\"}</div>\n <div style=\"margin-top:6px\">tags: \\${tagsHtml}</div>\n \\${sources.length ? \\`<div style=\"margin-top:6px\" class=\"hint\">absorbed from \\${sources.length} task trace\\${sources.length === 1 ? \"\" : \"s\"}: \\${sources.slice(0, 4).map(esc).join(\", \")}\\${sources.length > 4 ? \"...\" : \"\"}</div>\\` : \"\"}\n </div>\n <div class=\"ax-wf__panel ax-wf__panel--full\">\n <h3>Recent runs (\\${runs.length})</h3>\n <div class=\"ax-wf__runs\">\\${runsHtml}</div>\n </div>\n <details class=\"ax-wf__panel ax-wf__panel--full\">\n <summary>Definition (raw JSON)</summary>\n <pre class=\"ax-wf__json\">\\${esc(JSON.stringify(wf, null, 2))}</pre>\n </details>\\`\n\n $$(\".ax-wf__run-row\").forEach(el => {\n el.addEventListener(\"click\", (ev) => {\n // Re-run button shouldn't open the run drawer for the OLD run.\n // The actual rerun handler attaches separately below.\n const t = ev.target\n if (t && t.tagName === \"BUTTON\" && t.dataset.rerun) return\n openRun(el.dataset.runid)\n })\n })\n $$(\"[data-rerun]\").forEach(el => {\n el.addEventListener(\"click\", (ev) => {\n ev.stopPropagation()\n rerunFrom(el.dataset.rerun, wf.id, triggerNode ? triggerNode.id : null)\n })\n })\n\n // Run button — toggles the inline payload form. Cancel hides it; Run\n // now POSTs to /workflows/<id>/run (proxied by the dashboard) and opens\n // the run drawer on the new runId. The form is built lazily on first\n // open so we have the trigger config + inputSchema in scope.\n const runToggle = $(\"#wf-run-toggle\")\n if (runToggle) runToggle.addEventListener(\"click\", () => {\n const form = $(\"#wf-run-form\")\n const showing = !form.hasAttribute(\"hidden\")\n if (showing) form.setAttribute(\"hidden\", \"\")\n else {\n form.removeAttribute(\"hidden\")\n if (!form.dataset.built) {\n buildRunForm(triggerNode, triggerCfg)\n form.dataset.built = \"1\"\n }\n }\n })\n const runCancel = $(\"#wf-run-cancel\")\n if (runCancel) runCancel.addEventListener(\"click\", () => $(\"#wf-run-form\").setAttribute(\"hidden\", \"\"))\n const runGo = $(\"#wf-run-go\")\n if (runGo) runGo.addEventListener(\"click\", () => runWorkflow(wf.id))\n }\n\n // Friendly run-form builder: derives input fields from the trigger's\n // inputSchema (if declared) or from a hook-event preset, falling back\n // to the raw JSON textarea when neither path is available. The \"Or\n // edit the raw JSON payload\" details element is the always-available\n // power-user escape hatch.\n function buildRunForm(triggerNode, triggerCfg) {\n const fieldsEl = $(\"#wf-run-fields\")\n if (!fieldsEl) return\n const schema = pickTriggerSchema(triggerNode, triggerCfg)\n if (!schema) {\n fieldsEl.innerHTML = '<div class=\"ax-wf__run-hint\">No declared inputSchema. Use the raw JSON below to send the payload your trigger expects.</div>'\n return\n }\n const props = schema.properties || {}\n const required = new Set(Array.isArray(schema.required) ? schema.required : [])\n const rows = Object.keys(props).map(k => renderRunField(k, props[k], required.has(k))).join(\"\")\n fieldsEl.innerHTML = '<div class=\"ax-wf__run-fields-grid\">' + rows + '</div>'\n }\n\n // Pick an inputSchema for the run form: trigger.config.inputSchema\n // wins, then per-hook-event presets for trigger.hook, otherwise null.\n function pickTriggerSchema(triggerNode, triggerCfg) {\n if (triggerCfg && triggerCfg.inputSchema && triggerCfg.inputSchema.properties) {\n return triggerCfg.inputSchema\n }\n if (triggerNode && triggerNode.type === \"trigger.hook\" && triggerCfg && triggerCfg.event) {\n return HOOK_EVENT_PRESETS[triggerCfg.event] || null\n }\n return null\n }\n\n function renderRunField(key, prop, isRequired) {\n const desc = (prop && prop.description) || \"\"\n const t = (prop && prop.type) || \"string\"\n const def = prop && prop.default\n const placeholder = def !== undefined ? esc(JSON.stringify(def)) : \"\"\n let control = \"\"\n if (Array.isArray(prop && prop.enum)) {\n const opts = prop.enum.map(v => '<option value=\"' + esc(String(v)) + '\">' + esc(String(v)) + '</option>').join(\"\")\n control = '<select data-run-key=\"' + esc(key) + '\" data-run-type=\"enum\">' + (isRequired ? \"\" : '<option value=\"\"></option>') + opts + '</select>'\n } else if (t === \"boolean\") {\n control = '<input type=\"checkbox\" data-run-key=\"' + esc(key) + '\" data-run-type=\"boolean\" ' + (def === true ? \"checked\" : \"\") + ' />'\n } else if (t === \"number\" || t === \"integer\") {\n control = '<input type=\"number\" data-run-key=\"' + esc(key) + '\" data-run-type=\"number\" placeholder=\"' + placeholder + '\" />'\n } else if (t === \"array\") {\n control = '<input type=\"text\" data-run-key=\"' + esc(key) + '\" data-run-type=\"list\" placeholder=\"comma,separated,values\" />'\n } else if (t === \"object\") {\n control = '<textarea data-run-key=\"' + esc(key) + '\" data-run-type=\"json\" rows=\"2\" spellcheck=\"false\">{}</textarea>'\n } else {\n control = '<input type=\"text\" data-run-key=\"' + esc(key) + '\" data-run-type=\"string\" placeholder=\"' + placeholder + '\" />'\n }\n const reqMark = isRequired ? '<span class=\"ax-wf__run-req\" title=\"required\">*</span>' : \"\"\n return '<label class=\"ax-wf__run-fld\">'\n + '<span class=\"ax-wf__run-label\"><code>' + esc(key) + '</code>' + reqMark + ' <span class=\"hint\">' + esc(t) + '</span></span>'\n + control\n + (desc ? '<span class=\"ax-wf__run-hint\">' + esc(desc) + '</span>' : '')\n + '</label>'\n }\n\n // Hook-event payload presets — when a workflow's trigger.hook fires\n // off on:gitlab-issue (etc.), the payload it sees is hook-specific.\n // Encoding the shape here lets the run form render typed fields even\n // though the workflow author didn't declare a custom inputSchema.\n // Add new entries as new on:* hooks land in src/workflows/hooks.ts.\n const HOOK_EVENT_PRESETS = {\n \"on:gitlab-issue\": {\n properties: {\n iid: { type: \"number\", description: \"Issue iid\" },\n title: { type: \"string\", description: \"Issue title\" },\n description: { type: \"string\", description: \"Issue body markdown\" },\n action: { type: \"string\", enum: [\"open\", \"reopen\", \"update\", \"close\"] },\n labels: { type: \"array\", description: \"Current labels (array of strings)\" },\n author: { type: \"string\", description: \"Reporter's username\" },\n url: { type: \"string\", description: \"GitLab URL of the issue\" },\n project: { type: \"string\", description: \"org/repo path\" },\n },\n required: [\"iid\", \"project\", \"action\"],\n },\n \"on:gitlab-mr\": {\n properties: {\n iid: { type: \"number\", description: \"MR iid\" },\n title: { type: \"string\", description: \"MR title\" },\n description: { type: \"string\", description: \"MR body\" },\n action: { type: \"string\", enum: [\"open\", \"reopen\", \"update\", \"approved\", \"merge\", \"close\"] },\n state: { type: \"string\", enum: [\"opened\", \"merged\", \"closed\"] },\n source_branch: { type: \"string\" },\n target_branch: { type: \"string\" },\n labels: { type: \"array\" },\n url: { type: \"string\" },\n project: { type: \"string\" },\n },\n required: [\"iid\", \"project\", \"action\"],\n },\n \"on:gitlab-note\": {\n properties: {\n noteId: { type: \"string\" },\n noteableType: { type: \"string\", enum: [\"merge_request\", \"issue\"] },\n noteableIid: { type: \"string\" },\n text: { type: \"string\", description: \"Comment body\" },\n author: { type: \"string\" },\n project: { type: \"string\" },\n },\n required: [\"noteableType\", \"noteableIid\", \"text\"],\n },\n \"on:gitlab-pipeline\": {\n properties: {\n pipelineId: { type: \"number\" },\n status: { type: \"string\", enum: [\"success\", \"failed\", \"canceled\", \"running\", \"pending\"] },\n ref: { type: \"string\", description: \"Branch or tag\" },\n project: { type: \"string\" },\n },\n required: [\"status\", \"project\"],\n },\n }\n\n /** Re-run a completed run with the same payload it was originally\n * triggered with. Fetches the full run (the listing /runs?summary=1\n * strips context) to recover the original trigger payload, then\n * POSTs to /workflows/<id>/run. Force is set if we synthesized\n * a non-manual trigger event the first time around. The dispatcher\n * stores the trigger node's output bundle in run.context keyed by\n * the node id — so we look up via triggerNodeId, not the literal\n * string \"trigger\" (which is just the conventional id). */\n async function rerunFrom(runId, workflowId, triggerNodeId) {\n if (!runId || !workflowId) return\n let payload = {}\n let force = false\n try {\n const detail = await fetchJSON(\"/api/workflows/runs/\" + encodeURIComponent(runId))\n const run = detail.run || detail\n const ctx = run && run.context || {}\n // First preference: index context by the trigger node's id. Falls\n // back to context.trigger for older runs (gitlab-sdlc-loop, etc.).\n const triggerOutput = (triggerNodeId && ctx[triggerNodeId]) || ctx.trigger\n if (triggerOutput && typeof triggerOutput === \"object\") {\n // Some triggers wrap the input under event.payload; most are flat.\n payload = triggerOutput.event && triggerOutput.event.payload\n ? triggerOutput.event.payload\n : triggerOutput\n }\n // entityRef.backend \"channel\" means the original was a synthesized\n // event (force-fired). Mirror force=true so the rerun isn't blocked\n // by the daemon's 409 guard for non-manual triggers.\n force = !!(run && run.entityRef && run.entityRef.backend === \"channel\")\n } catch (e) {\n toast(\"Couldn't load original run payload: \" + e.message + \" — re-running with empty payload\")\n }\n try {\n const res = await postJSON(\"/workflows/\" + encodeURIComponent(workflowId) + \"/run\", { payload, force })\n if (res.runId) {\n toast(\"re-run started: \" + res.runId.slice(0, 8))\n await openRun(res.runId)\n } else {\n toast(\"re-run accepted (no runId returned)\")\n }\n } catch (e) {\n toast(\"re-run failed: \" + e.message)\n }\n }\n\n async function runWorkflow(workflowId) {\n const payloadEl = $(\"#wf-run-payload\")\n const forceEl = $(\"#wf-run-force\")\n let payload = {}\n // Field-form path: collect values from the structured inputs first.\n // Empty values are dropped so the server's default-handling stays\n // intact (don't overwrite a default with \"\").\n const fieldEls = document.querySelectorAll(\"[data-run-key]\")\n if (fieldEls.length > 0) {\n fieldEls.forEach(el => {\n const k = el.getAttribute(\"data-run-key\")\n const t = el.getAttribute(\"data-run-type\")\n let v\n if (t === \"boolean\") v = el.checked\n else if (t === \"number\") {\n const raw = el.value\n if (raw === \"\") return\n v = Number(raw)\n }\n else if (t === \"list\") {\n const raw = (el.value || \"\").trim()\n if (!raw) return\n v = raw.split(\",\").map(s => s.trim()).filter(Boolean)\n }\n else if (t === \"json\") {\n try {\n const raw = (el.value || \"\").trim()\n if (!raw) return\n v = JSON.parse(raw)\n } catch (e) {\n toast(\"Field '\" + k + \"' is not valid JSON: \" + e.message)\n throw e\n }\n }\n else {\n const raw = (el.value || \"\").trim()\n if (!raw) return\n v = raw\n }\n if (v !== undefined) payload[k] = v\n })\n }\n // Raw JSON textarea overrides field values when the operator typed\n // anything substantive — power-user escape hatch.\n try {\n const txt = (payloadEl && payloadEl.value || \"{}\").trim()\n if (txt && txt !== \"{}\") {\n const fromTextarea = JSON.parse(txt)\n if (fromTextarea && typeof fromTextarea === \"object\") {\n payload = { ...payload, ...fromTextarea }\n }\n }\n } catch (e) {\n toast(\"Raw JSON is not valid: \" + e.message)\n return\n }\n const force = !!(forceEl && forceEl.checked)\n try {\n const res = await postJSON(\"/workflows/\" + encodeURIComponent(workflowId) + \"/run\", { payload, force })\n if (res.runId) {\n toast(\"run started: \" + res.runId.slice(0, 8))\n $(\"#wf-run-form\").setAttribute(\"hidden\", \"\")\n await openRun(res.runId)\n } else {\n toast(\"run accepted (no runId returned — check /traces)\")\n }\n } catch (e) {\n // 409 from the daemon means non-manual trigger without force=true.\n // Hint the operator to flip the checkbox.\n const msg = String(e.message || e)\n if (msg.indexOf(\"409\") >= 0 || msg.toLowerCase().indexOf(\"trigger is\") >= 0) {\n toast(\"non-manual trigger — tick Force to fire anyway\")\n } else {\n toast(\"run failed: \" + msg)\n }\n }\n }\n\n // --- Run drawer ------------------------------------------------------\n\n async function openRun(runId) {\n state.activeRunId = runId\n const res = await fetchJSON(\"/api/workflows/runs/\" + encodeURIComponent(runId))\n paintRun(res.run || res)\n connectRunStream(runId)\n const panel = $(\"#wf-run-panel\")\n panel.classList.remove(\"hidden\")\n panel.setAttribute(\"aria-hidden\", \"false\")\n }\n\n function closeRun() {\n state.activeRunId = null\n if (state.sse) { state.sse.close(); state.sse = null }\n const panel = $(\"#wf-run-panel\")\n panel.classList.add(\"hidden\")\n panel.setAttribute(\"aria-hidden\", \"true\")\n }\n\n function paintRun(run) {\n $(\"#wf-run-title\").textContent = run.workflowId + \" run\"\n $(\"#wf-run-meta\").textContent = run.id.slice(0, 8) + \" · \" + run.entityRef.backend + \":\" + run.entityRef.id + \" · home=\" + run.homeNode\n const pill = $(\"#wf-run-status\")\n pill.textContent = run.status\n pill.className = \"ax-wf__status-pill \" + run.status\n // V2: history entries are per-node executions, not state transitions.\n // Each entry has { at, nodeId, status, inputKeys, output?, note? }.\n $(\"#wf-run-timeline\").innerHTML = (run.history || []).map(h => {\n const failed = h.status === \"failed\" || h.status === \"timeout\"\n const outputHint = h.output ? Object.keys(h.output).slice(0, 3).map(k => k + \"=\" + briefVal(h.output[k])).join(\" · \") : \"\"\n return \\`<div class=\"ax-wf__timeline-entry \\${failed ? \"result-failed\" : \"\"}\">\n <div class=\"time\">\\${new Date(h.at).toLocaleString()}</div>\n <div class=\"move\"><strong>\\${esc(h.nodeId)}</strong> · \\${esc(h.status)}</div>\n \\${h.inputKeys && h.inputKeys.length ? \\`<div class=\"agent\">inputs: \\${h.inputKeys.map(esc).join(\", \")}</div>\\` : \"\"}\n \\${outputHint ? \\`<div class=\"agent\">output: \\${esc(outputHint)}</div>\\` : \"\"}\n \\${h.note ? \\`<div class=\"agent\">\\${esc(h.note)}</div>\\` : \"\"}\n </div>\\`\n }).join(\"\")\n }\n\n function connectRunStream(runId) {\n if (state.sse) state.sse.close()\n try {\n state.sse = new EventSource(\"/api/workflows/runs/\" + encodeURIComponent(runId) + \"/stream\")\n $(\"#wf-run-live\").textContent = \"live\"\n state.sse.onmessage = (evt) => {\n try {\n const run = JSON.parse(evt.data)\n paintRun(run)\n } catch { /* ignore */ }\n }\n state.sse.onerror = () => { $(\"#wf-run-live\").textContent = \"disconnected\" }\n } catch { $(\"#wf-run-live\").textContent = \"sse unavailable\" }\n }\n\n // --- Actions ---------------------------------------------------------\n\n async function select(id) {\n state.selectedId = id\n // Keep URL hash in sync so refreshes and editor deep-links stay on the\n // same workflow. Use replaceState so the browser's back button doesn't\n // accumulate selection history.\n try { history.replaceState(null, \"\", \"#\" + encodeURIComponent(id)) } catch (_) { /* */ }\n renderList()\n renderDetail()\n }\n\n async function refresh() {\n try {\n // summary=1 strips the heavy per-run context (webhook payloads can be\n // 10-50KB each — at limit=100 the listing was multi-MB and gating\n // first paint behind the slowest serialization path). Run-detail\n // and SSE stream still return the full shape unchanged.\n const [workflows, runs] = await Promise.all([\n fetchJSON(\"/api/workflows\"),\n fetchJSON(\"/api/workflows/runs?limit=100&summary=1\"),\n ])\n state.workflows = workflows.workflows || workflows\n state.runs = runs.runs || runs\n // If the URL hash names a workflow (e.g. /workflows#my-workflow)\n // and nothing is selected yet, auto-select it. Editor's \"History\"\n // button links here with the hash, so users land on the right detail.\n if (!state.selectedId && location.hash) {\n const fromHash = decodeURIComponent(location.hash.slice(1))\n if (state.workflows.some(w => w.id === fromHash)) state.selectedId = fromHash\n }\n renderList()\n renderDetail()\n } catch (e) {\n toast(\"Failed to load workflows: \" + e.message)\n }\n }\n\n function toast(msg) {\n const el = $(\"#wf-toast\")\n el.textContent = msg\n el.hidden = false\n setTimeout(() => { el.hidden = true }, 2400)\n }\n\n function esc(s) {\n return String(s ?? \"\")\n .replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\").replace(/'/g, \"&#39;\")\n }\n\n function relTime(iso) {\n const d = new Date(iso).getTime()\n const s = Math.floor((Date.now() - d) / 1000)\n if (s < 60) return s + \"s ago\"\n if (s < 3600) return Math.floor(s/60) + \"m ago\"\n if (s < 86400) return Math.floor(s/3600) + \"h ago\"\n return Math.floor(s/86400) + \"d ago\"\n }\n\n /** Compact single-line preview of a node-output value for the timeline. */\n function briefVal(v) {\n if (v === null || v === undefined) return \"\"\n if (typeof v === \"string\") return v.length > 48 ? v.slice(0, 45) + \"…\" : v\n if (typeof v === \"number\" || typeof v === \"boolean\") return String(v)\n try {\n const s = JSON.stringify(v)\n return s.length > 48 ? s.slice(0, 45) + \"…\" : s\n } catch { return \"?\" }\n }\n\n // --- Bindings --------------------------------------------------------\n\n // Run lifecycle action — pause / resume / cancel via setStatus on the\n // backend RunStore, mirroring \\`agentx workflow pause/resume/cancel\\`.\n // Opens a tiny confirm dialog for cancel since it's destructive.\n async function runAction(action) {\n if (!state.activeRunId) return\n if (action === \"cancel\" && !confirm(\"Cancel this run? In-flight work will stop.\")) return\n const statusByAction = { pause: \"paused\", resume: \"running\", cancel: \"canceled\" }\n const status = statusByAction[action]\n if (!status) return\n try {\n const r = await fetch(\"/api/workflows/runs/\" + encodeURIComponent(state.activeRunId) + \"/status\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-Requested-With\": \"agentx-board\" },\n credentials: \"same-origin\",\n body: JSON.stringify({ status }),\n })\n if (!r.ok) throw new Error(\"HTTP \" + r.status + \": \" + (await r.text()).slice(0, 120))\n toast(action + \"d\")\n refresh()\n // Refresh the open run panel so the status pill updates immediately.\n if (state.activeRunId) openRun(state.activeRunId)\n } catch (e) {\n toast(\"action failed: \" + e.message)\n }\n }\n\n document.addEventListener(\"DOMContentLoaded\", () => {\n $(\"#wf-refresh\").addEventListener(\"click\", refresh)\n $(\"#wf-run-close\").addEventListener(\"click\", closeRun)\n $(\"#wf-filter\").addEventListener(\"input\", (e) => {\n state.filter = e.target.value\n renderList()\n })\n const projSel = $(\"#wf-project-filter\")\n if (projSel) {\n projSel.addEventListener(\"change\", (e) => {\n state.projectFilter = e.target.value\n renderList()\n })\n }\n document.querySelectorAll('#wf-run-actions [data-run-action]').forEach((b) => {\n b.addEventListener('click', () => runAction(b.getAttribute('data-run-action')))\n })\n document.addEventListener(\"keydown\", (e) => {\n if (e.key === \"r\" && !e.metaKey && document.activeElement?.tagName !== \"INPUT\") refresh()\n if (e.key === \"Escape\") closeRun()\n })\n refresh()\n // Light polling keeps run counts fresh without needing a global SSE.\n setInterval(refresh, 15000)\n })\n})();\n`\n","// --- Procedures page (Tier 3 — reusable building blocks) ---\n//\n// Surfaces the verb library that workflows compose. Three sections:\n// 1. Built-in actions — typed, schema-validated steps registered\n// process-globally via src/actions/builtin/.\n// Workflow nodes invoke these via type:\n// \"action.builtin\" with config.name + input.\n// 2. Workflow templates — scaffolds emitted by `agentx workflow init\n// --template <name>`. The base shape an\n// operator starts from before refining.\n// 3. Agent skills — per-agent toolkits surfaced for visibility;\n// actual editing happens under Settings.\n//\n// Reads come from existing daemon endpoints (no new server work):\n// GET /api/actions/builtin — built-in catalog (each entry\n// has inputSchema, outputSchema,\n// description, timeoutMs)\n// Templates and skills are bundled with the daemon (filesystem-served).\n// We surface only the names + summaries here; the editor is the place\n// to wire one into a real workflow.\n//\n// This page is intentionally read-only. Procedures are added by writing\n// code (new action.builtin module or new template YAML), not by the UI.\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\n\nexport interface ProceduresPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderProceduresPage(opts: ProceduresPageOpts = {}): string {\n const body = `<div class=\"ax-proc__root\">\n <header class=\"ax-proc__header\">\n <h1>Procedures</h1>\n <p class=\"hint\">Reusable building blocks workflows compose. See <a href=\"/admin/wiki/architecture/three-tier\" target=\"_blank\">three-tier model</a> for how this fits with System and Processes.</p>\n </header>\n\n <section class=\"ax-proc__panel\">\n <header>\n <h2>Built-in actions</h2>\n <span id=\"ax-proc__actions-count\" class=\"hint\">0</span>\n </header>\n <p class=\"hint\">Typed steps registered at boot. Reference from any workflow as <code>type: action.builtin</code> with <code>config.name</code> matching the action's name and <code>config.input</code> matching its input schema.</p>\n <ul id=\"ax-proc__actions\" class=\"ax-proc__list\" aria-live=\"polite\"></ul>\n </section>\n\n <section class=\"ax-proc__panel\">\n <header>\n <h2>Sub-workflows</h2>\n <span id=\"ax-proc__subworkflows-count\" class=\"hint\">0</span>\n </header>\n <p class=\"hint\">Active workflows whose trigger is <code>trigger.manual</code> with a typed <code>inputSchema</code> can be reused as procedures. Reference from another workflow as <code>type: subProcess</code> with <code>config.workflowId</code> and an <code>inputMap</code>.</p>\n <ul id=\"ax-proc__subworkflows\" class=\"ax-proc__list\" aria-live=\"polite\"></ul>\n </section>\n\n <section class=\"ax-proc__panel\">\n <header>\n <h2>Workflow templates</h2>\n <span class=\"hint\">scaffold via <code>agentx workflow init &lt;id&gt; --template &lt;name&gt;</code></span>\n </header>\n <ul class=\"ax-proc__list\">\n <li class=\"ax-proc__item\"><div class=\"ax-proc__item-head\"><strong>linear</strong><span class=\"hint\">trigger → agent → end (smallest valid graph)</span></div></li>\n <li class=\"ax-proc__item\"><div class=\"ax-proc__item-head\"><strong>branching</strong><span class=\"hint\">classify, then route on RESULT to one of N branches</span></div></li>\n <li class=\"ax-proc__item\"><div class=\"ax-proc__item-head\"><strong>extract</strong><span class=\"hint\">extract.structured with a JSON-schema-shaped output</span></div></li>\n <li class=\"ax-proc__item\"><div class=\"ax-proc__item-head\"><strong>retry</strong><span class=\"hint\">per-node retry policy with a branch fallback path</span></div></li>\n </ul>\n </section>\n\n <section class=\"ax-proc__panel\">\n <header>\n <h2>Architecture</h2>\n </header>\n <p class=\"hint\">AgentX is layered: <strong>System</strong> (agents, channels, mesh) → <strong>Processes</strong> (workflows that turn intent into work) → <strong>Procedures</strong> (this page: reusable steps a process composes). Higher tiers depend on lower; never the reverse.</p>\n <p class=\"hint\">Adding a new procedure is a code change (a new <code>action.builtin</code> module under <code>src/actions/builtin/</code> or a new template YAML in <code>src/workflows/templates/</code>) — not a UI flow. This page is the read-only catalog.</p>\n </section>\n</div>`\n\n return renderShell({\n title: \"AgentX · Procedures\",\n activeTab: \"procedures\",\n subtitle: \"Procedures\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: PROCEDURES_PAGE_CSS,\n scripts: `<script>${PROCEDURES_PAGE_SCRIPT}</script>`,\n })\n}\n\nconst PROCEDURES_PAGE_CSS = `\n.ax-proc__root { padding: 18px 24px 40px; max-width: 1100px; margin: 0 auto; color: var(--ax-text); }\n.ax-proc__header h1 { margin: 0 0 4px; font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }\n.ax-proc__header .hint { color: var(--ax-muted); font-size: 12px; margin: 0 0 18px; }\n.ax-proc__panel { margin: 18px 0; padding: 14px 16px; border: 1px solid var(--ax-border); border-radius: 6px; background: var(--ax-bg-elev); }\n.ax-proc__panel > header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 6px; gap: 10px; flex-wrap: wrap; }\n.ax-proc__panel h2 { font-size: 14px; margin: 0; font-weight: 600; letter-spacing: -0.005em; }\n.ax-proc__panel .hint { color: var(--ax-muted); font-size: 11px; }\n.ax-proc__panel code { font-family: var(--ax-mono); font-size: 11px; padding: 1px 5px; background: var(--ax-surface); border: 1px solid var(--ax-border); border-radius: 3px; }\n.ax-proc__list { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }\n.ax-proc__item { padding: 10px 12px; background: var(--ax-surface); border: 1px solid var(--ax-border); border-radius: 4px; }\n.ax-proc__item-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }\n.ax-proc__item strong { font-size: 13px; font-family: var(--ax-mono); }\n.ax-proc__item-head .hint { font-size: 11px; color: var(--ax-muted); flex: 1; text-align: right; }\n.ax-proc__schema { margin-top: 6px; }\n.ax-proc__schema summary { font-size: 11px; color: var(--ax-muted); cursor: pointer; padding: 4px 0; user-select: none; }\n.ax-proc__schema summary:hover { color: var(--ax-text); }\n.ax-proc__schema pre { margin: 6px 0 0; padding: 8px 10px; font-family: var(--ax-mono); font-size: 11px; line-height: 1.4; background: var(--ax-bg); border: 1px solid var(--ax-border); border-radius: 3px; overflow: auto; max-height: 240px; white-space: pre-wrap; word-break: break-word; }\n.ax-proc__io { width: 100%; border-collapse: collapse; font-size: 12px; margin: 6px 0 0; }\n.ax-proc__io th { text-align: left; font-weight: 500; color: var(--ax-muted); font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; padding: 4px 8px; border-bottom: 1px solid var(--ax-border); }\n.ax-proc__io td { padding: 6px 8px; border-bottom: 1px solid var(--ax-border); vertical-align: top; }\n.ax-proc__io td.ax-proc__io-name { font-family: var(--ax-mono); color: var(--ax-text); font-weight: 500; white-space: nowrap; }\n.ax-proc__io td.ax-proc__io-type { font-family: var(--ax-mono); font-size: 11px; color: var(--ax-text-2); }\n.ax-proc__req { display: inline-block; padding: 1px 6px; font-size: 10px; font-family: var(--ax-mono); border: 1px solid color-mix(in oklch, var(--ax-warn, #f59e0b) 40%, var(--ax-border)); border-radius: 3px; color: var(--ax-warn, #f59e0b); }\n.ax-proc__link { color: var(--ax-accent, #6366f1); text-decoration: none; font-size: 11px; }\n.ax-proc__link:hover { text-decoration: underline; }\n`\n\nconst PROCEDURES_PAGE_SCRIPT = `\n(function(){\n function $(sel){ return document.querySelector(sel) }\n function esc(s){ return String(s||\"\").replace(/[&<>\"']/g, function(c){ return ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'})[c] }) }\n function token(){ try { return localStorage.getItem('ax_token') || '' } catch { return '' } }\n function headers(){ var h = { 'Accept':'application/json' }; var t = token(); if (t) h.Authorization = 'Bearer ' + t; return h }\n\n async function loadActions(){\n try {\n var r = await fetch('/api/actions/builtin', { headers: headers() })\n if (!r.ok) throw new Error('HTTP ' + r.status)\n var data = await r.json()\n var actions = Array.isArray(data) ? data : (data.actions || data.builtins || [])\n $('#ax-proc__actions-count').textContent = String(actions.length)\n if (!actions.length) {\n $('#ax-proc__actions').innerHTML = '<li class=\"ax-proc__item\"><div class=\"ax-proc__item-head\"><span class=\"hint\">No built-in actions registered. Check src/actions/builtin/index.ts is wired at daemon boot.</span></div></li>'\n return\n }\n $('#ax-proc__actions').innerHTML = actions.map(function(a){\n var schemaIn = a.inputSchema ? '<details class=\"ax-proc__schema\"><summary>Input schema</summary><pre>' + esc(JSON.stringify(a.inputSchema, null, 2)) + '</pre></details>' : ''\n var schemaOut = a.outputSchema ? '<details class=\"ax-proc__schema\"><summary>Output schema</summary><pre>' + esc(JSON.stringify(a.outputSchema, null, 2)) + '</pre></details>' : ''\n var t = a.timeoutMs ? '<span class=\"hint\">timeout=' + Math.round(a.timeoutMs/1000) + 's</span>' : ''\n return '<li class=\"ax-proc__item\">' +\n '<div class=\"ax-proc__item-head\"><strong>' + esc(a.name || 'unnamed') + '</strong>' + t + '</div>' +\n (a.description ? '<p class=\"hint\" style=\"margin:6px 0 0\">' + esc(a.description) + '</p>' : '') +\n schemaIn + schemaOut +\n '</li>'\n }).join('')\n } catch (e) {\n $('#ax-proc__actions').innerHTML = '<li class=\"ax-proc__item\"><div class=\"ax-proc__item-head\"><span class=\"hint\">Failed to load actions: ' + esc(e.message) + '</span></div></li>'\n }\n }\n\n /** A workflow qualifies as a \"procedure\" — i.e. composable as a\n * subProcess from another workflow — when:\n * - it's active (state=active, status=active or unspecified)\n * - its trigger is trigger.manual (no live channel race)\n * - it declares a non-empty inputSchema (the contract)\n * Without inputSchema, callers wouldn't know what to pass; without\n * trigger.manual, dispatching it from a parent would race with the\n * channel's own delivery. */\n function isProcedureShaped(wf){\n if (!wf || wf.state !== 'active') return false\n if (wf.status && wf.status !== 'active') return false\n var trig = (wf.nodes || []).find(function(n){ return n && n.type === 'trigger.manual' })\n if (!trig) return false\n var schema = trig.config && trig.config.inputSchema\n if (!schema || !schema.properties) return false\n return Object.keys(schema.properties).length > 0\n }\n\n async function loadSubWorkflows(){\n try {\n var r = await fetch('/api/workflows', { headers: headers() })\n if (!r.ok) throw new Error('HTTP ' + r.status)\n var data = await r.json()\n var workflows = (data.workflows || []).filter(isProcedureShaped)\n $('#ax-proc__subworkflows-count').textContent = String(workflows.length)\n if (!workflows.length) {\n $('#ax-proc__subworkflows').innerHTML = '<li class=\"ax-proc__item\"><div class=\"ax-proc__item-head\"><span class=\"hint\">No active workflows currently expose a typed inputSchema. Promote a draft from /workflows once its inputSchema is defined.</span></div></li>'\n return\n }\n $('#ax-proc__subworkflows').innerHTML = workflows.map(function(wf){\n var trig = (wf.nodes || []).find(function(n){ return n.type === 'trigger.manual' })\n var schema = trig && trig.config && trig.config.inputSchema || { properties: {}, required: [] }\n var required = new Set(Array.isArray(schema.required) ? schema.required : [])\n var rows = Object.keys(schema.properties || {}).map(function(k){\n var p = schema.properties[k] || {}\n var enumStr = Array.isArray(p['enum']) ? ' [' + p['enum'].map(esc).join(' | ') + ']' : ''\n var def = p['default'] !== undefined ? ' default=' + esc(JSON.stringify(p['default'])) : ''\n var req = required.has(k) ? '<span class=\"ax-proc__req\">required</span>' : ''\n return '<tr><td class=\"ax-proc__io-name\">' + esc(k) + '</td>' +\n '<td class=\"ax-proc__io-type\">' + esc(String(p.type || 'any')) + esc(enumStr) + esc(def) + '</td>' +\n '<td>' + req + '</td>' +\n '<td class=\"hint\">' + esc(p.description || '') + '</td></tr>'\n }).join('')\n var inputCount = Object.keys(schema.properties || {}).length\n var ownerLine = wf.ownerAgent ? ' · owner: ' + esc(wf.ownerAgent) : ''\n return '<li class=\"ax-proc__item\">' +\n '<div class=\"ax-proc__item-head\"><strong>' + esc(wf.id) + '</strong>' +\n '<span class=\"hint\">' + inputCount + ' input' + (inputCount === 1 ? '' : 's') + ownerLine + '</span></div>' +\n (wf.title ? '<p class=\"hint\" style=\"margin:6px 0 0\">' + esc(wf.title) + '</p>' : '') +\n '<div style=\"margin-top:6px\">' +\n '<a href=\"/workflows#' + esc(wf.id) + '\" class=\"ax-proc__link\">Open in /workflows ↗</a>' +\n ' · <code style=\"font-size:11px\">subProcess { workflowId: \"' + esc(wf.id) + '\" }</code>' +\n '</div>' +\n (rows ? '<details class=\"ax-proc__schema\"><summary>Input contract</summary>' +\n '<table class=\"ax-proc__io\"><thead><tr><th>field</th><th>type</th><th></th><th>description</th></tr></thead><tbody>' + rows + '</tbody></table>' +\n '</details>' : '') +\n '</li>'\n }).join('')\n } catch (e) {\n $('#ax-proc__subworkflows').innerHTML = '<li class=\"ax-proc__item\"><div class=\"ax-proc__item-head\"><span class=\"hint\">Failed to load sub-workflows: ' + esc(e.message) + '</span></div></li>'\n }\n }\n\n function bootAll(){ loadActions(); loadSubWorkflows() }\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootAll)\n else bootAll()\n})();\n`\n","import { renderShell, esc, type TopbarPeer } from \"..\"\n\n// --- Task page — watch one agent work ---\n//\n// Replaces the full-screen `#task-modal` on Live, which streamed this SSE\n// endpoint into a box you couldn't link to, couldn't reload, and couldn't\n// keep open next to anything else.\n//\n// (An earlier note here claimed it also replaced the agent page's `#td-panel`.\n// It doesn't — that drawer is the Test-drive sandbox, a different feature that\n// still needs its own page.)\n//\n// A running task is a place, not a popup. Giving it a URL means it can be\n// shared, bookmarked, reopened after a refresh, and left open on a second\n// monitor while you work — none of which a modal allows.\n//\n// The page does one job: show what this agent is doing right now, and let you\n// steer it. Exactly two actions, both about the running turn:\n// send — queue a message as the next turn (current turn keeps running)\n// stop — cancel the current turn\n//\n// Reads: GET /api/task/stream?node=&agent=&task= (SSE: start / chunk / end)\n// Writes: POST /api/task/action?node=&task=&kind=cancel|followup\n\nexport interface TaskPageOpts {\n taskId: string\n agentId: string\n agentName?: string\n nodeUrl: string\n channel?: string\n /** Finished task opened from history: read the stored record once instead\n * of holding an SSE connection open for a stream that will never arrive. */\n archived?: boolean\n /** The request, when the caller already knows it. A live task's stream never\n * replays what was asked, so Live passes it through the URL. */\n ask?: string\n askAt?: string\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderTaskPage(opts: TaskPageOpts): string {\n const who = opts.agentName || opts.agentId\n\n const body = `<div class=\"ax-task-page\"\n data-task-id=\"${esc(opts.taskId)}\"\n data-agent-id=\"${esc(opts.agentId)}\"\n data-node-url=\"${esc(opts.nodeUrl)}\"\n data-archived=\"${opts.archived ? \"1\" : \"\"}\"\n data-ask=\"${esc(opts.ask || \"\")}\"\n data-ask-at=\"${esc(opts.askAt || \"\")}\">\n\n <header class=\"ax-task-page__head\">\n <nav class=\"ax-crumbs\" aria-label=\"Breadcrumb\">\n <a href=\"/live\">Live</a>\n <span class=\"ax-crumbs__sep\">/</span>\n <a href=\"/agents/${esc(opts.agentId)}/history?node=${encodeURIComponent(opts.nodeUrl)}&name=${encodeURIComponent(who)}\"><span class=\"ax-mention\">@${esc(opts.agentId)}</span></a>\n <span class=\"ax-crumbs__sep\">/</span>\n <span class=\"ax-crumbs__here\">Task</span>\n </nav>\n <div class=\"ax-task-page__meta\">\n ${opts.channel ? `<span class=\"ax-badge ax-badge--mono ax-badge--ghost\">${esc(opts.channel)}</span>` : \"\"}\n <span class=\"ax-badge ax-badge--mono\" id=\"task-status\">connecting…</span>\n <span class=\"ax-mono ax-muted\" id=\"task-elapsed\"></span>\n </div>\n </header>\n\n <section class=\"ax-task-page__ask\" id=\"task-ask\" hidden>\n <div class=\"ax-task-page__ask-head\">\n <span class=\"ax-ev__label ax-ev__label--soft\">request</span>\n <span class=\"ax-task-page__ask-who\" id=\"task-ask-who\"></span>\n <span class=\"ax-task-page__ask-when\" id=\"task-ask-when\"></span>\n </div>\n <div class=\"ax-task-page__ask-body\" id=\"task-ask-body\"></div>\n </section>\n\n <div class=\"ax-task-page__output\" id=\"task-output\"></div>\n\n <footer class=\"ax-task-page__compose\"${opts.archived ? \" hidden\" : \"\"}>\n <textarea id=\"task-input\" class=\"ax-task-page__input\" rows=\"2\"\n placeholder=\"Send a message to this chat — the current turn keeps running, your message dispatches as the next turn. ⌘/Ctrl+Enter to send.\"></textarea>\n <div class=\"ax-task-page__actions\">\n <span class=\"ax-task-page__hint\" id=\"task-hint\"></span>\n <button type=\"button\" id=\"task-stop\" class=\"ax-btn ax-btn--danger\">✕ stop</button>\n <button type=\"button\" id=\"task-send\" class=\"ax-btn ax-btn--primary\">send →</button>\n </div>\n </footer>\n</div>`\n\n return renderShell({\n title: `AgentX · ${who}`,\n activeTab: \"live\",\n subtitle: \"Task\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: TASK_PAGE_CSS,\n scripts: `<script>${TASK_PAGE_JS}</script>`,\n })\n}\n\nconst TASK_PAGE_CSS = `\n/* The shell gives <main> no padding, so page content butted straight against\n the sticky topbar. Breathe. */\n.ax-task-page {\n display: flex; flex-direction: column; gap: var(--ax-gap);\n max-width: 1000px; margin: 0 auto; padding: 22px var(--ax-pad) var(--ax-pad);\n min-height: calc(100vh - 130px);\n}\n/* Breadcrumb, not a back button — it says where you are, and every step up is\n somewhere you can actually go. */\n.ax-crumbs { display: flex; align-items: center; gap: 8px; font-size: var(--ax-fs-sm); min-width: 0; }\n.ax-crumbs a { color: var(--ax-text-2); font-weight: 600; text-decoration: none; }\n.ax-crumbs a:hover { color: var(--ax-accent); text-decoration: none; }\n.ax-crumbs__sep { color: var(--ax-border-2); }\n.ax-crumbs__here { color: var(--ax-text); font-weight: 700; }\n.ax-task-page__head {\n display: flex; align-items: center; justify-content: space-between;\n gap: var(--ax-gap); flex-wrap: wrap;\n}\n.ax-task-page__who { display: flex; align-items: center; gap: 12px; min-width: 0; }\n.ax-task-page__back { font-size: var(--ax-fs-sm); font-weight: 600; white-space: nowrap; }\n.ax-task-page__name { color: var(--ax-text-2); }\n.ax-task-page__meta { display: flex; align-items: center; gap: 8px; }\n.ax-badge.is-live { border-color: var(--ax-accent); color: var(--ax-accent); }\n.ax-badge.is-done { border-color: var(--ax-ok); color: var(--ax-ok); }\n.ax-badge.is-err { border-color: var(--ax-err); color: var(--ax-err); }\n\n/* The output is the page. It grows to fill whatever room is left. */\n.ax-task-page__output {\n flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 10px;\n background: var(--ax-surface); color: var(--ax-text-2);\n border: var(--ax-border-w) solid var(--ax-border);\n border-radius: var(--ax-radius-lg); box-shadow: var(--ax-shadow);\n padding: var(--ax-pad); font-size: var(--ax-fs-sm); line-height: 1.6; min-height: 320px;\n}\n/* A \"display: flex\" rule beats the user-agent's [hidden] rule, so an archived\n task still showed a send/stop box that could do nothing. (No backticks in\n this file's CSS comments — it lives in a TS template literal and one\n backtick silently truncates the whole stylesheet.) */\n.ax-task-page__compose[hidden] { display: none; }\n.ax-task-page__compose {\n display: flex; flex-direction: column; gap: 8px;\n background: var(--ax-surface); border: var(--ax-border-w) solid var(--ax-border);\n border-radius: var(--ax-radius-lg); padding: var(--ax-pad-sm);\n}\n.ax-task-page__input {\n width: 100%; resize: vertical; background: var(--ax-bg); color: var(--ax-text);\n border: var(--ax-border-w) solid var(--ax-border); border-radius: var(--ax-radius);\n padding: 10px 12px; font: inherit; font-size: var(--ax-fs-sm);\n}\n.ax-task-page__input:focus { outline: none; border-color: var(--ax-accent); }\n.ax-task-page__actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }\n.ax-task-page__hint { flex: 1; font-size: var(--ax-fs-xs); color: var(--ax-muted); }\n/* The request that started everything. The page used to render the agent's\n work with no sign of what it had been asked — you could read a whole\n transcript without learning why it ran. */\n.ax-task-page__ask[hidden] { display: none; }\n.ax-task-page__ask {\n background: var(--ax-surface); border: var(--ax-border-w) solid var(--ax-border);\n border-left-width: 4px; border-left-color: var(--ax-accent);\n border-radius: var(--ax-radius-lg); padding: 14px 16px;\n box-shadow: var(--ax-shadow);\n}\n.ax-task-page__ask-head { display: flex; align-items: center; gap: 8px; font-size: var(--ax-fs-xs); }\n.ax-task-page__ask-who { color: var(--ax-text-2); font-weight: 600; }\n.ax-task-page__ask-when { margin-left: auto; color: var(--ax-muted); font-family: var(--ax-mono); }\n.ax-task-page__ask-body {\n margin-top: 8px; font-size: var(--ax-fs-sm); line-height: 1.55; color: var(--ax-text);\n word-break: break-word; max-height: 200px; overflow: auto;\n}\n/* Same markdown rhythm as a reply block. */\n.ax-task-page__ask-body > *:first-child { margin-top: 0; }\n.ax-task-page__ask-body > *:last-child { margin-bottom: 0; }\n.ax-task-page__ask-body p { margin: 0 0 8px; white-space: pre-wrap; }\n.ax-task-page__ask-body ul, .ax-task-page__ask-body ol { margin: 0 0 8px; padding-left: 20px; }\n.ax-task-page__ask-body code {\n font-family: var(--ax-mono); font-size: 0.92em; padding: 1px 5px;\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 5px;\n}\n\n/* Markdown inside a reply. Tight vertical rhythm — a reply is a block in a\n stream, not a document, so headings and paragraphs must not push the next\n event off the screen. */\n.ax-ev__text > *:first-child { margin-top: 0; }\n.ax-ev__text > *:last-child { margin-bottom: 0; }\n.ax-ev__text p { margin: 0 0 8px; white-space: pre-wrap; }\n.ax-ev__text h3, .ax-ev__text h4, .ax-ev__text h5, .ax-ev__text h6 {\n margin: 12px 0 6px; font-size: var(--ax-fs); font-weight: 700; color: var(--ax-text);\n}\n.ax-ev__text ul, .ax-ev__text ol { margin: 0 0 8px; padding-left: 20px; }\n.ax-ev__text li { margin: 2px 0; }\n.ax-ev__text code {\n font-family: var(--ax-mono); font-size: 0.92em; padding: 1px 5px;\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 5px; color: var(--ax-text);\n}\n.ax-ev__text pre.ax-ev__code code { background: none; border: none; padding: 0; }\n.ax-ev__text strong { color: var(--ax-text); font-weight: 700; }\n\n/* Stream events. The agent's output is not a wall of text — it is a sequence\n of tool calls, results, internal reasoning and replies. Rendering each as\n its own block with a coloured rail makes a long run scannable: you can find\n the tool that failed without reading everything above it. */\n.ax-ev { border-left: 2px solid var(--ax-border-2); padding: 2px 0 2px 12px; }\n.ax-ev--tool { border-color: var(--ax-info); }\n.ax-ev--tool-result { border-color: var(--ax-border-2); }\n.ax-ev--tool-result.is-err { border-color: var(--ax-err); }\n.ax-ev--thought { border-color: var(--ax-muted); }\n.ax-ev--text { border-color: var(--ax-accent); }\n.ax-ev--system {\n border-color: var(--ax-border-2); color: var(--ax-muted); font-size: var(--ax-fs-xs);\n font-family: var(--ax-mono); padding-top: 4px; padding-bottom: 4px;\n}\n.ax-ev--system.is-done { border-color: var(--ax-accent); }\n.ax-ev--error { border-color: var(--ax-err); }\n.ax-ev__head { display: flex; align-items: center; gap: 8px; font-size: var(--ax-fs-xs); color: var(--ax-text-2); }\n.ax-ev__label { text-transform: uppercase; letter-spacing: 0.06em; font-size: 10px; font-family: var(--ax-mono); }\n.ax-ev__label--soft { color: var(--ax-muted); }\n.ax-ev__label--tool { color: var(--ax-info); }\n.ax-ev__label--result { color: var(--ax-muted); }\n.ax-ev__label--text { color: var(--ax-accent); }\n.ax-ev__label--error { color: var(--ax-err); }\n.ax-ev__time { margin-left: auto; font-family: var(--ax-mono); color: var(--ax-muted); }\n.ax-ev__tool { font-family: var(--ax-mono); color: var(--ax-info); font-size: var(--ax-fs-xs); }\n.ax-ev__code {\n margin: 4px 0 0; padding: 8px 10px; background: var(--ax-bg-elev);\n border: 1px solid var(--ax-border); border-radius: 3px; font-family: var(--ax-mono);\n font-size: var(--ax-fs-xs); white-space: pre-wrap; word-break: break-word;\n line-height: 1.5; color: var(--ax-text-2); max-height: 200px; overflow: auto;\n}\n.ax-ev__code--muted { color: var(--ax-muted); }\n.ax-ev__code--err { color: var(--ax-err); border-color: color-mix(in oklch, var(--ax-err) 35%, var(--ax-border)); }\n.ax-ev__thought {\n margin-top: 4px; color: var(--ax-text-2); font-style: italic;\n font-size: var(--ax-fs-sm); line-height: 1.55; text-wrap: pretty;\n}\n.ax-ev__text {\n margin-top: 4px; color: var(--ax-text); font-size: var(--ax-fs-sm);\n line-height: 1.55; text-wrap: pretty; white-space: pre-wrap; word-break: break-word;\n}\n\n`\n\nconst TASK_PAGE_JS = `\n(function () {\n var root = document.querySelector('.ax-task-page');\n if (!root) return;\n var taskId = root.getAttribute('data-task-id');\n var agentId = root.getAttribute('data-agent-id');\n var nodeUrl = root.getAttribute('data-node-url');\n\n var outputEl = document.getElementById('task-output');\n var statusEl = document.getElementById('task-status');\n var elapsedEl = document.getElementById('task-elapsed');\n var inputEl = document.getElementById('task-input');\n var hintEl = document.getElementById('task-hint');\n var sendBtn = document.getElementById('task-send');\n var stopBtn = document.getElementById('task-stop');\n\n var buffer = '';\n var raf = 0;\n var startedAt = Date.now();\n var finished = false;\n\n function setStatus(label, kind) {\n statusEl.textContent = label;\n statusEl.className = 'ax-badge ax-badge--mono' + (kind ? ' is-' + kind : '');\n }\n\n function esc(x) {\n return String(x == null ? '' : x).replace(/[&<>\"']/g, function (c) {\n return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c];\n });\n }\n\n // --- Structured stream rendering ---\n //\n // The daemon emits one line per event with a sigil prefix. Rendering each\n // kind as its own block — tool call, tool result, internal reasoning, reply,\n // error — is what makes a long run scannable: you can find the tool that\n // failed without reading everything above it.\n\n // --- Minimal markdown for assistant replies ---\n //\n // Agents answer in markdown, and the stream used to render it as literal\n // characters: **bold**, backticks and fenced blocks all showed as source.\n //\n // Deliberately small and dependency-free — this is a self-hosted dashboard\n // with a strict no-CDN posture, and a full parser is not worth a bundle for\n // bold/code/lists/headings/links, which is what replies actually contain.\n //\n // SECURITY: escape first, THEN apply markup. Agent output is text from the\n // outside world (a Telegram message can steer what an agent echoes), so it\n // must never be able to inject HTML. Fenced blocks are extracted before any\n // inline pass so their contents are never treated as markup.\n function mdToHtml(src) {\n var fences = [];\n var text = String(src == null ? '' : src).replace(/\\\\u0060\\\\u0060\\\\u0060([\\\\s\\\\S]*?)\\\\u0060\\\\u0060\\\\u0060/g, function (_m, code) {\n fences.push(code.replace(/^[a-zA-Z0-9_-]*\\\\n/, ''));\n return '\\\\u0000FENCE' + (fences.length - 1) + '\\\\u0000';\n });\n\n text = esc(text);\n\n // Block level, line by line.\n var lines = text.split('\\\\n');\n var out = [];\n var listOpen = null;\n var para = [];\n function flushPara() {\n if (!para.length) return;\n out.push('<p>' + para.join('\\\\n') + '</p>');\n para = [];\n }\n // Note: closing a list must NOT flush the open paragraph — closeList()\n // runs before every plain line, so doing both here emitted one <p> per\n // line and defeated the grouping entirely. Callers flush explicitly.\n function closeList() { if (listOpen) { out.push('</' + listOpen + '>'); listOpen = null; } }\n for (var i = 0; i < lines.length; i++) {\n var ln = lines[i];\n var h = ln.match(/^(#{1,4})\\\\s+(.*)$/);\n if (h) { flushPara(); closeList(); out.push('<h' + (h[1].length + 2) + '>' + inline(h[2]) + '</h' + (h[1].length + 2) + '>'); continue; }\n var ul = ln.match(/^\\\\s*[-*]\\\\s+(.*)$/);\n if (ul) {\n flushPara();\n if (listOpen !== 'ul') { closeList(); out.push('<ul>'); listOpen = 'ul'; }\n out.push('<li>' + inline(ul[1]) + '</li>'); continue;\n }\n var ol = ln.match(/^\\\\s*\\\\d+\\\\.\\\\s+(.*)$/);\n if (ol) {\n flushPara();\n if (listOpen !== 'ol') { closeList(); out.push('<ol>'); listOpen = 'ol'; }\n out.push('<li>' + inline(ol[1]) + '</li>'); continue;\n }\n closeList();\n if (ln.trim() === '') { flushPara(); continue; }\n // Consecutive non-blank lines are ONE paragraph, not one each. Agents\n // dump JSON and log tails into replies; a <p> per line turned those into\n // a column of double-spaced fragments. Blank lines still break.\n para.push(inline(ln));\n }\n flushPara();\n closeList();\n\n var html = out.join('\\\\n');\n // Restore fenced blocks last so nothing inside them was ever parsed.\n html = html.replace(/(?:<p>)?\\\\u0000FENCE(\\\\d+)\\\\u0000(?:<\\\\/p>)?/g, function (_m, n) {\n return '<pre class=\"ax-ev__code\">' + esc(fences[Number(n)]) + '</pre>';\n });\n return html;\n }\n\n function inline(s) {\n return s\n .replace(/\\\\u0060([^\\\\u0060]+)\\\\u0060/g, '<code>$1</code>')\n .replace(/\\\\*\\\\*([^*]+)\\\\*\\\\*/g, '<strong>$1</strong>')\n .replace(/(^|[^*])\\\\*([^*]+)\\\\*/g, '$1<em>$2</em>')\n // Links: only http(s), and the label is already escaped.\n .replace(/\\\\[([^\\\\]]+)\\\\]\\\\((https?:\\\\/\\\\/[^\\\\s)]+)\\\\)/g,\n '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>');\n }\n\n var openText = null;\n var openRaw = '';\n function block(cls, html) {\n var ev = document.createElement('div');\n ev.className = 'ax-ev ' + cls;\n ev.innerHTML = html;\n outputEl.appendChild(ev);\n return ev;\n }\n function label(kind, text) {\n return '<div class=\"ax-ev__head\"><span class=\"ax-ev__label ax-ev__label--' + kind + '\">' +\n esc(text) + '</span></div>';\n }\n function systemEvent(text, done) {\n closeText();\n block('ax-ev--system' + (done ? ' is-done' : ''),\n '<div class=\"ax-ev__head\"><span class=\"ax-ev__label ax-ev__label--soft\">system</span><span>' +\n esc(text) + '</span></div>');\n }\n function toolUse(text) {\n closeText();\n // 'ToolName({...})' — best-effort split on the first paren.\n var o = text.indexOf('('), c = text.lastIndexOf(')');\n var name = o > 0 ? text.slice(0, o) : text;\n var args = (o > 0 && c > o) ? text.slice(o + 1, c) : '';\n block('ax-ev--tool',\n '<div class=\"ax-ev__head\"><span class=\"ax-ev__label ax-ev__label--tool\">tool call</span>' +\n '<span class=\"ax-ev__tool\">' + esc(name) + '</span></div>' +\n (args ? '<pre class=\"ax-ev__code\">' + esc(args) + '</pre>' : ''));\n }\n function toolResult(text) {\n closeText();\n var isErr = text.indexOf('[error] ') === 0;\n var body = isErr ? text.slice(8) : text;\n block('ax-ev--tool-result' + (isErr ? ' is-err' : ''),\n label('result', isErr ? 'tool error' : 'tool result') +\n '<pre class=\"ax-ev__code ' + (isErr ? 'ax-ev__code--err' : 'ax-ev__code--muted') + '\">' +\n esc(body) + '</pre>');\n }\n function thought(text) {\n closeText();\n block('ax-ev--thought', label('soft', 'internal') +\n '<div class=\"ax-ev__thought\">' + esc(text) + '</div>');\n }\n function errorEvent(text) {\n closeText();\n block('ax-ev--error', label('error', 'error') +\n '<pre class=\"ax-ev__code ax-ev__code--err\">' + esc(text) + '</pre>');\n }\n function replyLine(line) {\n if (!openText) {\n openText = block('ax-ev--text', label('text', 'response') + '<div class=\"ax-ev__text\"></div>')\n .querySelector('.ax-ev__text');\n openRaw = '';\n }\n openRaw += (openRaw ? '\\\\n' : '') + line;\n openText.setAttribute('data-raw', openRaw);\n }\n /** Re-render every open reply block from its accumulated source. Done once\n * per frame rather than per line — markdown is whole-block by nature\n * (a fence isn't valid until it closes). */\n function renderOpenText() {\n if (openText) openText.innerHTML = mdToHtml(openRaw);\n }\n /** Close the current reply block, rendering it on the way out. Every event\n * that interrupts a reply must go through here — nulling openText without\n * rendering leaves the block showing its label and nothing else, which is\n * exactly what happened before this existed. */\n function closeText() { renderOpenText(); openText = null; openRaw = ''; }\n\n function processLine(line) {\n if (line.indexOf('· ') === 0) return systemEvent(line.slice(2));\n if (line.indexOf('→ ') === 0) return toolUse(line.slice(2));\n if (line.indexOf('← ') === 0) return toolResult(line.slice(2));\n if (line.indexOf('💭 ') === 0) return thought(line.slice(2));\n if (line.indexOf('[error] ') === 0) return errorEvent(line.slice(8));\n if (line.indexOf('[task finished]') === 0) return systemEvent('task finished', true);\n if (line === '') {\n // Blank line = paragraph break inside a reply. Keep the block open.\n if (openText) { openRaw += '\\\\n\\\\n'; }\n return;\n }\n replyLine(line);\n }\n\n // Batch through requestAnimationFrame — a chatty agent emits hundreds of\n // chunks a second and one DOM write per chunk drops frames. Only the last\n // partial line is held back, so a half-arrived event never renders.\n var lineBuf = '';\n function flush() {\n raf = 0;\n if (!buffer) return;\n var atBottom = outputEl.scrollHeight - outputEl.scrollTop - outputEl.clientHeight < 60;\n lineBuf += buffer;\n buffer = '';\n var lines = lineBuf.split('\\\\n');\n lineBuf = lines.pop();\n for (var i = 0; i < lines.length; i++) processLine(lines[i]);\n if (atBottom) outputEl.scrollTop = outputEl.scrollHeight;\n }\n function append(text) {\n if (!text) return;\n buffer += text;\n if (!raf) raf = requestAnimationFrame(flush);\n }\n /** Render whatever partial line is left. Call when the stream ends. */\n function drain() {\n flush();\n if (lineBuf) { processLine(lineBuf); lineBuf = ''; }\n renderOpenText();\n }\n\n /** Render the request that started this run. Hidden until we have one —\n * an empty labelled box is worse than no box. */\n function showAsk(message, sender, startedAt) {\n var text = (message || '').trim();\n if (!text) return;\n document.getElementById('task-ask-body').innerHTML = mdToHtml(text);\n document.getElementById('task-ask-who').textContent = sender ? 'from ' + sender : '';\n var when = document.getElementById('task-ask-when');\n if (startedAt) {\n var d = new Date(startedAt);\n when.textContent = isNaN(d.getTime()) ? '' : d.toLocaleString();\n }\n document.getElementById('task-ask').hidden = false;\n }\n\n function tickElapsed() {\n if (finished) return;\n var s = Math.floor((Date.now() - startedAt) / 1000);\n var m = Math.floor(s / 60);\n elapsedEl.textContent = m > 0 ? m + 'm ' + (s % 60) + 's' : s + 's';\n }\n setInterval(tickElapsed, 1000);\n tickElapsed();\n\n // A request handed over by the caller renders immediately, before any\n // stream data arrives — so the page never shows work without the ask.\n var seededAsk = root.getAttribute('data-ask');\n if (seededAsk) showAsk(seededAsk, '', root.getAttribute('data-ask-at'));\n\n // Archived: one fetch of the stored record, no SSE. Opening a stream for a\n // task that ended hours ago would sit \"connecting…\" forever and then report\n // a disconnect, which reads as breakage rather than as history.\n if (root.getAttribute('data-archived')) {\n finished = true;\n elapsedEl.textContent = '';\n setStatus('loading…', '');\n fetch('/api/task/history?node=' + encodeURIComponent(nodeUrl)\n + '&agent=' + encodeURIComponent(agentId)\n + '&task=' + encodeURIComponent(taskId))\n .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })\n .then(function (rec) {\n setStatus(rec.ok ? 'archived' : 'failed', rec.ok ? 'done' : 'err');\n showAsk(rec.message, rec.sender, rec.startedAt);\n var tx = rec.transcript || '';\n if (tx) append(tx);\n // Only append the final reply when the transcript didn't already\n // carry it, so the answer isn't printed twice.\n if (rec.responseText && (!tx || tx.indexOf(rec.responseText) === -1)) {\n append('\\\\n\\\\n--- Final reply ---\\\\n' + rec.responseText);\n }\n if (rec.error) append('\\\\n\\\\n[error] ' + rec.error);\n if (rec.durationMs) {\n var s = Math.round(rec.durationMs / 1000);\n elapsedEl.textContent = s < 60 ? s + 's' : Math.floor(s / 60) + 'm ' + (s % 60) + 's';\n }\n drain();\n })\n .catch(function (e) { setStatus(e.message, 'err'); });\n return;\n }\n\n var url = '/api/task/stream?node=' + encodeURIComponent(nodeUrl)\n + '&agent=' + encodeURIComponent(agentId)\n + '&task=' + encodeURIComponent(taskId);\n var es;\n try { es = new EventSource(url); } catch (e) { setStatus('connect failed', 'err'); return; }\n\n es.addEventListener('start', function (ev) {\n setStatus('live', 'live');\n try {\n var d = JSON.parse(ev.data);\n if (d.message || d.sender) showAsk(d.message, d.sender, d.startedAt);\n if (d.initial) append(d.initial);\n if (d.done) { finished = true; setStatus('finished', 'done'); }\n } catch (e) {}\n });\n es.addEventListener('chunk', function (ev) {\n try { append(JSON.parse(ev.data).text || ''); } catch (e) {}\n });\n es.addEventListener('end', function () {\n finished = true;\n setStatus('finished', 'done');\n try { es.close(); } catch (e) {}\n drain();\n });\n es.addEventListener('error', function () {\n if (!finished) setStatus('disconnected', 'err');\n });\n\n function action(kind, message) {\n var q = '/api/task/action?node=' + encodeURIComponent(nodeUrl)\n + '&task=' + encodeURIComponent(taskId) + '&kind=' + encodeURIComponent(kind);\n return fetch(q, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(message ? { message: message } : {})\n }).then(function (r) { return r.json().catch(function () { return {}; }); });\n }\n\n sendBtn.addEventListener('click', function () {\n var text = (inputEl.value || '').trim();\n if (!text) { inputEl.focus(); return; }\n sendBtn.disabled = true;\n hintEl.textContent = 'queuing…';\n action('followup', text).then(function (r) {\n sendBtn.disabled = false;\n if (r && r.error) { hintEl.textContent = 'failed: ' + r.error; return; }\n inputEl.value = '';\n hintEl.textContent = 'queued — dispatches as the next turn';\n });\n });\n\n stopBtn.addEventListener('click', function () {\n stopBtn.disabled = true;\n hintEl.textContent = 'stopping…';\n action('cancel').then(function (r) {\n stopBtn.disabled = false;\n hintEl.textContent = (r && r.error) ? 'failed: ' + r.error : 'stop requested';\n });\n });\n\n inputEl.addEventListener('keydown', function (e) {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); sendBtn.click(); }\n });\n})();\n`\n","import { renderShell, esc, type TopbarPeer } from \"..\"\n\n// --- Agent history page ---\n//\n// Replaces the 360px right-hand drawer that used to slide over Live. A drawer\n// was the wrong container for this: the list is the thing you came to read, it\n// wants width for message previews, and every row leads somewhere — so it\n// needs to be linkable and back-navigable, which a drawer over another page\n// can't be.\n//\n// One job: what has this agent done recently. One action per row: open that\n// task. Reads GET /api/task/history?node=&agent=&limit=.\n\nexport interface HistoryPageOpts {\n agentId: string\n agentName?: string\n nodeUrl: string\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderHistoryPage(opts: HistoryPageOpts): string {\n const who = opts.agentName || opts.agentId\n\n const body = `<div class=\"ax-history\"\n data-agent-id=\"${esc(opts.agentId)}\"\n data-agent-name=\"${esc(who)}\"\n data-node-url=\"${esc(opts.nodeUrl)}\">\n <header class=\"ax-history__head\">\n <nav class=\"ax-crumbs\" aria-label=\"Breadcrumb\">\n <a href=\"/live\">Live</a>\n <span class=\"ax-crumbs__sep\">/</span>\n <span class=\"ax-mention\">@${esc(opts.agentId)}</span>\n <span class=\"ax-crumbs__sep\">/</span>\n <span class=\"ax-crumbs__here\">History</span>\n </nav>\n </header>\n <!-- Summary of the list below, not of the agent in general. Three numbers\n and the channel mix — enough to see \"mostly GitLab, two failures\" at a\n glance without reading 50 rows. Cost and health live on their own\n pages; repeating them here would make this a status report. -->\n <section class=\"ax-hist-sum\" id=\"hist-sum\" hidden>\n <div class=\"ax-hist-sum__nums\">\n <span><b id=\"hs-total\">0</b> tasks</span>\n <span class=\"ax-hist-sum__fail\" id=\"hs-fail-wrap\" hidden><b id=\"hs-fail\">0</b> failed</span>\n <span><b id=\"hs-time\">—</b> of agent time</span>\n <span class=\"ax-hist-sum__span\" id=\"hs-span\"></span>\n </div>\n <div class=\"ax-hist-sum__chips\" id=\"hs-chips\"></div>\n </section>\n <div class=\"ax-history__list\" id=\"hist-list\">\n <div class=\"ax-history__empty\">loading…</div>\n </div>\n</div>`\n\n return renderShell({\n title: `AgentX · ${who} history`,\n activeTab: \"live\",\n subtitle: \"History\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: HISTORY_CSS,\n scripts: `<script>${HISTORY_JS}</script>`,\n })\n}\n\nconst HISTORY_CSS = `\n/* <main> carries no padding in the shell — without this the breadcrumb sits\n flush against the sticky topbar. */\n.ax-history {\n display: flex; flex-direction: column; gap: var(--ax-gap);\n max-width: 1000px; margin: 0 auto; padding: 22px var(--ax-pad) var(--ax-pad);\n}\n.ax-crumbs { display: flex; align-items: center; gap: 8px; font-size: var(--ax-fs-sm); min-width: 0; }\n.ax-crumbs a { color: var(--ax-text-2); font-weight: 600; text-decoration: none; }\n.ax-crumbs a:hover { color: var(--ax-accent); text-decoration: none; }\n.ax-crumbs__sep { color: var(--ax-border-2); }\n.ax-crumbs__here { color: var(--ax-text); font-weight: 700; }\n.ax-history__head { display: flex; align-items: center; justify-content: space-between; gap: var(--ax-gap); flex-wrap: wrap; }\n.ax-history__who { display: flex; align-items: center; gap: 12px; min-width: 0; }\n.ax-history__back { font-size: var(--ax-fs-sm); font-weight: 600; white-space: nowrap; }\n.ax-history__name { color: var(--ax-text-2); }\n.ax-hist-sum {\n display: flex; align-items: center; justify-content: space-between; gap: var(--ax-gap);\n flex-wrap: wrap; padding: 10px 14px;\n background: var(--ax-surface-2); border: var(--ax-border-w) solid var(--ax-border);\n border-radius: var(--ax-radius);\n}\n.ax-hist-sum__nums { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; font-size: var(--ax-fs-sm); color: var(--ax-text-2); }\n.ax-hist-sum__nums b { color: var(--ax-text); font-family: var(--ax-mono); font-weight: 700; }\n.ax-hist-sum__fail b { color: var(--ax-err); }\n.ax-hist-sum__span { color: var(--ax-muted); font-size: var(--ax-fs-xs); }\n.ax-hist-sum__chips { display: flex; gap: 6px; flex-wrap: wrap; }\n.ax-history__list { display: flex; flex-direction: column; gap: 8px; }\n.ax-history__empty { color: var(--ax-muted); padding: var(--ax-pad); text-align: center; }\n\n/* One row per task. The whole row is the link — a row with a nested \"open\"\n button would be two targets for one intent. */\n.ax-history__row {\n display: block; text-decoration: none; color: inherit;\n background: var(--ax-surface); border: var(--ax-border-w) solid var(--ax-border);\n border-radius: var(--ax-radius); box-shadow: var(--ax-shadow);\n padding: 12px 14px; transition: border-color .1s, transform .06s, box-shadow .06s;\n}\n.ax-history__row:hover { border-color: var(--ax-accent); text-decoration: none; }\n.ax-history__row:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--ax-border); }\n.ax-history__row.is-err { border-color: var(--ax-red-e); }\n.ax-history__top { display: flex; align-items: center; gap: 8px; font-size: var(--ax-fs-xs); }\n.ax-history__flag { font-weight: 700; }\n.ax-history__flag.ok { color: var(--ax-ok); }\n.ax-history__flag.err { color: var(--ax-err); }\n.ax-history__when { margin-left: auto; color: var(--ax-muted); font-family: var(--ax-mono); }\n.ax-history__preview {\n margin-top: 6px; font-size: var(--ax-fs-sm); color: var(--ax-text-2);\n line-height: 1.5; overflow: hidden; display: -webkit-box;\n -webkit-line-clamp: 2; -webkit-box-orient: vertical;\n}\n.ax-history__foot { margin-top: 6px; font-size: var(--ax-fs-xs); color: var(--ax-muted); font-family: var(--ax-mono); }\n`\n\nconst HISTORY_JS = `\n(function () {\n var root = document.querySelector('.ax-history');\n if (!root) return;\n var agentId = root.getAttribute('data-agent-id');\n var agentName = root.getAttribute('data-agent-name');\n var nodeUrl = root.getAttribute('data-node-url');\n var listEl = document.getElementById('hist-list');\n\n function esc(s) {\n return String(s == null ? '' : s).replace(/[&<>\"']/g, function (c) {\n return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c];\n });\n }\n function ago(iso) {\n var t = new Date(iso).getTime();\n if (!t) return '';\n var s = Math.floor((Date.now() - t) / 1000);\n if (s < 60) return s + 's ago';\n var m = Math.floor(s / 60); if (m < 60) return m + 'm ago';\n var h = Math.floor(m / 60); if (h < 24) return h + 'h ago';\n return Math.floor(h / 24) + 'd ago';\n }\n function dur(ms) {\n if (!ms) return '—';\n var s = Math.round(ms / 1000);\n if (s < 60) return s + 's';\n var m = Math.floor(s / 60);\n if (m < 60) return m + 'm ' + (s % 60) + 's';\n // Totals across 50 tasks run to hours — \"226m 18s\" is arithmetic, not an\n // answer. Drop seconds once we're past an hour; nobody reads them there.\n return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';\n }\n function taskUrl(id, channel) {\n return '/tasks/' + encodeURIComponent(id)\n + '?agent=' + encodeURIComponent(agentId)\n + '&node=' + encodeURIComponent(nodeUrl)\n + '&name=' + encodeURIComponent(agentName)\n + (channel ? '&channel=' + encodeURIComponent(channel) : '')\n + '&archived=1';\n }\n\n /** Everything here is derived from the rows we already fetched — no second\n * request, and the numbers can never disagree with the list under them. */\n function summarize(items) {\n var fails = 0, totalMs = 0, byChannel = {};\n var oldest = Infinity, newest = 0;\n for (var i = 0; i < items.length; i++) {\n var it = items[i];\n if (!it.ok) fails++;\n totalMs += it.durationMs || 0;\n var ch = it.channel || 'other';\n byChannel[ch] = (byChannel[ch] || 0) + 1;\n var t = new Date(it.endedAt).getTime();\n if (t) { if (t < oldest) oldest = t; if (t > newest) newest = t; }\n }\n document.getElementById('hs-total').textContent = items.length;\n document.getElementById('hs-time').textContent = dur(totalMs);\n if (fails) {\n document.getElementById('hs-fail').textContent = fails;\n document.getElementById('hs-fail-wrap').hidden = false;\n }\n if (oldest !== Infinity && newest > oldest) {\n document.getElementById('hs-span').textContent = 'spanning ' + ago(oldest).replace(' ago', '');\n }\n var chips = Object.keys(byChannel).sort(function (a, b) { return byChannel[b] - byChannel[a]; });\n document.getElementById('hs-chips').innerHTML = chips.map(function (c) {\n return '<span class=\"ax-chip\">' + esc(c) + ' ' + byChannel[c] + '</span>';\n }).join('');\n document.getElementById('hist-sum').hidden = false;\n }\n\n fetch('/api/task/history?node=' + encodeURIComponent(nodeUrl)\n + '&agent=' + encodeURIComponent(agentId) + '&limit=50')\n .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })\n .then(function (items) {\n if (!Array.isArray(items) || !items.length) {\n listEl.innerHTML = '<div class=\"ax-history__empty\">No recorded tasks yet.</div>';\n return;\n }\n summarize(items);\n listEl.innerHTML = items.map(function (it) {\n return '<a class=\"ax-history__row' + (it.ok ? '' : ' is-err') + '\" href=\"' + esc(taskUrl(it.id, it.channel)) + '\">' +\n '<div class=\"ax-history__top\">' +\n '<span class=\"ax-history__flag ' + (it.ok ? 'ok\">✓' : 'err\">✗') + '</span>' +\n '<span class=\"ax-badge ax-badge--mono ax-badge--ghost\">' + esc(it.channel || '—') + '</span>' +\n (it.sender ? '<span class=\"ax-muted\">' + esc(it.sender) + '</span>' : '') +\n '<span class=\"ax-history__when\">' + esc(ago(it.endedAt)) + '</span>' +\n '</div>' +\n '<div class=\"ax-history__preview\">' + esc((it.message || '').slice(0, 300)) + '</div>' +\n '<div class=\"ax-history__foot\">' + esc(dur(it.durationMs)) +\n (it.error ? ' · ' + esc(it.error.slice(0, 90)) : '') +\n '</div>' +\n '</a>';\n }).join('');\n })\n .catch(function (e) {\n listEl.innerHTML = '<div class=\"ax-history__empty\" style=\"color:var(--ax-err)\">' + esc(e.message) + '</div>';\n });\n})();\n`\n","import type { IncomingMessage, ServerResponse } from \"http\"\nimport {\n LayoutStore,\n RunStore,\n WorkflowStore,\n lintWorkflow,\n workflowSchema,\n type WorkflowLayout,\n type WorkflowRun,\n} from \"@/workflows\"\nimport type { WorkflowDispatcher } from \"@/workflows/dispatcher\"\n\n// --- Workflows HTTP API ---\n//\n// Powers the /workflows dashboard (observability).\n// Reads require `dashboard:read`; writes require `dashboard:write`. The\n// caller passes in a `requireScope(req, res, [...])` shim so this module\n// stays decoupled from src/daemon/token-store.ts. The daemon wires the real\n// middleware at boot.\n//\n// Endpoints:\n// GET /api/workflows — list of workflow definitions\n// POST /api/workflows — create a new workflow (body = Workflow JSON)\n// GET /api/workflows/:id — one workflow\n// PUT /api/workflows/:id — full replace (body = Workflow JSON)\n// DELETE /api/workflows/:id — delete definition + its layout\n// POST /api/workflows/:id/validate — schema + lint on the submitted body\n// (read-only \"would this save?\" check)\n// GET /api/workflows/:id/layout — node positions for the editor\n// PUT /api/workflows/:id/layout — save node positions\n// GET /api/workflows/runs?limit=N — recent runs across all workflows\n// GET /api/workflows/runs/:id — single run detail\n// GET /api/workflows/runs/:id/stream — SSE stream of that run's latest snapshot\n//\n// The server is forgiving on authoring errors: save endpoints return a\n// structured `{ error, issues: [...] }` body so the editor can highlight\n// which field broke, rather than a bare 400.\n\nexport interface WorkflowsApiDeps {\n store: WorkflowStore\n runs: RunStore\n layouts: LayoutStore\n dispatcher?: WorkflowDispatcher\n /** Gate the request; write the 401/403 response and return falsy on failure. */\n requireScope: (req: IncomingMessage, res: ServerResponse, scopes: string[]) => unknown\n /** Called when a run mutates — daemon exposes its broadcaster so the SSE\n * stream can be woken rather than polled. Optional; the stream falls back\n * to polling if not provided. */\n runEvents?: {\n subscribe: (runId: string, handler: (run: WorkflowRun) => void) => () => void\n }\n}\n\n/** Router the daemon calls from its HTTP handler. Returns true if the\n * request was handled (response written), false if the path isn't ours. */\nexport function handleWorkflowsApi(req: IncomingMessage, res: ServerResponse, deps: WorkflowsApiDeps): boolean {\n const url = req.url || \"\"\n if (!url.startsWith(\"/api/workflows\")) return false\n\n const method = req.method || \"GET\"\n\n // /api/workflows (GET list, POST create)\n if (url === \"/api/workflows\") {\n if (method === \"GET\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n return sendJson(res, 200, { workflows: deps.store.list() })\n }\n if (method === \"POST\") {\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n const q = new URL(url, \"http://_\").searchParams\n const convertFromYaml = q.get(\"convertFromYaml\") === \"true\" || q.get(\"convertFromYaml\") === \"1\"\n return withBody(req, res, (body) => handleSave(res, deps, body, { mode: \"create\", convertFromYaml }))\n }\n }\n\n\n // /api/workflows/runs[...] (list / one / stream / status-mutation)\n // Reads (GET) and the new POST /:id/status both flow into handleRuns; the\n // sub-route enforces its own method and scope checks.\n if (url.startsWith(\"/api/workflows/runs\")) {\n if (method === \"GET\" && !deps.requireScope(req, res, [\"dashboard:read\"])) return true\n return handleRuns(req, res, deps, url)\n }\n\n // /api/workflows/signal/:name — POST manual signal emission.\n // Must also run before the /:id regex (signal name shouldn't be treated\n // as a workflow id).\n if (url.startsWith(\"/api/workflows/signal/\")) {\n if (method !== \"POST\") return sendJson(res, 405, { error: \"method not allowed\" })\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n if (!deps.dispatcher) return sendJson(res, 501, { error: \"dispatcher not enabled\" })\n const name = decodeURIComponent(url.replace(/^\\/api\\/workflows\\/signal\\//, \"\").split(\"?\")[0])\n if (!name) return sendJson(res, 400, { error: \"signal name required\" })\n return withBody(req, res, (body) => {\n const b = body as { scope?: \"workflow\" | \"global\"; workflowId?: string; payload?: Record<string, unknown> } | undefined\n const emission = deps.dispatcher!.emitSignal({\n name,\n scope: b?.scope,\n workflowId: b?.workflowId,\n payload: b?.payload,\n })\n return sendJson(res, 200, { ok: true, emission })\n })\n }\n\n // /api/workflows/:id[/layout|/validate] (various)\n const defMatch = url.match(/^\\/api\\/workflows\\/([^\\/?]+)(\\/layout|\\/validate)?(\\?.*)?$/)\n if (defMatch) {\n const id = decodeURIComponent(defMatch[1])\n const suffix = defMatch[2]\n\n if (suffix === \"/layout\") {\n if (method === \"GET\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n return sendJson(res, 200, { layout: deps.layouts.get(id) })\n }\n if (method === \"PUT\") {\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n return withBody(req, res, (body) => {\n try {\n const saved = deps.layouts.save(id, body as WorkflowLayout)\n return sendJson(res, 200, { layout: saved })\n } catch (e: any) {\n return sendJson(res, 400, { error: \"invalid layout\", message: e.message })\n }\n })\n }\n }\n\n if (suffix === \"/validate\" && method === \"POST\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n return withBody(req, res, (body) => {\n const parsed = workflowSchema.safeParse(body)\n if (!parsed.success) {\n return sendJson(res, 200, {\n ok: false,\n issues: parsed.error.issues.map((i) => ({ path: i.path.join(\".\"), message: i.message })),\n })\n }\n const lintIssues = lintWorkflow(parsed.data).map((m) => ({ path: \"\", message: m }))\n return sendJson(res, 200, { ok: lintIssues.length === 0, issues: lintIssues, workflow: parsed.data })\n })\n }\n\n if (!suffix) {\n if (method === \"GET\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n const wf = deps.store.get(id)\n if (!wf) return sendJson(res, 404, { error: \"workflow not found\" })\n return sendJson(res, 200, { workflow: wf })\n }\n if (method === \"PUT\") {\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n const q = new URL(url, \"http://_\").searchParams\n const convertFromYaml = q.get(\"convertFromYaml\") === \"true\" || q.get(\"convertFromYaml\") === \"1\"\n return withBody(req, res, (body) => handleSave(res, deps, body, { mode: \"update\", id, convertFromYaml }))\n }\n if (method === \"DELETE\") {\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n const existed = deps.store.delete(id)\n deps.layouts.delete(id)\n return sendJson(res, existed ? 200 : 404, existed ? { ok: true } : { error: \"workflow not found\" })\n }\n }\n }\n\n return sendJson(res, 404, { error: \"unknown workflows endpoint\", path: url })\n}\n\n\n// -------------------- save (create/update) --------------------\n\n// Top-level workflow keys the editor doesn't yet have UI controls for. When\n// the editor saves a workflow without these keys present in the raw payload\n// (typical when an older cached IIFE bundle is in the user's browser), we\n// preserve the on-disk value instead of letting Zod's default silently flip\n// the field back. Without this guard, server-only behaviours like\n// `mesh.allowRemote: true` get reset on every Save until the user\n// hard-refreshes — see 2026-04-23 incident where a cached editor repeatedly\n// stripped mesh and broke the cross-mesh broadcast.\n//\n// Add new keys here when you ship a server-side workflow feature ahead of\n// matching editor UI; remove a key once the editor sends it.\nconst EDITOR_BLIND_KEYS = [\"mesh\"] as const\n\nfunction handleSave(\n res: ServerResponse,\n deps: WorkflowsApiDeps,\n body: unknown,\n opts:\n | { mode: \"create\"; convertFromYaml?: boolean }\n | { mode: \"update\"; id: string; convertFromYaml?: boolean },\n): true {\n const parsed = workflowSchema.safeParse(body)\n if (!parsed.success) {\n return sendJson(res, 400, {\n error: \"invalid workflow\",\n issues: parsed.error.issues.map((i) => ({ path: i.path.join(\".\"), message: i.message })),\n })\n }\n const wf = parsed.data\n const lintIssues = lintWorkflow(wf)\n if (lintIssues.length > 0) {\n return sendJson(res, 400, {\n error: \"lint errors\",\n issues: lintIssues.map((m) => ({ path: \"\", message: m })),\n })\n }\n if (opts.mode === \"update\" && wf.id !== opts.id) {\n return sendJson(res, 400, { error: \"workflow id does not match path\" })\n }\n if (opts.mode === \"create\" && deps.store.get(wf.id)) {\n return sendJson(res, 409, { error: `workflow \"${wf.id}\" already exists` })\n }\n\n // Preserve editor-blind keys from disk. We check the RAW payload (not the\n // parsed one — Zod has already filled defaults) to distinguish \"editor\n // omitted this field\" from \"editor explicitly set the default value\".\n if (opts.mode === \"update\" && body && typeof body === \"object\") {\n const raw = body as Record<string, unknown>\n const existing = deps.store.get(opts.id)\n if (existing) {\n for (const key of EDITOR_BLIND_KEYS) {\n if (!(key in raw) && key in existing) {\n ;(wf as Record<string, unknown>)[key] = (existing as Record<string, unknown>)[key]\n }\n }\n }\n }\n\n // store.save() round-trips YAML-authored workflows back to YAML\n // (preserving comments) by default; the only way it surfaces a\n // yaml-related error here is if the round-trip itself fails (e.g. the\n // source file has a structure the merger doesn't understand). In that\n // case we offer the editor a \"convert to JSON\" escape hatch via the\n // 409 + kind:yaml-authored response. Other throws bubble up as 500;\n // catching here is also a backstop against the throw escaping the\n // `req.on(\"end\")` callback and taking the dashboard down with it.\n let saved\n try {\n saved = deps.store.save(wf, { convertFromYaml: opts.convertFromYaml })\n } catch (e: any) {\n const message: string = e?.message || String(e)\n const yamlPath: string | undefined = e?.yamlPath\n const yamlSibling = !!yamlPath || /yaml round-trip failed/.test(message)\n return sendJson(res, yamlSibling ? 409 : 500, {\n error: yamlSibling\n ? \"couldn't preserve comments while saving back to the YAML file. Re-submit with ?convertFromYaml=true to convert this workflow to JSON (comments will be lost).\"\n : message,\n kind: yamlSibling ? \"yaml-authored\" : \"save-failed\",\n ...(yamlPath ? { path: yamlPath } : {}),\n })\n }\n // Prune stale layout entries if any nodes were removed since the last save.\n deps.layouts.sync(saved.id, saved.nodes.map((n) => n.id))\n return sendJson(res, 200, { workflow: saved })\n}\n\n// -------------------- runs --------------------\n\nfunction handleRuns(req: IncomingMessage, res: ServerResponse, deps: WorkflowsApiDeps, url: string): boolean {\n const q = new URL(url, \"http://_\").searchParams\n const trail = url.replace(/^\\/api\\/workflows\\/runs/, \"\")\n if (trail === \"\" || trail.startsWith(\"?\")) {\n const limit = Math.max(1, Math.min(500, Number(q.get(\"limit\") || 50)))\n const workflowId = q.get(\"workflowId\") || undefined\n const summary = q.get(\"summary\") === \"1\" || q.get(\"summary\") === \"true\"\n const runs = deps.runs.list({ workflowId, limit })\n if (!summary) return sendJson(res, 200, { runs })\n // Drop the heavyweight `context` (per-node outputs + the full trigger\n // event payload — webhook bodies routinely run 10–50KB each). The list\n // UI on /workflows only uses metadata + the last history entry, so the\n // payload would otherwise gate first paint on the slowest serialization\n // path. Run-detail (GET /runs/:id) returns the full shape unchanged.\n const slim = runs.map((r) => ({\n id: r.id,\n workflowId: r.workflowId,\n workflowVersion: r.workflowVersion,\n homeNode: r.homeNode,\n status: r.status,\n pending: r.pending,\n entityRef: r.entityRef,\n // Trim history to last 5 entries — that's enough for the timeline\n // preview without dragging full agent outputs into the listing.\n history: (r.history || []).slice(-5),\n parentRunId: r.parentRunId,\n parentNodeId: r.parentNodeId,\n rootRunId: r.rootRunId,\n depth: r.depth,\n createdAt: r.createdAt,\n updatedAt: r.updatedAt,\n }))\n return sendJson(res, 200, { runs: slim })\n }\n // POST /api/workflows/runs/<id>/status body: { status: \"paused\"|\"running\"|\"canceled\" }\n // Mirrors `agentx workflow pause/resume/cancel <runId>`.\n const statusMatch = trail.match(/^\\/([^\\/?]+)\\/status$/)\n if (statusMatch) {\n if ((req.method || \"GET\").toUpperCase() !== \"POST\") return sendJson(res, 405, { error: \"method not allowed\" })\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n const runId = decodeURIComponent(statusMatch[1])\n return withBody(req, res, (body) => {\n const status = (body as any)?.status\n if (status !== \"paused\" && status !== \"running\" && status !== \"canceled\") {\n return sendJson(res, 400, { error: \"status must be paused|running|canceled\" })\n }\n const updated = deps.runs.setStatus(runId, status)\n if (!updated) return sendJson(res, 404, { error: \"run not found\" })\n return sendJson(res, 200, { ok: true, runId, status })\n })\n }\n const runMatch = trail.match(/^\\/([^\\/?]+)(\\/stream)?$/)\n if (runMatch) {\n const runId = decodeURIComponent(runMatch[1])\n if (runMatch[2]) return streamRun(req, res, deps, runId)\n const run = deps.runs.get(runId)\n if (!run) return sendJson(res, 404, { error: \"run not found\" })\n return sendJson(res, 200, { run })\n }\n return sendJson(res, 404, { error: \"unknown runs endpoint\" })\n}\n\n/** SSE run-detail stream. When `deps.runEvents` is provided, the stream is\n * push-driven; otherwise it polls the run-store every 2s. Either way the\n * client sees a complete snapshot per message. */\nfunction streamRun(req: IncomingMessage, res: ServerResponse, deps: WorkflowsApiDeps, runId: string): boolean {\n const run = deps.runs.get(runId)\n if (!run) { sendJson(res, 404, { error: \"run not found\" }); return true }\n\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n \"Connection\": \"keep-alive\",\n \"X-Accel-Buffering\": \"no\",\n })\n res.write(`data: ${JSON.stringify(run)}\\n\\n`)\n\n let closed = false\n const onEnd = () => { closed = true; unsubscribe?.(); clearInterval(timer); try { res.end() } catch { /* */ } }\n req.on(\"close\", onEnd)\n\n const emit = (r: WorkflowRun) => { if (closed) return; try { res.write(`data: ${JSON.stringify(r)}\\n\\n`) } catch { onEnd() } }\n\n const unsubscribe = deps.runEvents?.subscribe(runId, emit)\n\n // Fallback poll — also acts as the source when `runEvents` is missing,\n // and as a safety net when an event is missed during a fs flush.\n let lastUpdated = run.updatedAt\n const timer = setInterval(() => {\n if (closed) return\n const latest = deps.runs.get(runId)\n if (latest && latest.updatedAt !== lastUpdated) {\n lastUpdated = latest.updatedAt\n emit(latest)\n }\n }, 2000)\n\n return true\n}\n\n// -------------------- helpers --------------------\n\nfunction withBody(req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => boolean | true | Promise<boolean | true>): true {\n let raw = \"\"\n req.setEncoding(\"utf8\")\n req.on(\"data\", (chunk) => { raw += chunk })\n req.on(\"end\", () => {\n let parsed: unknown\n try { parsed = raw ? JSON.parse(raw) : {} } catch (e: any) {\n sendJson(res, 400, { error: \"invalid JSON body\", message: e.message })\n return\n }\n // Defensive double-wrap: handler() may throw synchronously (e.g.\n // workflowStore.save() refusing to overwrite a YAML-authored\n // workflow). The throw would escape the Promise.resolve below\n // because Promise.resolve only wraps the RETURN value — a sync\n // throw beats it. Without this try, the throw bubbles to the\n // IncomingMessage event listener and from there to the global\n // uncaughtException, killing the dashboard. We caught one such\n // crash 2026-05-09 on peer; never again.\n let result: ReturnType<typeof handler>\n try {\n result = handler(parsed)\n } catch (e: any) {\n try { sendJson(res, 500, { error: \"handler threw (sync)\", message: e?.message ?? String(e) }) }\n catch { /* response already sent */ }\n return\n }\n void Promise.resolve(result).catch((e: any) => {\n try { sendJson(res, 500, { error: \"handler threw\", message: e?.message ?? String(e) }) }\n catch { /* response already sent */ }\n })\n })\n req.on(\"error\", (e: any) => sendJson(res, 400, { error: \"body read failed\", message: e.message }))\n return true\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): true {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(body))\n return true\n}\n"],"mappings":"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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAsRdI,GAAiB,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA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AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiNjBI,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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C,aACtB,CAAC,CACH,CAEA,IAAMD,GAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuHfC,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAwkBX,IA5cU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IA6cV,QAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,aACT,MAAOD,EAAK,MACZ,cAAeA,EAAK,aACtB,CAAC,CACH,CCzkBO,SAASE,GAAmBC,EAAyB,CAAC,EAAW,CACtE,IAAMC,EAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QA6Bb,OAAOC,EAAY,CACjB,MAAO,uBACP,UAAW,WACX,SAAU,WACV,MAAOF,EAAK,MACZ,cAAeA,EAAK,cACpB,KAAAC,EACA,IAAKE,GACL,QAAS,WAAWC,aACtB,CAAC,CACH,CAEA,IAAMD,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6LpBC,GAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8kBlBG,GAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwHjBC,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0DA8MhBC,GAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ECtQlB,IAAMC,GAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ECNjB,IAAMC,GAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;WCM3B,IAAMC,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;WCL1B,IAAMC,GAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;WCD9B,IAAMC,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAiBd,CAAC,CACH,CAEA,IAAMD,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8PlBC,GAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuYdG,GAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EC9d9B,SAASC,GAAqBC,EAA2B,CAAC,EAAW,CAC1E,IAAMC,EAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QA8Cb,OAAOC,EAAY,CACjB,MAAO,yBACP,UAAW,aACX,SAAU,aACV,MAAOF,EAAK,MACZ,cAAeA,EAAK,cACpB,KAAAC,EACA,IAAKE,GACL,QAAS,WAAWC,aACtB,CAAC,CACH,CAEA,IAAMD,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4BtBC,GAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EC7ExB,SAASC,GAAeC,EAA4B,CACzD,IAAMC,EAAMD,EAAK,WAAaA,EAAK,QAE7BE,EAAO;AAAA,qBACMC,EAAIH,EAAK,MAAM;AAAA,sBACdG,EAAIH,EAAK,OAAO;AAAA,sBAChBG,EAAIH,EAAK,OAAO;AAAA,sBAChBA,EAAK,SAAW,IAAM;AAAA,iBAC3BG,EAAIH,EAAK,KAAO,EAAE;AAAA,oBACfG,EAAIH,EAAK,OAAS,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAMfG,EAAIH,EAAK,OAAO,kBAAkB,mBAAmBA,EAAK,OAAO,UAAU,mBAAmBC,CAAG,gCAAgCE,EAAIH,EAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,QAKlKA,EAAK,QAAU,yDAAyDG,EAAIH,EAAK,OAAO,WAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yCAiBpEA,EAAK,SAAW,UAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAWnE,OAAOI,EAAY,CACjB,MAAO,eAAYH,IACnB,UAAW,OACX,SAAU,OACV,MAAOD,EAAK,MACZ,cAAeA,EAAK,cACpB,KAAAE,EACA,IAAKG,GACL,QAAS,WAAWC,aACtB,CAAC,CACH,CAEA,IAAMD,GAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgJhBC,GAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EChOd,SAASC,GAAkBC,EAA+B,CAC/D,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","names":["decayOf","action","clocks","now","ageMs","clockMinutes","ratio","rankByDecay","actions","a","i","x","y","ICON","svg","d","size","secLabel","icon","text","countId","rightHtml","renderMonitorPage","opts","renderShell","MONITOR_CSS","injectFns","decayOf","rankByDecay","MONITOR_SCRIPT","computeBands","runs","minGapMs","maxBands","sorted","a","b","clusters","r","end","last","at","best","i","gap","total","s","bands","c","gaps","packTracks","ends","t","e","buildTimeline","marks","marksByRun","m","list","byLane","band","span","left","width","lane","lanes","_","createHash","readFileSync","statSync","resolve","dirname","fileURLToPath","cache","assetUrl","name","hit","url","here","file","stat","tag","ICON","svg","d","s","PERSPECTIVES","SUB","renderActivityPage","opts","view","isMap","renderShell","v","t","ic","i","assetUrl","ACTIVITY_CSS","injectFns","computeBands","packTracks","buildTimeline","ACTIVITY_SCRIPT","renderWorkflowEditorPage","opts","body","renderShell","HOST_CSS","assetUrl","init_config","createServer","appendFileSync","existsSync","mkdirSync","readFile","fileURLToPath","dirname","resolve","SORTABLE_JS","existsSync","readFileSync","writeFileSync","rmSync","mkdirSync","copyFileSync","readdirSync","resolve","init_config","ICONS","renderAdminPage","opts","tokenScript","chrome","pageHead","healthStrip","renderShell","adminPageBody","TOAST_HTML","ADMIN_PAGE_CSS","TOAST_SCRIPT","ROW_CARD_SCRIPT","ADMIN_HEALTH_SCRIPT","ADMIN_PAGE_SCRIPT","business","getTopbarFeatures","ADMIN_PAGE_BODY","sectionHead","witBanner","secLabel","handleAdminGet","_req","res","peers","localToken","renderAdminPage","handleAdminConfigGet","cfg","readConfigRaw","handleAdminApi","req","path","proxyDaemonSvg","body","readJsonBody","dispatch","getAdminState","addAgent","editAgent","deleteAgent","setAgentAccess","addTelegramAccount","deleteTelegramAccount","toggleTelegram","addCron","deleteCron","replaceConfigRaw","listTokens","createToken","revokeToken","testDriveAgent","addIntegration","removeIntegration","editIntegration","editTelegramAccount","configureSlack","toggleSlack","configureDiscord","toggleDiscord","configureGitLab","toggleGitLab","proxyDaemonJson","upsertAction","deleteAction","runActionFromAdmin","proxyDaemonPostJson","previewCron","addWebhook","editWebhook","deleteWebhook","addMeshPeer","deleteMeshPeer","toggleMesh","listFilesForAgent","readFileForAgent","writeFileForAgent","addSkillForAgent","removeSkillForAgent","upsertOrgEntry","deleteOrgEntry","upsertProject","deleteProject","upsertClient","deleteClient","upsertContact","deleteContact","upsertBoard","deleteBoard","upsertBoardColumn","deleteBoardColumn","updateNotifications","updateWebhookTriggers","updateMeshHealth","key","handler","sendJson","result","e","status","data","ok","err","raw","c","readJsonDir","relPath","dir","resolve","existsSync","out","entry","readdirSync","readFileSync","file","agents","id","a","i","creds","credStatus","k","v","telegramAccounts","telegram","acc","slack","discord","gitlab","whatsapp","crons","webhooks","w","mesh","p","daemonUrl","businessCfg","business","notificationsCfg","notifications","actions","b","boards","access","summary","mutateAgentxConfig","TokenStore","r","name","scopes","expiresInDays","token","record","rec","message","chatId","daemonToken","loadDaemonConfig","headers","text","parsed","tier","mentions","normaliseMentions","model","personality","workspace","block","claudeMd","mkdirSync","writeFileSync","patch","m","agentBinding","botUsername","botTokenEnv","pathOnDaemon","url","daemonTarget","workspaceFor","agentId","def","queryParam","overview","listAgentFiles","readAgentFile","content","writeAgentFile","slug","title","createAgentSkill","deleteAgentSkill","kind","label","credentials","metadata","list","idx","x","WEBHOOK_SOURCES","source","secretEnv","description","node","before","enabled","appTokenEnv","tokenEnv","host","webhookPort","expr","getNextCronDate","human","tz","next","cursor","schedule","agent","prompt","backupPath","copyFileSync","s","role","days","start","end","reportsTo","utilization","pm","client","respondWithin","standing","had","provided","f","projectsRaw","primaryToolLabel","timeRangeDays","closedWindowDays","prev","boardId","columnId","col","ActionStore","actionSchema","issue","saved","runAction","action","inputs","interval","timeout","changes","n","init_config","resolve","renderGraphPage","opts","tokenScript","body","renderShell","GRAPH_PAGE_CSS","GRAPH_PAGE_SCRIPT","handleGraphGet","_req","res","peers","localToken","renderGraphPage","handleGraphApi","req","path","body","readJsonBody","store","openStore","dispatch","getGraphState","approveClassification","rejectClassification","createNode","updateNode","deleteNode","saveSchema","key","handler","sendJson","result","e","cfg","loadDaemonConfig","baseDir","resolve","GraphStore","schema","nodes","recent","index","seen","c","pending","msgHash","findPending","s","sanitizeAxes","hit","id","slug","level","parentId","axes","node","src","out","nodeId","clean","k","v","status","data","ok","err","raw","resolve","existsSync","Database","renderObservabilityPage","opts","body","renderShell","OBS_PAGE_CSS","OBS_PAGE_SCRIPT","RATE_PER_M","modelFamily","model","m","rowCost","row","r","base","tier2Surcharge","openReadOnly","path","resolve","existsSync","db","Database","handleObservabilityGet","_req","res","peers","renderObservabilityPage","parseQuery","req","url","limitRaw","limit","agent","sendJson","status","body","handleObservabilityApi","slug","opened","where","params","sql","rows","labelled","plainErrorLabel","buildSummary","buildActivity","range","parseRange","buildCost","data","csv","toCostCsv","e","error","now","todayStart","yesterdayStart","yStart","last24","tasksToday","tasksYesterday","errorsToday","avgDuration","dRows","p","q","p50","p95","spendToday","s","spendYesterday","perAgent","perChannel","knownAgents","recentlyActive","idleAgents","a","recentErrorsLabelled","opts","last7d","hourly","daily","raw","n","usageAll","today","yesterday","last7Cutoff","last14Cutoff","spend7d","spend30d","tier2Today","tier2_7d","tasks7d","tasksPrev7d","spendPrev7d","dayMap","byAgentRange","c","family","rate","inTok","outTok","crTok","cwTok","d","agg","days","b","tasksRange","inputRange","outputRange","cacheReadRange","cacheCreateRange","costRange","agent_id","v","header","top","resolve","existsSync","Database","openReadOnly","path","resolve","existsSync","db","Database","now","_lastOpenReadOnlyErrAt","sendJson","res","status","body","parseSince","raw","m","n","unit","ms","handleLedgerApi","req","slug","opened","url","since","source","project","limitRaw","limit","sinceClause","sinceClauseDecisions","eventsBySource","totalDecisions","decisionsByOutcome","totalDivergences","divergencesBySource","inFlight","where","params","sql","e","renderLedgerPage","opts","renderShell","renderCostPage","opts","renderShell","renderProjectsPage","opts","body","renderShell","PROJECTS_PAGE_CSS","PROJECTS_PAGE_SCRIPT","resolve","join","existsSync","readdirSync","readFileSync","Database","FORGE_PATH","CHAT_ID_RE","PREVIEW_RE","projectFromChatId","chatId","m","projectFromPreview","text","inferProject","raw","subject","preview","ctx","forgePath","projectId","head","rest","refKey","r","SUBJECT_REF_RE","HEADER_REF_RE","kindOf","word","primaryRef","d","mentionedMrs","limit","out","seen","n","refsOf","primary","TTL_MS","GITHUB_TTL_MS","MAX_REFS","CONCURRENCY","cache","refsToLookUp","dispatches","out","sorted","a","b","d","github","ref","refsOf","key","refKey","fetchForgeStatus","wants","cfg","fetchImpl","now","result","todo","w","hit","i","item","fetchGithub","fetchGitlab","getJson","url","headers","ac","timer","r","gl","base","path","j","hp","openLedger","path","resolve","existsSync","db","Database","now","_lastOpenLedgerErrAt","sendJson","res","status","body","CHANNEL_DEF","upstreamChannel","source","intent","raw","ctxChan","suffix","payload","chan","issueEvent","colorForClient","id","known","palette","h","i","isSystemAgent","agentId","daemonConfig","cfg","matchContact","contactMap","initiatorFrom","aid","initialsFor","ctx","senderUsername","senderName","senderId","isSystemSender","display","kind","jobId","username","name","chatKindFor","u","ghs","wid","s","f","v","parts","inputPreviewFrom","candidates","t","d","c","shortSubject","subject","m","buildFleetSnapshot","windowH","sinceMs","events","decisionsByEvent","resolutionsByDecision","ids","e","placeholders","_","params","decRows","resRows","r","clientMap","projectsByClient","channelMap","initiatorMap","dispatches","businessProjects","agentToClient","agentClients","ev","contact","inputPreview","project","inferProject","baseClientId","baseProjectId","clientFromProject","upstream","chanDef","initiatorId","initiatorKind","init","resKey","startedAt","resolvedAt","active","duration","outcome","clientId","projectId","orgClient","clients","a","b","agents","agentSeen","business","titleOf","role","title","def","channels","channelSeen","initiators","gitlabHost","buildLocalActivityGraphSnapshot","opened","_daemonConfigRef","mergeFleetSnapshots","forges","nodeId","snap","existing","merged","ch","reattribute","projectFromPreview","withForgeStatus","gl","forge","fetchForgeStatus","refsToLookUp","setDaemonConfigForActivityGraph","handleActivityGraphApi","req","url","clampWindow","TICK_MS","handleActivityGraphStream","build","stopped","stop","hb","built","start","findTaskRecord","startedAtMs","fuzzMs","day","dir","yDay","yDir","scanDir","best","bestDelta","readdirSync","parsed","readFileSync","join","ts","recMs","n","handleActivityGraphDetail","dispatchId","sep","eventId","decidedBy","dec","resn","fullInput","longInput","response","transcriptLen","rec","attribution","existsSync","readFileSync","resolve","init_config","renderAgentPage","opts","agentId","tokenScript","agentIdScript","body","AGENT_PAGE_BODY","esc","renderShell","AGENT_PAGE_CSS","AGENT_PAGE_SCRIPT","handleAgentPageGet","_req","res","agentId","peers","localToken","renderAgentPage","handleAgentApi","req","path","body","readJsonBody","url","qs","match","sendJson","sub","key","handler","getAgentState","patchAgent","patchAgentCapability","upsertAgentMcp","deleteAgentMcp","getIdentityFiles","getIdentityFile","putIdentityFile","getSkills","postCreateSkill","deleteSkill","getSkillTree","getSkillFile","putSkillFile","getDepsHint","dispatchInstallPackage","dispatchInstallDeps","getChannelsForAgent","getRecentActivity","listHandovers","createHandover","releaseHandover","result","e","readConfigRaw","file","resolve","existsSync","readFileSync","getAgentDef","cfg","def","overview","listAgentFiles","fields","k","summary","mutateAgentxConfig","name","command","args","a","env","v","s","n","next","relPath","readAgentFile","writeAgentFile","slug","createAgentSkill","deleteAgentSkill","tree","listSkillTree","deps","detectSkillDeps","readSkillFile","writeSkillFile","pkg","worker","pickWorker","message","dispatchTask","hint","skillPath","explicit","loadDaemonConfig","id","workerAgent","chatId","daemonUrl","headers","auth","r","text","data","out","tg","acct","gitlab","m","agent","handoverStore","HandoverStore","store","incoming","outgoing","pageAgentId","channel","toAgent","fromAgent","accountId","expiresAt","rec","status","ok","err","raw","c","UI_LABELS","GLOSSARY","renderLivePage","opts","labelsScript","UI_LABELS","rightExtras","body","renderShell","LIVE_PAGE_CSS","LIVE_PAGE_SCRIPT","MESH_CSS","MESH_SHARED_SCRIPT","MESH_DRILL_SCRIPT","MESH_ANALYTICS_SCRIPT","MESH_OPS_SCRIPT","ICONS","head","icon","title","lead","id","action","sectionHead","TABS","RANGES","renderMeshPage","opts","chrome","pageHead","healthStrip","toolbar","t","r","activityView","lifetimeView","opsView","body","renderShell","MESH_CSS","MESH_SHARED_SCRIPT","MESH_TABS_SCRIPT","MESH_DRILL_SCRIPT","MESH_ANALYTICS_SCRIPT","MESH_OPS_SCRIPT","fetchMeshAnalytics","nodes","q","timeoutMs","ac","timer","qs","results","node","headers","r","e","mergeMeshAnalytics","windowDays","totals","days","origins","causes","rotReasons","jobs","threads","rotTotal","rotTokSum","rotTokN","rotMax","traces","withSteps","data","cur","k","o","c","a","j","t","sinces","n","b","reason","count","proxyNodeAnalytics","res","targetUrl","upstreamPath","target","body","fetchMeshDay","mergeMeshDay","day","conversations","cause","renderBoardsPage","opts","subheader","body","renderShell","BOARDS_PAGE_CSS","BOARDS_PAGE_SCRIPT","renderGlossaryPage","opts","body","GLOSSARY","g","alias","esc","renderShell","UI_LABELS","GLOSSARY_PAGE_CSS","renderWorkflowsPage","opts","body","renderShell","WORKFLOWS_PAGE_CSS","injectFns","workflowHealth","WORKFLOWS_PAGE_SCRIPT","renderProceduresPage","opts","body","renderShell","PROCEDURES_PAGE_CSS","PROCEDURES_PAGE_SCRIPT","renderTaskPage","opts","who","body","esc","renderShell","TASK_PAGE_CSS","TASK_PAGE_JS","renderHistoryPage","opts","who","body","esc","renderShell","HISTORY_CSS","HISTORY_JS","handleWorkflowsApi","req","res","deps","url","method","sendJson","q","convertFromYaml","withBody","body","handleSave","handleRuns","name","b","emission","defMatch","id","suffix","saved","e","parsed","workflowSchema","i","lintIssues","lintWorkflow","m","wf","existed","EDITOR_BLIND_KEYS","opts","raw","existing","key","message","yamlPath","yamlSibling","n","trail","limit","workflowId","summary","runs","slim","r","statusMatch","runId","status","runMatch","streamRun","run","closed","onEnd","unsubscribe","timer","emit","lastUpdated","latest","handler","chunk","result","startBoardDashboard","config","dashboard","boards","port","bind","token","sources","board","buildWorkSource","e","wfNodeId","wfDir","resolve","workflowStore","WorkflowStore","workflowRuns","RunStore","workflowLayouts","LayoutStore","setDaemonConfigForActivityGraph","setTopbarFeatures","createServer","req","res","handleRequest","sendJson","displayHost","b","DASHBOARD_PAGES","ctx","url","path","method","recordSurfaceUse","wz","wizardState","renderLivePage","buildTopbarPeers","renderBoardsPage","node","resolveNodeTargets","body","d","c","r","targets","wanted","readJson","pick","n","qs","headers","daemonUrl","data","renderWorkflowEditorPage","renderActivityPage","renderMonitorPage","ac","timeout","monitorTargets","nodes","op","readMonitorBody","renderMeshPage","renderGlossaryPage","renderWorkflowsPage","renderProceduresPage","renderProcessesPage","taskPage","agentId","nodeUrl","renderTaskPage","histPage","renderHistoryPage","handleWizardGet","handleWizardPost","handleStartDaemonPost","handleAdminGet","handleGraphGet","handleObservabilityGet","peerId","handleActivityGraphFleet","handleActivityGraphStream","hours","buildFleetActivitySnapshot","detailMatch","decoded","sep","nodeName","realId","localNodeName","peerByName","peer","findPeer","proxyAdminToPeer","stripped","m","handleObservabilityApi","streamDaemonLogs","runDoctorChecks","running","result","renderLedgerPage","handleLedgerApi","renderCostPage","renderProjectsPage","getOrCreateWikiHandler","handleActivityGraphApi","handleActivityGraphDetail","agentPageMatch","handleAgentPageGet","authHeader","got","tokStore","TokenStore","rec","recordHasScope","handleAdminConfigGet","handleGraphApi","handleAgentApi","handleAdminApi","SORTABLE_JS","rel","assetPath","resolveFromHere","buf","readAsset","ct","immutable","snap","buildLiveSnapshot","date","timezone","num","k","lo","hi","v","fetchMeshAnalytics","day","fetchMeshDay","q","upstream","agent","channel","chat","kind","key","task","proxyNodeAnalytics","startLiveStream","u","taskId","proxyTaskStream","limit","proxyTaskHistory","proxyTaskAction","list","drafted","draftIssue","publicAgentMatch","requireScopedToken","proxyPublicAgentMessage","closed","controllers","heartbeat","t","ctrl","reader","dec","done","value","idx","frame","workflowId","summary","local","remoteLists","timer","byId","merged","a","out","slimRun","runDetailMatch","runId","runMatch","handleWorkflowsApi","s","scopes","presented","extractToken","scope","membersMatch","source","members","milestonesMatch","ms","moveMatch","itemOpMatch","boardId","rawItemId","itemId","toCol","fromCol","applyTransition","audit","auditActor","columns","totals","loadBoardColumns","detail","updated","created","slug","target","query","empty","baseLabels","extraLabel","search","milestone","assignee","openDays","closedDays","hasOpen","hasClosed","open","closedCol","item","openColumns","stage","deriveStage","i","p","diff","transitionDiff","gl","daemon","agentUsernames","mapping","GitLabWorkSource","args","entry","dir","existsSync","mkdirSync","line","appendFileSync","fwd","fetchDaemonAgents","signal","base","cronQuery","healthRes","agentsRes","meshRes","cronsRes","cronRunsRes","agents","h","jobs","job","history","meshResSafe","_wikiHandler","wikiDir","createWikiHandler","_meshJsonCache","j","fetchMeshPeers","primaryUrl","dash","seen","meshPeers","send","ev","tick","allowed","peers","tokenForNode","upstreamCtl","upstreamRes","required","agentDef","message","text","upstreamPath","parsed","DRAFT_CONVENTIONS","rough","boardCtx","userPrompt","content","extractJson","labels","x","fenced","start","depth","here","dirname","fileURLToPath","readFile","status","reject","chunks","raw","resolvePeerDashboardUrl","ctxConfig","cfg","primary","extras","fleet","withForgeStatus","localNodeId","parts","localSnap","buildLocalActivityGraphSnapshot","peerEntries","peerParts","mergeFleetSnapshots","spawn","execSync","cmd","home","candidates","file","f","child","buffer","onData","chunk","hb","cleanup","pathWithQuery","xrw","respBody","id","match"]}