agentix-cli 0.15.0 → 0.17.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 (148) hide show
  1. package/README.md +12 -6
  2. package/dist/agent-4AOMVXRJ.js +2 -0
  3. package/dist/board-dashboard-CXIVJKDP.js +7282 -0
  4. package/dist/board-dashboard-CXIVJKDP.js.map +1 -0
  5. package/dist/chunk-34NG7UDX.js +11 -0
  6. package/dist/chunk-34NG7UDX.js.map +1 -0
  7. package/dist/chunk-4HP6UC2X.js +13 -0
  8. package/dist/chunk-4HP6UC2X.js.map +1 -0
  9. package/dist/chunk-5F3J55LG.js +2 -0
  10. package/dist/chunk-5F3J55LG.js.map +1 -0
  11. package/dist/chunk-5WNGHDFC.js +164 -0
  12. package/dist/chunk-5WNGHDFC.js.map +1 -0
  13. package/dist/{chunk-7MBTMCKY.js → chunk-63UYRJIC.js} +2 -2
  14. package/dist/chunk-64P4QHSZ.js +2 -0
  15. package/dist/chunk-64P4QHSZ.js.map +1 -0
  16. package/dist/chunk-6PPIFDOQ.js +1742 -0
  17. package/dist/chunk-6PPIFDOQ.js.map +1 -0
  18. package/dist/chunk-ABCHHNG6.js +625 -0
  19. package/dist/chunk-ABCHHNG6.js.map +1 -0
  20. package/dist/chunk-AFRHKA7B.js +2 -0
  21. package/dist/chunk-AFRHKA7B.js.map +1 -0
  22. package/dist/chunk-BFZUG66X.js +45 -0
  23. package/dist/chunk-BFZUG66X.js.map +1 -0
  24. package/dist/chunk-CDNWCYF7.js +2 -0
  25. package/dist/chunk-CDNWCYF7.js.map +1 -0
  26. package/dist/{chunk-6MKAXEE5.js → chunk-EKTPCSN4.js} +2 -2
  27. package/dist/{chunk-6MKAXEE5.js.map → chunk-EKTPCSN4.js.map} +1 -1
  28. package/dist/chunk-EW27SCCC.js +2 -0
  29. package/dist/chunk-EW27SCCC.js.map +1 -0
  30. package/dist/chunk-HCFSHGK4.js +46 -0
  31. package/dist/chunk-HCFSHGK4.js.map +1 -0
  32. package/dist/chunk-HHJGKD5B.js +71 -0
  33. package/dist/chunk-HHJGKD5B.js.map +1 -0
  34. package/dist/chunk-LEIYC3BF.js +104 -0
  35. package/dist/chunk-LEIYC3BF.js.map +1 -0
  36. package/dist/{chunk-HLRVDCNH.js → chunk-MBEQASP3.js} +2 -2
  37. package/dist/chunk-N67JZAXU.js +2 -0
  38. package/dist/chunk-N67JZAXU.js.map +1 -0
  39. package/dist/{chunk-RGUYBH77.js → chunk-SRHLXZRF.js} +2 -2
  40. package/dist/chunk-TT2Q7YMZ.js +118 -0
  41. package/dist/chunk-TT2Q7YMZ.js.map +1 -0
  42. package/dist/{chunk-HI24KAEY.js → chunk-WJXU5F4Z.js} +3 -4
  43. package/dist/{chunk-HI24KAEY.js.map → chunk-WJXU5F4Z.js.map} +1 -1
  44. package/dist/chunk-WZEYII2S.js +6 -0
  45. package/dist/chunk-WZEYII2S.js.map +1 -0
  46. package/dist/chunk-XDJSYQST.js +6 -0
  47. package/dist/chunk-XDJSYQST.js.map +1 -0
  48. package/dist/chunk-YPOELN6Z.js +798 -0
  49. package/dist/chunk-YPOELN6Z.js.map +1 -0
  50. package/dist/{chunk-QZP65I2H.js → chunk-ZE2YFX4K.js} +2 -2
  51. package/dist/cli.js +1 -302
  52. package/dist/cli.js.map +1 -1
  53. package/dist/{compaction-JBZZ7I7Y.js → compaction-X46EQEPS.js} +6 -6
  54. package/dist/compaction-X46EQEPS.js.map +1 -0
  55. package/dist/{config-DCJYECL4.js → config-KCYHMKCT.js} +2 -2
  56. package/dist/config-mutator-QP4GLBOE.js +2 -0
  57. package/dist/context-planner-G2J7FNNU.js +17 -0
  58. package/dist/context-planner-G2J7FNNU.js.map +1 -0
  59. package/dist/editor-chat-R54E65W5.js +137 -0
  60. package/dist/editor-chat-R54E65W5.js.map +1 -0
  61. package/dist/heal-JDXUPRET.js +2 -0
  62. package/dist/inbox-L5ZZ623Y.js +315 -0
  63. package/dist/inbox-L5ZZ623Y.js.map +1 -0
  64. package/dist/index.d.ts +1996 -18
  65. package/dist/index.js +22 -49
  66. package/dist/index.js.map +1 -1
  67. package/dist/ingest-whatsapp-NS4OWDOU.js +6 -0
  68. package/dist/ingest-whatsapp-NS4OWDOU.js.map +1 -0
  69. package/dist/memory-extract-WH4FZ456.js +2 -0
  70. package/dist/processes-XF5CV4LK.js +256 -0
  71. package/dist/processes-XF5CV4LK.js.map +1 -0
  72. package/dist/program-FFWEILAS.js +2 -0
  73. package/dist/providers-AKQ4HTB4.js +2 -0
  74. package/dist/query-PKGYE5HR.js +2 -0
  75. package/dist/registry-IFMDJWQ7.js +2 -0
  76. package/dist/scheduler-63D5KFO7.js +2 -0
  77. package/dist/scheduler-63D5KFO7.js.map +1 -0
  78. package/dist/slack-PSQLVTGB.js +3 -0
  79. package/dist/slack-PSQLVTGB.js.map +1 -0
  80. package/dist/store-5MJOO74U.js +2 -0
  81. package/dist/store-5MJOO74U.js.map +1 -0
  82. package/dist/task-store-NM7VKNLE.js +2 -0
  83. package/dist/task-store-NM7VKNLE.js.map +1 -0
  84. package/dist/telegram-W4SLFMNY.js +3 -0
  85. package/dist/telegram-W4SLFMNY.js.map +1 -0
  86. package/dist/timers-PCB4ZIBD.js +2 -0
  87. package/dist/timers-PCB4ZIBD.js.map +1 -0
  88. package/dist/types-C7IFH42Z.js +2 -0
  89. package/dist/types-C7IFH42Z.js.map +1 -0
  90. package/dist/types-OIFU3KSQ.js +2 -0
  91. package/dist/types-OIFU3KSQ.js.map +1 -0
  92. package/dist/usage-dashboard-AXPXB2KP.js +803 -0
  93. package/dist/usage-dashboard-AXPXB2KP.js.map +1 -0
  94. package/dist/web/workflow-editor.global.js +1427 -0
  95. package/dist/web/workflow-editor.global.js.map +1 -0
  96. package/dist/whatsapp-DCHRBRVV.js +3 -0
  97. package/dist/whatsapp-DCHRBRVV.js.map +1 -0
  98. package/dist/wiki-UDPYFJBL.js +2 -0
  99. package/dist/wiki-UDPYFJBL.js.map +1 -0
  100. package/dist/workspace-setup-C4VB3QZV.js +2 -0
  101. package/dist/workspace-setup-C4VB3QZV.js.map +1 -0
  102. package/package.json +10 -3
  103. package/dist/agent-LRSSVFDR.js +0 -2
  104. package/dist/board-dashboard-DAPHL6W2.js +0 -4508
  105. package/dist/board-dashboard-DAPHL6W2.js.map +0 -1
  106. package/dist/chunk-3XULOWYB.js +0 -199
  107. package/dist/chunk-3XULOWYB.js.map +0 -1
  108. package/dist/chunk-6U2XOQHU.js +0 -7
  109. package/dist/chunk-6U2XOQHU.js.map +0 -1
  110. package/dist/chunk-AU37OREL.js +0 -108
  111. package/dist/chunk-AU37OREL.js.map +0 -1
  112. package/dist/chunk-B4Q4X7D5.js +0 -6
  113. package/dist/chunk-B4Q4X7D5.js.map +0 -1
  114. package/dist/chunk-CXY2RYNI.js +0 -11
  115. package/dist/chunk-CXY2RYNI.js.map +0 -1
  116. package/dist/chunk-IEVEMKAI.js +0 -168
  117. package/dist/chunk-IEVEMKAI.js.map +0 -1
  118. package/dist/chunk-KXZMYLHQ.js +0 -46
  119. package/dist/chunk-KXZMYLHQ.js.map +0 -1
  120. package/dist/chunk-M7HKBG3V.js +0 -2
  121. package/dist/chunk-M7HKBG3V.js.map +0 -1
  122. package/dist/chunk-RB7265LO.js +0 -12
  123. package/dist/chunk-RB7265LO.js.map +0 -1
  124. package/dist/chunk-WVNSWBCU.js +0 -138
  125. package/dist/chunk-WVNSWBCU.js.map +0 -1
  126. package/dist/compaction-JBZZ7I7Y.js.map +0 -1
  127. package/dist/config-mutator-6EJHSP3Y.js +0 -2
  128. package/dist/heal-WKY5I4KB.js +0 -2
  129. package/dist/memory-extract-AD7KMRPC.js +0 -2
  130. package/dist/providers-VUIWO3ZK.js +0 -2
  131. package/dist/registry-5ND7GLFJ.js +0 -2
  132. package/dist/scheduler-FCXOXCN5.js +0 -2
  133. package/dist/usage-dashboard-QX3CF6KK.js +0 -404
  134. package/dist/usage-dashboard-QX3CF6KK.js.map +0 -1
  135. package/dist/workspace-setup-ZWTT42XT.js +0 -2
  136. /package/dist/{agent-LRSSVFDR.js.map → agent-4AOMVXRJ.js.map} +0 -0
  137. /package/dist/{chunk-7MBTMCKY.js.map → chunk-63UYRJIC.js.map} +0 -0
  138. /package/dist/{chunk-HLRVDCNH.js.map → chunk-MBEQASP3.js.map} +0 -0
  139. /package/dist/{chunk-RGUYBH77.js.map → chunk-SRHLXZRF.js.map} +0 -0
  140. /package/dist/{chunk-QZP65I2H.js.map → chunk-ZE2YFX4K.js.map} +0 -0
  141. /package/dist/{config-DCJYECL4.js.map → config-KCYHMKCT.js.map} +0 -0
  142. /package/dist/{config-mutator-6EJHSP3Y.js.map → config-mutator-QP4GLBOE.js.map} +0 -0
  143. /package/dist/{heal-WKY5I4KB.js.map → heal-JDXUPRET.js.map} +0 -0
  144. /package/dist/{memory-extract-AD7KMRPC.js.map → memory-extract-WH4FZ456.js.map} +0 -0
  145. /package/dist/{providers-VUIWO3ZK.js.map → program-FFWEILAS.js.map} +0 -0
  146. /package/dist/{registry-5ND7GLFJ.js.map → providers-AKQ4HTB4.js.map} +0 -0
  147. /package/dist/{scheduler-FCXOXCN5.js.map → query-PKGYE5HR.js.map} +0 -0
  148. /package/dist/{workspace-setup-ZWTT42XT.js.map → registry-IFMDJWQ7.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/daemon/board-dashboard.ts","../src/daemon/vendor/sortable.ts","../src/daemon/ui-labels.ts","../src/daemon/admin-panel.ts","../src/daemon/file-ops.ts","../src/daemon/topbar.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type ServerResponse } from \"http\"\nimport { appendFileSync, existsSync, mkdirSync } from \"fs\"\nimport { 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 { UI_LABELS, GLOSSARY } from \"./ui-labels\"\nimport { handleWizardGet, handleWizardPost, wizardState } from \"./setup-wizard\"\nimport { handleAdminGet, handleAdminApi, handleAdminConfigGet } from \"./admin-panel\"\nimport { TokenStore, recordHasScope, extractToken, type TokenRecord } from \"./token-store\"\nimport { renderTopbar, TOPBAR_HEAD, TOPBAR_CSS, TOPBAR_SCRIPT, 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// Mirrors src/daemon/usage-dashboard.ts — inline HTML/CSS/JS, manual routing,\n// 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 const server = createServer(async (req, res) => {\n try {\n await handleRequest(req, res, { boards, sources, token, config })\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\ninterface Ctx {\n boards: BoardConfig[]\n sources: Map<string, WorkSource>\n token?: string\n config: DaemonConfig\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 if (method === \"GET\" && path === \"/\") {\n // Routing priority:\n // 1. No agents yet → send operator to the setup wizard.\n // 2. Agents exist but no boards → live view.\n // 3. Boards configured → Kanban landing page.\n const wz = wizardState()\n if (!wz.configExists || wz.agentCount === 0) {\n res.writeHead(302, { Location: \"/setup\" })\n res.end()\n return\n }\n const html = ctx.boards.length === 0 ? renderLiveHtml(ctx.config) : renderBoardHtml(ctx.config)\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(html)\n return\n }\n if (method === \"GET\" && path === \"/live\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderLiveHtml(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(renderGlossaryHtml(ctx.config))\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\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 // Cross-mesh admin proxy: when a non-primary peer is selected (header\n // X-Agentx-Peer or ?peer=), forward the whole request to the peer's\n // dashboard with its own token. The peer's board-server handles it as\n // a normal admin call against its own agentx.json.\n if (path.startsWith(\"/api/admin/\")) {\n const peerId = String(req.headers[\"x-agentx-peer\"] || \"\").trim() || url.searchParams.get(\"peer\") || \"\"\n if (peerId && peerId !== \"primary\") {\n const peer = findPeer(peerId, ctx.config)\n if (!peer) { sendJson(res, 404, { error: `unknown peer: ${peerId}` }); return }\n await proxyAdminToPeer(req, res, peer, path + (url.search || \"\"))\n return\n }\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/\")) {\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 // 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/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 // 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\"\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 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 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\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 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}\ninterface LiveSnapshot {\n ts: string\n nodes: NodeLive[]\n}\n\nasync function fetchDaemonAgents(url: string, token?: string, signal?: AbortSignal): 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 [healthRes, agentsRes, meshRes] = 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 ])\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 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 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// 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\nasync function buildLiveSnapshot(daemon: DaemonConfig): Promise<LiveSnapshot> {\n const dash = daemon.dashboard\n const primaryUrl = dash.daemonUrl.replace(/\\/+$/, \"\")\n const primaryToken = dash.token\n // Sources: primary + configured extras + auto-discovered mesh peers.\n const seen = new Map<string, { name: string; url: string; token?: string }>()\n seen.set(primaryUrl, { name: \"primary\", url: primaryUrl, token: primaryToken })\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 ac = new AbortController()\n const timeout = setTimeout(() => ac.abort(), 3000)\n try {\n const meshPeers = await fetchMeshPeers(primaryUrl, primaryToken, ac.signal)\n for (const p of meshPeers) if (!seen.has(p.url)) seen.set(p.url, p)\n const nodes = await Promise.all([...seen.values()].map((d) => fetchDaemonAgents(d.url, d.token, ac.signal)))\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// --- 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\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\nfunction renderBoardHtml(config: DaemonConfig): string {\n const rightExtras = `<div id=\"conn\" class=\"conn ok\" title=\"ready\">●</div>`\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 const topbar = renderTopbar({\n activeTab: \"boards\",\n subtitle: \"Boards\",\n subheader,\n rightExtras,\n peers: buildTopbarPeers(config),\n })\n return `<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"dark\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>AgentX · Boards</title>\n${TOPBAR_HEAD}\n<style>${BOARD_CSS}\\n${TOPBAR_CSS}</style>\n</head>\n<body>\n${topbar}\n<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<script>${BOARD_JS}</script>\n${TOPBAR_SCRIPT}\n</body>\n</html>`\n}\n\n// --- Live full-screen page (/live) ---\n\nfunction renderLiveHtml(config: DaemonConfig): 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 const topbar = renderTopbar({\n activeTab: \"live\",\n subtitle: UI_LABELS.subtitle,\n rightExtras,\n peers: buildTopbarPeers(config),\n })\n return `<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"dark\" data-density=\"normal\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${escapeHtmlServer(UI_LABELS.brand)} · ${escapeHtmlServer(UI_LABELS.subtitle)}</title>\n${TOPBAR_HEAD}\n<style>${TOPBAR_CSS}\\n${LIVE_CSS}</style>\n</head>\n<body>\n<div class=\"ax-app\">\n ${topbar}\n <section id=\"statstrip\" class=\"ax-statstrip\"></section>\n <main id=\"grid\" class=\"ax-live__body\"></main>\n</div>\n<aside id=\"history-panel\" class=\"history-panel hidden\" aria-hidden=\"true\">\n <header>\n <h2 id=\"history-panel-title\">${escapeHtmlServer(UI_LABELS.historyPanelTitle)}</h2>\n <span class=\"history-panel-source\" id=\"history-panel-source\"></span>\n <button class=\"history-panel-close\" id=\"history-panel-close\" aria-label=\"Close\">×</button>\n </header>\n <div id=\"history-panel-body\" class=\"history-panel-body\"></div>\n</aside>\n<div id=\"task-modal\" class=\"task-modal hidden\" aria-hidden=\"true\">\n <div class=\"task-modal-backdrop\"></div>\n <div class=\"task-modal-card\" role=\"dialog\" aria-modal=\"true\">\n <header>\n <span class=\"task-modal-channel\" id=\"task-modal-channel\"></span>\n <h2 id=\"task-modal-title\">${escapeHtmlServer(UI_LABELS.taskModalTitle)}</h2>\n <span class=\"task-modal-status\" id=\"task-modal-status\">${escapeHtmlServer(UI_LABELS.taskModalConnecting)}</span>\n <button class=\"task-modal-close\" id=\"task-modal-close\" aria-label=\"Close\">×</button>\n </header>\n <div id=\"task-modal-output\" class=\"task-modal-output\"></div>\n </div>\n</div>\n${labelsScript}\n${TOPBAR_SCRIPT}\n<script>${LIVE_JS}</script>\n</body>\n</html>`\n}\n\n/** Minimal HTML escaper for server-rendered label text. */\nfunction escapeHtmlServer(s: string): string {\n return String(s).replace(/[&<>\"']/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" }[c] as string))\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 return [primary, ...extras]\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 */\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 */\nconst THEME_SWITCH_SCRIPT = `<script>\n(function(){\n try {\n var t = localStorage.getItem('ax-theme') || 'dark';\n document.documentElement.setAttribute('data-theme', t);\n } catch (e) {}\n function wire() {\n var sw = document.querySelectorAll('[data-theme-opt]');\n if (!sw.length) return;\n var current = document.documentElement.getAttribute('data-theme') || 'dark';\n sw.forEach(function(b){\n if (b.getAttribute('data-theme-opt') === current) b.classList.add('is-active');\n b.addEventListener('click', function(){\n var t = b.getAttribute('data-theme-opt');\n document.documentElement.setAttribute('data-theme', t);\n try { localStorage.setItem('ax-theme', t); } catch (e) {}\n sw.forEach(function(x){ x.classList.toggle('is-active', x === b); });\n });\n });\n }\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', wire);\n } else { wire(); }\n})();\n</script>`\n\n/** /glossary — plain-English definitions of the terms that appear in the dashboard. */\nfunction renderGlossaryHtml(config: DaemonConfig): string {\n const items = GLOSSARY.map((g) => {\n const alias = g.alias ? `<span class=\"alias\" title=\"Schema key\">${escapeHtmlServer(g.alias)}</span>` : \"\"\n return `<article class=\"term\"><h3>${escapeHtmlServer(g.term)}${alias}</h3><p>${escapeHtmlServer(g.definition)}</p></article>`\n }).join(\"\")\n const topbar = renderTopbar({ activeTab: \"glossary\", subtitle: \"Glossary\", peers: buildTopbarPeers(config) })\n return `<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"dark\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${escapeHtmlServer(UI_LABELS.brand)} · Glossary</title>\n${TOPBAR_HEAD}\n<style>\n:root{\n --ax-bg:oklch(0.16 0.010 265);--ax-bg-elev:oklch(0.19 0.012 265);\n --ax-surface:oklch(0.21 0.012 265);--ax-surface-2:oklch(0.24 0.014 265);\n --ax-border:oklch(0.29 0.014 265);--ax-border-2:oklch(0.35 0.016 265);\n --ax-text:oklch(0.95 0.005 265);--ax-text-2:oklch(0.80 0.008 265);\n --ax-muted:oklch(0.60 0.010 265);--ax-accent:oklch(0.78 0.13 165);\n --ax-font:\"IBM Plex Sans\",-apple-system,\"Segoe UI\",sans-serif;\n --ax-mono:\"IBM Plex Mono\",ui-monospace,\"SF Mono\",monospace;\n color-scheme:dark;\n}\n[data-theme=\"light\"]{--ax-bg:oklch(0.98 0.002 265);--ax-bg-elev:oklch(0.96 0.003 265);\n --ax-surface:oklch(0.99 0.002 265);--ax-surface-2:oklch(0.955 0.003 265);--ax-border:oklch(0.88 0.006 265);\n --ax-border-2:oklch(0.78 0.008 265);--ax-text:oklch(0.22 0.010 265);\n --ax-text-2:oklch(0.36 0.010 265);--ax-muted:oklch(0.54 0.010 265);\n --ax-accent:oklch(0.55 0.14 165);color-scheme:light}\n[data-theme=\"crt\"]{--ax-bg:#05140a;--ax-bg-elev:#061a0d;--ax-surface:#08201f;--ax-surface-2:#0b2922;\n --ax-border:#164a30;--ax-border-2:#1f6a44;--ax-text:#b7ffcc;\n --ax-text-2:#83e3a8;--ax-muted:#4f9a73;--ax-accent:#6dff9e;\n --ax-font:\"IBM Plex Mono\",ui-monospace,monospace}\n${TOPBAR_CSS}\n*{box-sizing:border-box}\nhtml,body{margin:0;min-height:100vh;background:var(--ax-bg);color:var(--ax-text);\n font-family:var(--ax-font);font-size:14px;line-height:1.55;\n -webkit-font-smoothing:antialiased;font-feature-settings:\"ss01\",\"cv01\"}\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{color:var(--ax-muted);margin:0 0 28px;font-size:13px;line-height:1.6}\n.lead code{font-family:var(--ax-mono);font-size:12px;background:var(--ax-surface);\n padding:1px 6px;border-radius:3px;border:1px solid var(--ax-border)}\narticle.term{background:var(--ax-surface);border:1px solid var(--ax-border);\n border-radius:6px;padding:16px 20px;margin:10px 0}\narticle.term h3{font-size:14px;margin:0 0 6px;display:flex;align-items:center;\n gap:10px;font-weight:600;letter-spacing:-0.005em}\narticle.term .alias{font-size:10px;text-transform:uppercase;letter-spacing:0.06em;\n color:var(--ax-muted);background:var(--ax-bg);padding:2px 7px;border-radius:3px;\n font-weight:500;font-family:var(--ax-mono);border:1px solid var(--ax-border)}\narticle.term p{margin:0;color:var(--ax-text-2);font-size:13px;line-height:1.6}\n</style>\n</head>\n<body>\n${topbar}\n<main>\n <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</main>\n${TOPBAR_SCRIPT}\n</body>\n</html>`\n}\n\nconst LIVE_CSS = `\n/* --- Design tokens (ported from /Users/macbookpro/Downloads/agentx-redesign) --- */\n:root {\n --ax-bg: oklch(0.16 0.010 265);\n --ax-bg-elev: oklch(0.19 0.012 265);\n --ax-surface: oklch(0.21 0.012 265);\n --ax-surface-2: oklch(0.24 0.014 265);\n --ax-border: oklch(0.29 0.014 265);\n --ax-border-2: oklch(0.35 0.016 265);\n --ax-text: oklch(0.95 0.005 265);\n --ax-text-2: oklch(0.80 0.008 265);\n --ax-muted: oklch(0.60 0.010 265);\n --ax-accent: oklch(0.78 0.13 165); /* live green */\n --ax-accent-2: oklch(0.55 0.11 165);\n --ax-warn: oklch(0.80 0.14 75);\n --ax-err: oklch(0.68 0.19 25);\n --ax-info: oklch(0.78 0.10 220);\n --ax-radius: 6px;\n --ax-pad: 16px;\n --ax-pad-sm: 10px;\n --ax-gap: 12px;\n --ax-font: \"IBM Plex Sans\", -apple-system, \"Segoe UI\", sans-serif;\n --ax-mono: \"IBM Plex Mono\", ui-monospace, \"SF Mono\", Consolas, monospace;\n --ax-fs: 13px;\n --ax-fs-sm: 12px;\n --ax-fs-xs: 11px;\n /* Legacy aliases so history-panel / task-modal CSS keeps working. */\n --bg: var(--ax-bg); --card: var(--ax-surface); --node: var(--ax-bg-elev);\n --border: var(--ax-border); --text: var(--ax-text); --muted: var(--ax-muted);\n --accent: var(--ax-accent); --green: var(--ax-accent); --yellow: var(--ax-warn);\n --red: var(--ax-err); --blue: var(--ax-info); --gray: var(--ax-muted);\n color-scheme: dark;\n}\n[data-theme=\"light\"] {\n --ax-bg: oklch(0.98 0.002 265);\n --ax-bg-elev: oklch(0.96 0.003 265);\n --ax-surface: oklch(0.99 0.002 265);\n --ax-surface-2: oklch(0.955 0.003 265);\n --ax-border: oklch(0.88 0.006 265);\n --ax-border-2: oklch(0.78 0.008 265);\n --ax-text: oklch(0.22 0.010 265);\n --ax-text-2: oklch(0.36 0.010 265);\n --ax-muted: oklch(0.54 0.010 265);\n --ax-accent: oklch(0.55 0.14 165);\n color-scheme: light;\n}\n[data-theme=\"crt\"] {\n --ax-bg: #05140a;\n --ax-bg-elev: #061a0d;\n --ax-surface: #08201f;\n --ax-surface-2: #0b2922;\n --ax-border: #164a30;\n --ax-border-2: #1f6a44;\n --ax-text: #b7ffcc;\n --ax-text-2: #83e3a8;\n --ax-muted: #4f9a73;\n --ax-accent: #6dff9e;\n --ax-font: \"IBM Plex Mono\", ui-monospace, monospace;\n}\n\n/* Theme switcher — segmented control in the topbar. */\n.ax-theme-switch { display: inline-flex; gap: 0; border: 1px solid var(--ax-border-2);\n border-radius: 4px; overflow: hidden; margin-right: 4px; }\n.ax-theme-switch button { background: transparent; border: none; color: var(--ax-muted);\n padding: 3px 9px; font: inherit; font-size: 10px; cursor: pointer;\n letter-spacing: 0.04em; text-transform: uppercase; font-family: var(--ax-mono);\n border-right: 1px solid var(--ax-border-2); }\n.ax-theme-switch button:last-child { border-right: none; }\n.ax-theme-switch button:hover { color: var(--ax-text); background: var(--ax-surface-2); }\n.ax-theme-switch button.is-active { color: var(--ax-accent);\n background: color-mix(in oklch, var(--ax-accent) 12%, var(--ax-surface)); }\n\n* { box-sizing: border-box; }\nhtml, body { margin: 0; min-height: 100vh; background: var(--ax-bg); color: var(--ax-text);\n font-family: var(--ax-font); font-size: var(--ax-fs);\n -webkit-font-smoothing: antialiased; font-feature-settings: \"ss01\", \"cv01\"; }\npre, code, .ax-mono { font-family: var(--ax-mono); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }\n.ax-muted { color: var(--ax-muted); }\n.ax-accent { color: var(--ax-accent); }\n\n/* --- App chrome --- */\n.ax-app { min-height: 100vh; display: flex; flex-direction: column; }\n.ax-topbar { display: flex; align-items: center; justify-content: space-between;\n padding: 8px 18px; border-bottom: 1px solid var(--ax-border);\n background: var(--ax-bg-elev); position: sticky; top: 0; z-index: 20; }\n.ax-topbar__left { display: flex; align-items: center; gap: 18px; }\n.ax-brand { display: flex; align-items: center; gap: 10px; font-weight: 600; letter-spacing: -0.01em; }\n.ax-brand__mark { font-family: var(--ax-mono); font-size: 12px; padding: 2px 8px;\n border: 1px solid var(--ax-border-2); color: var(--ax-accent); border-radius: 4px; }\n.ax-brand__name { font-size: 14px; }\n.ax-brand__subtitle { font-size: var(--ax-fs-xs); color: var(--ax-muted);\n border-left: 1px solid var(--ax-border); padding-left: 12px; margin-left: 4px; }\n.ax-topbar__tabs { display: flex; gap: 2px; }\n.ax-topbar__tab { background: transparent; border: none; color: var(--ax-text-2);\n padding: 8px 14px; font: inherit; cursor: pointer; font-size: var(--ax-fs-sm);\n border-bottom: 2px solid transparent; letter-spacing: -0.005em; text-decoration: none; }\n.ax-topbar__tab:hover { color: var(--ax-text); }\n.ax-topbar__tab.is-active { color: var(--ax-text); border-bottom-color: var(--ax-accent); }\n.ax-topbar__right { display: flex; align-items: center; gap: 10px; font-size: var(--ax-fs-xs); color: var(--ax-muted); }\n.ax-topbar__right .ax-mono { color: var(--ax-text-2); }\n\n/* --- Dots / badges / chips --- */\n.ax-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--ax-muted); }\n.ax-dot--ok, .ax-dot--live { background: var(--ax-accent); }\n.ax-dot--warn { background: var(--ax-warn); }\n.ax-dot--err { background: var(--ax-err); }\n.ax-dot--off { background: var(--ax-border-2); }\n.ax-dot--pulse { box-shadow: 0 0 0 0 currentColor;\n animation: ax-pulse 1.8s infinite; color: var(--ax-accent); }\n@keyframes ax-pulse {\n 0% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--ax-accent) 70%, transparent); }\n 70% { box-shadow: 0 0 0 6px color-mix(in oklch, var(--ax-accent) 0%, transparent); }\n 100% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--ax-accent) 0%, transparent); }\n}\n.ax-badge { display: inline-flex; align-items: center; gap: 4px;\n padding: 1px 7px; border-radius: 3px; font-size: var(--ax-fs-xs); line-height: 16px;\n border: 1px solid var(--ax-border-2); color: var(--ax-text-2); background: transparent; }\n.ax-badge--mono { font-family: var(--ax-mono); letter-spacing: 0.02em;\n text-transform: uppercase; font-size: 10px; }\n.ax-badge--accent { color: var(--ax-accent);\n border-color: color-mix(in oklch, var(--ax-accent) 50%, transparent); }\n.ax-badge--warn { color: var(--ax-warn);\n border-color: color-mix(in oklch, var(--ax-warn) 50%, transparent); }\n.ax-badge--ghost { color: var(--ax-muted); border-color: var(--ax-border); background: var(--ax-surface-2); }\n.ax-chip { display: inline-block; padding: 0 6px; font-family: var(--ax-mono);\n font-size: 10px; line-height: 16px; border-radius: 3px; color: var(--ax-muted);\n border: 1px solid var(--ax-border); background: var(--ax-surface); }\n\n/* --- Stat strip --- */\n.ax-statstrip { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));\n gap: var(--ax-gap); padding: var(--ax-pad) var(--ax-pad) 0; }\n.ax-stat { background: var(--ax-surface); border: 1px solid var(--ax-border);\n border-radius: var(--ax-radius); padding: 14px 16px; }\n.ax-stat__label { font-size: var(--ax-fs-xs); text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--ax-muted); display: flex; align-items: center; gap: 6px; }\n.ax-stat__value { font-size: 24px; font-weight: 600; margin-top: 4px; letter-spacing: -0.02em;\n font-family: var(--ax-mono); font-variant-numeric: tabular-nums; }\n.ax-stat__sub { font-size: var(--ax-fs-xs); color: var(--ax-muted); margin-top: 2px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ax-stat--live .ax-stat__value { color: var(--ax-accent); }\n.ax-stat--warn .ax-stat__value { color: var(--ax-warn); }\n.ax-stat--err .ax-stat__value { color: var(--ax-err); }\n\n/* --- Section titles --- */\n.ax-section-title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }\n.ax-section-title__label { font-size: var(--ax-fs-xs); text-transform: uppercase;\n letter-spacing: 0.08em; color: var(--ax-muted); }\n.ax-section-title__right { display: flex; align-items: center; gap: 8px;\n font-size: var(--ax-fs-xs); color: var(--ax-muted); }\n\n/* --- Live body --- */\n.ax-live__body { display: flex; flex-direction: column; gap: var(--ax-gap); padding: var(--ax-pad); }\n.ax-node { background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 8px; overflow: hidden; }\n.ax-node > header { background: transparent; padding: 12px 16px;\n border-bottom: 1px solid var(--ax-border); display: flex; align-items: center; gap: 10px; }\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 { 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.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 { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));\n gap: var(--ax-gap); padding: 12px 14px; }\n\n/* --- Agent cards --- */\n.ax-card { background: var(--ax-surface); border: 1px solid var(--ax-border);\n border-radius: var(--ax-radius); padding: var(--ax-pad); }\n.ax-agent { display: flex; flex-direction: column; gap: 10px; }\n.ax-agent.is-handling { border-color: color-mix(in oklch, var(--ax-accent) 35%, var(--ax-border)); }\n.ax-agent.is-errored { border-color: color-mix(in oklch, var(--ax-err) 35%, var(--ax-border)); }\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 { color: var(--ax-accent); font-size: 15px; font-weight: 500;\n font-family: var(--ax-mono); letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ax-agent__name { font-size: var(--ax-fs-sm); color: var(--ax-muted); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n.ax-agent__tier { display: flex; gap: 4px; flex-shrink: 0; }\n.ax-agent__model { font-size: var(--ax-fs-xs); color: var(--ax-muted); font-family: var(--ax-mono); margin-top: -4px; }\n.ax-agent__stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }\n.ax-ministat { padding: 8px 10px; background: var(--ax-bg); border: 1px solid var(--ax-border); border-radius: 4px; }\n.ax-ministat__label { display: flex; align-items: center; gap: 4px; font-size: 10px;\n color: var(--ax-muted); text-transform: uppercase; letter-spacing: 0.06em; }\n.ax-ministat__value { font-size: 18px; font-weight: 600; margin-top: 2px;\n letter-spacing: -0.02em; font-family: var(--ax-mono); font-variant-numeric: tabular-nums; }\n.ax-ministat--live .ax-ministat__value { color: var(--ax-accent); }\n.ax-ministat--warn .ax-ministat__value { color: var(--ax-warn); }\n.ax-ministat--err .ax-ministat__value { color: var(--ax-err); }\n.ax-agent__spark { border-top: 1px dashed var(--ax-border); padding-top: 8px; color: var(--ax-accent); }\n.ax-agent__spark-caption { display: flex; justify-content: space-between; font-size: 10px;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ax-muted); margin-top: 2px; }\n.ax-agent__spark svg { display: block; width: 100%; height: 28px; }\n.ax-agent__running { display: flex; flex-direction: column; gap: 6px; }\n.ax-agent__task { 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; font: inherit; color: var(--ax-text); }\n.ax-agent__task:hover { border-color: var(--ax-accent); }\n.ax-agent__task-head { display: flex; align-items: center; gap: 6px; font-size: var(--ax-fs-xs); color: var(--ax-accent); }\n.ax-agent__task-head .elapsed { margin-left: auto; font-family: var(--ax-mono); color: var(--ax-muted); }\n.ax-agent__task-body { font-size: var(--ax-fs-sm); margin-top: 4px;\n display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;\n line-height: 1.35; }\n.ax-agent__summary { border-top: 1px dashed var(--ax-border); padding-top: 8px; }\n.ax-agent__summary-caption { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--ax-muted); }\n.ax-agent__summary-text { 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.ax-agent__summary.is-fail .ax-agent__summary-caption { color: var(--ax-err); }\n.ax-agent__foot { display: flex; justify-content: space-between; align-items: center;\n font-size: var(--ax-fs-xs); color: var(--ax-muted); border-top: 1px solid var(--ax-border);\n padding-top: 8px; }\n.ax-linkbtn { background: transparent; border: none; color: var(--ax-text-2);\n padding: 2px 6px; font: inherit; cursor: pointer; font-size: var(--ax-fs-sm); text-decoration: none; }\n.ax-linkbtn:hover { color: var(--ax-accent); }\n\n.ax-empty { padding: 32px 10px; text-align: center; color: var(--ax-muted);\n font-size: var(--ax-fs-sm); border: 1px dashed var(--ax-border); border-radius: 4px; margin: 12px 14px; }\n\n@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.4 } }\n\n.history-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 360px;\n background: var(--node); border-left: 1px solid var(--border); z-index: 900;\n display: flex; flex-direction: column; box-shadow: -8px 0 24px rgba(0,0,0,0.4);\n transition: transform 0.2s ease; }\n.history-panel.hidden { transform: translateX(100%); pointer-events: none; }\n.history-panel > header { display: flex; align-items: center; gap: 10px; padding: 12px 14px;\n background: #10131c; border-bottom: 1px solid var(--border); }\n.history-panel > header h2 { margin: 0; font-size: 13px; font-weight: 600; flex: 1; color: var(--text); }\n.history-panel-source { font-size: 10px; color: var(--muted); font-family: ui-monospace, monospace; }\n.history-panel-close { background: transparent; border: none; color: var(--muted); font-size: 20px;\n cursor: pointer; padding: 0 6px; line-height: 1; }\n.history-panel-close:hover { color: var(--text); }\n.history-panel-body { flex: 1; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 6px; }\n.history-item { background: var(--card); border: 1px solid var(--border); border-radius: 6px;\n padding: 9px 11px; cursor: pointer; display: flex; flex-direction: column; gap: 4px; }\n.history-item:hover { border-color: var(--accent); }\n.history-item .top { display: flex; align-items: center; gap: 6px; font-size: 10px; color: var(--muted); }\n.history-item .top .channel { background: rgba(99,102,241,0.18); color: var(--accent);\n font-size: 9px; text-transform: uppercase; padding: 1px 6px; border-radius: 3px; letter-spacing: 0.5px; }\n.history-item .top .ok { color: var(--green); }\n.history-item .top .err { color: var(--red); }\n.history-item .top .when { margin-left: auto; font-family: ui-monospace, monospace; }\n.history-item .preview { color: var(--text); font-size: 12px; overflow: hidden; text-overflow: ellipsis;\n white-space: nowrap; }\n.history-item .duration { font-size: 10px; color: var(--muted); font-family: ui-monospace, monospace; }\n.history-empty { color: var(--muted); font-size: 12px; text-align: center; padding: 20px 8px; font-style: italic; }\n\n.task-modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 24px; }\n.task-modal.hidden { display: none; }\n.task-modal-backdrop { position: absolute; inset: 0; background: color-mix(in oklch, var(--ax-bg) 60%, black); }\n.task-modal-card { position: relative; width: min(900px, 92vw); height: min(640px, 86vh);\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.task-modal-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.task-modal-card > header h2 { margin: 0; font-size: 14px; font-weight: 600; flex: 1;\n color: var(--ax-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; letter-spacing: -0.005em; }\n.task-modal-channel { font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em;\n background: color-mix(in oklch, var(--ax-accent) 14%, transparent); color: var(--ax-accent);\n padding: 2px 7px; border-radius: 3px; font-family: var(--ax-mono); }\n.task-modal-status { font-size: 11px; color: var(--ax-muted); font-family: var(--ax-mono); }\n.task-modal-status.live { color: var(--ax-accent); }\n.task-modal-status.done { color: var(--ax-accent); opacity: 0.75; }\n.task-modal-status.err { color: var(--ax-err); }\n.task-modal-close { background: transparent; border: none; color: var(--ax-muted); font-size: 22px;\n cursor: pointer; padding: 0 6px; line-height: 1; }\n.task-modal-close:hover { color: var(--ax-text); }\n\n/* Event timeline — colored left borders per event kind.\n Structure: <div class=\"ax-ev ax-ev--<kind>\"><div class=\"ax-ev__head\">...</div><pre/div class=\"ax-ev__body\"/></div> */\n.task-modal-output { margin: 0; flex: 1; overflow: auto; padding: 16px 20px;\n background: var(--ax-bg); display: flex; flex-direction: column; gap: 14px; }\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 { 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.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 { 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.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 { 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.ax-ev__text { 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.ax-ev--thinking { display: flex; align-items: center; gap: 8px; padding: 8px 0 0 12px;\n color: var(--ax-muted); font-size: var(--ax-fs-sm); }\n.ax-ev__spinner { 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@keyframes ax-spin { to { transform: rotate(360deg); } }\n`\n\nconst LIVE_JS = `\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 let tasks = 0, durationMs = 0, errors = 0, inputTokens = 0, outputTokens = 0, cacheRead = 0, cacheCreate = 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 tasks += u.tasks || 0;\n durationMs += u.totalDuration || 0;\n errors += u.errors || 0;\n inputTokens += u.inputTokens || 0;\n outputTokens += u.outputTokens || 0;\n cacheRead += u.cacheReadTokens || 0;\n cacheCreate += u.cacheCreateTokens || 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 const totalTokens = inputTokens + outputTokens + cacheRead + cacheCreate;\n const topChannels = Object.keys(byChannel).sort((a, b) => byChannel[b] - byChannel[a]).slice(0, 3)\n .map(ch => ch + ' (' + byChannel[ch] + ')').join(' · ');\n\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 stat({ label: 'running now', value: summary.busy,\n sub: summary.busy === 0 ? 'nothing active' : 'across ' + Object.keys(byChannel).length + ' channels',\n variant: summary.busy > 0 ? 'live' : '' }) +\n stat({ label: 'tasks today', value: tasks, sub: topChannels || 'no activity yet' }) +\n stat({ label: 'tokens today', value: fmtTokens(totalTokens),\n sub: fmtDuration(durationMs) + ' of agent time' }) +\n stat({ label: L.errorsCount || 'failed', value: errors + summary.errors,\n sub: (errors + summary.errors) === 0 ? 'all clean' : 'check history',\n variant: (errors + summary.errors) > 0 ? 'err' : '' });\n}\n\nfunction stat({ label, value, sub, variant, pulse }) {\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 (sub ? '<div class=\"ax-stat__sub\">' + escapeHtml(String(sub)) + '</div>' : '') +\n '</div>';\n}\n\nfunction fmtDuration(ms) {\n if (!ms) return '0s';\n const s = Math.floor(ms / 1000);\n if (s < 60) return s + 's';\n const m = Math.floor(s / 60);\n if (m < 60) return m + 'm';\n const h = Math.floor(m / 60);\n const rm = m % 60;\n return h + 'h ' + (rm < 10 ? '0' : '') + rm + 'm';\n}\n\nfunction fmtTokens(n) {\n if (n < 1000) return String(n);\n if (n < 1_000_000) return (n / 1000).toFixed(1).replace(/\\\\.0$/, '') + 'k';\n return (n / 1_000_000).toFixed(2).replace(/\\\\.00$/, '') + 'M';\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 const errored = (a.errors || 0) > 0;\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)\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 return '<button class=\"ax-agent__task\"' + dataAttrs + ' 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 '</button>';\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 // Sparkline — last 24 hourly task counts\n const sparkBlock = (Array.isArray(a.hourlyTasks) && a.hourlyTasks.length)\n ? '<div class=\"ax-agent__spark\">' + renderSpark(a.hourlyTasks) +\n '<div class=\"ax-agent__spark-caption\"><span>tasks · last 24h</span><span class=\"ax-mono\">' +\n a.hourlyTasks.reduce(function(s,v){return s+v}, 0) + ' total</span></div>' +\n '</div>'\n : '';\n\n // Mini stats row\n const miniStats = '<div class=\"ax-agent__stats\">' +\n '<div class=\"ax-ministat' + (busy ? ' ax-ministat--live' : '') + '\">' +\n '<div class=\"ax-ministat__label\">handling</div>' +\n '<div class=\"ax-ministat__value\">' + (a.active || 0) + '</div>' +\n '</div>' +\n '<div class=\"ax-ministat\">' +\n '<div class=\"ax-ministat__label\">today</div>' +\n '<div class=\"ax-ministat__value\">' + (a.total || 0) + '</div>' +\n '</div>' +\n '<div class=\"ax-ministat' + (errored ? ' ax-ministat--err' : '') + '\">' +\n '<div class=\"ax-ministat__label\">failed</div>' +\n '<div class=\"ax-ministat__value\">' + (a.errors || 0) + '</div>' +\n '</div>' +\n '</div>';\n\n const lastActiveText = a.lastActive ? 'last active ' + fmtAgo(a.lastActive) : (L.neverRan || 'not used yet');\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--accent\"><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 card.innerHTML =\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 (a.model ? '<div class=\"ax-agent__model\">' + escapeHtml(shortenModel(a.model)) + '</div>' : '') +\n miniStats +\n sparkBlock +\n runningBlock +\n summaryBlock +\n '<div class=\"ax-agent__foot\"><span>' + escapeHtml(lastActiveText) + '</span>' + recentLink + '</div>';\n return card;\n}\n\n/** Inline SVG sparkline — polyline + dot on the last point. 100% width, 28px high. */\nfunction renderSpark(data) {\n const w = 280, h = 28;\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 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 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 conn.className = 'conn ok'; conn.title = 'connected';\n try { render(JSON.parse(ev.data)); } catch {}\n });\n es.addEventListener('error', () => {\n conn.className = 'conn err'; conn.title = 'reconnecting…';\n es.close(); setTimeout(open, 2000);\n });\n };\n open();\n}\n\n// Re-render every second to tick elapsed counters even when no new snapshot arrives.\nlet lastSnap = null;\nfetch('/api/live').then(r => r.json()).then(s => { lastSnap = s; render(s); }).catch(() => {});\nsetInterval(() => { if (lastSnap) {\n // Patch elapsed times without a fresh snapshot\n document.querySelectorAll('.agent .task .elapsed').forEach((el) => { /* placeholder: server snapshot advances on stream */ });\n} }, 1000);\nconnect();\n\n// --- Task output modal ---\n// Per-modal parser state so consecutive chunks coalesce cleanly.\nconst taskModal = {\n el: document.getElementById('task-modal'),\n output: document.getElementById('task-modal-output'),\n title: document.getElementById('task-modal-title'),\n status: document.getElementById('task-modal-status'),\n channel: document.getElementById('task-modal-channel'),\n closeBtn: document.getElementById('task-modal-close'),\n backdrop: null,\n es: null,\n currentTaskId: null,\n /** Trailing line fragment from the last chunk (completes on next newline). */\n lineBuf: '',\n /** Active text event DOM node — consecutive plain-text lines coalesce into it. */\n openTextEv: null,\n};\ntaskModal.backdrop = taskModal.el && taskModal.el.querySelector('.task-modal-backdrop');\n\nfunction setStatus(label, kind) {\n if (!taskModal.status) return;\n taskModal.status.textContent = label;\n taskModal.status.className = 'task-modal-status' + (kind ? ' ' + kind : '');\n}\n\nfunction resetOutput() {\n if (!taskModal.output) return;\n taskModal.output.innerHTML = '';\n taskModal.lineBuf = '';\n taskModal.openTextEv = null;\n}\n\n/**\n * Feed a raw transcript chunk to the event renderer. Handles partial lines\n * (buffered until the next newline) and coalesces bare text lines into a\n * single \"assistant text\" event until a structured marker arrives.\n *\n * Prefixes emitted by the backend formatter (src/agents/registry.ts):\n * '· ' system (init, done)\n * '→ ' tool_use\n * '← ' tool_result (or '← [error] ' for is_error)\n * '💭 ' thinking / thought block\n * anything assistant text\n */\nfunction appendOutput(text) {\n if (!text || !taskModal.output) return;\n const atBottom = taskModal.output.scrollTop + taskModal.output.clientHeight >= taskModal.output.scrollHeight - 30;\n taskModal.lineBuf += text;\n const lines = taskModal.lineBuf.split('\\\\n');\n taskModal.lineBuf = lines.pop();\n for (const line of lines) processStreamLine(line);\n if (atBottom) taskModal.output.scrollTop = taskModal.output.scrollHeight;\n}\n\nfunction processStreamLine(line) {\n if (line.startsWith('· ')) { closeOpenText(); renderSystemEvent(line.slice(2)); return; }\n if (line.startsWith('→ ')) { closeOpenText(); renderToolUseEvent(line.slice(2)); return; }\n if (line.startsWith('← ')) { closeOpenText(); renderToolResultEvent(line.slice(2)); return; }\n if (line.startsWith('💭 ')) { closeOpenText(); renderThoughtEvent(line.slice(2)); return; }\n if (line.startsWith('[error] ')) { closeOpenText(); renderErrorEvent(line.slice(8)); return; }\n if (line.startsWith('[task finished]')) { closeOpenText(); renderSystemEvent('task finished', true); return; }\n if (line === '') {\n // Blank line = paragraph break in assistant text. Keep the block open but\n // insert a blank line so long replies stay readable.\n if (taskModal.openTextEv) {\n const body = taskModal.openTextEv.querySelector('.ax-ev__text');\n if (body) body.appendChild(document.createTextNode('\\\\n\\\\n'));\n }\n return;\n }\n appendToOpenText(line);\n}\n\nfunction closeOpenText() { taskModal.openTextEv = null; }\n\nfunction appendToOpenText(line) {\n if (!taskModal.openTextEv) {\n const ev = document.createElement('div');\n ev.className = 'ax-ev ax-ev--text';\n ev.innerHTML = '<div class=\"ax-ev__head\"><span class=\"ax-ev__label ax-ev__label--text\">response</span></div><div class=\"ax-ev__text\"></div>';\n taskModal.output.appendChild(ev);\n taskModal.openTextEv = ev;\n }\n const body = taskModal.openTextEv.querySelector('.ax-ev__text');\n if (body.childNodes.length > 0) body.appendChild(document.createTextNode('\\\\n'));\n body.appendChild(document.createTextNode(line));\n}\n\nfunction renderSystemEvent(text, done) {\n const ev = document.createElement('div');\n ev.className = 'ax-ev ax-ev--system' + (done ? ' is-done' : '');\n ev.innerHTML = '<div class=\"ax-ev__head\"><span class=\"ax-ev__label ax-ev__label--soft\">system</span>' +\n '<span>' + escapeHtml(text) + '</span></div>';\n taskModal.output.appendChild(ev);\n}\n\nfunction renderToolUseEvent(text) {\n // text looks like 'ToolName({...input...})' — best-effort split on the first paren.\n const openIdx = text.indexOf('(');\n const closeIdx = text.lastIndexOf(')');\n const name = openIdx > 0 ? text.slice(0, openIdx) : text;\n const args = (openIdx > 0 && closeIdx > openIdx) ? text.slice(openIdx + 1, closeIdx) : '';\n const ev = document.createElement('div');\n ev.className = 'ax-ev ax-ev--tool';\n const head =\n '<div class=\"ax-ev__head\">' +\n '<span class=\"ax-ev__label ax-ev__label--tool\">tool call</span>' +\n '<span class=\"ax-ev__tool\">' + escapeHtml(name) + '</span>' +\n '</div>';\n const body = args ? '<pre class=\"ax-ev__code\">' + escapeHtml(args) + '</pre>' : '';\n ev.innerHTML = head + body;\n taskModal.output.appendChild(ev);\n}\n\nfunction renderToolResultEvent(text) {\n const isErr = text.startsWith('[error] ');\n const body = isErr ? text.slice('[error] '.length) : text;\n const ev = document.createElement('div');\n ev.className = 'ax-ev ax-ev--tool-result' + (isErr ? ' is-err' : '');\n ev.innerHTML =\n '<div class=\"ax-ev__head\">' +\n '<span class=\"ax-ev__label ax-ev__label--result\">' + (isErr ? 'tool error' : 'tool result') + '</span>' +\n '</div>' +\n '<pre class=\"ax-ev__code ' + (isErr ? 'ax-ev__code--err' : 'ax-ev__code--muted') + '\">' + escapeHtml(body) + '</pre>';\n taskModal.output.appendChild(ev);\n}\n\nfunction renderThoughtEvent(text) {\n const ev = document.createElement('div');\n ev.className = 'ax-ev ax-ev--thought';\n ev.innerHTML =\n '<div class=\"ax-ev__head\">' +\n '<span class=\"ax-ev__label ax-ev__label--soft\">internal</span>' +\n '</div>' +\n '<div class=\"ax-ev__thought\">' + escapeHtml(text) + '</div>';\n taskModal.output.appendChild(ev);\n}\n\nfunction renderErrorEvent(text) {\n const ev = document.createElement('div');\n ev.className = 'ax-ev ax-ev--error';\n ev.innerHTML =\n '<div class=\"ax-ev__head\"><span class=\"ax-ev__label ax-ev__label--error\">error</span></div>' +\n '<pre class=\"ax-ev__code ax-ev__code--err\">' + escapeHtml(text) + '</pre>';\n taskModal.output.appendChild(ev);\n}\n\nfunction closeTaskModal() {\n if (!taskModal.el) return;\n taskModal.el.classList.add('hidden');\n taskModal.el.setAttribute('aria-hidden', 'true');\n if (taskModal.es) { try { taskModal.es.close(); } catch {} taskModal.es = null; }\n taskModal.currentTaskId = null;\n}\n\nfunction openTaskModal(opts) {\n if (!taskModal.el || !opts.taskId || !opts.nodeUrl) return;\n if (taskModal.currentTaskId === opts.taskId) { taskModal.el.classList.remove('hidden'); return; }\n const L = window.UI_LABELS || {};\n closeTaskModal();\n taskModal.currentTaskId = opts.taskId;\n taskModal.el.classList.remove('hidden');\n taskModal.el.setAttribute('aria-hidden', 'false');\n taskModal.title.textContent = opts.agentName + ' · ' + (opts.preview || 'task ' + opts.taskId);\n taskModal.title.title = opts.preview || '';\n taskModal.channel.textContent = opts.channel || '—';\n resetOutput();\n setStatus(L.taskModalConnecting || 'connecting…', '');\n const url = '/api/task/stream?node=' + encodeURIComponent(opts.nodeUrl)\n + '&agent=' + encodeURIComponent(opts.agentId)\n + '&task=' + encodeURIComponent(opts.taskId);\n let es;\n try { es = new EventSource(url); } catch (e) { setStatus('connect failed', 'err'); return; }\n taskModal.es = es;\n es.addEventListener('start', (ev) => {\n setStatus(L.taskModalLive || 'live', 'live');\n try { const data = JSON.parse(ev.data); if (data.initial) appendOutput(data.initial); if (data.done) setStatus(L.taskModalFinished || 'finished', 'done'); } catch {}\n });\n es.addEventListener('chunk', (ev) => {\n try { const data = JSON.parse(ev.data); appendOutput(data.text || ''); } catch {}\n });\n es.addEventListener('end', () => {\n setStatus(L.taskModalFinished || 'finished', 'done');\n try { es.close(); } catch {}\n taskModal.es = null;\n });\n es.addEventListener('error', () => {\n if (es.readyState === 2) {\n setStatus('disconnected', 'err');\n taskModal.es = null;\n }\n });\n}\n\nif (taskModal.closeBtn) taskModal.closeBtn.addEventListener('click', closeTaskModal);\nif (taskModal.backdrop) taskModal.backdrop.addEventListener('click', closeTaskModal);\ndocument.addEventListener('keydown', (e) => { if (e.key === 'Escape' && taskModal.el && !taskModal.el.classList.contains('hidden')) closeTaskModal(); });\n\n// --- History panel ---\nconst historyPanel = {\n el: document.getElementById('history-panel'),\n body: document.getElementById('history-panel-body'),\n title: document.getElementById('history-panel-title'),\n source: document.getElementById('history-panel-source'),\n closeBtn: document.getElementById('history-panel-close'),\n current: null,\n};\n\nfunction closeHistoryPanel() {\n if (!historyPanel.el) return;\n historyPanel.el.classList.add('hidden');\n historyPanel.el.setAttribute('aria-hidden', 'true');\n historyPanel.current = null;\n}\n\nasync function openHistoryPanel(opts) {\n if (!historyPanel.el) return;\n const L = window.UI_LABELS || {};\n historyPanel.current = { agentId: opts.agentId, nodeUrl: opts.nodeUrl };\n historyPanel.title.textContent = (opts.agentName || opts.agentId) + ' · ' + (L.historyPanelTitle || 'Recent activities');\n historyPanel.source.textContent = opts.nodeUrl;\n historyPanel.body.innerHTML = '<div class=\"history-empty\">' + escapeHtml(L.historyLoading || 'loading…') + '</div>';\n historyPanel.el.classList.remove('hidden');\n historyPanel.el.setAttribute('aria-hidden', 'false');\n const url = '/api/task/history?node=' + encodeURIComponent(opts.nodeUrl)\n + '&agent=' + encodeURIComponent(opts.agentId) + '&limit=50';\n try {\n const r = await fetch(url);\n if (!r.ok) throw new Error('HTTP ' + r.status);\n const items = await r.json();\n renderHistoryList(items, opts);\n } catch (e) {\n historyPanel.body.innerHTML = '<div class=\"history-empty\" style=\"color:var(--red)\">' + escapeHtml(e.message) + '</div>';\n }\n}\n\nfunction renderHistoryList(items, opts) {\n const L = window.UI_LABELS || {};\n if (!Array.isArray(items) || items.length === 0) {\n historyPanel.body.innerHTML = '<div class=\"history-empty\">' + escapeHtml(L.historyEmpty || 'No recorded tasks yet.') + '</div>';\n return;\n }\n historyPanel.body.innerHTML = '';\n for (const it of items) {\n const div = document.createElement('div');\n div.className = 'history-item';\n div.dataset.taskId = it.id;\n const flag = it.ok ? '<span class=\"ok\">✓</span>' : '<span class=\"err\">✗</span>';\n const dur = it.durationMs ? fmtElapsed(it.durationMs) : '—';\n const when = it.endedAt ? fmtAgoShort(it.endedAt) : '';\n const channel = '<span class=\"channel\">' + escapeHtml(it.channel || '—') + '</span>';\n const sender = it.sender ? ' · ' + escapeHtml(it.sender) : '';\n div.innerHTML =\n '<div class=\"top\">' + flag + channel + sender + '<span class=\"when\">' + escapeHtml(when) + '</span></div>' +\n '<div class=\"preview\">' + escapeHtml((it.message || '').slice(0, 200)) + '</div>' +\n '<div class=\"duration\">' + escapeHtml(dur) + (it.error ? ' · ' + escapeHtml(it.error.slice(0, 80)) : '') + '</div>';\n div.addEventListener('click', () => openTaskRecord({\n taskId: it.id, agentId: opts.agentId, nodeUrl: opts.nodeUrl,\n channel: it.channel, agentName: opts.agentName,\n preview: it.message || '',\n }));\n historyPanel.body.appendChild(div);\n }\n}\n\nfunction fmtAgoShort(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';\n const m = Math.floor(s / 60); if (m < 60) return m + 'm';\n const h = Math.floor(m / 60); if (h < 24) return h + 'h';\n return Math.floor(h / 24) + 'd';\n}\n\n// Open the task modal in \"history\" mode — fetch the stored record once,\n// dump the transcript + final response, no SSE.\nasync function openTaskRecord(opts) {\n if (!taskModal.el) return;\n const L = window.UI_LABELS || {};\n closeTaskModal();\n taskModal.currentTaskId = opts.taskId;\n taskModal.el.classList.remove('hidden');\n taskModal.el.setAttribute('aria-hidden', 'false');\n taskModal.title.textContent = (opts.agentName || opts.agentId) + ' · ' + (opts.preview || 'task ' + opts.taskId);\n taskModal.title.title = opts.preview || '';\n taskModal.channel.textContent = opts.channel || '—';\n resetOutput();\n setStatus(L.historyLoading || 'loading…', '');\n const url = '/api/task/history?node=' + encodeURIComponent(opts.nodeUrl)\n + '&agent=' + encodeURIComponent(opts.agentId) + '&task=' + encodeURIComponent(opts.taskId);\n try {\n const r = await fetch(url);\n if (!r.ok) throw new Error('HTTP ' + r.status);\n const rec = await r.json();\n setStatus(rec.ok ? (L.taskModalArchived || 'archived') : (L.taskModalFinished || 'failed'), rec.ok ? 'done' : 'err');\n if (rec.transcript) appendOutput(rec.transcript);\n if (rec.responseText && rec.transcript.indexOf(rec.responseText) === -1) {\n appendOutput('\\\\n\\\\n--- ' + (L.taskModalFinalResponse || 'Final reply') + ' ---\\\\n' + rec.responseText);\n }\n if (rec.error) appendOutput('\\\\n\\\\n[error] ' + rec.error);\n } catch (e) {\n setStatus(L.taskModalLoadFailed || 'couldn\\\\'t load', 'err');\n appendOutput('Error: ' + e.message);\n }\n}\n\nif (historyPanel.closeBtn) historyPanel.closeBtn.addEventListener('click', closeHistoryPanel);\n\n// Click delegation on the agent grid — opens the modal for any task card,\n// or the history panel for the \"history →\" link.\ndocument.getElementById('grid').addEventListener('click', (e) => {\n const taskEl = e.target.closest('.ax-agent__task[data-task-id]');\n if (taskEl) {\n e.preventDefault();\n openTaskModal({\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') || taskEl.textContent || '',\n });\n return;\n }\n const recentEl = e.target.closest('[data-recent]');\n if (recentEl) {\n e.preventDefault();\n openHistoryPanel({\n agentId: recentEl.dataset.agentId,\n agentName: recentEl.dataset.agentName,\n nodeUrl: recentEl.dataset.nodeUrl,\n });\n }\n});\n`\n\nconst BOARD_CSS = `\n/* --- Design tokens (ops-daemon, matches /live and /admin) --- */\n:root {\n --ax-bg: oklch(0.16 0.010 265); --ax-bg-elev: oklch(0.19 0.012 265);\n --ax-surface: oklch(0.21 0.012 265); --ax-surface-2: oklch(0.24 0.014 265);\n --ax-border: oklch(0.29 0.014 265); --ax-border-2: oklch(0.35 0.016 265);\n --ax-text: oklch(0.95 0.005 265); --ax-text-2: oklch(0.80 0.008 265);\n --ax-muted: oklch(0.60 0.010 265); --ax-accent: oklch(0.78 0.13 165);\n --ax-warn: oklch(0.80 0.14 75); --ax-err: oklch(0.68 0.19 25);\n --ax-info: oklch(0.78 0.10 220);\n --ax-font: \"IBM Plex Sans\", -apple-system, \"Segoe UI\", sans-serif;\n --ax-mono: \"IBM Plex Mono\", ui-monospace, \"SF Mono\", Consolas, monospace;\n /* Legacy aliases so the existing CSS keeps working. */\n --bg: var(--ax-bg); --card: var(--ax-surface); --col: var(--ax-bg-elev);\n --col-hdr: var(--ax-surface); --border: var(--ax-border);\n --border-soft: color-mix(in oklch, var(--ax-border) 35%, transparent);\n --text: var(--ax-text); --muted: var(--ax-muted); --accent: var(--ax-accent);\n --green: var(--ax-accent); --yellow: var(--ax-warn); --orange: var(--ax-warn);\n --red: var(--ax-err); --blue: var(--ax-info); --gray: var(--ax-muted);\n color-scheme: dark;\n}\n[data-theme=\"light\"]{--ax-bg:oklch(0.98 0.002 265);--ax-bg-elev:oklch(0.96 0.003 265);\n --ax-surface:oklch(0.99 0.002 265);--ax-surface-2:oklch(0.955 0.003 265);\n --ax-border:oklch(0.88 0.006 265);--ax-border-2:oklch(0.78 0.008 265);\n --ax-text:oklch(0.22 0.010 265);--ax-text-2:oklch(0.36 0.010 265);\n --ax-muted:oklch(0.54 0.010 265);--ax-accent:oklch(0.55 0.14 165);color-scheme:light}\n[data-theme=\"crt\"]{--ax-bg:#05140a;--ax-bg-elev:#061a0d;--ax-surface:#08201f;\n --ax-surface-2:#0b2922;--ax-border:#164a30;--ax-border-2:#1f6a44;\n --ax-text:#b7ffcc;--ax-text-2:#83e3a8;--ax-muted:#4f9a73;--ax-accent:#6dff9e;\n --ax-font:\"IBM Plex Mono\",ui-monospace,monospace}\n.ax-theme-switch{display:inline-flex;border:1px solid var(--ax-border-2);\n border-radius:4px;overflow:hidden;margin-right:4px}\n.ax-theme-switch button{background:transparent;border:none;color:var(--ax-muted);\n padding:3px 9px;font:inherit;font-size:10px;cursor:pointer;letter-spacing:0.04em;\n text-transform:uppercase;font-family:var(--ax-mono);border-right:1px solid var(--ax-border-2)}\n.ax-theme-switch button:last-child{border-right:none}\n.ax-theme-switch button:hover{color:var(--ax-text);background:var(--ax-surface-2)}\n.ax-theme-switch button.is-active{color:var(--ax-accent);\n background:color-mix(in oklch,var(--ax-accent) 12%,var(--ax-surface))}\n* { box-sizing: border-box; }\nhtml, body { margin: 0; height: 100%; background: var(--ax-bg); color: var(--ax-text);\n font-family: var(--ax-font); font-size: 13px; line-height: 1.5;\n -webkit-font-smoothing: antialiased; font-feature-settings: \"ss01\", \"cv01\"; }\npre, code, .mono { font-family: var(--ax-mono); font-variant-numeric: tabular-nums; }\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`\n\nconst BOARD_JS = `\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 (clawd).\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","// 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","// 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 \"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 (reasoning + tools + files), an SDK provider (OpenAI, Anthropic API, Ollama), or an Orchestrator that coordinates sub-agents.\",\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","import { existsSync, readFileSync, writeFileSync, rmSync } 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 { renderTopbar, TOPBAR_HEAD, TOPBAR_CSS, TOPBAR_SCRIPT, type TopbarPeer } from \"./topbar\"\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(renderAdminHtml(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 \"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 \"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 }\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 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 }))\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.botToken || \"\",\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 })) : []\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 }\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 return { exists: true, agents, telegram, slack, discord, gitlab, whatsapp, crons, webhooks, mesh, daemonUrl, nodeName: cfg.node?.name }\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 = body?.tier === \"sdk\" ? \"sdk\" : \"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 require(\"fs\").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\", \"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 cfg.channels.telegram.accounts[id] = {\n botToken: \"${\" + botTokenEnv + \"}\",\n botUsername,\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.botToken = \"${\" + 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/**\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\nconst WEBHOOK_SOURCES = [\"gitlab\", \"github\", \"sentry\", \"stripe\", \"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 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 if (!cfg.agents?.[agentId]) throw new Error(`Unknown agent \"${agentId}\".`)\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 cfg.webhooks.push(entry)\n return `added webhook \"${id}\" (${source} → ${agentId})`\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 { require(\"fs\").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 { loadDaemonConfig } = require(\"./config\")\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// 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\nfunction renderAdminHtml(peers: TopbarPeer[], localToken?: string): string {\n const topbar = renderTopbar({ activeTab: \"admin\", subtitle: \"Settings\", peers })\n // Inject the local dashboard token so the admin UI can send it as a\n // Bearer header on every same-origin request. Scoped per-page — only\n // useful while this admin view is open in the browser.\n const tokenScript = localToken\n ? `<script>window.AX_LOCAL_TOKEN = ${JSON.stringify(localToken)};</script>`\n : \"\"\n return `<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"dark\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>AgentX · Settings</title>\n${TOPBAR_HEAD}\n<style>\n/* --- Design tokens (ops-daemon, matches /live) --- */\n:root{\n --ax-bg:oklch(0.16 0.010 265);--ax-bg-elev:oklch(0.19 0.012 265);\n --ax-surface:oklch(0.21 0.012 265);--ax-surface-2:oklch(0.24 0.014 265);\n --ax-border:oklch(0.29 0.014 265);--ax-border-2:oklch(0.35 0.016 265);\n --ax-text:oklch(0.95 0.005 265);--ax-text-2:oklch(0.80 0.008 265);\n --ax-muted:oklch(0.60 0.010 265);\n --ax-accent:oklch(0.78 0.13 165);--ax-warn:oklch(0.80 0.14 75);\n --ax-err:oklch(0.68 0.19 25);--ax-info:oklch(0.78 0.10 220);\n --ax-font:\"IBM Plex Sans\",-apple-system,\"Segoe UI\",sans-serif;\n --ax-mono:\"IBM Plex Mono\",ui-monospace,\"SF Mono\",Consolas,monospace;\n /* Legacy aliases so existing class names inherit the new palette. */\n --bg:var(--ax-bg);--card:var(--ax-surface);--border:var(--ax-border);\n --text:var(--ax-text);--muted:var(--ax-muted);--accent:var(--ax-accent);\n --green:var(--ax-accent);--red:var(--ax-err);--yellow:var(--ax-warn);\n color-scheme:dark;\n}\n[data-theme=\"light\"]{--ax-bg:oklch(0.98 0.002 265);--ax-bg-elev:oklch(0.96 0.003 265);\n --ax-surface:oklch(0.99 0.002 265);--ax-surface-2:oklch(0.955 0.003 265);\n --ax-border:oklch(0.88 0.006 265);--ax-border-2:oklch(0.78 0.008 265);\n --ax-text:oklch(0.22 0.010 265);--ax-text-2:oklch(0.36 0.010 265);\n --ax-muted:oklch(0.54 0.010 265);--ax-accent:oklch(0.55 0.14 165);color-scheme:light}\n[data-theme=\"crt\"]{--ax-bg:#05140a;--ax-bg-elev:#061a0d;--ax-surface:#08201f;\n --ax-surface-2:#0b2922;--ax-border:#164a30;--ax-border-2:#1f6a44;\n --ax-text:#b7ffcc;--ax-text-2:#83e3a8;--ax-muted:#4f9a73;--ax-accent:#6dff9e;\n --ax-font:\"IBM Plex Mono\",ui-monospace,monospace}\n${TOPBAR_CSS}\n*{box-sizing:border-box}\nhtml,body{margin:0;min-height:100vh;background:var(--ax-bg);color:var(--ax-text);\n font-family:var(--ax-font);font-size:13px;line-height:1.5;\n -webkit-font-smoothing:antialiased;font-feature-settings:\"ss01\",\"cv01\"}\ncode,.mono{font-family:var(--ax-mono);font-variant-numeric:tabular-nums}\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</style>\n\n${tokenScript}\n<style>\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</style>\n</head>\n<body>\n${topbar}\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 <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=\"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 <h2>Agents</h2>\n <p class=\"lead\">Each agent is a role with its own trigger words and folder. Delete removes the config only — the agent's folder stays on disk.</p>\n <div id=\"agent-list\" class=\"list\"></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=\"sdk\">Anthropic API (SDK)</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 <h2>Channels</h2>\n <p class=\"lead\">Everywhere agents can answer from. Pick a connector on the left to configure or check status.</p>\n <div class=\"ch-split\">\n <aside class=\"ch-menu\" id=\"ch-menu\"></aside>\n <div class=\"ch-pane\">\n <div id=\"ch-telegram\" class=\"ch-view\"><div id=\"tg-section\" class=\"section-block\"></div></div>\n <div id=\"ch-whatsapp\" class=\"ch-view\" hidden></div>\n <div id=\"ch-slack\" class=\"ch-view\" hidden><div id=\"slack-section\" class=\"section-block\"></div></div>\n <div id=\"ch-discord\" class=\"ch-view\" hidden></div>\n <div id=\"ch-gitlab\" class=\"ch-view\" hidden></div>\n <div id=\"ch-github\" class=\"ch-view\" hidden></div>\n </div>\n </div>\n </section>\n\n <section id=\"tab-crons\" class=\"tab\">\n <h2>Schedules</h2>\n <p class=\"lead\">Recurring jobs that nudge an agent without a human trigger. Use cron syntax — e.g. <code>0 9 * * 1</code> for Mondays at 9am.</p>\n <div id=\"cron-list\" class=\"list\"></div>\n <div class=\"add-form\">\n <h3>Add a schedule</h3>\n <div class=\"rowf\">\n <div><label>Schedule id<span class=\"hint\">(lowercase)</span></label><input id=\"c-id\" placeholder=\"weekly-report\" /></div>\n <div><label>Agent</label><select id=\"c-agent\"></select></div>\n </div>\n <label>Cron expression<span class=\"hint\">(<a href=\"https://crontab.guru/\" target=\"_blank\" style=\"color:var(--accent)\">crontab.guru</a> can help)</span></label>\n <input id=\"c-schedule\" placeholder=\"0 9 * * 1\" />\n <div id=\"c-preview\" class=\"hint-block\" style=\"margin-top:4px;min-height:32px\"></div>\n <label>Prompt<span class=\"hint\">(what the agent should do on every tick)</span></label>\n <textarea id=\"c-prompt\" placeholder=\"Send me the weekly sales summary.\"></textarea>\n <div class=\"actions\"><button class=\"primary\" onclick=\"addCron()\">Add schedule</button><div id=\"c-msg\" class=\"msg\"></div></div>\n </div>\n </section>\n\n <section id=\"tab-webhooks\" class=\"tab\">\n <h2>Webhooks</h2>\n <p class=\"lead\">Each webhook is an inbound URL an external service POSTs to. We translate the payload into a readable message and route it to the agent you bind here. Defaults: GitLab, GitHub, Sentry, Stripe, Discord, Slack, custom.</p>\n <div id=\"webhook-list\" class=\"list\"></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=\"mtgl-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=\"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=\"MTGL 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 <h2>Mesh — team network</h2>\n <p class=\"lead\">Link AgentX instances across machines so they share work. Each peer is another daemon's URL; an optional token secures the connection.</p>\n <div class=\"toggle-switch\"><input type=\"checkbox\" id=\"mesh-toggle\" /> <label for=\"mesh-toggle\" style=\"color:var(--text);margin:0\">Mesh enabled on this node</label></div>\n <div id=\"mesh-peers\" class=\"list\"></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=\"clawd-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 </section>\n\n <section id=\"tab-tokens\" class=\"tab\">\n <h2>Access tokens</h2>\n <p class=\"lead\">Scoped tokens let external apps and mesh peers call AgentX. Pick the narrowest scope that covers what the caller needs — tokens can't be recovered if leaked, so short expiries are a good habit.</p>\n <div id=\"token-list\" class=\"list\"></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 <h2>Advanced — raw agentx.json</h2>\n <p class=\"lead\">Direct editor for the full config. Saving creates a timestamped backup so you can roll back. After a save, the daemon is asked to hot-reload — if a field needs a restart, you'll see it in the dashboard.</p>\n <textarea id=\"raw-editor\" class=\"raw\" spellcheck=\"false\"></textarea>\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<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 } 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: { icon: '🦊', label: 'GitLab', hint: 'In GitLab → Settings → Webhooks; tick Push events, Issue events, Pipeline events.' },\n github: { icon: '🐙', label: 'GitHub', hint: 'In GitHub → Repo Settings → Webhooks; pick individual events.' },\n sentry: { icon: '🛡', label: 'Sentry', hint: 'In Sentry → Project Settings → Alerts → Webhooks.' },\n stripe: { icon: '💳', label: 'Stripe', hint: 'In Stripe Dashboard → Developers → Webhooks.' },\n discord: { icon: '💬', label: 'Discord', hint: 'In Discord → Channel Settings → Integrations → Webhooks.' },\n slack: { icon: '#️⃣', label: 'Slack', hint: 'Slack Outgoing Webhooks / Events API — point at the URL below.' },\n custom: { icon: '🔗', label: 'Custom', hint: 'Any service that can POST JSON — the payload is forwarded as-is.' },\n };\n if (!state.webhooks.length) {\n list.innerHTML = '<div class=\"empty\">No webhooks registered.</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 div = document.createElement('div');\n div.className = 'row-card';\n div.style.flexWrap = 'wrap';\n div.style.gap = '10px';\n const statusChip = w.enabled\n ? '<span class=\"chip\" style=\"background:rgba(34,197,94,0.15);color:var(--green)\">enabled</span>'\n : '<span class=\"chip off\">disabled</span>';\n div.innerHTML =\n '<div class=\"info\" style=\"min-width:240px\"><h3>' + meta.icon + ' ' + escapeHtml(w.id) + '</h3>' +\n '<div class=\"meta\">' + statusChip +\n '<span class=\"chip\">' + escapeHtml(meta.label) + '</span>' +\n 'agent: <b>' + escapeHtml(w.agentId) + '</b>' +\n (w.secretEnv ? ' · secret: <code style=\"font-family:ui-monospace,monospace\">$\\{' + escapeHtml(w.secretEnv) + '}</code>' : '') +\n (w.description ? '<br><span style=\"color:var(--muted)\">' + escapeHtml(w.description) + '</span>' : '') +\n '</div>' +\n '</div>' +\n '<div style=\"flex:1;min-width:300px\">' +\n '<code style=\"display:block;font-family:ui-monospace,monospace;font-size:11px;background:#0e1119;padding:8px 10px;border-radius:4px;word-break:break-all\">' + escapeHtml(url) + '</code>' +\n '<div style=\"font-size:10px;color:var(--muted);margin-top:4px\">' + escapeHtml(meta.hint) + '</div>' +\n '</div>' +\n '<div style=\"display:flex;gap:6px\">' +\n '<button class=\"ghost\" data-copy=\"' + escapeHtml(url) + '\">Copy URL</button>' +\n '<button class=\"ghost\" data-toggle-wh=\"' + escapeHtml(w.id) + '\" data-enabled=\"' + (w.enabled ? '1' : '0') + '\">' + (w.enabled ? 'Disable' : 'Enable') + '</button>' +\n '<button class=\"danger\" data-rm-wh=\"' + escapeHtml(w.id) + '\">Delete</button>' +\n '</div>';\n div.querySelector('button[data-copy]').addEventListener('click', (e) => {\n navigator.clipboard.writeText(e.currentTarget.dataset.copy).then(() => {\n const b = e.currentTarget; const old = b.textContent;\n b.textContent = 'Copied'; setTimeout(() => { b.textContent = old; }, 1200);\n });\n });\n div.querySelector('button[data-rm-wh]').addEventListener('click', () => deleteWebhookAction(w.id));\n div.querySelector('button[data-toggle-wh]').addEventListener('click', () => toggleWebhook(w.id, !w.enabled));\n list.appendChild(div);\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: [] };\n $('mesh-toggle').checked = !!m.enabled;\n const peers = $('mesh-peers');\n if (!m.peers.length) {\n peers.innerHTML = '<div class=\"empty\">No mesh peers.</div>';\n } else {\n peers.innerHTML = '';\n for (const p of m.peers) {\n const div = document.createElement('div');\n div.className = 'row-card';\n div.innerHTML =\n '<div class=\"info\"><h3>' + escapeHtml(p.name) + '</h3>' +\n '<div class=\"meta\">' +\n (p.hasToken ? '<span class=\"chip\" style=\"background:rgba(34,197,94,0.15);color:var(--green)\">authenticated</span>' : '<span class=\"chip off\">no token</span>') +\n '<code style=\"font-family:ui-monospace,monospace\">' + escapeHtml(p.url) + '</code>' +\n '</div>' +\n '</div>' +\n '<button class=\"danger\" data-rm-peer=\"' + escapeHtml(p.url) + '\">Remove</button>';\n div.querySelector('button[data-rm-peer]').addEventListener('click', () => removeMeshPeer(p.url));\n peers.appendChild(div);\n }\n }\n}\n\n$('mesh-toggle').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\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 renderAgents() {\n const list = $('agent-list');\n if (!state.agents.length) { list.innerHTML = '<div class=\"empty\">No agents yet.</div>'; return; }\n list.innerHTML = '';\n for (const a of state.agents) {\n const div = document.createElement('div');\n div.className = 'row-card';\n const accessChip = a.access === 'public'\n ? '<span class=\"chip\" style=\"background:rgba(34,197,94,0.15);color:var(--green)\">public API</span>'\n : '<span class=\"chip off\">private</span>';\n div.innerHTML =\n '<div class=\"info\"><h3>' + escapeHtml(a.name) + '</h3>' +\n '<div class=\"meta\">' +\n '<span class=\"chip\">' + escapeHtml(a.tier || '') + '</span>' +\n (a.model ? '<span class=\"chip\">' + escapeHtml(a.model) + '</span>' : '') +\n accessChip +\n '<b>' + escapeHtml(a.id) + '</b> · triggers: ' + (a.mentions.map(escapeHtml).join(', ') || '—') +\n '</div>' +\n '</div>' +\n '<button class=\"primary\" data-test=\"' + escapeHtml(a.id) + '\" data-name=\"' + escapeHtml(a.name) + '\" style=\"margin-right:6px;padding:6px 12px;font-size:12px\">Test drive</button>' +\n '<button class=\"ghost\" data-edit=\"' + escapeHtml(a.id) + '\" style=\"margin-right:6px\">Edit</button>' +\n '<button class=\"ghost\" data-files=\"' + escapeHtml(a.id) + '\" data-name=\"' + escapeHtml(a.name) + '\" style=\"margin-right:6px\">Files</button>' +\n '<button class=\"ghost\" data-toggle=\"' + escapeHtml(a.id) + '\" data-access=\"' + escapeHtml(a.access) + '\" style=\"margin-right:6px\">' + (a.access === 'public' ? 'Make private' : 'Make public') + '</button>' +\n '<button class=\"danger\" data-id=\"' + escapeHtml(a.id) + '\">Delete</button>';\n div.querySelector('button.danger').addEventListener('click', () => deleteAgent(a.id));\n div.querySelector('button.ghost[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 div.querySelector('button[data-test]').addEventListener('click', (e) => {\n const btn = e.currentTarget;\n openTestDrive(btn.dataset.test, btn.dataset.name);\n });\n div.querySelector('button[data-edit]').addEventListener('click', () => openAgentEdit(a));\n div.querySelector('button[data-files]').addEventListener('click', (e) => {\n const btn = e.currentTarget;\n openFilesModal(btn.dataset.files, btn.dataset.name);\n });\n list.appendChild(div);\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\nfunction renderChannels() {\n renderChannelsMenu();\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: 'slack', icon: 'SL', label: 'Slack' },\n { id: 'discord', icon: 'DC', label: 'Discord' },\n { id: 'gitlab', icon: 'GL', label: 'GitLab' },\n { id: 'github', icon: 'GH', label: 'GitHub' },\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 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 startWhatsAppPolling();\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) { list.innerHTML = '<div class=\"empty\">No schedules yet.</div>'; return; }\n list.innerHTML = '';\n for (const c of state.crons) {\n const div = document.createElement('div');\n div.className = 'row-card';\n div.innerHTML =\n '<div class=\"info\"><h3>' + escapeHtml(c.id) + '</h3>' +\n '<div class=\"meta\"><span class=\"chip\">' + escapeHtml(c.schedule) + '</span> agent: <b>' + escapeHtml(c.agent) + '</b> — ' + escapeHtml(c.prompt) + '</div>' +\n '</div>' +\n '<button class=\"danger\" data-id=\"' + escapeHtml(c.id) + '\">Delete</button>';\n div.querySelector('button.danger').addEventListener('click', () => deleteCron(c.id));\n list.appendChild(div);\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// 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) { list.innerHTML = '<div class=\"empty\">No tokens issued.</div>'; return; }\n list.innerHTML = '';\n for (const t of tokens) {\n const div = document.createElement('div');\n div.className = 'row-card';\n const status = t.revokedAt\n ? '<span class=\"chip\" style=\"background:rgba(239,68,68,0.15);color:var(--red)\">revoked</span>'\n : (t.expiresAt && Date.parse(t.expiresAt) < Date.now()\n ? '<span class=\"chip\" style=\"background:rgba(245,158,11,0.15);color:var(--yellow)\">expired</span>'\n : '<span class=\"chip\" style=\"background:rgba(34,197,94,0.15);color:var(--green)\">active</span>');\n const expiry = t.expiresAt ? ' · expires ' + new Date(t.expiresAt).toLocaleDateString() : ' · no expiry';\n const lastUsed = t.lastUsedAt ? ' · last used ' + new Date(t.lastUsedAt).toLocaleString() : ' · never used';\n div.innerHTML =\n '<div class=\"info\"><h3>' + escapeHtml(t.name) + '</h3>' +\n '<div class=\"meta\">' + status +\n '<b>' + escapeHtml(t.id) + '</b> · <code style=\"font-family:ui-monospace,monospace\">' + escapeHtml(t.prefix) + '…</code> · scopes: ' + (t.scopes.map(escapeHtml).join(', ') || '—') + expiry + lastUsed +\n '</div>' +\n '</div>' +\n (t.revokedAt ? '' : '<button class=\"danger\" data-id=\"' + escapeHtml(t.id) + '\">Revoke</button>');\n const del = div.querySelector('button.danger');\n if (del) del.addEventListener('click', () => revokeToken(t.id));\n $('token-list').appendChild(div);\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\nasync function loadRaw() {\n try {\n const r = await fetch('/api/admin/config');\n const txt = await r.text();\n $('raw-editor').value = txt;\n showMsg($('r-msg'), 'ok', 'Loaded.');\n } catch (e) { showMsg($('r-msg'), 'err', e.message); }\n}\n\nasync function saveRaw() {\n const raw = $('raw-editor').value;\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 } catch (e) { showMsg($('r-msg'), 'err', e.message); }\n}\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</script>\n${TOPBAR_SCRIPT}\n</body>\n</html>`\n}\n","import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, statSync } from \"fs\"\nimport { resolve, relative, dirname, join, extname, basename } from \"path\"\n\n// --- Scoped file operations for the admin panel ---\n//\n// Lets the dashboard read/write identity files (CLAUDE.md / SOUL.md /\n// IDENTITY.md) and skill files (.claude/skills/<slug>/SKILL.md) WITHIN an\n// agent's workspace without ever touching the rest of the filesystem.\n// Every public function resolves the requested path against the agent's\n// workspace and refuses anything that would escape via .. symlinks, etc.\n\nconst MAX_FILE_BYTES = 200 * 1024 // 200 KB — plenty for CLAUDE.md / skills.\nconst ALLOWED_EXTENSIONS = new Set([\".md\", \".markdown\", \".txt\"])\nconst IDENTITY_FILENAMES = new Set([\n \"CLAUDE.md\", \"SOUL.md\", \"IDENTITY.md\", \"PERSONA.md\", \"AGENT.md\", \"BOOTSTRAP.md\",\n])\n\nexport interface FileOverview {\n identity: Array<{\n path: string // relative to workspace, e.g. \"CLAUDE.md\"\n title: string\n size: number\n exists: boolean // canonical files can be listed as templates even if missing\n }>\n skills: Array<{\n slug: string // dir name under .claude/skills/\n title: string // first # heading in SKILL.md or fallback to slug\n size: number\n path: string // relative to workspace, e.g. \".claude/skills/foo/SKILL.md\"\n }>\n workspace: string // absolute, for display only\n}\n\n/**\n * Resolve a user-supplied path to an absolute path strictly within `workspace`.\n * Throws if the result would escape. Returns the normalised absolute path.\n */\nfunction resolveInWorkspace(workspace: string, relPath: string): string {\n if (!workspace) throw new Error(\"Agent workspace is not configured.\")\n const wsAbs = resolve(workspace)\n if (!existsSync(wsAbs)) {\n // Create the workspace lazily — a fresh agent scaffolded via the wizard\n // may have an empty directory created but not yet with file structure.\n mkdirSync(wsAbs, { recursive: true })\n }\n // Reject absolute-path attempts outright.\n if (!relPath || relPath.startsWith(\"/\") || relPath.match(/^[a-zA-Z]:/)) {\n throw new Error(`Path must be relative to the workspace: ${relPath}`)\n }\n const target = resolve(wsAbs, relPath)\n const rel = relative(wsAbs, target)\n // rel === \"\" means target IS the workspace dir — fine for list ops.\n // rel starts with \"..\" iff target is outside the workspace.\n if (rel.startsWith(\"..\")) {\n throw new Error(`Path escapes the workspace: ${relPath}`)\n }\n return target\n}\n\nfunction assertEditable(absPath: string): void {\n const ext = extname(absPath).toLowerCase()\n if (!ALLOWED_EXTENSIONS.has(ext)) {\n throw new Error(`Only text files (.md, .markdown, .txt) can be edited from the admin panel (got ${ext}).`)\n }\n}\n\nfunction extractTitle(markdown: string, fallback: string): string {\n const line = markdown.split(/\\r?\\n/).find((l) => l.trim().startsWith(\"#\"))\n if (!line) return fallback\n return line.replace(/^#+\\s*/, \"\").trim().slice(0, 120) || fallback\n}\n\n/**\n * List the identity files + skills for an agent. Identity \"canonical\" names\n * that don't exist are still reported (exists: false) so the UI can offer to\n * create them with a sensible default.\n */\nexport function listAgentFiles(workspace: string): FileOverview {\n const wsAbs = resolveInWorkspace(workspace, \".\")\n const identity: FileOverview[\"identity\"] = []\n const seen = new Set<string>()\n\n // Every .md in the workspace root is identity material; canonical names\n // sort first.\n let rootEntries: string[] = []\n try { rootEntries = readdirSync(wsAbs) } catch { /* empty */ }\n for (const name of IDENTITY_FILENAMES) {\n const abs = join(wsAbs, name)\n if (existsSync(abs) && statSync(abs).isFile()) {\n const size = statSync(abs).size\n const content = size > 0 && size < MAX_FILE_BYTES ? safeRead(abs) : \"\"\n identity.push({ path: name, title: extractTitle(content, name), size, exists: true })\n seen.add(name)\n } else {\n identity.push({ path: name, title: name, size: 0, exists: false })\n seen.add(name)\n }\n }\n for (const name of rootEntries) {\n if (seen.has(name)) continue\n const abs = join(wsAbs, name)\n try {\n const st = statSync(abs)\n if (!st.isFile()) continue\n if (!ALLOWED_EXTENSIONS.has(extname(name).toLowerCase())) continue\n const content = st.size > 0 && st.size < MAX_FILE_BYTES ? safeRead(abs) : \"\"\n identity.push({ path: name, title: extractTitle(content, name), size: st.size, exists: true })\n } catch { /* skip unreadable */ }\n }\n\n // Skills live under .claude/skills/<slug>/SKILL.md\n const skills: FileOverview[\"skills\"] = []\n const skillsRoot = join(wsAbs, \".claude\", \"skills\")\n if (existsSync(skillsRoot)) {\n try {\n for (const slug of readdirSync(skillsRoot)) {\n const dir = join(skillsRoot, slug)\n let st\n try { st = statSync(dir) } catch { continue }\n if (!st.isDirectory()) continue\n const file = join(dir, \"SKILL.md\")\n if (!existsSync(file)) continue\n const stf = statSync(file)\n const content = stf.size < MAX_FILE_BYTES ? safeRead(file) : \"\"\n skills.push({\n slug,\n title: extractTitle(content, slug),\n size: stf.size,\n path: `.claude/skills/${slug}/SKILL.md`,\n })\n }\n } catch { /* */ }\n }\n skills.sort((a, b) => a.slug.localeCompare(b.slug))\n\n return { identity, skills, workspace: wsAbs }\n}\n\nfunction safeRead(abs: string): string {\n try { return readFileSync(abs, \"utf-8\") } catch { return \"\" }\n}\n\n/**\n * Read a single file within the workspace. Throws if the path is outside the\n * workspace, has a disallowed extension, or doesn't exist.\n */\nexport function readAgentFile(workspace: string, relPath: string): { path: string; content: string } {\n const abs = resolveInWorkspace(workspace, relPath)\n assertEditable(abs)\n if (!existsSync(abs)) return { path: relPath, content: \"\" }\n if (statSync(abs).size > MAX_FILE_BYTES) {\n throw new Error(`File is too large to edit in the admin panel (> ${MAX_FILE_BYTES / 1024} KB).`)\n }\n return { path: relPath, content: readFileSync(abs, \"utf-8\") }\n}\n\n/**\n * Write a file. Creates parent dirs as needed. Rejects extensions / size\n * outside the allow-list the same way readAgentFile does.\n */\nexport function writeAgentFile(workspace: string, relPath: string, content: string): { path: string; bytes: number } {\n const abs = resolveInWorkspace(workspace, relPath)\n assertEditable(abs)\n const buf = Buffer.from(content, \"utf-8\")\n if (buf.length > MAX_FILE_BYTES) {\n throw new Error(`File too large (${buf.length} > ${MAX_FILE_BYTES} bytes).`)\n }\n mkdirSync(dirname(abs), { recursive: true })\n writeFileSync(abs, buf)\n return { path: relPath, bytes: buf.length }\n}\n\n/**\n * Create a new skill scaffold at .claude/skills/<slug>/SKILL.md.\n */\nexport function createAgentSkill(\n workspace: string,\n slug: string,\n opts: { title?: string; content?: string } = {},\n): { slug: string; path: string } {\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(slug)) {\n throw new Error(\"Skill slug must be lowercase (letters, digits, -, _) and start with a letter or digit.\")\n }\n const relPath = `.claude/skills/${slug}/SKILL.md`\n const abs = resolveInWorkspace(workspace, relPath)\n if (existsSync(abs)) throw new Error(`Skill \"${slug}\" already exists.`)\n mkdirSync(dirname(abs), { recursive: true })\n const body = opts.content?.trim()\n ? opts.content\n : defaultSkillTemplate(slug, opts.title)\n writeFileSync(abs, body)\n return { slug, path: relPath }\n}\n\n/**\n * Remove a skill directory (.claude/skills/<slug>). Scoped to the workspace\n * so we can never touch anything else.\n */\nexport function deleteAgentSkill(workspace: string, slug: string): { slug: string } {\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(slug)) throw new Error(`Invalid skill slug: ${slug}`)\n const abs = resolveInWorkspace(workspace, `.claude/skills/${slug}`)\n if (!existsSync(abs)) throw new Error(`Skill \"${slug}\" not found.`)\n const st = statSync(abs)\n if (!st.isDirectory()) throw new Error(`Skill path is not a directory: ${slug}`)\n rmSync(abs, { recursive: true, force: true })\n return { slug }\n}\n\nfunction defaultSkillTemplate(slug: string, title?: string): string {\n const displayTitle = title?.trim() || slug\n return `# ${displayTitle}\n\nDescribe what this skill does and when the agent should use it. Keep it under ~5 lines — the agent reads this to decide whether to invoke the skill.\n\n## When to use\n- Trigger condition 1\n- Trigger condition 2\n\n## How to use\nDocument the arguments, commands, or files the skill expects. Example:\n\n\\`\\`\\`bash\n# Example invocation\nclaude-code ${basename(slug)} --arg value\n\\`\\`\\`\n`\n}\n","// --- Shared top-chrome for every dashboard surface ---\n//\n// Four pages render in different files (live + kanban + glossary in\n// board-dashboard.ts, settings in admin-panel.ts) but must share: design\n// tokens, fonts, theme switcher, brand, primary tab nav, and a mesh-node\n// selector. This module exports the CSS + HTML snippets so each page\n// composes exactly the same chrome and we never drift them again.\n//\n// Nothing here is page-specific — callers pass in { activeTab, subtitle,\n// subheader? } and compose their own <main> below.\n\nexport type TopbarTab = \"live\" | \"boards\" | \"admin\" | \"glossary\"\n\nexport interface TopbarPeer {\n /** Stable id: primary node id, or URL for configured daemons */\n id: string\n name: string\n /** Absolute URL of the peer's dashboard (http://host:port) — selector\n * navigates to <url>/admin when switched. */\n dashboardUrl: string\n /** True for the node hosting this dashboard (locks the default). */\n primary?: boolean\n /** Optional; shown in the dropdown as \"managing via token\" hint. */\n tokenScope?: string\n}\n\nexport interface TopbarOpts {\n activeTab: TopbarTab\n /** Text after the brand, e.g. \"Live\" / \"Settings\" / \"Boards\". */\n subtitle: string\n /** Optional HTML inserted as a second row under the topbar. */\n subheader?: string\n /** Peers to list in the mesh selector. */\n peers?: TopbarPeer[]\n /** id of the peer currently being managed — defaults to the primary. */\n currentPeerId?: string\n /** Optional right-side HTML added before the theme switcher (clocks,\n * connection dots, etc.). */\n rightExtras?: string\n}\n\n/**\n * Font link tags + tiny inline bootstrap script. Callers drop this in <head>.\n */\nexport const TOPBAR_HEAD = `<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<script>(function(){try{var t=localStorage.getItem('ax-theme')||'dark';document.documentElement.setAttribute('data-theme',t)}catch(e){}})();</script>`\n\n/**\n * CSS for topbar + subheader + theme switch + mesh selector. No token\n * definitions — those are per-page because each page's CSS block owns its\n * own :root overrides.\n */\nexport const TOPBAR_CSS = `\n.ax-topbar{display:flex;align-items:center;justify-content:space-between;\n padding:8px 18px;border-bottom:1px solid var(--ax-border);\n background:var(--ax-bg-elev);position:sticky;top:0;z-index:20;gap:10px;flex-wrap:nowrap}\n.ax-topbar__left{display:flex;align-items:center;gap:18px;min-width:0}\n.ax-brand{display:flex;align-items:center;gap:10px;font-weight:600;letter-spacing:-0.01em;flex-shrink:0}\n.ax-brand__mark{font-family:var(--ax-mono);font-size:12px;padding:2px 8px;\n border:1px solid var(--ax-border-2);color:var(--ax-accent);border-radius:4px}\n.ax-brand__name{font-size:14px}\n.ax-brand__subtitle{font-size:11px;color:var(--ax-muted);\n border-left:1px solid var(--ax-border);padding-left:12px;margin-left:4px}\n.ax-topbar__tabs{display:flex;gap:2px;min-width:0;overflow-x:auto}\n.ax-topbar__tab{background:transparent;border:none;color:var(--ax-text-2);\n padding:8px 14px;font:inherit;cursor:pointer;font-size:12px;text-decoration:none;\n border-bottom:2px solid transparent;letter-spacing:-0.005em;white-space:nowrap}\n.ax-topbar__tab:hover{color:var(--ax-text)}\n.ax-topbar__tab.is-active{color:var(--ax-text);border-bottom-color:var(--ax-accent)}\n.ax-topbar__right{display:flex;align-items:center;gap:10px;font-size:11px;\n color:var(--ax-muted);flex-shrink:0}\n.ax-topbar__right .ax-mono{color:var(--ax-text-2)}\n\n/* Mesh selector */\n.ax-mesh-sel{position:relative;display:inline-flex;align-items:center;gap:6px;\n padding:4px 10px;border:1px solid var(--ax-border-2);border-radius:4px;\n background:var(--ax-surface);cursor:pointer;font:inherit;font-size:11px;\n color:var(--ax-text-2);white-space:nowrap}\n.ax-mesh-sel:hover{border-color:var(--ax-accent);color:var(--ax-text)}\n.ax-mesh-sel.is-proxy{background:color-mix(in oklch,var(--ax-accent) 14%,var(--ax-surface));\n border-color:color-mix(in oklch,var(--ax-accent) 45%,var(--ax-border-2));color:var(--ax-accent)}\n.ax-mesh-sel .lbl{font-size:9px;letter-spacing:0.06em;text-transform:uppercase;color:var(--ax-muted);margin-right:2px}\n.ax-mesh-sel .name{font-weight:500;color:var(--ax-text)}\n.ax-mesh-sel .chev{font-size:9px;opacity:0.6;margin-left:2px}\n.ax-mesh-menu{position:absolute;top:calc(100% + 4px);right:0;min-width:240px;\n background:var(--ax-surface);border:1px solid var(--ax-border-2);\n border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,0.4);padding:4px;z-index:30;display:none}\n.ax-mesh-sel.is-open .ax-mesh-menu{display:block}\n.ax-mesh-menu a{display:block;padding:7px 10px;border-radius:4px;text-decoration:none;\n color:var(--ax-text-2);font-size:12px}\n.ax-mesh-menu a:hover{background:var(--ax-surface-2);color:var(--ax-text)}\n.ax-mesh-menu a.is-current{background:color-mix(in oklch,var(--ax-accent) 12%,var(--ax-surface));\n color:var(--ax-accent);border:1px solid color-mix(in oklch,var(--ax-accent) 30%,var(--ax-border-2))}\n.ax-mesh-menu a .row{display:flex;align-items:center;gap:8px}\n.ax-mesh-menu a .row .dot{width:6px;height:6px;border-radius:50%;background:var(--ax-accent)}\n.ax-mesh-menu a.is-offline .row .dot{background:var(--ax-border-2)}\n.ax-mesh-menu a .url{font-family:var(--ax-mono);font-size:10px;color:var(--ax-muted);margin-top:2px}\n\n/* Theme switcher segmented control */\n.ax-theme-switch{display:inline-flex;border:1px solid var(--ax-border-2);\n border-radius:4px;overflow:hidden}\n.ax-theme-switch button{background:transparent;border:none;color:var(--ax-muted);\n padding:3px 9px;font:inherit;font-size:10px;cursor:pointer;letter-spacing:0.04em;\n text-transform:uppercase;font-family:var(--ax-mono);border-right:1px solid var(--ax-border-2)}\n.ax-theme-switch button:last-child{border-right:none}\n.ax-theme-switch button:hover{color:var(--ax-text);background:var(--ax-surface-2,var(--ax-surface))}\n.ax-theme-switch button.is-active{color:var(--ax-accent);\n background:color-mix(in oklch,var(--ax-accent) 12%,var(--ax-surface))}\n\n/* Subheader — page-specific toolbar below the main topbar. Kanban uses it\n * for the board picker + search + filters; others can use it for context\n * bars if they want. */\n.ax-subheader{display:flex;align-items:center;gap:10px;padding:10px 18px;\n background:var(--ax-bg-elev);border-bottom:1px solid var(--ax-border);\n flex-wrap:wrap}\n.ax-subheader > * {flex-shrink:0}\n.ax-subheader .spacer{flex:1}`\n\n/**\n * Shared click wiring for the theme switcher + mesh selector. Safe to call\n * multiple times — only binds once per element.\n */\nexport const TOPBAR_SCRIPT = `<script>\n(function(){\n function wireTheme(){\n var sw = document.querySelectorAll('[data-theme-opt]');\n if (!sw.length) return;\n var current = document.documentElement.getAttribute('data-theme') || 'dark';\n sw.forEach(function(b){\n if (b.dataset.wired) return; b.dataset.wired = '1';\n if (b.getAttribute('data-theme-opt') === current) b.classList.add('is-active');\n b.addEventListener('click', function(){\n var t = b.getAttribute('data-theme-opt');\n document.documentElement.setAttribute('data-theme', t);\n try { localStorage.setItem('ax-theme', t); } catch (e) {}\n sw.forEach(function(x){ x.classList.toggle('is-active', x === b); });\n });\n });\n }\n function currentPeer(){\n try { return localStorage.getItem('ax-peer') || 'primary'; } catch { return 'primary'; }\n }\n function setCurrentPeer(id){\n try {\n if (id && id !== 'primary') localStorage.setItem('ax-peer', id);\n else localStorage.removeItem('ax-peer');\n } catch {}\n }\n function reflectPeerPill(){\n var sel = document.querySelector('.ax-mesh-sel');\n if (!sel) return;\n var cur = currentPeer();\n sel.classList.toggle('is-proxy', cur !== 'primary');\n // If a proxy is active, update the visible pill label + highlight.\n var picked = null;\n sel.querySelectorAll('.ax-mesh-menu a').forEach(function(a){\n a.classList.toggle('is-current', a.getAttribute('data-peer-id') === cur || (cur === 'primary' && a.getAttribute('data-peer-id') === 'primary'));\n if (a.getAttribute('data-peer-id') === cur) picked = a;\n });\n if (picked) {\n var name = picked.querySelector('span'); // first <span> inside .row\n var visibleName = sel.querySelector('.name');\n if (name && visibleName) visibleName.textContent = name.textContent.replace(/ · primary/, '');\n }\n }\n function wireMesh(){\n var sel = document.querySelector('.ax-mesh-sel');\n if (!sel || sel.dataset.wired) return; sel.dataset.wired = '1';\n sel.addEventListener('click', function(e){\n var link = e.target.closest('.ax-mesh-menu a[data-peer-action=\"proxy\"]');\n if (link) {\n e.preventDefault();\n setCurrentPeer(link.getAttribute('data-peer-id'));\n reflectPeerPill();\n sel.classList.remove('is-open');\n // Reload so the admin panel re-reads state from the selected peer.\n location.reload();\n return;\n }\n if (e.target.closest('.ax-mesh-menu a')) return; // plain navigate anchor\n sel.classList.toggle('is-open');\n });\n document.addEventListener('click', function(e){\n if (!sel.contains(e.target)) sel.classList.remove('is-open');\n });\n reflectPeerPill();\n }\n function wire(){ wireTheme(); wireMesh(); }\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', wire);\n } else { wire(); }\n})();\n</script>`\n\n/**\n * Render the topbar element. Caller places this at the start of <body>.\n * Emits a matching .ax-subheader row only when `subheader` HTML is provided.\n */\nexport function renderTopbar(opts: TopbarOpts): string {\n const peers = opts.peers && opts.peers.length > 0 ? opts.peers : [{ id: \"local\", name: \"this daemon\", dashboardUrl: \"\", primary: true }]\n const currentId = opts.currentPeerId || peers.find((p) => p.primary)?.id || peers[0].id\n const current = peers.find((p) => p.id === currentId) || peers[0]\n\n const meshMenu = peers.map((p) => {\n const isCurrent = p.id === currentId\n const primaryMark = p.primary ? ' <span style=\"color:var(--ax-muted);font-size:10px\">· primary</span>' : \"\"\n const proxyable = !!p.tokenScope && !p.primary\n // Proxyable peers: use data-peer-id so the click handler switches locally\n // via localStorage + reload. Un-proxyable (no token): legacy navigate.\n const href = proxyable\n ? \"#\"\n : p.dashboardUrl\n ? `${p.dashboardUrl.replace(/\\/+$/, \"\")}/admin`\n : \"#\"\n const dataAttr = proxyable ? ` data-peer-id=\"${esc(p.id)}\" data-peer-action=\"proxy\"` : p.primary ? ` data-peer-id=\"primary\" data-peer-action=\"proxy\"` : \"\"\n const hint = proxyable\n ? '<div class=\"url\" style=\"color:var(--ax-accent)\">manage from this dashboard ✓</div>'\n : p.tokenScope\n ? `<div class=\"url\">${esc(p.tokenScope)}</div>`\n : p.dashboardUrl\n ? '<div class=\"url\" style=\"color:var(--ax-muted)\">(open peer dashboard ↗)</div>'\n : \"\"\n return `<a href=\"${esc(href)}\" class=\"${isCurrent ? \"is-current\" : \"\"}\"${dataAttr}>\n <div class=\"row\"><span class=\"dot\"></span><span>${esc(p.name)}${primaryMark}</span></div>\n ${p.dashboardUrl ? `<div class=\"url\">${esc(p.dashboardUrl)}</div>` : \"\"}\n ${hint}\n </a>`\n }).join(\"\")\n\n const meshSelector = `<div class=\"ax-mesh-sel\" aria-haspopup=\"menu\">\n <span class=\"lbl\">managing</span>\n <span class=\"name\">${esc(current.name)}</span>\n <span class=\"chev\">▾</span>\n <div class=\"ax-mesh-menu\" role=\"menu\">${meshMenu}</div>\n </div>`\n\n const tabs = [\n { id: \"live\", label: \"Live\", href: \"/live\" },\n { id: \"boards\", label: \"Boards\", href: \"/\" },\n { id: \"admin\", label: \"Settings\", href: \"/admin\" },\n { id: \"glossary\", label: \"Glossary\", href: \"/glossary\" },\n ].map((t) => {\n const cls = t.id === opts.activeTab ? \"ax-topbar__tab is-active\" : \"ax-topbar__tab\"\n return `<a href=\"${t.href}\" class=\"${cls}\">${t.label}</a>`\n }).join(\"\")\n\n const header = `<header class=\"ax-topbar\">\n <div class=\"ax-topbar__left\">\n <div class=\"ax-brand\">\n <span class=\"ax-brand__mark\">AX</span>\n <span class=\"ax-brand__name\">AgentX</span>\n <span class=\"ax-brand__subtitle\">${esc(opts.subtitle)}</span>\n </div>\n <nav class=\"ax-topbar__tabs\">${tabs}</nav>\n </div>\n <div class=\"ax-topbar__right\">\n ${opts.rightExtras || \"\"}\n ${meshSelector}\n <div class=\"ax-theme-switch\" role=\"tablist\" aria-label=\"Theme\">\n <button data-theme-opt=\"dark\">Dark</button>\n <button data-theme-opt=\"light\">Light</button>\n <button data-theme-opt=\"crt\">CRT</button>\n </div>\n </div>\n</header>${opts.subheader ? `<div class=\"ax-subheader\">${opts.subheader}</div>` : \"\"}`\n return header\n}\n\nfunction esc(s: string): string {\n return String(s || \"\").replace(/[&<>\"']/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" }[c] as string))\n}\n"],"mappings":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6MAF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeAF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuTXC,GAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAknBVC,GAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0SZC,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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;","names":["init_config","createServer","appendFileSync","existsSync","mkdirSync","resolve","SORTABLE_JS","UI_LABELS","GLOSSARY","existsSync","readFileSync","writeFileSync","rmSync","resolve","init_config","existsSync","readFileSync","writeFileSync","mkdirSync","readdirSync","rmSync","statSync","resolve","relative","dirname","join","extname","basename","MAX_FILE_BYTES","ALLOWED_EXTENSIONS","IDENTITY_FILENAMES","resolveInWorkspace","workspace","relPath","wsAbs","target","assertEditable","absPath","ext","extractTitle","markdown","fallback","line","l","listAgentFiles","identity","seen","rootEntries","name","abs","size","content","safeRead","st","skills","skillsRoot","slug","dir","file","stf","a","b","readAgentFile","writeAgentFile","buf","createAgentSkill","opts","body","defaultSkillTemplate","deleteAgentSkill","title","TOPBAR_HEAD","TOPBAR_CSS","TOPBAR_SCRIPT","renderTopbar","opts","peers","currentId","p","current","meshMenu","isCurrent","primaryMark","proxyable","href","dataAttr","esc","hint","meshSelector","tabs","t","cls","s","c","handleAdminGet","_req","res","peers","localToken","renderAdminHtml","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","editTelegramAccount","configureSlack","toggleSlack","configureDiscord","toggleDiscord","configureGitLab","toggleGitLab","proxyDaemonJson","previewCron","addWebhook","editWebhook","deleteWebhook","addMeshPeer","deleteMeshPeer","toggleMesh","listFilesForAgent","readFileForAgent","writeFileForAgent","addSkillForAgent","removeSkillForAgent","key","handler","sendJson","result","e","status","data","ok","err","raw","c","file","resolve","existsSync","readFileSync","agents","id","a","telegramAccounts","telegram","acc","slack","discord","gitlab","whatsapp","crons","webhooks","w","mesh","p","daemonUrl","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","writeFileSync","patch","m","k","agentBinding","botUsername","botTokenEnv","pathOnDaemon","url","daemonTarget","workspaceFor","agentId","def","queryParam","overview","listAgentFiles","readAgentFile","content","writeAgentFile","slug","title","createAgentSkill","deleteAgentSkill","WEBHOOK_SOURCES","source","secretEnv","description","entry","x","before","enabled","appTokenEnv","tokenEnv","host","webhookPort","expr","getNextCronDate","human","tz","next","cursor","i","schedule","agent","prompt","backupPath","s","renderAdminHtml","peers","localToken","topbar","renderTopbar","tokenScript","TOPBAR_HEAD","TOPBAR_CSS","TOPBAR_SCRIPT","startBoardDashboard","config","dashboard","boards","port","bind","token","sources","board","buildWorkSource","e","createServer","req","res","handleRequest","sendJson","displayHost","b","ctx","url","path","method","wz","wizardState","html","renderLiveHtml","renderBoardHtml","renderGlossaryHtml","handleWizardGet","handleWizardPost","handleAdminGet","buildTopbarPeers","peerId","peer","findPeer","proxyAdminToPeer","authHeader","got","tokStore","TokenStore","rec","recordHasScope","handleAdminConfigGet","handleAdminApi","SORTABLE_JS","snap","buildLiveSnapshot","startLiveStream","u","nodeUrl","agentId","taskId","proxyTaskStream","limit","proxyTaskHistory","headers","r","list","body","readJson","drafted","draftIssue","publicAgentMatch","requireScopedToken","proxyPublicAgentMessage","membersMatch","source","members","milestonesMatch","ms","moveMatch","itemOpMatch","boardId","rawItemId","itemId","toCol","c","fromCol","applyTransition","audit","auditActor","columns","totals","loadBoardColumns","detail","updated","created","query","empty","baseLabels","extraLabel","search","milestone","assignee","openDays","closedDays","hasOpen","hasClosed","open","closed","closedCol","item","openColumns","stage","deriveStage","k","i","a","p","v","diff","transitionDiff","gl","daemon","agentUsernames","mapping","GitLabWorkSource","args","entry","dir","resolve","existsSync","mkdirSync","line","appendFileSync","fwd","fetchDaemonAgents","signal","base","healthRes","agentsRes","meshRes","agents","h","meshResSafe","_meshJsonCache","j","fetchMeshPeers","primaryUrl","dash","primaryToken","seen","d","key","ac","timeout","meshPeers","nodes","send","ev","data","tick","timer","target","allowed","peers","tokenForNode","upstreamCtl","upstreamRes","reader","value","done","required","extractToken","s","agentDef","message","daemonUrl","upstream","text","upstreamPath","DRAFT_CONVENTIONS","rough","boardCtx","userPrompt","content","parsed","extractJson","labels","x","fenced","start","depth","status","reject","chunks","raw","topbar","renderTopbar","TOPBAR_HEAD","BOARD_CSS","TOPBAR_CSS","BOARD_JS","TOPBAR_SCRIPT","labelsScript","UI_LABELS","rightExtras","escapeHtmlServer","LIVE_CSS","LIVE_JS","resolvePeerDashboardUrl","ctxConfig","cfg","primary","extras","pathWithQuery","ct","xrw","respBody","id","match","renderGlossaryHtml","config","items","GLOSSARY","g","alias","escapeHtmlServer","topbar","renderTopbar","buildTopbarPeers","UI_LABELS","TOPBAR_HEAD","TOPBAR_CSS","TOPBAR_SCRIPT","LIVE_CSS","LIVE_JS","BOARD_CSS","BOARD_JS"]}