agentix-cli 0.17.0 → 0.19.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.
- package/README.md +75 -27
- package/dist/agent-57MG4LBM.js +2 -0
- package/dist/audit-JU3HWISB.js +3 -0
- package/dist/audit-JU3HWISB.js.map +1 -0
- package/dist/{board-dashboard-CXIVJKDP.js → board-dashboard-HXDOYGIG.js} +2237 -69
- package/dist/board-dashboard-HXDOYGIG.js.map +1 -0
- package/dist/chunk-23AUU4S2.js +102 -0
- package/dist/chunk-23AUU4S2.js.map +1 -0
- package/dist/{chunk-EKTPCSN4.js → chunk-4ITQP3XL.js} +2 -2
- package/dist/chunk-4MREHZP5.js +111 -0
- package/dist/chunk-4MREHZP5.js.map +1 -0
- package/dist/chunk-4YCH6IZV.js +2 -0
- package/dist/chunk-53NRX2NA.js +2 -0
- package/dist/chunk-53NRX2NA.js.map +1 -0
- package/dist/chunk-6B74TO2J.js +743 -0
- package/dist/chunk-6B74TO2J.js.map +1 -0
- package/dist/chunk-7P5QRX5W.js +108 -0
- package/dist/chunk-7P5QRX5W.js.map +1 -0
- package/dist/chunk-A6BKSZQA.js +3 -0
- package/dist/chunk-A6BKSZQA.js.map +1 -0
- package/dist/{chunk-6PPIFDOQ.js → chunk-CNNLREHD.js} +67 -22
- package/dist/{chunk-6PPIFDOQ.js.map → chunk-CNNLREHD.js.map} +1 -1
- package/dist/chunk-DJEZ2HVU.js +5 -0
- package/dist/chunk-DJEZ2HVU.js.map +1 -0
- package/dist/chunk-DLVOHCTA.js +5 -0
- package/dist/{chunk-MBEQASP3.js.map → chunk-DLVOHCTA.js.map} +1 -1
- package/dist/chunk-E4ZZILAK.js +15 -0
- package/dist/chunk-E4ZZILAK.js.map +1 -0
- package/dist/{chunk-63UYRJIC.js → chunk-IKBHHTIT.js} +2 -2
- package/dist/chunk-JGXE2YNV.js +46 -0
- package/dist/chunk-JGXE2YNV.js.map +1 -0
- package/dist/chunk-JTJIPNXY.js +2 -0
- package/dist/chunk-JTJIPNXY.js.map +1 -0
- package/dist/chunk-MBOLXKA2.js +6 -0
- package/dist/chunk-MBOLXKA2.js.map +1 -0
- package/dist/{inbox-L5ZZ623Y.js → chunk-MTRWXRFF.js} +69 -4
- package/dist/chunk-MTRWXRFF.js.map +1 -0
- package/dist/{processes-XF5CV4LK.js → chunk-NOF4HSGK.js} +3 -3
- package/dist/{processes-XF5CV4LK.js.map → chunk-NOF4HSGK.js.map} +1 -1
- package/dist/chunk-QW2OYR3N.js +7 -0
- package/dist/chunk-QW2OYR3N.js.map +1 -0
- package/dist/chunk-SR3X25TN.js +3 -0
- package/dist/chunk-SR3X25TN.js.map +1 -0
- package/dist/chunk-SV3I4CI7.js +440 -0
- package/dist/chunk-SV3I4CI7.js.map +1 -0
- package/dist/{chunk-TT2Q7YMZ.js → chunk-TC3BSUP7.js} +6 -6
- package/dist/{chunk-TT2Q7YMZ.js.map → chunk-TC3BSUP7.js.map} +1 -1
- package/dist/chunk-VJUTQAPJ.js +2 -0
- package/dist/chunk-VJUTQAPJ.js.map +1 -0
- package/dist/chunk-VWTBHLMX.js +1 -0
- package/dist/chunk-WFHIIVLL.js +719 -0
- package/dist/chunk-WFHIIVLL.js.map +1 -0
- package/dist/chunk-WPPUOTY6.js +6 -0
- package/dist/chunk-WPPUOTY6.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/{compaction-X46EQEPS.js → compaction-BTUJJ3LJ.js} +4 -4
- package/dist/config-QXWKEQON.js +2 -0
- package/dist/config-mutator-53XGR2NT.js +2 -0
- package/dist/conflict-detector-D7VIYPDI.js +2 -0
- package/dist/conflict-detector-D7VIYPDI.js.map +1 -0
- package/dist/{context-planner-G2J7FNNU.js → context-planner-I7ID7XF3.js} +3 -3
- package/dist/debug-N3B5NVJU.js +2 -0
- package/dist/discover-XJXQXXIO.js +4 -0
- package/dist/discover-XJXQXXIO.js.map +1 -0
- package/dist/doctor-SEOUFHSL.js +2 -0
- package/dist/{editor-chat-R54E65W5.js → editor-chat-RVLCKKB7.js} +2 -2
- package/dist/heal-YEQYWX2L.js +2 -0
- package/dist/inbox-BIN2S5XN.js +2 -0
- package/dist/index.d.ts +1292 -122
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/{ingest-whatsapp-NS4OWDOU.js → ingest-whatsapp-LZE62INA.js} +2 -2
- package/dist/loader-5CA3O5GM.js +2 -0
- package/dist/loader-6IHUCPVO.js +2 -0
- package/dist/memory-extract-KRCHD3OP.js +2 -0
- package/dist/migrate-v2-QO7SLKIM.js +4 -0
- package/dist/migrate-v2-QO7SLKIM.js.map +1 -0
- package/dist/processes-RJ6RDP32.js +2 -0
- package/dist/program-SASDC2GV.js +2 -0
- package/dist/providers-WVCMIW67.js +2 -0
- package/dist/query-HZYYRCLU.js +2 -0
- package/dist/recipes-EPLE2SNT.js +2 -0
- package/dist/registry-HM4ELSXS.js +2 -0
- package/dist/rotation-memo-GMTZPMNE.js +13 -0
- package/dist/rotation-memo-GMTZPMNE.js.map +1 -0
- package/dist/scheduler-XVFIK4XH.js +2 -0
- package/dist/{slack-PSQLVTGB.js → slack-6P7ZJ32Y.js} +2 -2
- package/dist/store-BAPDZHXE.js +2 -0
- package/dist/{subagent-2SJHSSJS.js → subagent-WV7QKQ3L.js} +2 -2
- package/dist/{task-store-NM7VKNLE.js → task-store-RHISEU6V.js} +2 -2
- package/dist/task-store-RHISEU6V.js.map +1 -0
- package/dist/{telegram-W4SLFMNY.js → telegram-PIYDRM46.js} +2 -2
- package/dist/timers-5LPZN55J.js +2 -0
- package/dist/timers-5LPZN55J.js.map +1 -0
- package/dist/types-VECTMYTO.js +2 -0
- package/dist/types-VECTMYTO.js.map +1 -0
- package/dist/types-ZRLHHCGB.js +2 -0
- package/dist/types-ZRLHHCGB.js.map +1 -0
- package/dist/web/activity-graph.global.js +654 -0
- package/dist/web/activity-graph.global.js.map +1 -0
- package/dist/web/workflow-editor.global.js +35 -12
- package/dist/web/workflow-editor.global.js.map +1 -1
- package/dist/{whatsapp-DCHRBRVV.js → whatsapp-PFRDMUMT.js} +2 -2
- package/dist/{whatsapp-state-6D6FHHD2.js → whatsapp-state-XMTCSRXU.js} +2 -2
- package/dist/wiki-53W5NLZV.js +2 -0
- package/dist/wiki-53W5NLZV.js.map +1 -0
- package/dist/workspace-setup-FPGN5VNY.js +2 -0
- package/dist/workspace-setup-FPGN5VNY.js.map +1 -0
- package/package.json +13 -2
- package/dist/agent-4AOMVXRJ.js +0 -2
- package/dist/board-dashboard-CXIVJKDP.js.map +0 -1
- package/dist/chunk-34NG7UDX.js +0 -11
- package/dist/chunk-34NG7UDX.js.map +0 -1
- package/dist/chunk-4HP6UC2X.js +0 -13
- package/dist/chunk-4HP6UC2X.js.map +0 -1
- package/dist/chunk-5WNGHDFC.js +0 -164
- package/dist/chunk-5WNGHDFC.js.map +0 -1
- package/dist/chunk-ABCHHNG6.js +0 -625
- package/dist/chunk-ABCHHNG6.js.map +0 -1
- package/dist/chunk-FIOB3MEX.js +0 -2
- package/dist/chunk-HCFSHGK4.js +0 -46
- package/dist/chunk-HCFSHGK4.js.map +0 -1
- package/dist/chunk-HHJGKD5B.js +0 -71
- package/dist/chunk-HHJGKD5B.js.map +0 -1
- package/dist/chunk-LEIYC3BF.js +0 -104
- package/dist/chunk-LEIYC3BF.js.map +0 -1
- package/dist/chunk-MBEQASP3.js +0 -5
- package/dist/chunk-MCJUAE6V.js +0 -6
- package/dist/chunk-MCJUAE6V.js.map +0 -1
- package/dist/chunk-SRHLXZRF.js +0 -7
- package/dist/chunk-SRHLXZRF.js.map +0 -1
- package/dist/chunk-WZEYII2S.js +0 -6
- package/dist/chunk-WZEYII2S.js.map +0 -1
- package/dist/chunk-XDJSYQST.js +0 -6
- package/dist/chunk-XDJSYQST.js.map +0 -1
- package/dist/chunk-YPOELN6Z.js +0 -798
- package/dist/chunk-YPOELN6Z.js.map +0 -1
- package/dist/config-KCYHMKCT.js +0 -2
- package/dist/config-mutator-QP4GLBOE.js +0 -2
- package/dist/debug-3MEUO2P2.js +0 -2
- package/dist/heal-JDXUPRET.js +0 -2
- package/dist/inbox-L5ZZ623Y.js.map +0 -1
- package/dist/loader-PTCXLJEX.js +0 -2
- package/dist/memory-extract-WH4FZ456.js +0 -2
- package/dist/program-FFWEILAS.js +0 -2
- package/dist/providers-AKQ4HTB4.js +0 -2
- package/dist/query-PKGYE5HR.js +0 -2
- package/dist/registry-IFMDJWQ7.js +0 -2
- package/dist/scheduler-63D5KFO7.js +0 -2
- package/dist/store-5MJOO74U.js +0 -2
- package/dist/timers-PCB4ZIBD.js +0 -2
- package/dist/types-C7IFH42Z.js +0 -2
- package/dist/types-OIFU3KSQ.js +0 -2
- package/dist/usage-dashboard-AXPXB2KP.js +0 -803
- package/dist/usage-dashboard-AXPXB2KP.js.map +0 -1
- package/dist/wiki-UDPYFJBL.js +0 -2
- package/dist/workspace-setup-C4VB3QZV.js +0 -2
- /package/dist/{agent-4AOMVXRJ.js.map → agent-57MG4LBM.js.map} +0 -0
- /package/dist/{chunk-EKTPCSN4.js.map → chunk-4ITQP3XL.js.map} +0 -0
- /package/dist/{chunk-FIOB3MEX.js.map → chunk-4YCH6IZV.js.map} +0 -0
- /package/dist/{chunk-63UYRJIC.js.map → chunk-IKBHHTIT.js.map} +0 -0
- /package/dist/{config-KCYHMKCT.js.map → chunk-VWTBHLMX.js.map} +0 -0
- /package/dist/{compaction-X46EQEPS.js.map → compaction-BTUJJ3LJ.js.map} +0 -0
- /package/dist/{config-mutator-QP4GLBOE.js.map → config-QXWKEQON.js.map} +0 -0
- /package/dist/{debug-3MEUO2P2.js.map → config-mutator-53XGR2NT.js.map} +0 -0
- /package/dist/{context-planner-G2J7FNNU.js.map → context-planner-I7ID7XF3.js.map} +0 -0
- /package/dist/{heal-JDXUPRET.js.map → debug-N3B5NVJU.js.map} +0 -0
- /package/dist/{loader-PTCXLJEX.js.map → doctor-SEOUFHSL.js.map} +0 -0
- /package/dist/{editor-chat-R54E65W5.js.map → editor-chat-RVLCKKB7.js.map} +0 -0
- /package/dist/{memory-extract-WH4FZ456.js.map → heal-YEQYWX2L.js.map} +0 -0
- /package/dist/{program-FFWEILAS.js.map → inbox-BIN2S5XN.js.map} +0 -0
- /package/dist/{ingest-whatsapp-NS4OWDOU.js.map → ingest-whatsapp-LZE62INA.js.map} +0 -0
- /package/dist/{providers-AKQ4HTB4.js.map → loader-5CA3O5GM.js.map} +0 -0
- /package/dist/{query-PKGYE5HR.js.map → loader-6IHUCPVO.js.map} +0 -0
- /package/dist/{registry-IFMDJWQ7.js.map → memory-extract-KRCHD3OP.js.map} +0 -0
- /package/dist/{scheduler-63D5KFO7.js.map → processes-RJ6RDP32.js.map} +0 -0
- /package/dist/{store-5MJOO74U.js.map → program-SASDC2GV.js.map} +0 -0
- /package/dist/{task-store-NM7VKNLE.js.map → providers-WVCMIW67.js.map} +0 -0
- /package/dist/{timers-PCB4ZIBD.js.map → query-HZYYRCLU.js.map} +0 -0
- /package/dist/{types-C7IFH42Z.js.map → recipes-EPLE2SNT.js.map} +0 -0
- /package/dist/{types-OIFU3KSQ.js.map → registry-HM4ELSXS.js.map} +0 -0
- /package/dist/{wiki-UDPYFJBL.js.map → scheduler-XVFIK4XH.js.map} +0 -0
- /package/dist/{slack-PSQLVTGB.js.map → slack-6P7ZJ32Y.js.map} +0 -0
- /package/dist/{workspace-setup-C4VB3QZV.js.map → store-BAPDZHXE.js.map} +0 -0
- /package/dist/{subagent-2SJHSSJS.js.map → subagent-WV7QKQ3L.js.map} +0 -0
- /package/dist/{telegram-W4SLFMNY.js.map → telegram-PIYDRM46.js.map} +0 -0
- /package/dist/{whatsapp-DCHRBRVV.js.map → whatsapp-PFRDMUMT.js.map} +0 -0
- /package/dist/{whatsapp-state-6D6FHHD2.js.map → whatsapp-state-XMTCSRXU.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/daemon/board-dashboard.ts","../src/daemon/vendor/sortable.ts","../src/daemon/admin-panel.ts","../src/daemon/file-ops.ts","../src/daemon/ui/pages/admin.ts","../src/daemon/graph-panel.ts","../src/daemon/ui/pages/graph.ts","../src/daemon/agent-panel.ts","../src/daemon/ui/pages/agent.ts","../src/daemon/ui-labels.ts","../src/daemon/ui/pages/live.ts","../src/daemon/ui/pages/boards.ts","../src/daemon/ui/pages/glossary.ts","../src/daemon/ui/pages/workflows.ts","../src/daemon/ui/pages/workflow-editor.ts","../src/daemon/workflows-api.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type ServerResponse } from \"http\"\nimport { appendFileSync, existsSync, mkdirSync } from \"fs\"\nimport { readFile } from \"fs/promises\"\nimport { fileURLToPath } from \"url\"\nimport { dirname, resolve } from \"path\"\nimport type { DaemonConfig } from \"./config\"\nimport type { BoardConfig, BoardColumn } from \"@/boards/config\"\nimport { deriveStage, transitionDiff } from \"@/boards/config\"\nimport type { WorkSource, WorkItem } from \"@/business/work-pool\"\nimport { GitLabWorkSource } from \"@/business/work-pool\"\nimport { SORTABLE_JS } from \"./vendor/sortable\"\nimport { handleWizardGet, handleWizardPost, wizardState } from \"./setup-wizard\"\nimport { handleAdminGet, handleAdminApi, handleAdminConfigGet } from \"./admin-panel\"\nimport { handleGraphGet, handleGraphApi } from \"./graph-panel\"\nimport { handleAgentPageGet, handleAgentApi } from \"./agent-panel\"\nimport { renderLivePage } from \"./ui/pages/live\"\nimport { renderBoardsPage } from \"./ui/pages/boards\"\nimport { renderGlossaryPage } from \"./ui/pages/glossary\"\nimport { renderWorkflowsPage } from \"./ui/pages/workflows\"\nimport { renderWorkflowEditorPage } from \"./ui/pages/workflow-editor\"\nimport { handleWorkflowsApi } from \"./workflows-api\"\nimport { LayoutStore, RunStore, WorkflowStore, type WorkflowRun } from \"@/workflows\"\nimport { TokenStore, recordHasScope, extractToken, type TokenRecord } from \"./token-store\"\nimport 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 // Workflow stores are lightweight (filesystem-backed, no network). Created\n // once per dashboard so the /workflows page + editor API don't re-scan\n // directories on every request.\n //\n // Home-node id comes from the mesh config when available; falls back to a\n // stable \"local\" so single-node runs work out of the box. Until the\n // integration seam commits, the dispatcher itself isn't booted in the\n // daemon — the editor's view over these stores is read/write, but the\n // engine's run-time dispatch + mesh forwarding aren't wired yet.\n const wfNodeId = config.node?.id || \"local\"\n const wfDir = config.workflows?.dir ? resolve(process.cwd(), config.workflows.dir) : undefined\n const workflowStore = new WorkflowStore(wfDir ? { baseDir: wfDir } : undefined)\n const workflowRuns = new RunStore(wfDir ? { baseDir: wfDir, nodeId: wfNodeId } : { nodeId: wfNodeId })\n const workflowLayouts = new LayoutStore(wfDir ? { baseDir: wfDir } : undefined)\n\n const server = createServer(async (req, res) => {\n try {\n await handleRequest(req, res, { boards, sources, token, config, workflowStore, workflowRuns, workflowLayouts })\n } catch (e: any) {\n sendJson(res, 500, { error: e.message || \"internal error\" })\n }\n })\n\n server.listen(port, bind, () => {\n const displayHost = bind === \"0.0.0.0\" ? \"localhost\" : bind\n if (boards.length === 0) {\n console.log(`\\n Live dashboard: http://${displayHost}:${port}\\n`)\n console.log(\" (no boards configured — '/' serves the live view; add a board with 'agentx board add')\")\n } else {\n console.log(`\\n Kanban dashboard: http://${displayHost}:${port}\\n`)\n console.log(` Boards: ${boards.map((b) => b.id).join(\", \")}`)\n }\n if (bind === \"127.0.0.1\" && !token) {\n console.log(\" (bound to localhost; no auth token — safe default)\")\n } else if (bind !== \"127.0.0.1\" && !token) {\n console.log(\" ⚠ bound to a non-loopback address without a token — writes are UNAUTHENTICATED\")\n }\n })\n}\n\ninterface Ctx {\n boards: BoardConfig[]\n sources: Map<string, WorkSource>\n token?: string\n config: DaemonConfig\n workflowStore: WorkflowStore\n workflowRuns: RunStore\n workflowLayouts: LayoutStore\n}\n\nasync function handleRequest(req: IncomingMessage, res: ServerResponse, ctx: Ctx): Promise<void> {\n const url = new URL(req.url || \"/\", \"http://localhost\")\n const path = url.pathname\n const method = (req.method || \"GET\").toUpperCase()\n\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\")\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PATCH, OPTIONS\")\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization, X-Requested-With\")\n if (method === \"OPTIONS\") { res.writeHead(204); res.end(); return }\n\n 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 peers = buildTopbarPeers(ctx.config)\n const html = ctx.boards.length === 0\n ? renderLivePage({ peers })\n : renderBoardsPage({ peers })\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(renderLivePage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/glossary\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderGlossaryPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/workflows\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderWorkflowsPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n if (method === \"GET\" && path === \"/workflows/editor\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderWorkflowEditorPage({ peers: buildTopbarPeers(ctx.config) }))\n return\n }\n\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 // /admin/graph — Intent Knowledge Graph: pending approvals + taxonomy editor.\n if (method === \"GET\" && path === \"/admin/graph\") {\n handleGraphGet(req, res, buildTopbarPeers(ctx.config), ctx.token)\n return\n }\n // /admin/agents/<id> — dedicated per-agent page with md editor + skill mgr.\n const agentPageMatch = method === \"GET\" && path.match(/^\\/admin\\/agents\\/([a-z0-9][a-z0-9_-]*)$/)\n if (agentPageMatch) {\n handleAgentPageGet(req, res, agentPageMatch[1], buildTopbarPeers(ctx.config), ctx.token)\n return\n }\n // Cross-mesh 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/graph/\")) {\n await handleGraphApi(req, res, path)\n return\n }\n if (path.startsWith(\"/api/admin/agent/\")) {\n await handleAgentApi(req, res, path)\n return\n }\n if (path.startsWith(\"/api/admin/\")) {\n await handleAdminApi(req, res, path)\n return\n }\n if (method === \"GET\" && path === \"/sortable.min.js\") {\n res.writeHead(200, { \"Content-Type\": \"application/javascript; charset=utf-8\", \"Cache-Control\": \"public, max-age=86400\" })\n res.end(SORTABLE_JS)\n return\n }\n\n // Serve the web-bundled editor artifact. Built by `tsup --config tsup.web.config.ts`\n // to dist/web/workflow-editor.js. Any `/assets/<name>` request is mapped\n // 1:1 into dist/web so future bundles (graph editor, ...) can live there\n // without another route registration.\n if (method === \"GET\" && path.startsWith(\"/assets/\")) {\n const rel = path.slice(\"/assets/\".length)\n // Allow dotted stems (e.g. \"workflow-editor.global.js\") but keep the\n // whitelist narrow to script/map/style files.\n if (!/^[a-zA-Z0-9._-]+\\.(js|map|css)$/.test(rel)) {\n sendJson(res, 400, { error: \"invalid asset name\" }); return\n }\n const assetPath = resolveFromHere(\"web/\" + rel)\n try {\n const buf = await readAsset(assetPath)\n const ct = rel.endsWith(\".map\") ? \"application/json; charset=utf-8\"\n : rel.endsWith(\".css\") ? \"text/css; charset=utf-8\"\n : \"application/javascript; charset=utf-8\"\n res.writeHead(200, { \"Content-Type\": ct, \"Cache-Control\": \"public, max-age=60\" })\n res.end(buf)\n } catch {\n sendJson(res, 404, { error: \"asset not found\", hint: \"run: npm run build:web\" })\n }\n return\n }\n\n // Live mesh view endpoints (JSON snapshot + SSE push every 2s)\n if (method === \"GET\" && path === \"/api/live\") {\n try {\n const snap = await buildLiveSnapshot(ctx.config)\n sendJson(res, 200, snap)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n if (method === \"GET\" && path === \"/api/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\" || method === \"PUT\"\n if (isWrite && path.startsWith(\"/api/\")) {\n const xr = req.headers[\"x-requested-with\"]\n if (xr !== \"agentx-board\") { sendJson(res, 400, { error: \"missing X-Requested-With: agentx-board\" }); return }\n }\n\n // Read-through proxies for runs. The dashboard's local RunStore only\n // sees runs home-noded on THIS machine, but the user typically wants\n // the cross-fleet view: runs fired on clawd-server when GitLab events\n // land there, runs fired on Mac when local channels fire. We merge\n // the local list with the main-daemon's list and return the union.\n if (method === \"GET\" && path === \"/api/workflows/runs\") {\n try {\n const url = new URL(req.url || \"/\", \"http://localhost\")\n const limit = Math.max(1, Math.min(500, Number(url.searchParams.get(\"limit\") || 50)))\n const workflowId = url.searchParams.get(\"workflowId\") || undefined\n const local = ctx.workflowRuns.list({ limit, workflowId })\n const qs = `?limit=${limit}${workflowId ? `&workflowId=${encodeURIComponent(workflowId)}` : \"\"}`\n // Fan out to every configured peer daemon in parallel. Primary\n // (dashboard.daemonUrl) gets the dashboard.token; entries in\n // dashboard.daemons use their own per-peer token.\n const targets: Array<{ url: string; token?: string }> = [\n { url: ctx.config.dashboard.daemonUrl, token: ctx.config.dashboard.token },\n ...(ctx.config.dashboard.daemons || []).map((d) => ({ url: d.url, token: d.token })),\n ]\n const remoteLists = await Promise.all(targets.map(async (t) => {\n const headers: Record<string, string> = {}\n if (t.token) headers[\"Authorization\"] = `Bearer ${t.token}`\n try {\n const r = await fetch(`${t.url.replace(/\\/+$/, \"\")}/api/workflows/runs${qs}`, { headers })\n if (!r.ok) return []\n const data = await r.json() as { runs?: WorkflowRun[] }\n return Array.isArray(data.runs) ? data.runs : []\n } catch { return [] }\n }))\n const byId = new Map<string, WorkflowRun>()\n for (const r of local) byId.set(r.id, r)\n for (const list of remoteLists) for (const r of list) byId.set(r.id, r)\n const merged = Array.from(byId.values()).sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1)).slice(0, limit)\n sendJson(res, 200, { runs: merged })\n } catch (e: any) {\n sendJson(res, 502, { error: \"runs fetch failed\", message: e.message || String(e) })\n }\n return\n }\n const runDetailMatch = method === \"GET\" && path.match(/^\\/api\\/workflows\\/runs\\/([^\\/]+)$/)\n if (runDetailMatch) {\n const runId = decodeURIComponent(runDetailMatch[1])\n const local = ctx.workflowRuns.get(runId)\n if (local) { sendJson(res, 200, { run: local }); return }\n // Try every peer until one returns the run. First hit wins.\n const targets: Array<{ url: string; token?: string }> = [\n { url: ctx.config.dashboard.daemonUrl, token: ctx.config.dashboard.token },\n ...(ctx.config.dashboard.daemons || []).map((d) => ({ url: d.url, token: d.token })),\n ]\n for (const t of targets) {\n try {\n const headers: Record<string, string> = {}\n if (t.token) headers[\"Authorization\"] = `Bearer ${t.token}`\n const r = await fetch(`${t.url.replace(/\\/+$/, \"\")}/api/workflows/runs/${encodeURIComponent(runId)}`, { headers })\n if (r.ok) {\n sendJson(res, 200, await r.json())\n return\n }\n } catch { /* try next */ }\n }\n sendJson(res, 404, { error: \"run not found on any configured peer\" })\n return\n }\n\n // Workflow-builder chat (proxies to main daemon where the dispatcher\n // + AgentRegistry live). The board-dashboard serves /workflows/editor\n // but runs its own workflow stores; the chat endpoint needs the\n // running agent registry, which only the main daemon has.\n if (method === \"POST\" && path === \"/api/workflows/editor/chat\") {\n try {\n const body = await readJson(req)\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" }\n if (ctx.config.dashboard.token) headers[\"Authorization\"] = `Bearer ${ctx.config.dashboard.token}`\n const daemonUrl = ctx.config.dashboard.daemonUrl.replace(/\\/+$/, \"\")\n const r = await fetch(`${daemonUrl}/api/workflows/editor/chat`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body ?? {}),\n })\n const data = await r.json().catch(() => ({ error: `HTTP ${r.status}` }))\n sendJson(res, r.status, data)\n } catch (e: any) {\n sendJson(res, 502, { error: \"daemon unreachable\", message: e.message || String(e) })\n }\n return\n }\n\n // Workflow API — read (dashboard:read) / write (dashboard:write). Fine-\n // grained scope checks let read-only scoped tokens still populate the\n // /workflows observability page when dashboard.token isn't configured.\n if (path.startsWith(\"/api/workflows\")) {\n const handled = handleWorkflowsApi(req, res, {\n store: ctx.workflowStore,\n runs: ctx.workflowRuns,\n layouts: ctx.workflowLayouts,\n requireScope: (r, s, scopes) => {\n // When ctx.token is set the legacy check above already verified\n // write scope on any /api call, so we don't re-gate here.\n // When it's not set (localhost default), enforce scopes ourselves\n // only if the caller *presented* a token — otherwise allow through\n // to preserve the loopback \"safe default\" UX.\n if (ctx.token) return true\n const presented = extractToken(r as any)\n if (!presented) return true\n const rec = new TokenStore().verify(presented)\n if (!rec) { sendJson(s, 401, { error: \"invalid or revoked token\" }); return false }\n for (const scope of scopes) {\n if (!recordHasScope(rec, scope)) { sendJson(s, 403, { error: `token missing scope: ${scope}`, scopes: rec.scopes }); return false }\n }\n return true\n },\n })\n if (handled) return\n }\n\n if (method === \"GET\" && path === \"/api/boards\") {\n sendJson(res, 200, ctx.boards.map((b) => ({\n id: b.id, name: b.name, source: b.source,\n primaryToolLabel: b.primaryToolLabel, labels: b.labels,\n columns: b.columns, timeRangeDays: b.timeRangeDays,\n })))\n return\n }\n\n // /api/boards/:id/members — project members (for assignee picker)\n const membersMatch = path.match(/^\\/api\\/boards\\/([a-z0-9_-]+)\\/members$/)\n if (membersMatch && method === \"GET\") {\n const source = ctx.sources.get(membersMatch[1]) as GitLabWorkSource | undefined\n if (!source) { sendJson(res, 404, { error: \"unknown board\" }); return }\n try {\n const members = typeof source.listMembers === \"function\" ? await source.listMembers() : []\n sendJson(res, 200, members)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // /api/boards/:id/milestones — project milestones (for milestone picker)\n const milestonesMatch = path.match(/^\\/api\\/boards\\/([a-z0-9_-]+)\\/milestones$/)\n if (milestonesMatch && method === \"GET\") {\n const source = ctx.sources.get(milestonesMatch[1]) as GitLabWorkSource | undefined\n if (!source) { sendJson(res, 404, { error: \"unknown board\" }); return }\n try {\n const ms = typeof source.listMilestones === \"function\" ? await source.listMilestones() : []\n sendJson(res, 200, ms)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // /api/boards/:id/items[/...]\n // Legacy shape: /items/:itemId/move (move-only). New shape: /items/:itemId (GET/PATCH), /items (POST, GET).\n const moveMatch = path.match(/^\\/api\\/boards\\/([a-z0-9_-]+)\\/items\\/(.+?)\\/move$/)\n const itemOpMatch = path.match(/^\\/api\\/boards\\/([a-z0-9_-]+)\\/items(?:\\/(.+))?$/)\n\n if (moveMatch && method === \"PATCH\") {\n const [, boardId, rawItemId] = moveMatch\n const itemId = decodeURIComponent(rawItemId)\n const board = ctx.boards.find((b) => b.id === boardId)\n if (!board) { sendJson(res, 404, { error: `unknown board: ${boardId}` }); return }\n const source = ctx.sources.get(boardId)\n if (!source) { sendJson(res, 503, { error: `board \"${boardId}\" source unavailable` }); return }\n const body = await readJson(req)\n const toCol = board.columns.find((c) => c.id === body.to)\n const fromCol = board.columns.find((c) => c.id === body.from)\n if (!toCol) { sendJson(res, 400, { error: `unknown 'to' column: ${body.to}` }); return }\n try {\n await applyTransition(source, itemId, fromCol, toCol)\n audit({ actor: auditActor(req), action: \"move\", boardId, itemId, payload: { from: body.from, to: body.to } })\n sendJson(res, 200, { ok: true })\n } catch (e: any) { sendJson(res, 502, { error: e.message || \"transition failed\" }) }\n return\n }\n\n if (itemOpMatch) {\n const [, boardId, rawItemId] = itemOpMatch\n const itemId = rawItemId ? decodeURIComponent(rawItemId) : undefined\n const board = ctx.boards.find((b) => b.id === boardId)\n if (!board) { sendJson(res, 404, { error: `unknown board: ${boardId}` }); return }\n const source = ctx.sources.get(boardId) as GitLabWorkSource | undefined\n if (!source) { sendJson(res, 503, { error: `board \"${boardId}\" source unavailable` }); return }\n\n // GET /items — column dump\n if (method === \"GET\" && !itemId) {\n const { columns, totals } = await loadBoardColumns(board, source, url.searchParams)\n sendJson(res, 200, { columns, totals })\n return\n }\n\n // GET /items/:itemId — full detail\n if (method === \"GET\" && itemId) {\n try {\n const detail = typeof source.getItem === \"function\" ? await source.getItem(itemId) : null\n if (!detail) { sendJson(res, 404, { error: \"not found\" }); return }\n sendJson(res, 200, detail)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // PATCH /items/:itemId — update\n if (method === \"PATCH\" && itemId) {\n const body = await readJson(req)\n try {\n const updated = typeof source.updateItem === \"function\"\n ? await source.updateItem(itemId, body)\n : null\n if (!updated) { sendJson(res, 501, { error: \"update unsupported\" }); return }\n audit({ actor: auditActor(req), action: \"update\", boardId, itemId, payload: body })\n sendJson(res, 200, updated)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n\n // POST /items — create new issue\n if (method === \"POST\" && !itemId) {\n const body = await readJson(req)\n if (!body.title) { sendJson(res, 400, { error: \"title required\" }); return }\n if (!source.capabilities.create || !source.create) {\n sendJson(res, 400, { error: `source ${source.type} does not support create` }); return\n }\n try {\n const created = await source.create({\n title: body.title,\n description: body.description,\n labels: body.labels,\n assigneeUsernames: body.assigneeUsernames,\n milestoneTitle: body.milestoneTitle,\n projectHint: body.projectHint,\n })\n audit({ actor: auditActor(req), action: \"create\", boardId, itemId: created.id, payload: { title: body.title } })\n sendJson(res, 201, created)\n } catch (e: any) { sendJson(res, 502, { error: e.message }) }\n return\n }\n }\n\n 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\n/** Resolve a path relative to THIS module's directory at runtime. After\n * tsup bundles dist/board-dashboard-*.js is placed in dist/, so\n * `../../dist/web/foo.js` is relative to wherever the bundled module\n * actually runs. `fileURLToPath(import.meta.url)` is the ESM equivalent\n * of __filename. */\nfunction resolveFromHere(rel: string): string {\n const here = dirname(fileURLToPath(import.meta.url))\n return resolve(here, rel)\n}\n\nasync function readAsset(path: string): Promise<Buffer> {\n return readFile(path)\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json; charset=utf-8\" })\n res.end(JSON.stringify(body))\n}\n\nasync function readJson(req: IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n req.on(\"data\", (c) => chunks.push(c))\n req.on(\"end\", () => {\n try {\n const raw = Buffer.concat(chunks).toString(\"utf-8\")\n resolve(raw ? JSON.parse(raw) : {})\n } catch (e) { reject(e) }\n })\n req.on(\"error\", reject)\n })\n}\n\n// --- HTML ---\n\n\n// --- Live full-screen page (/live) ---\n\n\n\n/**\n * Derive a peer's board-dashboard URL from a dashboard.daemons[] entry.\n * Uses the explicit dashboardUrl override if set; otherwise swaps the\n * daemon's port (18800 / 19900 / 18810) for :4202, matching our convention\n * that the dashboard runs alongside the daemon.\n */\nfunction resolvePeerDashboardUrl(d: { url: string; dashboardUrl?: string }): string {\n if (d.dashboardUrl) return d.dashboardUrl.replace(/\\/+$/, \"\")\n return d.url.replace(/\\/+$/, \"\").replace(/:(?:1[89][89]00|18810)$/, \":4202\")\n}\n\n/**\n * Build the peers list shown in the topbar mesh selector. Combines the\n * primary daemon + any `dashboard.daemons[]` entries.\n */\nfunction buildTopbarPeers(config: DaemonConfig, ctxConfig?: DaemonConfig): TopbarPeer[] {\n const cfg = ctxConfig || config\n const primary: TopbarPeer = {\n id: \"primary\",\n name: cfg.node?.name || \"this daemon\",\n dashboardUrl: \"\", // empty = stay on current origin\n primary: true,\n }\n const extras: TopbarPeer[] = (cfg.dashboard?.daemons || []).map((d) => ({\n id: d.url.replace(/\\/+$/, \"\"),\n name: d.name,\n dashboardUrl: resolvePeerDashboardUrl(d),\n tokenScope: (d.dashboardToken || d.token) ? \"proxy-ready\" : undefined,\n }))\n 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 */\n\n\n\n\n\n","// Vendored SortableJS 1.15.2 — MIT, https://github.com/SortableJS/Sortable\n// Served by board-dashboard at /sortable.min.js. Do not edit by hand.\nexport const SORTABLE_JS = \"/*! Sortable 1.15.2 - MIT | git://github.com/SortableJS/Sortable.git */\\n!function(t,e){\\\"object\\\"==typeof exports&&\\\"undefined\\\"!=typeof module?module.exports=e():\\\"function\\\"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){\\\"use strict\\\";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?e(Object(i),!0).forEach(function(t){var e,n;e=o,t=i[n=t],n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach(function(t){Object.defineProperty(o,t,Object.getOwnPropertyDescriptor(i,t))})}return o}function o(t){return(o=\\\"function\\\"==typeof Symbol&&\\\"symbol\\\"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&\\\"function\\\"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?\\\"symbol\\\":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)Object.prototype.hasOwnProperty.call(o,n)&&(t[n]=o[n])}return t}).apply(this,arguments)}function i(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};for(var n,o={},i=Object.keys(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function r(t){return function(t){if(Array.isArray(t))return l(t)}(t)||function(t){if(\\\"undefined\\\"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t[\\\"@@iterator\\\"])return Array.from(t)}(t)||function(t,e){if(t){if(\\\"string\\\"==typeof t)return l(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return\\\"Map\\\"===(n=\\\"Object\\\"===n&&t.constructor?t.constructor.name:n)||\\\"Set\\\"===n?Array.from(t):\\\"Arguments\\\"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(t,e):void 0}}(t)||function(){throw new TypeError(\\\"Invalid attempt to spread non-iterable instance.\\\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\\\")}()}function l(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n<e;n++)o[n]=t[n];return o}function t(t){if(\\\"undefined\\\"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\\\\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),s=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),n=t(/iP(ad|od|hone)/i),c=t(/chrome/i)&&t(/android/i),d={capture:!1,passive:!1};function h(t,e,n){t.addEventListener(e,n,!y&&d)}function f(t,e,n){t.removeEventListener(e,n,!y&&d)}function p(t,e){if(e&&(\\\">\\\"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(\\\">\\\"!==e[0]||t.parentNode===n)&&p(t,e)||o&&t===n)return t}while(t!==n&&(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode))}var i;return null}var g,m=/\\\\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?\\\"add\\\":\\\"remove\\\"](e):(o=(\\\" \\\"+t.className+\\\" \\\").replace(m,\\\" \\\").replace(\\\" \\\"+e+\\\" \\\",\\\" \\\"),t.className=(o+(n?\\\" \\\"+e:\\\"\\\")).replace(m,\\\" \\\")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,\\\"\\\"):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf(\\\"webkit\\\"))?\\\"-webkit-\\\"+e:e]=n+(\\\"string\\\"==typeof n?\\\"\\\":\\\"px\\\")}}function v(t,e){var n=\\\"\\\";if(\\\"string\\\"==typeof t)n=t;else do{var o=R(t,\\\"transform\\\")}while(o&&\\\"none\\\"!==o&&(n=o+\\\" \\\"+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function b(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&(\\\"none\\\"!==R(i,\\\"transform\\\")||n&&\\\"static\\\"!==R(i,\\\"position\\\"))){var h=i.getBoundingClientRect();a-=h.top+parseInt(R(i,\\\"border-top-width\\\")),l-=h.left+parseInt(R(i,\\\"border-left-width\\\")),s=a+r.height,c=l+r.width;break}}while(i=i.parentNode);return o&&t!==window&&(o=(e=v(i||t))&&e.a,t=e&&e.d,e&&(s=(a/=t)+(u/=t),c=(l/=o)+(d/=o))),{top:a,left:l,bottom:s,right:c,width:d,height:u}}}function Y(t,e,n){for(var o=M(t,!0),i=X(t)[e];o;){var r=X(o)[n];if(!(\\\"top\\\"===n||\\\"left\\\"===n?r<=i:i<=r))return o;if(o===O())break;o=M(o,!1)}return!1}function B(t,e,n,o){for(var i=0,r=0,a=t.children;r<a.length;){if(\\\"none\\\"!==a[r].style.display&&a[r]!==Ft.ghost&&(o||a[r]!==Ft.dragged)&&P(a[r],n.draggable,t,!1)){if(i===e)return a[r];i++}r++}return null}function F(t,e){for(var n=t.lastElementChild;n&&(n===Ft.ghost||\\\"none\\\"===R(n,\\\"display\\\")||e&&!p(n,e));)n=n.previousElementSibling;return n||null}function j(t,e){var n=0;if(!t||!t.parentNode)return-1;for(;t=t.previousElementSibling;)\\\"TEMPLATE\\\"===t.nodeName.toUpperCase()||t===Ft.clone||e&&!p(t,e)||n++;return n}function E(t){var e=0,n=0,o=O();if(t)do{var i=v(t),r=i.a,i=i.d}while(e+=t.scrollLeft*r,n+=t.scrollTop*i,t!==o&&(t=t.parentNode));return[e,n]}function M(t,e){if(!t||!t.getBoundingClientRect)return O();var n=t,o=!1;do{if(n.clientWidth<n.scrollWidth||n.clientHeight<n.scrollHeight){var i=R(n);if(n.clientWidth<n.scrollWidth&&(\\\"auto\\\"==i.overflowX||\\\"scroll\\\"==i.overflowX)||n.clientHeight<n.scrollHeight&&(\\\"auto\\\"==i.overflowY||\\\"scroll\\\"==i.overflowY)){if(!n.getBoundingClientRect||n===document.body)return O();if(o||e)return n;o=!0}}}while(n=n.parentNode);return O()}function D(t,e){return Math.round(t.top)===Math.round(e.top)&&Math.round(t.left)===Math.round(e.left)&&Math.round(t.height)===Math.round(e.height)&&Math.round(t.width)===Math.round(e.width)}function S(e,n){return function(){var t;g||(1===(t=arguments).length?e.call(this,t[0]):e.apply(this,t),g=setTimeout(function(){g=void 0},n))}}function H(t,e,n){t.scrollLeft+=e,t.scrollTop+=n}function _(t){var e=window.Polymer,n=window.jQuery||window.Zepto;return e&&e.dom?e.dom(t).cloneNode(!0):n?n(t).clone(!0)[0]:t.cloneNode(!0)}function C(t,e){R(t,\\\"position\\\",\\\"absolute\\\"),R(t,\\\"top\\\",e.top),R(t,\\\"left\\\",e.left),R(t,\\\"width\\\",e.width),R(t,\\\"height\\\",e.height)}function T(t){R(t,\\\"position\\\",\\\"\\\"),R(t,\\\"top\\\",\\\"\\\"),R(t,\\\"left\\\",\\\"\\\"),R(t,\\\"width\\\",\\\"\\\"),R(t,\\\"height\\\",\\\"\\\")}function L(n,o,i){var r={};return Array.from(n.children).forEach(function(t){var e;P(t,o.draggable,n,!1)&&!t.animated&&t!==i&&(e=X(t),r.left=Math.min(null!==(t=r.left)&&void 0!==t?t:1/0,e.left),r.top=Math.min(null!==(t=r.top)&&void 0!==t?t:1/0,e.top),r.right=Math.max(null!==(t=r.right)&&void 0!==t?t:-1/0,e.right),r.bottom=Math.max(null!==(t=r.bottom)&&void 0!==t?t:-1/0,e.bottom))}),r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}var K=\\\"Sortable\\\"+(new Date).getTime();function x(){var e,o=[];return{captureAnimationState:function(){o=[],this.options.animation&&[].slice.call(this.el.children).forEach(function(t){var e,n;\\\"none\\\"!==R(t,\\\"display\\\")&&t!==Ft.ghost&&(o.push({target:t,rect:X(t)}),e=I({},o[o.length-1].rect),!t.thisAnimationDuration||(n=v(t,!0))&&(e.top-=n.f,e.left-=n.e),t.fromRect=e)})},addAnimationState:function(t){o.push(t)},removeAnimationState:function(t){o.splice(function(t,e){for(var n in t)if(t.hasOwnProperty(n))for(var o in e)if(e.hasOwnProperty(o)&&e[o]===t[n][o])return Number(n);return-1}(o,{target:t}),1)},animateAll:function(t){var c=this;if(!this.options.animation)return clearTimeout(e),void(\\\"function\\\"==typeof t&&t());var u=!1,d=0;o.forEach(function(t){var e=0,n=t.target,o=n.fromRect,i=X(n),r=n.prevFromRect,a=n.prevToRect,l=t.rect,s=v(n,!0);s&&(i.top-=s.f,i.left-=s.e),n.toRect=i,n.thisAnimationDuration&&D(r,i)&&!D(o,i)&&(l.top-i.top)/(l.left-i.left)==(o.top-i.top)/(o.left-i.left)&&(t=l,s=r,r=a,a=c.options,e=Math.sqrt(Math.pow(s.top-t.top,2)+Math.pow(s.left-t.left,2))/Math.sqrt(Math.pow(s.top-r.top,2)+Math.pow(s.left-r.left,2))*a.animation),D(i,o)||(n.prevFromRect=o,n.prevToRect=i,e=e||c.options.animation,c.animate(n,l,i,e)),e&&(u=!0,d=Math.max(d,e),clearTimeout(n.animationResetTimer),n.animationResetTimer=setTimeout(function(){n.animationTime=0,n.prevFromRect=null,n.fromRect=null,n.prevToRect=null,n.thisAnimationDuration=null},e),n.thisAnimationDuration=e)}),clearTimeout(e),u?e=setTimeout(function(){\\\"function\\\"==typeof t&&t()},d):\\\"function\\\"==typeof t&&t(),o=[]},animate:function(t,e,n,o){var i,r;o&&(R(t,\\\"transition\\\",\\\"\\\"),R(t,\\\"transform\\\",\\\"\\\"),i=(r=v(this.el))&&r.a,r=r&&r.d,i=(e.left-n.left)/(i||1),r=(e.top-n.top)/(r||1),t.animatingX=!!i,t.animatingY=!!r,R(t,\\\"transform\\\",\\\"translate3d(\\\"+i+\\\"px,\\\"+r+\\\"px,0)\\\"),this.forRepaintDummy=t.offsetWidth,R(t,\\\"transition\\\",\\\"transform \\\"+o+\\\"ms\\\"+(this.options.easing?\\\" \\\"+this.options.easing:\\\"\\\")),R(t,\\\"transform\\\",\\\"translate3d(0,0,0)\\\"),\\\"number\\\"==typeof t.animated&&clearTimeout(t.animated),t.animated=setTimeout(function(){R(t,\\\"transition\\\",\\\"\\\"),R(t,\\\"transform\\\",\\\"\\\"),t.animated=!1,t.animatingX=!1,t.animatingY=!1},o))}}}var A=[],N={initializeByDefault:!0},W={mount:function(e){for(var t in N)!N.hasOwnProperty(t)||t in e||(e[t]=N[t]);A.forEach(function(t){if(t.pluginName===e.pluginName)throw\\\"Sortable: Cannot mount plugin \\\".concat(e.pluginName,\\\" more than once\\\")}),A.push(e)},pluginEvent:function(e,n,o){var t=this;this.eventCanceled=!1,o.cancel=function(){t.eventCanceled=!0};var i=e+\\\"Global\\\";A.forEach(function(t){n[t.pluginName]&&(n[t.pluginName][i]&&n[t.pluginName][i](I({sortable:n},o)),n.options[t.pluginName]&&n[t.pluginName][e]&&n[t.pluginName][e](I({sortable:n},o)))})},initializePlugins:function(n,o,i,t){for(var e in A.forEach(function(t){var e=t.pluginName;(n.options[e]||t.initializeByDefault)&&((t=new t(n,o,n.options)).sortable=n,t.options=n.options,n[e]=t,a(i,t.defaults))}),n.options){var r;n.options.hasOwnProperty(e)&&(void 0!==(r=this.modifyOption(n,e,n.options[e]))&&(n.options[e]=r))}},getEventProperties:function(e,n){var o={};return A.forEach(function(t){\\\"function\\\"==typeof t.eventProperties&&a(o,t.eventProperties.call(n[t.pluginName],e))}),o},modifyOption:function(e,n,o){var i;return A.forEach(function(t){e[t.pluginName]&&t.optionListeners&&\\\"function\\\"==typeof t.optionListeners[n]&&(i=t.optionListeners[n].call(e[t.pluginName],o))}),i}};function z(t){var e=t.sortable,n=t.rootEl,o=t.name,i=t.targetEl,r=t.cloneEl,a=t.toEl,l=t.fromEl,s=t.oldIndex,c=t.newIndex,u=t.oldDraggableIndex,d=t.newDraggableIndex,h=t.originalEvent,f=t.putSortable,p=t.extraEventProperties;if(e=e||n&&n[K]){var g,m=e.options,t=\\\"on\\\"+o.charAt(0).toUpperCase()+o.substr(1);!window.CustomEvent||y||w?(g=document.createEvent(\\\"Event\\\")).initEvent(o,!0,!0):g=new CustomEvent(o,{bubbles:!0,cancelable:!0}),g.to=a||n,g.from=l||n,g.item=i||n,g.clone=r,g.oldIndex=s,g.newIndex=c,g.oldDraggableIndex=u,g.newDraggableIndex=d,g.originalEvent=h,g.pullMode=f?f.lastPutMode:void 0;var v,b=I(I({},p),W.getEventProperties(o,e));for(v in b)g[v]=b[v];n&&n.dispatchEvent(g),m[t]&&m[t].call(e,g)}}function G(t,e){var n=(o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{}).evt,o=i(o,U);W.pluginEvent.bind(Ft)(t,e,I({dragEl:V,parentEl:Z,ghostEl:$,rootEl:Q,nextEl:J,lastDownEl:tt,cloneEl:et,cloneHidden:nt,dragStarted:gt,putSortable:st,activeSortable:Ft.active,originalEvent:n,oldIndex:ot,oldDraggableIndex:rt,newIndex:it,newDraggableIndex:at,hideGhostForTarget:Rt,unhideGhostForTarget:Xt,cloneNowHidden:function(){nt=!0},cloneNowShown:function(){nt=!1},dispatchSortableEvent:function(t){q({sortable:e,name:t,originalEvent:n})}},o))}var U=[\\\"evt\\\"];function q(t){z(I({putSortable:st,cloneEl:et,targetEl:V,rootEl:Q,oldIndex:ot,oldDraggableIndex:rt,newIndex:it,newDraggableIndex:at},t))}var V,Z,$,Q,J,tt,et,nt,ot,it,rt,at,lt,st,ct,ut,dt,ht,ft,pt,gt,mt,vt,bt,yt,wt=!1,Et=!1,Dt=[],St=!1,_t=!1,Ct=[],Tt=!1,xt=[],Ot=\\\"undefined\\\"!=typeof document,Mt=n,At=w||y?\\\"cssFloat\\\":\\\"float\\\",Nt=Ot&&!c&&!n&&\\\"draggable\\\"in document.createElement(\\\"div\\\"),It=function(){if(Ot){if(y)return!1;var t=document.createElement(\\\"x\\\");return t.style.cssText=\\\"pointer-events:auto\\\",\\\"auto\\\"===t.style.pointerEvents}}(),Pt=function(t,e){var n=R(t),o=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),i=B(t,0,e),r=B(t,1,e),a=i&&R(i),l=r&&R(r),s=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+X(i).width,t=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+X(r).width;if(\\\"flex\\\"===n.display)return\\\"column\\\"===n.flexDirection||\\\"column-reverse\\\"===n.flexDirection?\\\"vertical\\\":\\\"horizontal\\\";if(\\\"grid\\\"===n.display)return n.gridTemplateColumns.split(\\\" \\\").length<=1?\\\"vertical\\\":\\\"horizontal\\\";if(i&&a.float&&\\\"none\\\"!==a.float){e=\\\"left\\\"===a.float?\\\"left\\\":\\\"right\\\";return!r||\\\"both\\\"!==l.clear&&l.clear!==e?\\\"horizontal\\\":\\\"vertical\\\"}return i&&(\\\"block\\\"===a.display||\\\"flex\\\"===a.display||\\\"table\\\"===a.display||\\\"grid\\\"===a.display||o<=s&&\\\"none\\\"===n[At]||r&&\\\"none\\\"===n[At]&&o<s+t)?\\\"vertical\\\":\\\"horizontal\\\"},kt=function(t){function l(r,a){return function(t,e,n,o){var i=t.options.group.name&&e.options.group.name&&t.options.group.name===e.options.group.name;if(null==r&&(a||i))return!0;if(null==r||!1===r)return!1;if(a&&\\\"clone\\\"===r)return r;if(\\\"function\\\"==typeof r)return l(r(t,e,n,o),a)(t,e,n,o);e=(a?t:e).options.group.name;return!0===r||\\\"string\\\"==typeof r&&r===e||r.join&&-1<r.indexOf(e)}}var e={},n=t.group;n&&\\\"object\\\"==o(n)||(n={name:n}),e.name=n.name,e.checkPull=l(n.pull,!0),e.checkPut=l(n.put),e.revertClone=n.revertClone,t.group=e},Rt=function(){!It&&$&&R($,\\\"display\\\",\\\"none\\\")},Xt=function(){!It&&$&&R($,\\\"display\\\",\\\"\\\")};Ot&&!c&&document.addEventListener(\\\"click\\\",function(t){if(Et)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),Et=!1},!0);function Yt(t){if(V){t=t.touches?t.touches[0]:t;var e=(i=t.clientX,r=t.clientY,Dt.some(function(t){var e=t[K].options.emptyInsertThreshold;if(e&&!F(t)){var n=X(t),o=i>=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Bt(t){V&&V.parentNode[K]._isOutsideThisEl(t.target)}function Ft(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw\\\"Sortable: `el` must be an HTMLElement, not \\\".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?\\\">li\\\":\\\">*\\\",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Pt(t,this.options)},ghostClass:\\\"sortable-ghost\\\",chosenClass:\\\"sortable-chosen\\\",dragClass:\\\"sortable-drag\\\",ignore:\\\"a, img\\\",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData(\\\"Text\\\",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:\\\"data-id\\\",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:\\\"sortable-fallback\\\",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Ft.supportPointer&&\\\"PointerEvent\\\"in window&&!u,emptyInsertThreshold:5};for(n in W.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in kt(e),this)\\\"_\\\"===o.charAt(0)&&\\\"function\\\"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Nt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,\\\"pointerdown\\\",this._onTapStart):(h(t,\\\"mousedown\\\",this._onTapStart),h(t,\\\"touchstart\\\",this._onTapStart)),this.nativeDraggable&&(h(t,\\\"dragover\\\",this),h(t,\\\"dragenter\\\",this)),Dt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,x())}function jt(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent(\\\"Event\\\")).initEvent(\\\"move\\\",!0,!0):s=new CustomEvent(\\\"move\\\",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Ht(t){t.draggable=!1}function Lt(){Tt=!1}function Kt(t){return setTimeout(t,0)}function Wt(t){return clearTimeout(t)}Ft.prototype={constructor:Ft,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(mt=null)},_getDirection:function(t,e){return\\\"function\\\"==typeof this.options.direction?this.options.direction.call(this,t,e,V):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&\\\"touch\\\"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){xt.length=0;var e=t.getElementsByTagName(\\\"input\\\"),n=e.length;for(;n--;){var o=e[n];o.checked&&xt.push(o)}}(o),!V&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||\\\"SELECT\\\"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||tt===l)){if(ot=j(l),rt=j(l,t.draggable),\\\"function\\\"==typeof c){if(c.call(this,e,l,this))return q({sortable:n,rootEl:s,name:\\\"filter\\\",targetEl:l,toEl:o,fromEl:o}),G(\\\"filter\\\",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c=c&&c.split(\\\",\\\").some(function(t){if(t=P(s,t.trim(),o,!1))return q({sortable:n,rootEl:t,name:\\\"filter\\\",targetEl:l,fromEl:o,toEl:o}),G(\\\"filter\\\",n,{evt:e}),!0}))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!V&&n.parentNode===r&&(o=X(n),Q=r,Z=(V=n).parentNode,J=V.nextSibling,tt=n,lt=a.group,ct={target:Ft.dragged=V,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ct.clientX-o.left,pt=ct.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,V.style[\\\"will-change\\\"]=\\\"all\\\",o=function(){G(\\\"delayEnded\\\",i,{evt:t}),Ft.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(V.draggable=!0),i._triggerDragStart(t,e),q({sortable:i,name:\\\"choose\\\",originalEvent:t}),k(V,a.chosenClass,!0))},a.ignore.split(\\\",\\\").forEach(function(t){b(V,t.trim(),Ht)}),h(l,\\\"dragover\\\",Yt),h(l,\\\"mousemove\\\",Yt),h(l,\\\"touchmove\\\",Yt),h(l,\\\"mouseup\\\",i._onDrop),h(l,\\\"touchend\\\",i._onDrop),h(l,\\\"touchcancel\\\",i._onDrop),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,V.draggable=!0),G(\\\"delayStart\\\",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():Ft.eventCanceled?this._onDrop():(h(l,\\\"mouseup\\\",i._disableDelayedDrag),h(l,\\\"touchend\\\",i._disableDelayedDrag),h(l,\\\"touchcancel\\\",i._disableDelayedDrag),h(l,\\\"mousemove\\\",i._delayedDragTouchMoveHandler),h(l,\\\"touchmove\\\",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,\\\"pointermove\\\",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){V&&Ht(V),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;f(t,\\\"mouseup\\\",this._disableDelayedDrag),f(t,\\\"touchend\\\",this._disableDelayedDrag),f(t,\\\"touchcancel\\\",this._disableDelayedDrag),f(t,\\\"mousemove\\\",this._delayedDragTouchMoveHandler),f(t,\\\"touchmove\\\",this._delayedDragTouchMoveHandler),f(t,\\\"pointermove\\\",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||\\\"touch\\\"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,\\\"pointermove\\\",this._onTouchMove):h(document,e?\\\"touchmove\\\":\\\"mousemove\\\",this._onTouchMove):(h(V,\\\"dragend\\\",this),h(Q,\\\"dragstart\\\",this._onDragStart));try{document.selection?Kt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;wt=!1,Q&&V?(G(\\\"dragStarted\\\",this,{evt:e}),this.nativeDraggable&&h(document,\\\"dragover\\\",Bt),n=this.options,t||k(V,n.dragClass,!1),k(V,n.ghostClass,!0),Ft.active=this,t&&this._appendGhost(),q({sortable:this,name:\\\"start\\\",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(ut){this._lastX=ut.clientX,this._lastY=ut.clientY,Rt();for(var t=document.elementFromPoint(ut.clientX,ut.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(ut.clientX,ut.clientY))!==e;)e=t;if(V.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:ut.clientX,clientY:ut.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=(t=e).parentNode);Xt()}},_onTouchMove:function(t){if(ct){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=$&&v($,!0),a=$&&r&&r.a,l=$&&r&&r.d,e=Mt&&yt&&E(yt),a=(i.clientX-ct.clientX+o.x)/(a||1)+(e?e[0]-Ct[0]:0)/(a||1),l=(i.clientY-ct.clientY+o.y)/(l||1)+(e?e[1]-Ct[1]:0)/(l||1);if(!Ft.active&&!wt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))<n)return;this._onDragStart(t,!0)}$&&(r?(r.e+=a-(dt||0),r.f+=l-(ht||0)):r={a:1,b:0,c:0,d:1,e:a,f:l},r=\\\"matrix(\\\".concat(r.a,\\\",\\\").concat(r.b,\\\",\\\").concat(r.c,\\\",\\\").concat(r.d,\\\",\\\").concat(r.e,\\\",\\\").concat(r.f,\\\")\\\"),R($,\\\"webkitTransform\\\",r),R($,\\\"mozTransform\\\",r),R($,\\\"msTransform\\\",r),R($,\\\"transform\\\",r),dt=a,ht=l,ut=i),t.cancelable&&t.preventDefault()}},_appendGhost:function(){if(!$){var t=this.options.fallbackOnBody?document.body:Q,e=X(V,!0,Mt,!0,t),n=this.options;if(Mt){for(yt=t;\\\"static\\\"===R(yt,\\\"position\\\")&&\\\"none\\\"===R(yt,\\\"transform\\\")&&yt!==document;)yt=yt.parentNode;yt!==document.body&&yt!==document.documentElement?(yt===document&&(yt=O()),e.top+=yt.scrollTop,e.left+=yt.scrollLeft):yt=O(),Ct=E(yt)}k($=V.cloneNode(!0),n.ghostClass,!1),k($,n.fallbackClass,!0),k($,n.dragClass,!0),R($,\\\"transition\\\",\\\"\\\"),R($,\\\"transform\\\",\\\"\\\"),R($,\\\"box-sizing\\\",\\\"border-box\\\"),R($,\\\"margin\\\",0),R($,\\\"top\\\",e.top),R($,\\\"left\\\",e.left),R($,\\\"width\\\",e.width),R($,\\\"height\\\",e.height),R($,\\\"opacity\\\",\\\"0.8\\\"),R($,\\\"position\\\",Mt?\\\"absolute\\\":\\\"fixed\\\"),R($,\\\"zIndex\\\",\\\"100000\\\"),R($,\\\"pointerEvents\\\",\\\"none\\\"),Ft.ghost=$,t.appendChild($),R($,\\\"transform-origin\\\",ft/parseInt($.style.width)*100+\\\"% \\\"+pt/parseInt($.style.height)*100+\\\"%\\\")}},_onDragStart:function(t,e){var n=this,o=t.dataTransfer,i=n.options;G(\\\"dragStart\\\",this,{evt:t}),Ft.eventCanceled?this._onDrop():(G(\\\"setupClone\\\",this),Ft.eventCanceled||((et=_(V)).removeAttribute(\\\"id\\\"),et.draggable=!1,et.style[\\\"will-change\\\"]=\\\"\\\",this._hideClone(),k(et,this.options.chosenClass,!1),Ft.clone=et),n.cloneId=Kt(function(){G(\\\"clone\\\",n),Ft.eventCanceled||(n.options.removeCloneOnHide||Q.insertBefore(et,V),n._hideClone(),q({sortable:n,name:\\\"clone\\\"}))}),e||k(V,i.dragClass,!0),e?(Et=!0,n._loopId=setInterval(n._emulateDragOver,50)):(f(document,\\\"mouseup\\\",n._onDrop),f(document,\\\"touchend\\\",n._onDrop),f(document,\\\"touchcancel\\\",n._onDrop),o&&(o.effectAllowed=\\\"move\\\",i.setData&&i.setData.call(n,o,V)),h(document,\\\"drop\\\",n),R(V,\\\"transform\\\",\\\"translateZ(0)\\\")),wt=!0,n._dragStartId=Kt(n._dragStarted.bind(n,e,t)),h(document,\\\"selectstart\\\",n),gt=!0,u&&R(document.body,\\\"user-select\\\",\\\"none\\\"))},_onDragOver:function(n){var o,i,r,t,e,a=this.el,l=n.target,s=this.options,c=s.group,u=Ft.active,d=lt===c,h=s.sort,f=st||u,p=this,g=!1;if(!Tt){if(void 0!==n.preventDefault&&n.cancelable&&n.preventDefault(),l=P(l,s.draggable,a,!0),O(\\\"dragOver\\\"),Ft.eventCanceled)return g;if(V.contains(n.target)||l.animated&&l.animatingX&&l.animatingY||p._ignoreWhileAnimating===l)return A(!1);if(Et=!1,u&&!s.disabled&&(d?h||(i=Z!==Q):st===this||(this.lastPutMode=lt.checkPull(this,u,V,n))&&c.checkPut(this,u,V,n))){if(r=\\\"vertical\\\"===this._getDirection(n,l),o=X(V),O(\\\"dragOverValid\\\"),Ft.eventCanceled)return g;if(i)return Z=Q,M(),this._hideClone(),O(\\\"revert\\\"),Ft.eventCanceled||(J?Q.insertBefore(V,J):Q.appendChild(V)),A(!0);var m=F(a,s.draggable);if(m&&(S=n,c=r,x=X(F((D=this).el,D.options.draggable)),D=L(D.el,D.options,$),!(c?S.clientX>D.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>D.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,$),e?t.clientX<_.left-10||t.clientY<C.top&&t.clientX<C.right:t.clientY<_.top-10||t.clientY<C.bottom&&t.clientX<C.left)){var v=B(a,0,s,!0);if(v===V)return A(!1);if(E=X(l=v),!1!==jt(Q,a,V,o,l,E,n,!1))return M(),a.insertBefore(V,v),Z=a,N(),A(!0)}else if(l.parentNode===a){var b,y,w,E=X(l),D=V.parentNode!==a,S=(S=V.animated&&V.toRect||o,x=l.animated&&l.toRect||E,_=(e=r)?S.left:S.top,t=e?S.right:S.bottom,C=e?S.width:S.height,v=e?x.left:x.top,S=e?x.right:x.bottom,x=e?x.width:x.height,!(_===v||t===S||_+C/2===v+x/2)),_=r?\\\"top\\\":\\\"left\\\",C=Y(l,\\\"top\\\",\\\"top\\\")||Y(V,\\\"top\\\",\\\"top\\\"),v=C?C.scrollTop:void 0;if(mt!==l&&(y=E[_],St=!1,_t=!S&&s.invertSwap||D),0!==(b=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,t=o?n.top:n.left,o=o?n.bottom:n.right,n=!1;if(!a)if(l&&bt<c*i){if(St=!St&&(1===vt?t+c*r/2<s:s<o-c*r/2)?!0:St)n=!0;else if(1===vt?s<t+bt:o-bt<s)return-vt}else if(t+c*(1-i)/2<s&&s<o-c*(1-i)/2)return function(t){return j(V)<j(t)?1:-1}(e);if((n=n||a)&&(s<t+c*r/2||o-c*r/2<s))return t+c/2<s?1:-1;return 0}(n,l,E,r,S?1:s.swapThreshold,null==s.invertedSwapThreshold?s.swapThreshold:s.invertedSwapThreshold,_t,mt===l)))for(var T=j(V);(w=Z.children[T-=b])&&(\\\"none\\\"===R(w,\\\"display\\\")||w===$););if(0===b||w===l)return A(!1);vt=b;var x=(mt=l).nextElementSibling,D=!1,S=jt(Q,a,V,o,l,E,n,D=1===b);if(!1!==S)return 1!==S&&-1!==S||(D=1===S),Tt=!0,setTimeout(Lt,30),M(),D&&!x?a.appendChild(V):l.parentNode.insertBefore(V,D?x:l),C&&H(C,0,v-C.scrollTop),Z=V.parentNode,void 0===y||_t||(bt=Math.abs(y-X(l)[_])),N(),A(!0)}}else{if(m===V)return A(!1);if((l=m&&a===n.target?m:l)&&(E=X(l)),!1!==jt(Q,a,V,o,l,E,n,!!l))return M(),m&&m.nextSibling?a.insertBefore(V,m.nextSibling):a.appendChild(V),Z=a,N(),A(!0)}if(a.contains(V))return A(!1)}return!1}function O(t,e){G(t,p,I({evt:n,isOwner:d,axis:r?\\\"vertical\\\":\\\"horizontal\\\",revert:i,dragRect:o,targetRect:E,canSort:h,fromSortable:f,target:l,completed:A,onMove:function(t,e){return jt(Q,a,V,o,t,X(t),n,e)},changed:N},e))}function M(){O(\\\"dragOverAnimationCapture\\\"),p.captureAnimationState(),p!==f&&f.captureAnimationState()}function A(t){return O(\\\"dragOverCompleted\\\",{insertion:t}),t&&(d?u._hideClone():u._showClone(p),p!==f&&(k(V,(st||u).options.ghostClass,!1),k(V,s.ghostClass,!0)),st!==p&&p!==Ft.active?st=p:p===Ft.active&&st&&(st=null),f===p&&(p._ignoreWhileAnimating=l),p.animateAll(function(){O(\\\"dragOverAnimationComplete\\\"),p._ignoreWhileAnimating=null}),p!==f&&(f.animateAll(),f._ignoreWhileAnimating=null)),(l===V&&!V.animated||l===a&&!l.animated)&&(mt=null),s.dragoverBubble||n.rootEl||l===document||(V.parentNode[K]._isOutsideThisEl(n.target),t||Yt(n)),!s.dragoverBubble&&n.stopPropagation&&n.stopPropagation(),g=!0}function N(){it=j(V),at=j(V,s.draggable),q({sortable:p,name:\\\"change\\\",toEl:a,newIndex:it,newDraggableIndex:at,originalEvent:n})}},_ignoreWhileAnimating:null,_offMoveEvents:function(){f(document,\\\"mousemove\\\",this._onTouchMove),f(document,\\\"touchmove\\\",this._onTouchMove),f(document,\\\"pointermove\\\",this._onTouchMove),f(document,\\\"dragover\\\",Yt),f(document,\\\"mousemove\\\",Yt),f(document,\\\"touchmove\\\",Yt)},_offUpEvents:function(){var t=this.el.ownerDocument;f(t,\\\"mouseup\\\",this._onDrop),f(t,\\\"touchend\\\",this._onDrop),f(t,\\\"pointerup\\\",this._onDrop),f(t,\\\"touchcancel\\\",this._onDrop),f(document,\\\"selectstart\\\",this)},_onDrop:function(t){var e=this.el,n=this.options;it=j(V),at=j(V,n.draggable),G(\\\"drop\\\",this,{evt:t}),Z=V&&V.parentNode,it=j(V),at=j(V,n.draggable),Ft.eventCanceled||(St=_t=wt=!1,clearInterval(this._loopId),clearTimeout(this._dragStartTimer),Wt(this.cloneId),Wt(this._dragStartId),this.nativeDraggable&&(f(document,\\\"drop\\\",this),f(e,\\\"dragstart\\\",this._onDragStart)),this._offMoveEvents(),this._offUpEvents(),u&&R(document.body,\\\"user-select\\\",\\\"\\\"),R(V,\\\"transform\\\",\\\"\\\"),t&&(gt&&(t.cancelable&&t.preventDefault(),n.dropBubble||t.stopPropagation()),$&&$.parentNode&&$.parentNode.removeChild($),(Q===Z||st&&\\\"clone\\\"!==st.lastPutMode)&&et&&et.parentNode&&et.parentNode.removeChild(et),V&&(this.nativeDraggable&&f(V,\\\"dragend\\\",this),Ht(V),V.style[\\\"will-change\\\"]=\\\"\\\",gt&&!wt&&k(V,(st||this).options.ghostClass,!1),k(V,this.options.chosenClass,!1),q({sortable:this,name:\\\"unchoose\\\",toEl:Z,newIndex:null,newDraggableIndex:null,originalEvent:t}),Q!==Z?(0<=it&&(q({rootEl:Z,name:\\\"add\\\",toEl:Z,fromEl:Q,originalEvent:t}),q({sortable:this,name:\\\"remove\\\",toEl:Z,originalEvent:t}),q({rootEl:Z,name:\\\"sort\\\",toEl:Z,fromEl:Q,originalEvent:t}),q({sortable:this,name:\\\"sort\\\",toEl:Z,originalEvent:t})),st&&st.save()):it!==ot&&0<=it&&(q({sortable:this,name:\\\"update\\\",toEl:Z,originalEvent:t}),q({sortable:this,name:\\\"sort\\\",toEl:Z,originalEvent:t})),Ft.active&&(null!=it&&-1!==it||(it=ot,at=rt),q({sortable:this,name:\\\"end\\\",toEl:Z,originalEvent:t}),this.save())))),this._nulling()},_nulling:function(){G(\\\"nulling\\\",this),Q=V=Z=$=J=et=tt=nt=ct=ut=gt=it=at=ot=rt=mt=vt=st=lt=Ft.dragged=Ft.ghost=Ft.clone=Ft.active=null,xt.forEach(function(t){t.checked=!0}),xt.length=dt=ht=0},handleEvent:function(t){switch(t.type){case\\\"drop\\\":case\\\"dragend\\\":this._onDrop(t);break;case\\\"dragenter\\\":case\\\"dragover\\\":V&&(this._onDragOver(t),function(t){t.dataTransfer&&(t.dataTransfer.dropEffect=\\\"move\\\");t.cancelable&&t.preventDefault()}(t));break;case\\\"selectstart\\\":t.preventDefault()}},toArray:function(){for(var t,e=[],n=this.el.children,o=0,i=n.length,r=this.options;o<i;o++)P(t=n[o],r.draggable,this.el,!1)&&e.push(t.getAttribute(r.dataIdAttr)||function(t){var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;for(;n--;)o+=e.charCodeAt(n);return o.toString(36)}(t));return e},sort:function(t,e){var n={},o=this.el;this.toArray().forEach(function(t,e){e=o.children[e];P(e,this.options.draggable,o,!1)&&(n[t]=e)},this),e&&this.captureAnimationState(),t.forEach(function(t){n[t]&&(o.removeChild(n[t]),o.appendChild(n[t]))}),e&&this.animateAll()},save:function(){var t=this.options.store;t&&t.set&&t.set(this)},closest:function(t,e){return P(t,e||this.options.draggable,this.el,!1)},option:function(t,e){var n=this.options;if(void 0===e)return n[t];var o=W.modifyOption(this,t,e);n[t]=void 0!==o?o:e,\\\"group\\\"===t&&kt(n)},destroy:function(){G(\\\"destroy\\\",this);var t=this.el;t[K]=null,f(t,\\\"mousedown\\\",this._onTapStart),f(t,\\\"touchstart\\\",this._onTapStart),f(t,\\\"pointerdown\\\",this._onTapStart),this.nativeDraggable&&(f(t,\\\"dragover\\\",this),f(t,\\\"dragenter\\\",this)),Array.prototype.forEach.call(t.querySelectorAll(\\\"[draggable]\\\"),function(t){t.removeAttribute(\\\"draggable\\\")}),this._onDrop(),this._disableDelayedDragEvents(),Dt.splice(Dt.indexOf(this.el),1),this.el=t=null},_hideClone:function(){nt||(G(\\\"hideClone\\\",this),Ft.eventCanceled||(R(et,\\\"display\\\",\\\"none\\\"),this.options.removeCloneOnHide&&et.parentNode&&et.parentNode.removeChild(et),nt=!0))},_showClone:function(t){\\\"clone\\\"===t.lastPutMode?nt&&(G(\\\"showClone\\\",this),Ft.eventCanceled||(V.parentNode!=Q||this.options.group.revertClone?J?Q.insertBefore(et,J):Q.appendChild(et):Q.insertBefore(et,V),this.options.group.revertClone&&this.animate(V,et),R(et,\\\"display\\\",\\\"\\\"),nt=!1)):this._hideClone()}},Ot&&h(document,\\\"touchmove\\\",function(t){(Ft.active||wt)&&t.cancelable&&t.preventDefault()}),Ft.utils={on:h,off:f,css:R,find:b,is:function(t,e){return!!P(t,e,t,!1)},extend:function(t,e){if(t&&e)for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t},throttle:S,closest:P,toggleClass:k,clone:_,index:j,nextTick:Kt,cancelNextTick:Wt,detectDirection:Pt,getChild:B},Ft.get=function(t){return t[K]},Ft.mount=function(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];(e=e[0].constructor===Array?e[0]:e).forEach(function(t){if(!t.prototype||!t.prototype.constructor)throw\\\"Sortable: Mounted plugin must be a constructor function, not \\\".concat({}.toString.call(t));t.utils&&(Ft.utils=I(I({},Ft.utils),t.utils)),W.mount(t)})},Ft.create=function(t,e){return new Ft(t,e)};var zt,Gt,Ut,qt,Vt,Zt,$t=[],Qt=!(Ft.version=\\\"1.15.2\\\");function Jt(){$t.forEach(function(t){clearInterval(t.pid)}),$t=[]}function te(){clearInterval(Zt)}var ee,ne=S(function(n,t,e,o){if(t.scroll){var i,r=(n.touches?n.touches[0]:n).clientX,a=(n.touches?n.touches[0]:n).clientY,l=t.scrollSensitivity,s=t.scrollSpeed,c=O(),u=!1;Gt!==e&&(Gt=e,Jt(),zt=t.scroll,i=t.scrollFn,!0===zt&&(zt=M(e,!0)));var d=0,h=zt;do{var f=h,p=X(f),g=p.top,m=p.bottom,v=p.left,b=p.right,y=p.width,w=p.height,E=void 0,D=void 0,S=f.scrollWidth,_=f.scrollHeight,C=R(f),T=f.scrollLeft,p=f.scrollTop,D=f===c?(E=y<S&&(\\\"auto\\\"===C.overflowX||\\\"scroll\\\"===C.overflowX||\\\"visible\\\"===C.overflowX),w<_&&(\\\"auto\\\"===C.overflowY||\\\"scroll\\\"===C.overflowY||\\\"visible\\\"===C.overflowY)):(E=y<S&&(\\\"auto\\\"===C.overflowX||\\\"scroll\\\"===C.overflowX),w<_&&(\\\"auto\\\"===C.overflowY||\\\"scroll\\\"===C.overflowY)),T=E&&(Math.abs(b-r)<=l&&T+y<S)-(Math.abs(v-r)<=l&&!!T),p=D&&(Math.abs(m-a)<=l&&p+w<_)-(Math.abs(g-a)<=l&&!!p);if(!$t[d])for(var x=0;x<=d;x++)$t[x]||($t[x]={});$t[d].vx==T&&$t[d].vy==p&&$t[d].el===f||($t[d].el=f,$t[d].vx=T,$t[d].vy=p,clearInterval($t[d].pid),0==T&&0==p||(u=!0,$t[d].pid=setInterval(function(){o&&0===this.layer&&Ft.active._onTouchMove(Vt);var t=$t[this.layer].vy?$t[this.layer].vy*s:0,e=$t[this.layer].vx?$t[this.layer].vx*s:0;\\\"function\\\"==typeof i&&\\\"continue\\\"!==i.call(Ft.dragged.parentNode[K],e,t,n,Vt,$t[this.layer].el)||H($t[this.layer].el,e,t)}.bind({layer:d}),24))),d++}while(t.bubbleScroll&&h!==c&&(h=M(h,!1)));Qt=u}},30),c=function(t){var e=t.originalEvent,n=t.putSortable,o=t.dragEl,i=t.activeSortable,r=t.dispatchSortableEvent,a=t.hideGhostForTarget,t=t.unhideGhostForTarget;e&&(i=n||i,a(),e=e.changedTouches&&e.changedTouches.length?e.changedTouches[0]:e,e=document.elementFromPoint(e.clientX,e.clientY),t(),i&&!i.el.contains(e)&&(r(\\\"spill\\\"),this.onSpill({dragEl:o,putSortable:n})))};function oe(){}function ie(){}oe.prototype={startIndex:null,dragStart:function(t){t=t.oldDraggableIndex;this.startIndex=t},onSpill:function(t){var e=t.dragEl,n=t.putSortable;this.sortable.captureAnimationState(),n&&n.captureAnimationState();t=B(this.sortable.el,this.startIndex,this.options);t?this.sortable.el.insertBefore(e,t):this.sortable.el.appendChild(e),this.sortable.animateAll(),n&&n.animateAll()},drop:c},a(oe,{pluginName:\\\"revertOnSpill\\\"}),ie.prototype={onSpill:function(t){var e=t.dragEl,t=t.putSortable||this.sortable;t.captureAnimationState(),e.parentNode&&e.parentNode.removeChild(e),t.animateAll()},drop:c},a(ie,{pluginName:\\\"removeOnSpill\\\"});var re,ae,le,se,ce,ue=[],de=[],he=!1,fe=!1,pe=!1;function ge(n,o){de.forEach(function(t,e){e=o.children[t.sortableIndex+(n?Number(e):0)];e?o.insertBefore(t,e):o.appendChild(t)})}function me(){ue.forEach(function(t){t!==le&&t.parentNode&&t.parentNode.removeChild(t)})}return Ft.mount(new function(){function t(){for(var t in this.defaults={scroll:!0,forceAutoScrollFallback:!1,scrollSensitivity:30,scrollSpeed:10,bubbleScroll:!0},this)\\\"_\\\"===t.charAt(0)&&\\\"function\\\"==typeof this[t]&&(this[t]=this[t].bind(this))}return t.prototype={dragStarted:function(t){t=t.originalEvent;this.sortable.nativeDraggable?h(document,\\\"dragover\\\",this._handleAutoScroll):this.options.supportPointer?h(document,\\\"pointermove\\\",this._handleFallbackAutoScroll):t.touches?h(document,\\\"touchmove\\\",this._handleFallbackAutoScroll):h(document,\\\"mousemove\\\",this._handleFallbackAutoScroll)},dragOverCompleted:function(t){t=t.originalEvent;this.options.dragOverBubble||t.rootEl||this._handleAutoScroll(t)},drop:function(){this.sortable.nativeDraggable?f(document,\\\"dragover\\\",this._handleAutoScroll):(f(document,\\\"pointermove\\\",this._handleFallbackAutoScroll),f(document,\\\"touchmove\\\",this._handleFallbackAutoScroll),f(document,\\\"mousemove\\\",this._handleFallbackAutoScroll)),te(),Jt(),clearTimeout(g),g=void 0},nulling:function(){Vt=Gt=zt=Qt=Zt=Ut=qt=null,$t.length=0},_handleFallbackAutoScroll:function(t){this._handleAutoScroll(t,!0)},_handleAutoScroll:function(e,n){var o,i=this,r=(e.touches?e.touches[0]:e).clientX,a=(e.touches?e.touches[0]:e).clientY,t=document.elementFromPoint(r,a);Vt=e,n||this.options.forceAutoScrollFallback||w||y||u?(ne(e,this.options,t,n),o=M(t,!0),!Qt||Zt&&r===Ut&&a===qt||(Zt&&te(),Zt=setInterval(function(){var t=M(document.elementFromPoint(r,a),!0);t!==o&&(o=t,Jt()),ne(e,i.options,t,n)},10),Ut=r,qt=a)):this.options.bubbleScroll&&M(t,!0)!==O()?ne(e,this.options,M(t,!1),!1):Jt()}},a(t,{pluginName:\\\"scroll\\\",initializeByDefault:!0})}),Ft.mount(ie,oe),Ft.mount(new function(){function t(){this.defaults={swapClass:\\\"sortable-swap-highlight\\\"}}return t.prototype={dragStart:function(t){t=t.dragEl;ee=t},dragOverValid:function(t){var e=t.completed,n=t.target,o=t.onMove,i=t.activeSortable,r=t.changed,a=t.cancel;i.options.swap&&(t=this.sortable.el,i=this.options,n&&n!==t&&(t=ee,ee=!1!==o(n)?(k(n,i.swapClass,!0),n):null,t&&t!==ee&&k(t,i.swapClass,!1)),r(),e(!0),a())},drop:function(t){var e,n,o=t.activeSortable,i=t.putSortable,r=t.dragEl,a=i||this.sortable,l=this.options;ee&&k(ee,l.swapClass,!1),ee&&(l.swap||i&&i.options.swap)&&r!==ee&&(a.captureAnimationState(),a!==o&&o.captureAnimationState(),n=ee,t=(e=r).parentNode,l=n.parentNode,t&&l&&!t.isEqualNode(n)&&!l.isEqualNode(e)&&(i=j(e),r=j(n),t.isEqualNode(l)&&i<r&&r++,t.insertBefore(n,t.children[i]),l.insertBefore(e,l.children[r])),a.animateAll(),a!==o&&o.animateAll())},nulling:function(){ee=null}},a(t,{pluginName:\\\"swap\\\",eventProperties:function(){return{swapItem:ee}}})}),Ft.mount(new function(){function t(o){for(var t in this)\\\"_\\\"===t.charAt(0)&&\\\"function\\\"==typeof this[t]&&(this[t]=this[t].bind(this));o.options.avoidImplicitDeselect||(o.options.supportPointer?h(document,\\\"pointerup\\\",this._deselectMultiDrag):(h(document,\\\"mouseup\\\",this._deselectMultiDrag),h(document,\\\"touchend\\\",this._deselectMultiDrag))),h(document,\\\"keydown\\\",this._checkKeyDown),h(document,\\\"keyup\\\",this._checkKeyUp),this.defaults={selectedClass:\\\"sortable-selected\\\",multiDragKey:null,avoidImplicitDeselect:!1,setData:function(t,e){var n=\\\"\\\";ue.length&&ae===o?ue.forEach(function(t,e){n+=(e?\\\", \\\":\\\"\\\")+t.textContent}):n=e.textContent,t.setData(\\\"Text\\\",n)}}}return t.prototype={multiDragKeyDown:!1,isMultiDrag:!1,delayStartGlobal:function(t){t=t.dragEl;le=t},delayEnded:function(){this.isMultiDrag=~ue.indexOf(le)},setupClone:function(t){var e=t.sortable,t=t.cancel;if(this.isMultiDrag){for(var n=0;n<ue.length;n++)de.push(_(ue[n])),de[n].sortableIndex=ue[n].sortableIndex,de[n].draggable=!1,de[n].style[\\\"will-change\\\"]=\\\"\\\",k(de[n],this.options.selectedClass,!1),ue[n]===le&&k(de[n],this.options.chosenClass,!1);e._hideClone(),t()}},clone:function(t){var e=t.sortable,n=t.rootEl,o=t.dispatchSortableEvent,t=t.cancel;this.isMultiDrag&&(this.options.removeCloneOnHide||ue.length&&ae===e&&(ge(!0,n),o(\\\"clone\\\"),t()))},showClone:function(t){var e=t.cloneNowShown,n=t.rootEl,t=t.cancel;this.isMultiDrag&&(ge(!1,n),de.forEach(function(t){R(t,\\\"display\\\",\\\"\\\")}),e(),ce=!1,t())},hideClone:function(t){var e=this,n=(t.sortable,t.cloneNowHidden),t=t.cancel;this.isMultiDrag&&(de.forEach(function(t){R(t,\\\"display\\\",\\\"none\\\"),e.options.removeCloneOnHide&&t.parentNode&&t.parentNode.removeChild(t)}),n(),ce=!0,t())},dragStartGlobal:function(t){t.sortable;!this.isMultiDrag&&ae&&ae.multiDrag._deselectMultiDrag(),ue.forEach(function(t){t.sortableIndex=j(t)}),ue=ue.sort(function(t,e){return t.sortableIndex-e.sortableIndex}),pe=!0},dragStarted:function(t){var e,n=this,t=t.sortable;this.isMultiDrag&&(this.options.sort&&(t.captureAnimationState(),this.options.animation&&(ue.forEach(function(t){t!==le&&R(t,\\\"position\\\",\\\"absolute\\\")}),e=X(le,!1,!0,!0),ue.forEach(function(t){t!==le&&C(t,e)}),he=fe=!0)),t.animateAll(function(){he=fe=!1,n.options.animation&&ue.forEach(function(t){T(t)}),n.options.sort&&me()}))},dragOver:function(t){var e=t.target,n=t.completed,t=t.cancel;fe&&~ue.indexOf(e)&&(n(!1),t())},revert:function(t){var n,o,e=t.fromSortable,i=t.rootEl,r=t.sortable,a=t.dragRect;1<ue.length&&(ue.forEach(function(t){r.addAnimationState({target:t,rect:fe?X(t):a}),T(t),t.fromRect=a,e.removeAnimationState(t)}),fe=!1,n=!this.options.removeCloneOnHide,o=i,ue.forEach(function(t,e){e=o.children[t.sortableIndex+(n?Number(e):0)];e?o.insertBefore(t,e):o.appendChild(t)}))},dragOverCompleted:function(t){var e,n=t.sortable,o=t.isOwner,i=t.insertion,r=t.activeSortable,a=t.parentEl,l=t.putSortable,t=this.options;i&&(o&&r._hideClone(),he=!1,t.animation&&1<ue.length&&(fe||!o&&!r.options.sort&&!l)&&(e=X(le,!1,!0,!0),ue.forEach(function(t){t!==le&&(C(t,e),a.appendChild(t))}),fe=!0),o||(fe||me(),1<ue.length?(o=ce,r._showClone(n),r.options.animation&&!ce&&o&&de.forEach(function(t){r.addAnimationState({target:t,rect:se}),t.fromRect=se,t.thisAnimationDuration=null})):r._showClone(n)))},dragOverAnimationCapture:function(t){var e=t.dragRect,n=t.isOwner,t=t.activeSortable;ue.forEach(function(t){t.thisAnimationDuration=null}),t.options.animation&&!n&&t.multiDrag.isMultiDrag&&(se=a({},e),e=v(le,!0),se.top-=e.f,se.left-=e.e)},dragOverAnimationComplete:function(){fe&&(fe=!1,me())},drop:function(t){var e=t.originalEvent,n=t.rootEl,o=t.parentEl,i=t.sortable,r=t.dispatchSortableEvent,a=t.oldIndex,l=t.putSortable,s=l||this.sortable;if(e){var c,u,d,h=this.options,f=o.children;if(!pe)if(h.multiDragKey&&!this.multiDragKeyDown&&this._deselectMultiDrag(),k(le,h.selectedClass,!~ue.indexOf(le)),~ue.indexOf(le))ue.splice(ue.indexOf(le),1),re=null,z({sortable:i,rootEl:n,name:\\\"deselect\\\",targetEl:le,originalEvent:e});else{if(ue.push(le),z({sortable:i,rootEl:n,name:\\\"select\\\",targetEl:le,originalEvent:e}),e.shiftKey&&re&&i.el.contains(re)){var p=j(re),t=j(le);if(~p&&~t&&p!==t)for(var g,m=p<t?(g=p,t):(g=t,p+1);g<m;g++)~ue.indexOf(f[g])||(k(f[g],h.selectedClass,!0),ue.push(f[g]),z({sortable:i,rootEl:n,name:\\\"select\\\",targetEl:f[g],originalEvent:e}))}else re=le;ae=s}pe&&this.isMultiDrag&&(fe=!1,(o[K].options.sort||o!==n)&&1<ue.length&&(c=X(le),u=j(le,\\\":not(.\\\"+this.options.selectedClass+\\\")\\\"),!he&&h.animation&&(le.thisAnimationDuration=null),s.captureAnimationState(),he||(h.animation&&(le.fromRect=c,ue.forEach(function(t){var e;t.thisAnimationDuration=null,t!==le&&(e=fe?X(t):c,t.fromRect=e,s.addAnimationState({target:t,rect:e}))})),me(),ue.forEach(function(t){f[u]?o.insertBefore(t,f[u]):o.appendChild(t),u++}),a===j(le)&&(d=!1,ue.forEach(function(t){t.sortableIndex!==j(t)&&(d=!0)}),d&&(r(\\\"update\\\"),r(\\\"sort\\\")))),ue.forEach(function(t){T(t)}),s.animateAll()),ae=s),(n===o||l&&\\\"clone\\\"!==l.lastPutMode)&&de.forEach(function(t){t.parentNode&&t.parentNode.removeChild(t)})}},nullingGlobal:function(){this.isMultiDrag=pe=!1,de.length=0},destroyGlobal:function(){this._deselectMultiDrag(),f(document,\\\"pointerup\\\",this._deselectMultiDrag),f(document,\\\"mouseup\\\",this._deselectMultiDrag),f(document,\\\"touchend\\\",this._deselectMultiDrag),f(document,\\\"keydown\\\",this._checkKeyDown),f(document,\\\"keyup\\\",this._checkKeyUp)},_deselectMultiDrag:function(t){if(!(void 0!==pe&&pe||ae!==this.sortable||t&&P(t.target,this.options.draggable,this.sortable.el,!1)||t&&0!==t.button))for(;ue.length;){var e=ue[0];k(e,this.options.selectedClass,!1),ue.shift(),z({sortable:this.sortable,rootEl:this.sortable.el,name:\\\"deselect\\\",targetEl:e,originalEvent:t})}},_checkKeyDown:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!0)},_checkKeyUp:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!1)}},a(t,{pluginName:\\\"multiDrag\\\",utils:{select:function(t){var e=t.parentNode[K];e&&e.options.multiDrag&&!~ue.indexOf(t)&&(ae&&ae!==e&&(ae.multiDrag._deselectMultiDrag(),ae=e),k(t,e.options.selectedClass,!0),ue.push(t))},deselect:function(t){var e=t.parentNode[K],n=ue.indexOf(t);e&&e.options.multiDrag&&~n&&(k(t,e.options.selectedClass,!1),ue.splice(n,1))}},eventProperties:function(){var n=this,o=[],i=[];return ue.forEach(function(t){var e;o.push({multiDragElement:t,index:t.sortableIndex}),e=fe&&t!==le?-1:fe?j(t,\\\":not(.\\\"+n.options.selectedClass+\\\")\\\"):j(t),i.push({multiDragElement:t,index:e})}),{items:r(ue),clones:[].concat(de),oldIndicies:o,newIndicies:i}},optionListeners:{multiDragKey:function(t){return\\\"ctrl\\\"===(t=t.toLowerCase())?t=\\\"Control\\\":1<t.length&&(t=t.charAt(0).toUpperCase()+t.substr(1)),t}}})}),Ft});\"\n","import { existsSync, readFileSync, writeFileSync, rmSync } from \"fs\"\nimport { resolve } from \"path\"\nimport type { IncomingMessage, ServerResponse } from \"http\"\nimport { mutateAgentxConfig } from \"./config-mutate\"\nimport { TokenStore } from \"./token-store\"\nimport { loadDaemonConfig } from \"./config\"\nimport { listAgentFiles, readAgentFile, writeAgentFile, createAgentSkill, deleteAgentSkill } from \"./file-ops\"\nimport { getWhatsAppState } from \"./whatsapp-state\"\nimport type { TopbarPeer } from \"./topbar\"\nimport { renderAdminPage } from \"./ui/pages/admin\"\n\n// --- /admin panel: form-driven management for agents, channels, crons ---\n//\n// Built for the business operator who opened the dashboard and wants to\n// change something without editing JSON. Shares the same safe-write machinery\n// as `agentx board add` and the setup wizard.\n//\n// Tabs:\n// - Agents: list, add, delete\n// - Channels: Telegram accounts (add, toggle, delete)\n// - Crons: schedules (add with plain-English timing, delete)\n// - Advanced: raw agentx.json editor with validate + backup-on-save\n\n// ========================================================================\n// HTTP entry points\n// ========================================================================\n\nexport function handleAdminGet(_req: IncomingMessage, res: ServerResponse, peers: TopbarPeer[] = [], localToken?: string): void {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderAdminPage({ peers, localToken }))\n}\n\nexport async function handleAdminConfigGet(_req: IncomingMessage, res: ServerResponse): Promise<void> {\n const cfg = readConfigRaw()\n res.writeHead(200, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(cfg))\n}\n\nexport async function handleAdminApi(req: IncomingMessage, res: ServerResponse, path: string): Promise<void> {\n try {\n // Non-JSON carve-out: the WhatsApp QR rendered as SVG. Proxies to the\n // daemon process (different memory space) where the adapter actually\n // emits the QR.\n if (req.method === \"GET\" && path === \"/api/admin/channels/whatsapp/qr.svg\") {\n await proxyDaemonSvg(res, \"/whatsapp/qr.svg\")\n return\n }\n const body = req.method === \"GET\" ? undefined : await readJsonBody(req)\n const dispatch: Record<string, () => unknown> = {\n \"GET /api/admin/state\": () => getAdminState(),\n \"POST /api/admin/agents\": () => addAgent(body),\n \"PATCH /api/admin/agents\": () => editAgent(body),\n \"DELETE /api/admin/agents\": () => deleteAgent(body),\n \"POST /api/admin/agents/access\": () => setAgentAccess(body),\n \"POST /api/admin/channels/telegram\": () => addTelegramAccount(body),\n \"DELETE /api/admin/channels/telegram\": () => deleteTelegramAccount(body),\n \"POST /api/admin/channels/telegram/toggle\": () => toggleTelegram(body),\n \"POST /api/admin/crons\": () => addCron(body),\n \"DELETE /api/admin/crons\": () => deleteCron(body),\n \"POST /api/admin/config/raw\": () => replaceConfigRaw(body),\n \"GET /api/admin/tokens\": () => listTokens(),\n \"POST /api/admin/tokens\": () => createToken(body),\n \"DELETE /api/admin/tokens\": () => revokeToken(body),\n \"POST /api/admin/agents/test\": () => testDriveAgent(body),\n \"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\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\"])\n/** Extensions the skill-tree viewer is allowed to read/write. Broader than\n * identity editing — skills include shell scripts, lockfiles, configs. Kept\n * as an allow-list so a malicious prompt can't touch an arbitrary binary. */\nconst SKILL_EDITABLE_EXTENSIONS = new Set([\n \".md\", \".markdown\", \".txt\", \".json\", \".jsonc\", \".yaml\", \".yml\", \".toml\",\n \".sh\", \".bash\", \".zsh\", \".py\", \".js\", \".mjs\", \".ts\", \".tsx\", \".jsx\",\n \".css\", \".html\", \".lock\", \".cfg\", \".ini\", \".env\", \".gitignore\",\n])\n/** Depth limit when walking a skill dir tree — defensive against loops. */\nconst SKILL_TREE_MAX_DEPTH = 6\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\n// --- Skill directory tree + per-file I/O ---\n//\n// A skill is `.claude/skills/<slug>/` — usually SKILL.md plus scripts,\n// lockfiles, configs. The admin UI lets the operator browse the directory\n// tree, edit any text file, and dispatch \"install deps\" through the agent.\n\nexport interface SkillTreeEntry {\n /** Path relative to the skill dir root. Directories end with \"/\". */\n path: string\n kind: \"file\" | \"dir\"\n size: number\n editable: boolean // false for binaries or disallowed extensions\n}\n\nexport interface SkillDepsHint {\n /** Which install command the operator should run (or have an agent run). */\n manager: \"npm\" | \"pnpm\" | \"yarn\" | \"pip\" | \"bundle\" | null\n /** Filename that triggered the detection, relative to skill dir. */\n file: string | null\n /** The exact command we'd recommend. */\n command: string | null\n}\n\n/** List every file in `.claude/skills/<slug>/`, recursively. Scoped so the\n * caller can't escape the skill dir via symlinks or \"..\" segments. */\nexport function listSkillTree(workspace: string, slug: string): SkillTreeEntry[] {\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(slug)) throw new Error(`Invalid skill slug: ${slug}`)\n const skillRoot = resolveInWorkspace(workspace, `.claude/skills/${slug}`)\n if (!existsSync(skillRoot)) throw new Error(`Skill \"${slug}\" not found.`)\n const out: SkillTreeEntry[] = []\n walk(skillRoot, \"\", 0, out)\n out.sort((a, b) => {\n // dirs first, then alphabetical\n if (a.kind !== b.kind) return a.kind === \"dir\" ? -1 : 1\n return a.path.localeCompare(b.path)\n })\n return out\n\n function walk(absDir: string, relDir: string, depth: number, acc: SkillTreeEntry[]): void {\n if (depth > SKILL_TREE_MAX_DEPTH) return\n let entries: string[] = []\n try { entries = readdirSync(absDir) } catch { return }\n for (const name of entries) {\n if (name === \"node_modules\" || name === \".git\") continue\n const abs = join(absDir, name)\n const rel = relDir ? `${relDir}/${name}` : name\n let st\n try { st = statSync(abs) } catch { continue }\n if (st.isDirectory()) {\n acc.push({ path: `${rel}/`, kind: \"dir\", size: 0, editable: false })\n walk(abs, rel, depth + 1, acc)\n } else if (st.isFile()) {\n const ext = extname(name).toLowerCase()\n // name-only allowlist for dotfiles like .env / .gitignore (no extension)\n const allowedByName = SKILL_EDITABLE_EXTENSIONS.has(ext) ||\n SKILL_EDITABLE_EXTENSIONS.has(name.toLowerCase())\n acc.push({\n path: rel,\n kind: \"file\",\n size: st.size,\n editable: allowedByName && st.size < MAX_FILE_BYTES,\n })\n }\n }\n }\n}\n\n/** Read any file in a skill dir. Respects the skill-editable allowlist. */\nexport function readSkillFile(workspace: string, slug: string, relPath: string): { path: string; content: string } {\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(slug)) throw new Error(`Invalid skill slug: ${slug}`)\n if (!relPath || relPath.includes(\"..\")) throw new Error(`Invalid file path: ${relPath}`)\n const abs = resolveInWorkspace(workspace, `.claude/skills/${slug}/${relPath}`)\n if (!existsSync(abs) || !statSync(abs).isFile()) {\n throw new Error(`File not found: ${relPath}`)\n }\n const ext = extname(abs).toLowerCase()\n const name = basename(abs).toLowerCase()\n if (!SKILL_EDITABLE_EXTENSIONS.has(ext) && !SKILL_EDITABLE_EXTENSIONS.has(name)) {\n throw new Error(`File type not editable: ${relPath}`)\n }\n if (statSync(abs).size > MAX_FILE_BYTES) {\n throw new Error(`File too large to edit (> ${MAX_FILE_BYTES / 1024} KB): ${relPath}`)\n }\n return { path: relPath, content: readFileSync(abs, \"utf-8\") }\n}\n\n/** Write any allowed file inside a skill dir. Creates parents. */\nexport function writeSkillFile(\n workspace: string,\n slug: string,\n relPath: string,\n content: string,\n): { path: string; bytes: number } {\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(slug)) throw new Error(`Invalid skill slug: ${slug}`)\n if (!relPath || relPath.includes(\"..\")) throw new Error(`Invalid file path: ${relPath}`)\n const abs = resolveInWorkspace(workspace, `.claude/skills/${slug}/${relPath}`)\n const ext = extname(abs).toLowerCase()\n const name = basename(abs).toLowerCase()\n if (!SKILL_EDITABLE_EXTENSIONS.has(ext) && !SKILL_EDITABLE_EXTENSIONS.has(name)) {\n throw new Error(`File type not editable: ${relPath}`)\n }\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/** Detect which dependency manager a skill needs based on the files present. */\nexport function detectSkillDeps(workspace: string, slug: string): SkillDepsHint {\n if (!/^[a-z0-9][a-z0-9_-]*$/.test(slug)) throw new Error(`Invalid skill slug: ${slug}`)\n const skillRoot = resolveInWorkspace(workspace, `.claude/skills/${slug}`)\n if (!existsSync(skillRoot)) throw new Error(`Skill \"${slug}\" not found.`)\n const check = (name: string): boolean => existsSync(join(skillRoot, name))\n if (check(\"pnpm-lock.yaml\")) return { manager: \"pnpm\", file: \"pnpm-lock.yaml\", command: \"pnpm install\" }\n if (check(\"yarn.lock\")) return { manager: \"yarn\", file: \"yarn.lock\", command: \"yarn install\" }\n if (check(\"package.json\")) return { manager: \"npm\", file: \"package.json\", command: \"npm install\" }\n if (check(\"requirements.txt\")) return { manager: \"pip\", file: \"requirements.txt\", command: \"pip install -r requirements.txt\" }\n if (check(\"Gemfile\")) return { manager: \"bundle\", file: \"Gemfile\", command: \"bundle install\" }\n return { manager: null, file: null, command: null }\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","// --- Admin page (/admin) ---\n//\n// Single-file settings surface with seven tabs rendered in-place via a\n// client-side data-tab switcher:\n// Agents / Channels / Schedules / Webhooks / Mesh / Tokens / Advanced\n//\n// API is in admin-panel.ts; this module owns the HTML + CSS + client JS only.\n//\n// ### Why a single file (for now)\n//\n// The tabs share: a common data-fetch-and-render pipeline, the peer banner,\n// the files modal, and mutation helpers. Splitting them into seven files\n// would force those cross-tab primitives into a shared helper with a\n// wider interface; the savings aren't worth the ceremony at this size.\n// When the page grows past ~3k lines again, a tab-by-tab split becomes\n// worth the cost — the scaffold is already ready for it under\n// ui/pages/admin/<tab>.ts.\n\nimport {\n renderShell,\n pageHead, healthStrip, witBanner, sectionHead, secLabel,\n TOAST_HTML, TOAST_SCRIPT, ROW_CARD_SCRIPT,\n type TopbarPeer,\n} from \"..\"\n\n/** Feather-style stroked icons for each section head. Kept together at the\n * top so we can swap the icon set in one place if/when the design calls\n * for a change. */\nconst ICONS = {\n agents: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"10\" rx=\"2\"/><circle cx=\"12\" cy=\"5\" r=\"2\"/><path d=\"M12 7v4M8 16h.01M16 16h.01\"/></svg>`,\n channels: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\"/></svg>`,\n schedules: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>`,\n webhooks: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 12a3 3 0 1 0 3-3m-8 11a3 3 0 1 0 2.6-1.5L10 12m7 3a3 3 0 1 0-2-5.5\"/></svg>`,\n mesh: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"2\"/><circle cx=\"18\" cy=\"6\" r=\"2\"/><circle cx=\"6\" cy=\"18\" r=\"2\"/><circle cx=\"18\" cy=\"18\" r=\"2\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/><path d=\"M7.4 7.4l3.2 3.2M13.4 13.4l3.2 3.2M16.6 7.4l-3.2 3.2M10.6 13.4l-3.2 3.2\"/></svg>`,\n tokens: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 2l-2 2m-8 8a4 4 0 1 1-6 6m6-6l5-5 3 3-5 5m-3-3l3 3\"/></svg>`,\n advanced: `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 4h8l4 4v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><path d=\"M9 13l2 2 4-4\"/></svg>`,\n}\n\nexport interface AdminPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n localToken?: string\n}\n\nexport function renderAdminPage(opts: AdminPageOpts = {}): string {\n const tokenScript = opts.localToken\n ? `<script>window.AX_LOCAL_TOKEN = ${JSON.stringify(opts.localToken)};</script>`\n : \"\"\n\n // New page chrome — sits above the peer banner + tabs. Placeholder values\n // for the health strip live here; ADMIN_HEALTH_SCRIPT populates them from\n // /api/admin/config once the page loads.\n const chrome = pageHead({\n kicker: \"Settings\",\n title: \"Your team's control room\",\n lead: `Manage the agents, the places they answer from, and who's allowed to reach them. Everything here writes to <span class=\"mono muted\">agentx.json</span> on your machine — nothing leaves.`,\n }) + healthStrip([\n { kind: \"off\", num: \"—\", label: \"Agents online\" },\n { kind: \"off\", num: \"—\", label: \"Channels connected\" },\n { kind: \"off\", num: \"0\", label: \"Needs attention\" },\n { kind: \"off\", num: \"—\", label: \"Active tokens\" },\n ]).replace(\n \"<div class=\\\"ax-health-strip\\\">\",\n `<div class=\"ax-health-strip\" id=\"ax-health\">`,\n )\n\n return renderShell({\n title: \"AgentX · Settings\",\n activeTab: \"admin\",\n subtitle: \"Settings\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n noMain: true,\n body: chrome + ADMIN_PAGE_BODY + TOAST_HTML,\n css: ADMIN_PAGE_CSS,\n scripts: `${tokenScript}<script>${TOAST_SCRIPT}\\n${ROW_CARD_SCRIPT}\\n${ADMIN_HEALTH_SCRIPT}\\n${ADMIN_PAGE_SCRIPT}</script>`,\n })\n}\n\n// Populates the 4-up health strip + tab count bubbles from the same\n// /api/admin/config payload the main admin script already fetches. Runs\n// after the admin bootstrap attaches state under window.__AX_ADMIN.\nconst ADMIN_HEALTH_SCRIPT = `\n(function(){\n function fmt(n) { return n == null ? '—' : String(n); }\n async function refresh() {\n try {\n const headers = { 'Content-Type': 'application/json' };\n if (window.AX_LOCAL_TOKEN) headers['Authorization'] = 'Bearer ' + window.AX_LOCAL_TOKEN;\n const r = await fetch('/api/admin/config', { headers });\n if (!r.ok) return;\n const cfg = await r.json();\n const agents = Object.keys(cfg.agents || {});\n const channels = cfg.channels || {};\n const channelDefs = ['telegram','whatsapp','slack','discord','gitlab','github'];\n const enabled = channelDefs.filter(c => channels[c] && channels[c].enabled).length;\n const crons = Object.keys(cfg.crons || {}).length;\n const webhooks = Object.keys(cfg.webhooks || {}).length;\n const meshPeers = (cfg.mesh && Array.isArray(cfg.mesh.peers) ? cfg.mesh.peers.length : 0);\n\n // Health strip numbers\n const strip = document.getElementById('ax-health');\n if (strip) {\n const cards = strip.querySelectorAll('.ax-health-card');\n const setCard = (i, num, kind) => {\n const c = cards[i]; if (!c) return;\n const n = c.querySelector('.ax-hc-num'); if (n) n.textContent = fmt(num);\n const d = c.querySelector('.ax-hc-dot');\n if (d) { d.className = 'ax-hc-dot ax-hc-dot--' + kind; }\n };\n setCard(0, agents.length, agents.length > 0 ? 'ok' : 'off');\n setCard(1, enabled + '/' + channelDefs.length, enabled > 0 ? 'ok' : 'off');\n // \"Needs attention\" is a placeholder until we surface real warnings.\n setCard(2, 0, 'off');\n const tokenCount = Array.isArray(cfg.tokens) ? cfg.tokens.length : 0;\n setCard(3, tokenCount, tokenCount > 0 ? 'ok' : 'off');\n }\n\n // Tab counts\n const setCount = (tab, val) => {\n const btn = document.querySelector('nav.tabs button[data-tab=\"' + tab + '\"]');\n if (!btn) return;\n let c = btn.querySelector('.ax-tab-count');\n if (!c) {\n c = document.createElement('span');\n c.className = 'ax-tab-count';\n btn.appendChild(document.createTextNode(' '));\n btn.appendChild(c);\n }\n c.textContent = String(val);\n };\n setCount('agents', agents.length);\n setCount('channels', enabled + '/' + channelDefs.length);\n setCount('crons', crons);\n setCount('webhooks', webhooks);\n setCount('mesh', meshPeers);\n const tokenCount = Array.isArray(cfg.tokens) ? cfg.tokens.length : 0;\n setCount('tokens', tokenCount);\n } catch (e) { /* best-effort */ }\n }\n refresh();\n // Refresh again after the main admin script has had a chance to mutate config.\n document.addEventListener('ax-config-saved', refresh);\n})();`\n\nconst ADMIN_PAGE_BODY = `\n<div id=\"peer-banner\" class=\"peer-banner\">\n <span class=\"label\">Managing</span>\n <span class=\"name\" id=\"peer-banner-name\">—</span>\n <span>from this dashboard via scoped token.</span>\n <span class=\"spacer\"></span>\n <button id=\"peer-banner-back\">← Back to primary</button>\n</div>\n<nav class=\"tabs\">\n <button data-tab=\"agents\" class=\"active\">Agents</button>\n <button data-tab=\"channels\">Channels</button>\n <button data-tab=\"crons\">Schedules</button>\n <button data-tab=\"webhooks\">Webhooks</button>\n <button data-tab=\"mesh\">Mesh</button>\n <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 ${sectionHead({\n icon: ICONS.agents,\n title: \"Your agents\",\n lead: \"Each agent is a role with its own trigger words and workspace. Delete removes the config only — the folder on disk stays.\",\n })}\n ${witBanner({\n persistKey: \"agents\",\n bodyHtml: `<b>What's an agent, exactly?</b> Think of it as a specialised teammate. A sales agent handles pricing questions, a support agent answers tickets, a reports agent sends a weekly summary. You decide what they do and what to say to wake them up (their \"trigger words\", like <code>@support</code>).`,\n })}\n <div id=\"agent-list\" class=\"ax-stack\"></div>\n <div class=\"add-form\">\n <h3>Add a new agent</h3>\n <div class=\"rowf\">\n <div><label>Agent id<span class=\"hint\">(lowercase)</span></label><input id=\"a-id\" placeholder=\"sales\" /></div>\n <div><label>Agent name</label><input id=\"a-name\" placeholder=\"Sales Bot\" /></div>\n </div>\n <label>Trigger words<span class=\"hint\">(comma or space separated)</span></label>\n <input id=\"a-triggers\" placeholder=\"@sales, sales\" />\n <div class=\"rowf\">\n <div><label>AI engine</label><select id=\"a-tier\"><option value=\"claude-code\">Claude Code</option><option value=\"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:<id></code> scope)</span></label>\n <div class=\"actions\"><button class=\"primary\" onclick=\"addAgent()\">Add agent</button><div id=\"a-msg\" class=\"msg\"></div></div>\n </div>\n </section>\n\n <section id=\"tab-channels\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.channels,\n title: \"Where your agents show up\",\n lead: \"Connect a chat app, and your agents can answer from there. Set up once — they'll keep working in the background.\",\n })}\n ${witBanner({\n persistKey: \"channels\",\n bodyHtml: `<b>How does this work?</b> We give each channel a small \"bridge\" that forwards messages to AgentX. For chat apps (Telegram, Slack, Discord) you'll paste a bot token — we keep the value in your <code>.env</code> file and never send it over the network. For webhook-style sources (GitHub, GitLab) we hand you a URL to paste into their dashboard.`,\n })}\n\n <div id=\"ch-chatapps-label\">${secLabel({ label: \"Chat apps\" })}</div>\n <div class=\"ax-connectors\" id=\"ch-chatapps\"></div>\n\n <div id=\"ch-devtools-label\">${secLabel({ label: \"Developer tools\", rightHtml: `<span class=\"ax-pill\">Send events from GitHub, GitLab, Sentry, etc.</span>` })}</div>\n <div class=\"ax-connectors\" id=\"ch-devtools\"></div>\n\n <!-- Active-channel configuration panes. One shows at a time; clicking\n a connector card above flips the 'hidden' attribute on the\n matching div. Existing render* pipelines still populate these by id. -->\n <div id=\"ch-panes\" style=\"margin-top:22px\">\n <div id=\"ch-telegram\" hidden><div id=\"tg-section\" class=\"section-block\"></div></div>\n <div id=\"ch-whatsapp\" hidden></div>\n <div id=\"ch-slack\" hidden><div id=\"slack-section\" class=\"section-block\"></div></div>\n <div id=\"ch-discord\" hidden></div>\n <div id=\"ch-gitlab\" hidden></div>\n <div id=\"ch-github\" hidden></div>\n </div>\n </section>\n\n <section id=\"tab-crons\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.schedules,\n title: \"Schedules\",\n lead: \"Make an agent do something on a timer — no one has to ask. Daily summaries, weekly reports, hourly health checks.\",\n })}\n ${witBanner({\n persistKey: \"crons\",\n bodyHtml: `<b>What's a good schedule?</b> Anything recurring. A <i>Reports agent</i> sending Monday-morning numbers, a <i>Health agent</i> pinging you at 3am if a server's down, a <i>Standup agent</i> reminding the team. Pick a time below.`,\n })}\n <div id=\"cron-list\" class=\"ax-stack\" style=\"margin-bottom:22px\"></div>\n\n <div class=\"ax-builder\">\n <!-- Hidden fields shared between guided + expert modes. addCron()\n reads these canonical ids; the mode-specific inputs mirror\n their values in. -->\n <select id=\"c-agent\" style=\"display:none\"></select>\n <h3>Create a new schedule</h3>\n <p class=\"ax-builder__hint\">Build your timing here. Switch to expert mode if you'd rather type a cron expression.</p>\n <div class=\"ax-mode-switch\" id=\"sched-mode\">\n <button data-mode=\"simple\" class=\"is-active\">Guided</button>\n <button data-mode=\"expert\">Expert (cron)</button>\n </div>\n\n <!-- Guided mode — sentence builder. The pill-picks emit a cron\n string into the hidden #c-schedule input used by addCron(). -->\n <div id=\"sched-simple\">\n <div class=\"ax-sentence\">\n <span>Run</span>\n <select class=\"ax-pill-pick\" id=\"s-agent\"></select>\n <select class=\"ax-pill-pick\" id=\"s-freq\">\n <option value=\"daily\">every day</option>\n <option value=\"weekly\" selected>every week</option>\n <option value=\"weekdays\">every weekday</option>\n <option value=\"hourly\">every hour</option>\n <option value=\"15min\">every 15 minutes</option>\n </select>\n <div id=\"s-days\" class=\"ax-day-picker\">\n <button data-day=\"1\">M</button><button data-day=\"2\">T</button><button data-day=\"3\">W</button><button data-day=\"4\">T</button><button data-day=\"5\">F</button><button data-day=\"6\">S</button><button data-day=\"0\">S</button>\n </div>\n <span id=\"s-at\" class=\"ax-sentence__when\">at</span>\n <select class=\"ax-pill-pick ax-pill-pick--sm\" id=\"s-hour\">\n <option>6:00 am</option><option>7:00 am</option><option>8:00 am</option>\n <option selected>9:00 am</option><option>10:00 am</option><option>11:00 am</option>\n <option>12:00 pm</option><option>1:00 pm</option><option>2:00 pm</option>\n <option>3:00 pm</option><option>4:00 pm</option><option>5:00 pm</option>\n <option>6:00 pm</option><option>7:00 pm</option><option>8:00 pm</option>\n <option>9:00 pm</option><option>10:00 pm</option>\n </select>\n </div>\n\n <label class=\"ax-builder__lbl\">Give it a name <span class=\"opt\">lowercase, no spaces</span></label>\n <input class=\"ax-builder__inp\" id=\"c-id\" placeholder=\"weekly-sales-digest\" />\n\n <label class=\"ax-builder__lbl\">What should the agent do?</label>\n <textarea class=\"ax-builder__inp\" id=\"c-prompt\" placeholder=\"Send me the weekly sales summary, with the top 3 wins and anything that regressed.\"></textarea>\n\n <div class=\"ax-preview-box\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>\n <span id=\"s-preview-text\"></span>\n </div>\n\n <div style=\"display:flex;gap:8px;margin-top:16px;align-items:center\">\n <button class=\"ax-btn ax-btn--primary\" onclick=\"addCron()\">Add schedule</button>\n <div id=\"c-msg\" class=\"msg\"></div>\n <div style=\"flex:1\"></div>\n <span class=\"muted\" style=\"font-size:11.5px\">Cron equivalent: <span class=\"mono\" style=\"color:var(--ax-text-2)\" id=\"s-cron-echo\">0 9 * * 1</span></span>\n </div>\n </div>\n\n <!-- Expert mode — original cron + prompt form. Shares the #c-id /\n #c-prompt / #c-agent fields with guided mode so addCron() is\n mode-agnostic. -->\n <div id=\"sched-expert\" style=\"display:none\">\n <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px\">\n <div>\n <label class=\"ax-builder__lbl\">Schedule id <span class=\"opt\">lowercase, no spaces</span></label>\n <input class=\"ax-builder__inp\" id=\"c-id-expert\" placeholder=\"weekly-report\" />\n </div>\n <div>\n <label class=\"ax-builder__lbl\">Agent</label>\n <select class=\"ax-builder__inp\" id=\"c-agent-expert\"></select>\n </div>\n </div>\n <label class=\"ax-builder__lbl\">Cron expression <span class=\"opt\">standard five-field cron</span></label>\n <input class=\"ax-builder__inp mono\" id=\"c-schedule\" value=\"0 9 * * 1\" />\n <div id=\"c-preview\" class=\"ax-preview-box\" style=\"margin-top:10px\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>\n <span id=\"c-preview-text\">At 9:00 AM, only on Monday</span>\n </div>\n <div class=\"ax-cron-hints\">\n <div><code>0 9 * * 1</code>Mondays at 9am</div>\n <div><code>*/15 * * * *</code>Every 15 minutes</div>\n <div><code>0 0 1 * *</code>First of every month</div>\n <div><code>0 17 * * 5</code>Fridays at 5pm</div>\n </div>\n <label class=\"ax-builder__lbl\" style=\"margin-top:12px\">Prompt</label>\n <textarea class=\"ax-builder__inp\" id=\"c-prompt-expert\" placeholder=\"What should the agent do on every tick?\"></textarea>\n <div style=\"display:flex;gap:8px;margin-top:16px\">\n <button class=\"ax-btn ax-btn--primary\" onclick=\"addCronExpert()\">Add schedule</button>\n </div>\n </div>\n </div>\n </section>\n\n <section id=\"tab-webhooks\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.webhooks,\n title: \"Webhooks\",\n lead: \"A URL you give to another service. When something happens there — a PR is merged, an error fires — they POST to that URL, and we forward it to your agent as a readable message.\",\n })}\n ${witBanner({\n persistKey: \"webhooks\",\n bodyHtml: `<b>Not sure you need this?</b> If a service you use has a \"webhooks\" setting (GitHub, Stripe, Sentry, Typeform…), you probably do. It's how those services tell AgentX when something's worth looking at.`,\n })}\n <div id=\"webhook-list\" class=\"ax-stack\"></div>\n <div class=\"add-form\">\n <h3>Add a webhook</h3>\n <div class=\"rowf\">\n <div><label>Webhook id<span class=\"hint\">(lowercase)</span></label><input id=\"w-id\" placeholder=\"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 ${sectionHead({\n icon: ICONS.mesh,\n title: \"Mesh\",\n lead: \"Link this AgentX to other machines so they share work. Good for teams running AgentX on multiple laptops or a mix of laptop + server.\",\n })}\n <div id=\"mesh-hero\" class=\"ax-mesh-hero\"></div>\n ${witBanner({\n persistKey: \"mesh\",\n bodyHtml: `<b>Do I need mesh?</b> Only if you're running AgentX in more than one place. Solo users on one laptop can skip this entirely.`,\n })}\n ${secLabel({ label: \"Connected peers\" })}\n <div id=\"mesh-peers\" class=\"ax-stack\"></div>\n <div class=\"add-form\">\n <h3>Add a peer</h3>\n <div class=\"rowf\">\n <div><label>Peer name<span class=\"hint\">(human label)</span></label><input id=\"m-name\" placeholder=\"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 ${sectionHead({\n icon: ICONS.tokens,\n title: \"Access tokens\",\n lead: \"Passwords, but for software. Anything that wants to talk to AgentX from outside (a Slack bridge, another AgentX machine, a script) uses one.\",\n })}\n ${witBanner({\n persistKey: \"tokens\",\n bodyHtml: `<b>Tokens get scopes.</b> A scope is what the token is allowed to do. Give the narrowest one that works — a Slack bridge only needs to send messages, it doesn't need to edit settings. You can't recover a token after you close this page, so copy it immediately.`,\n })}\n <div id=\"token-list\" class=\"ax-stack\"></div>\n <div class=\"add-form\">\n <h3>Mint a new token</h3>\n <label>Name<span class=\"hint\">(who or what this is for — e.g. \"Slack bridge\", \"Mesh peer: laptop\")</span></label>\n <input id=\"t-name\" placeholder=\"Slack bridge\" />\n <label>Scopes<span class=\"hint\">(pick one or more)</span></label>\n <div id=\"t-scopes\" style=\"display:flex;flex-direction:column;gap:4px;margin-top:4px\"></div>\n <label>Expires after<span class=\"hint\">(in days; blank = never)</span></label>\n <input id=\"t-expires\" type=\"number\" min=\"1\" max=\"3650\" placeholder=\"90\" />\n <div class=\"actions\"><button class=\"primary\" onclick=\"createToken()\">Create token</button><div id=\"t-msg\" class=\"msg\"></div></div>\n <div id=\"t-reveal\" style=\"display:none;margin-top:14px;padding:14px 16px;border-radius:6px;background:rgba(34,197,94,0.08);border:1px solid rgba(34,197,94,0.3)\">\n <div style=\"font-size:12px;color:var(--muted);margin-bottom:6px\">Copy this now — it won't be shown again.</div>\n <code id=\"t-secret\" style=\"display:block;word-break:break-all;color:var(--green);font-family:ui-monospace,monospace;font-size:12px;padding:8px;background:#0e1119;border-radius:4px\"></code>\n </div>\n </div>\n </section>\n\n <section id=\"tab-advanced\" class=\"tab\">\n ${sectionHead({\n icon: ICONS.advanced,\n title: \"Raw configuration\",\n lead: \"Everything on the other tabs writes to this file. Edit here for bulk changes, diffs, or fields we haven't put in the UI yet. Saving creates a timestamped backup.\",\n })}\n ${witBanner({\n persistKey: \"advanced\",\n bodyHtml: `<b>Safe to look, careful editing.</b> We've pretty-printed and annotated each section so you can read it. Click any <span class=\"mono\" style=\"color:var(--ax-muted)\">▸</span> to fold. Switch to <b>Edit</b> to change values — invalid JSON is refused before saving.`,\n })}\n\n <div class=\"ax-jv-toolbar\">\n <input class=\"ax-jv-search\" id=\"jv-search\" placeholder=\"Search keys & values…\" />\n <div class=\"ax-jv-seg\" id=\"jv-mode\">\n <button data-jv-mode=\"tree\" class=\"is-active\">Tree</button>\n <button data-jv-mode=\"raw\">Raw</button>\n <button data-jv-mode=\"edit\">Edit</button>\n </div>\n <div class=\"ax-jv-spacer\"></div>\n <span class=\"muted mono\" id=\"jv-meta\" style=\"font-size:11px\">agentx.json</span>\n <button class=\"ax-btn\" id=\"jv-expand-all\">Expand all</button>\n <button class=\"ax-btn\" id=\"jv-collapse-all\">Collapse all</button>\n <button class=\"ax-btn ax-btn--primary\" id=\"jv-save\">Save</button>\n </div>\n <div class=\"ax-jv-viewer\" id=\"jv-viewer\"></div>\n <textarea id=\"raw-editor\" class=\"raw\" style=\"display:none\" spellcheck=\"false\"></textarea>\n <div id=\"r-msg\" class=\"msg\" style=\"margin-top:10px\"></div>\n <div class=\"actions\">\n <button class=\"primary\" onclick=\"saveRaw()\">Save config</button>\n <button class=\"ghost\" onclick=\"loadRaw()\">Reload from disk</button>\n <div id=\"r-msg\" class=\"msg\"></div>\n </div>\n <div class=\"hint-block\">The schema is documented at <code>docs/reference/config-schema.md</code>. Invalid JSON is refused; schema-level errors only surface after the daemon tries to use the new config.</div>\n </section>\n</main>\n`\nconst ADMIN_PAGE_CSS = `\n\n/* --- Inner tabs (Agents / Channels / Schedules / …) --- */\nnav.tabs{display:flex;gap:4px;padding:14px 24px 0;background:var(--ax-bg-elev);\n border-bottom:1px solid var(--ax-border);overflow-x:auto}\nnav.tabs button{background:transparent;border:none;color:var(--ax-muted);\n padding:8px 14px;font:inherit;font-size:12px;cursor:pointer;\n border-bottom:2px solid transparent;letter-spacing:-0.005em;\n border-top-left-radius:4px;border-top-right-radius:4px;\n transition:color 120ms,border-color 120ms,background 120ms}\nnav.tabs button:hover{color:var(--ax-text);background:color-mix(in oklch,var(--ax-accent) 4%,transparent)}\nnav.tabs button.active{color:var(--ax-text);border-bottom-color:var(--ax-accent);\n background:color-mix(in oklch,var(--ax-accent) 8%,transparent)}\n\nmain{max-width:960px;margin:0 auto;padding:20px 24px 60px}\nsection.tab{display:none}\nsection.tab.active{display:block}\n\nh2{font-size:16px;margin:0 0 4px;font-weight:600;letter-spacing:-0.01em}\n.lead{color:var(--ax-muted);margin:0 0 20px;font-size:12px;line-height:1.55;max-width:720px}\n\n/* --- Row cards --- */\n.list{display:flex;flex-direction:column;gap:6px;margin-bottom:22px}\n.row-card{background:var(--ax-surface);border:1px solid var(--ax-border);\n border-radius:6px;padding:12px 14px;display:flex;align-items:center;gap:12px;\n transition:border-color 120ms}\n.row-card:hover{border-color:var(--ax-border-2)}\n.row-card .info{flex:1;min-width:0}\n.row-card .info h3{margin:0 0 2px;font-size:13px;font-weight:600;letter-spacing:-0.005em}\n.row-card .info .meta{font-size:11px;color:var(--ax-muted);font-family:var(--ax-mono);\n overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.5}\n.row-card .info .meta b{color:var(--ax-text);font-weight:500}\n.row-card .info .meta a{color:var(--ax-accent)}\n\n/* --- Buttons --- */\nbutton.primary,button.ghost,button.danger{font-family:inherit;font-size:12px;cursor:pointer;\n border-radius:4px;transition:background 120ms,border-color 120ms,color 120ms}\nbutton.primary{background:color-mix(in oklch,var(--ax-accent) 12%,var(--ax-surface));\n border:1px solid color-mix(in oklch,var(--ax-accent) 40%,var(--ax-border-2));\n color:var(--ax-accent);padding:6px 14px;font-weight:500}\nbutton.primary:hover{background:color-mix(in oklch,var(--ax-accent) 20%,var(--ax-surface))}\nbutton.primary:disabled{opacity:0.4;cursor:not-allowed}\nbutton.ghost{background:var(--ax-surface);border:1px solid var(--ax-border-2);\n color:var(--ax-text);padding:6px 12px}\nbutton.ghost:hover{background:var(--ax-surface-2);border-color:var(--ax-accent)}\nbutton.danger{background:transparent;color:var(--ax-err);\n border:1px solid color-mix(in oklch,var(--ax-err) 35%,transparent);padding:5px 11px}\nbutton.danger:hover{background:color-mix(in oklch,var(--ax-err) 10%,transparent);\n border-color:color-mix(in oklch,var(--ax-err) 55%,transparent)}\n\n/* --- Forms --- */\n.add-form{background:var(--ax-surface);border:1px solid var(--ax-border);\n border-radius:6px;padding:16px 18px;margin-top:8px}\n.add-form h3{margin:0 0 10px;font-size:12px;font-weight:600;\n text-transform:uppercase;letter-spacing:0.06em;color:var(--ax-muted)}\nlabel{display:block;margin:8px 0 4px;font-size:11px;color:var(--ax-muted);\n text-transform:uppercase;letter-spacing:0.04em}\nlabel .hint{font-weight:400;font-size:11px;margin-left:6px;\n text-transform:none;letter-spacing:normal;color:var(--ax-muted);opacity:0.85}\ninput,textarea,select{width:100%;background:var(--ax-bg);color:var(--ax-text);\n border:1px solid var(--ax-border);border-radius:4px;padding:7px 10px;\n font:inherit;font-size:12px;transition:border-color 120ms}\ninput:focus,textarea:focus,select:focus{outline:none;border-color:var(--ax-accent)}\ntextarea{resize:vertical;min-height:64px;line-height:1.55}\ntextarea.raw{min-height:480px;font-family:var(--ax-mono);font-size:12px;line-height:1.6}\n\n.rowf{display:flex;gap:10px}\n.rowf > *{flex:1}\n.actions{display:flex;gap:8px;margin-top:14px;align-items:center;flex-wrap:wrap}\n\n/* --- Messages --- */\n.msg{margin:10px 0;padding:8px 12px;border-radius:4px;font-size:12px;\n display:none;line-height:1.5}\n.msg.ok{display:block;background:color-mix(in oklch,var(--ax-accent) 10%,transparent);\n color:var(--ax-accent);border:1px solid color-mix(in oklch,var(--ax-accent) 35%,transparent)}\n.msg.err{display:block;background:color-mix(in oklch,var(--ax-err) 10%,transparent);\n color:var(--ax-err);border:1px solid color-mix(in oklch,var(--ax-err) 35%,transparent)}\n.msg.warn{display:block;background:color-mix(in oklch,var(--ax-warn) 10%,transparent);\n color:var(--ax-warn);border:1px solid color-mix(in oklch,var(--ax-warn) 35%,transparent)}\n\n.empty{color:var(--ax-muted);font-style:italic;padding:28px 12px;font-size:12px;\n text-align:center;border:1px dashed var(--ax-border);border-radius:4px}\n\n/* --- Toggles + chips --- */\n.toggle-switch{display:flex;align-items:center;gap:10px;margin-bottom:14px;font-size:12px}\n.toggle-switch input{width:auto;accent-color:var(--ax-accent)}\n.chip{display:inline-block;font-size:10px;padding:1px 7px;line-height:16px;\n border-radius:3px;background:var(--ax-surface);border:1px solid var(--ax-border);\n color:var(--ax-muted);font-family:var(--ax-mono);margin-right:4px;\n text-transform:uppercase;letter-spacing:0.02em}\n.chip.off{background:var(--ax-surface-2);color:var(--ax-muted)}\n.hint-block{font-size:11px;color:var(--ax-muted);margin-top:8px;line-height:1.55}\n.hint-block code{background:var(--ax-bg);padding:1px 6px;border-radius:3px;\n color:var(--ax-text);font-family:var(--ax-mono);font-size:11px;\n border:1px solid var(--ax-border)}\n.section-block{margin-bottom:28px}\n\n/* --- Channels sidemenu --- */\n.ch-split{display:grid;grid-template-columns:220px 1fr;gap:16px;align-items:start}\n@media (max-width:720px){.ch-split{grid-template-columns:1fr}}\n.ch-menu{background:var(--ax-surface);border:1px solid var(--ax-border);\n border-radius:6px;padding:6px;display:flex;flex-direction:column;gap:2px;\n position:sticky;top:120px}\n.ch-menu button{background:transparent;border:1px solid transparent;\n color:var(--ax-text-2);padding:8px 10px;border-radius:4px;cursor:pointer;\n font:inherit;font-size:12px;text-align:left;display:flex;align-items:center;gap:10px;\n transition:background 120ms,border-color 120ms,color 120ms}\n.ch-menu button:hover{background:var(--ax-surface-2);color:var(--ax-text)}\n.ch-menu button.is-active{background:color-mix(in oklch,var(--ax-accent) 12%,var(--ax-surface));\n color:var(--ax-text);border-color:color-mix(in oklch,var(--ax-accent) 35%,var(--ax-border-2))}\n.ch-menu .ch-icon{font-family:var(--ax-mono);font-size:10px;line-height:16px;\n width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;\n background:var(--ax-bg);border:1px solid var(--ax-border);border-radius:3px;\n color:var(--ax-muted);flex-shrink:0}\n.ch-menu .ch-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ch-menu .ch-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;background:var(--ax-border-2)}\n.ch-menu .ch-dot--on{background:var(--ax-accent)}\n.ch-menu .ch-dot--off{background:var(--ax-warn)}\n.ch-view{display:block}\n.ch-view[hidden]{display:none}\n\n/* --- Files modal --- */\n.files-split{display:flex;flex:1;min-height:0}\n.files-picker{width:260px;min-width:220px;border-right:1px solid var(--ax-border);\n overflow-y:auto;padding:10px 8px;background:var(--ax-bg-elev)}\n.files-picker .group{font-size:10px;text-transform:uppercase;letter-spacing:0.06em;\n color:var(--ax-muted);padding:8px 10px 4px;font-weight:600}\n.files-picker .file{display:flex;align-items:center;gap:8px;padding:8px 10px;\n border-radius:4px;cursor:pointer;font-size:12px;color:var(--ax-text);\n border-left:2px solid transparent;transition:background 120ms}\n.files-picker .file:hover{background:color-mix(in oklch,var(--ax-accent) 6%,transparent)}\n.files-picker .file.active{background:color-mix(in oklch,var(--ax-accent) 12%,transparent);\n border-left-color:var(--ax-accent);color:var(--ax-text)}\n.files-picker .file.missing{color:var(--ax-muted);font-style:italic}\n.files-picker .file .info{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.files-picker .file .size{font-size:10px;color:var(--ax-muted);font-family:var(--ax-mono)}\n.files-picker .file .del{background:transparent;border:none;color:var(--ax-muted);\n cursor:pointer;padding:0 4px;font-size:13px}\n.files-picker .file .del:hover{color:var(--ax-err)}\n.files-editor{flex:1;display:flex;flex-direction:column;overflow:hidden}\n.files-editor-empty{flex:1;display:flex;align-items:center;justify-content:center;\n color:var(--ax-muted);font-style:italic;padding:40px;text-align:center;font-size:12px}\n.files-editor-head{padding:10px 16px;border-bottom:1px solid var(--ax-border);\n display:flex;align-items:center;gap:10px;background:var(--ax-bg-elev)}\n.files-editor-head .path{flex:1;font-family:var(--ax-mono);font-size:12px;color:var(--ax-muted)}\n.files-editor-body{flex:1;overflow:hidden;display:flex}\n.files-editor-body textarea{flex:1;background:var(--ax-bg);color:var(--ax-text);\n border:none;padding:14px 16px;font-family:var(--ax-mono);font-size:12px;line-height:1.6;resize:none}\n.files-editor-body textarea:focus{outline:none}\n.files-add{border-top:1px solid var(--ax-border);padding:10px 16px;background:var(--ax-bg-elev)}\n.files-add h4{margin:0 0 8px;font-size:11px;font-weight:600;color:var(--ax-muted);\n text-transform:uppercase;letter-spacing:0.06em}\n\n/* --- Test drive chat modal --- */\n.td-modal{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;\n justify-content:center;padding:24px}\n.td-modal.hidden{display:none}\n.td-backdrop{position:absolute;inset:0;background:color-mix(in oklch,var(--ax-bg) 60%,black)}\n.td-card{position:relative;width:min(640px,94vw);height:min(720px,90vh);\n background:var(--ax-surface);border:1px solid var(--ax-border-2);border-radius:8px;\n display:flex;flex-direction:column;box-shadow:0 18px 48px rgba(0,0,0,0.5);overflow:hidden}\n.td-card > header{display:flex;align-items:center;gap:10px;padding:12px 16px;\n border-bottom:1px solid var(--ax-border);background:var(--ax-bg-elev)}\n.td-card > header h3{margin:0;font-size:14px;font-weight:600;flex:1;letter-spacing:-0.005em}\n.td-card .chip-small{font-size:10px;padding:2px 7px;border-radius:3px;\n background:color-mix(in oklch,var(--ax-accent) 14%,transparent);\n color:var(--ax-accent);letter-spacing:0.04em;text-transform:uppercase;font-family:var(--ax-mono)}\n.td-close{background:transparent;border:none;color:var(--ax-muted);font-size:22px;\n cursor:pointer;padding:0 6px;line-height:1}\n.td-close:hover{color:var(--ax-text)}\n.td-body{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;\n gap:10px;background:var(--ax-bg)}\n.td-msg{max-width:82%;padding:8px 12px;border-radius:10px;font-size:13px;\n line-height:1.5;word-wrap:break-word;white-space:pre-wrap}\n.td-msg.user{align-self:flex-end;\n background:color-mix(in oklch,var(--ax-accent) 75%,var(--ax-bg));\n color:var(--ax-bg);font-weight:500;border-bottom-right-radius:2px}\n.td-msg.agent{align-self:flex-start;background:var(--ax-surface);color:var(--ax-text);\n border:1px solid var(--ax-border);border-bottom-left-radius:2px}\n.td-msg.err{align-self:flex-start;background:color-mix(in oklch,var(--ax-err) 10%,transparent);\n color:var(--ax-err);border:1px solid color-mix(in oklch,var(--ax-err) 35%,transparent);\n border-bottom-left-radius:2px}\n.td-msg.thinking{align-self:flex-start;color:var(--ax-muted);font-style:italic;\n background:transparent;padding:4px 8px}\n.td-empty{color:var(--ax-muted);text-align:center;padding:40px 12px;font-style:italic;font-size:13px}\n.td-footer{border-top:1px solid var(--ax-border);padding:10px 12px;display:flex;\n gap:8px;align-items:flex-end;background:var(--ax-bg-elev)}\n.td-footer textarea{flex:1;background:var(--ax-bg);color:var(--ax-text);\n border:1px solid var(--ax-border);border-radius:4px;padding:8px 10px;\n font:inherit;font-size:13px;min-height:40px;max-height:120px;resize:none}\n.td-footer textarea:focus{outline:none;border-color:var(--ax-accent)}\n.td-footer button{background:color-mix(in oklch,var(--ax-accent) 20%,var(--ax-surface));\n color:var(--ax-accent);border:1px solid color-mix(in oklch,var(--ax-accent) 40%,var(--ax-border-2));\n border-radius:4px;padding:8px 16px;font-weight:500;font-size:13px;cursor:pointer}\n.td-footer button:hover{background:color-mix(in oklch,var(--ax-accent) 28%,var(--ax-surface))}\n.td-footer button:disabled{opacity:0.4;cursor:not-allowed}\n.td-hint{padding:6px 16px;font-size:10px;color:var(--ax-muted);\n border-top:1px solid var(--ax-border);background:var(--ax-bg-elev);font-family:var(--ax-mono)}\n.peer-banner{display:none;padding:10px 20px;background:color-mix(in oklch,var(--ax-accent) 14%,var(--ax-surface));\n border-bottom:1px solid color-mix(in oklch,var(--ax-accent) 40%,var(--ax-border-2));\n color:var(--ax-accent);font-size:12px;align-items:center;gap:12px;font-family:var(--ax-mono)}\n.peer-banner.is-active{display:flex}\n.peer-banner .label{text-transform:uppercase;letter-spacing:0.06em;font-size:10px;opacity:0.8}\n.peer-banner .name{font-weight:600;color:var(--ax-accent)}\n.peer-banner .spacer{flex:1}\n.peer-banner button{background:transparent;border:1px solid currentColor;color:var(--ax-accent);\n padding:4px 10px;border-radius:4px;font:inherit;font-size:11px;cursor:pointer}\n.peer-banner button:hover{background:color-mix(in oklch,var(--ax-accent) 12%,transparent)}\n`\nconst ADMIN_PAGE_SCRIPT = `\nconst $ = (id) => document.getElementById(id);\nlet state = null;\n\nfunction currentPeer() {\n try { return localStorage.getItem('ax-peer') || 'primary'; } catch { return 'primary'; }\n}\nfunction setCurrentPeer(id) {\n try {\n if (id && id !== 'primary') localStorage.setItem('ax-peer', id);\n else localStorage.removeItem('ax-peer');\n } catch { /**/ }\n}\n\nasync function req(method, url, body) {\n const opts = { method, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'agentx-admin' } };\n const peer = currentPeer();\n if (peer !== 'primary') opts.headers['X-Agentx-Peer'] = peer;\n // If this dashboard has a local token configured, inject it on every\n // request so the new /api/admin/* auth gate lets us through. Peer-proxy\n // requests still work because the server-side proxy swaps this header\n // for the peer's own token before forwarding.\n if (window.AX_LOCAL_TOKEN) opts.headers['Authorization'] = 'Bearer ' + window.AX_LOCAL_TOKEN;\n if (body !== undefined) opts.body = JSON.stringify(body);\n const r = await fetch(url, opts);\n const data = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));\n return data;\n}\n\nfunction showMsg(el, kind, text) {\n el.className = 'msg ' + kind;\n el.textContent = text;\n if (kind === 'ok') setTimeout(() => { el.className = 'msg'; el.textContent = ''; }, 4000);\n}\n\nasync function refresh() {\n try {\n state = await req('GET', '/api/admin/state');\n if (!state.exists) {\n showMsg($('global-msg'), 'warn', 'No agentx.json found. Run the setup wizard first.');\n return;\n }\n renderAgents();\n renderChannels();\n renderCrons();\n renderWebhooks();\n renderMesh();\n initScheduleBuilder();\n initScheduleExpertAgentSelect();\n } catch (e) {\n showMsg($('global-msg'), 'err', e.message);\n }\n}\n\nfunction renderWebhooks() {\n const list = $('webhook-list');\n const daemonUrl = (state.daemonUrl || '').replace(/\\\\/+$/, '');\n const sources = {\n gitlab: { logoBg: '#FC6D26', label: 'GitLab', init: 'GL', hint: 'In GitLab → Settings → Webhooks; tick Push/Issue/Pipeline events.' },\n github: { logoBg: '#24292e', label: 'GitHub', init: 'GH', hint: 'In GitHub → Repo Settings → Webhooks; pick individual events.' },\n sentry: { logoBg: '#362D59', label: 'Sentry', init: 'SE', hint: 'In Sentry → Project Settings → Alerts → Webhooks.' },\n stripe: { logoBg: '#635BFF', label: 'Stripe', init: 'ST', hint: 'In Stripe Dashboard → Developers → Webhooks.' },\n discord: { logoBg: '#5865F2', label: 'Discord', init: 'DC', hint: 'In Discord → Channel Settings → Integrations → Webhooks.' },\n slack: { logoBg: '#4A154B', label: 'Slack', init: 'SL', hint: 'Slack Outgoing Webhooks / Events API — point at the URL below.' },\n custom: { logoBg: 'var(--ax-surface-3)', label: 'Custom', init: '?', hint: 'Any service that can POST JSON — payload is forwarded as-is.' },\n };\n if (!state.webhooks.length) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:42px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><h3 style=\"margin:0 0 4px;color:var(--ax-text);font-size:15px;font-weight:600\">No webhooks yet</h3><p style=\"margin:0;font-size:13px\">Register one below and paste the URL into the external service.</p></div>';\n } else {\n list.innerHTML = '';\n for (const w of state.webhooks) {\n const meta = sources[w.source] || sources.custom;\n const url = daemonUrl + '/webhook/' + encodeURIComponent(w.agentId) + '/' + encodeURIComponent(w.source);\n const missingSecret = w.source !== 'custom' && !w.secretEnv;\n const statusPill = missingSecret\n ? '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>signing secret missing</span>'\n : (w.enabled\n ? '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>receiving</span>'\n : '<span class=\"ax-pill ax-pill--off\">disabled</span>');\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n if (missingSecret) card.style.borderColor = 'color-mix(in oklch, var(--ax-warn) 35%, var(--ax-border))';\n const logoStyle = 'background:' + meta.logoBg + ';color:white;border-color:' + meta.logoBg;\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar\" style=\"' + logoStyle + '\">' + meta.init + '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(w.id) + ' ' + statusPill + '</div>' +\n '<div class=\"ax-sub\">' +\n escapeHtml(meta.label) + ' · routes to <b>' + escapeHtml(w.agentId) + '</b>' +\n (w.secretEnv ? ' · secret: <code>${' + escapeHtml(w.secretEnv) + '}</code>' : '') +\n (w.description ? ' · ' + escapeHtml(w.description) : '') +\n '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n '<button class=\"ax-btn\" data-toggle-wh=\"' + escapeHtml(w.id) + '\" data-enabled=\"' + (w.enabled ? '1' : '0') + '\">' + (w.enabled ? 'Disable' : 'Enable') + '</button>' +\n '<button class=\"ax-btn ax-btn--danger\" data-rm-wh=\"' + escapeHtml(w.id) + '\">Delete</button>' +\n '</div>' +\n '</div>' +\n '<div style=\"display:flex;align-items:center;gap:8px;background:var(--ax-bg);border:1px solid var(--ax-border);border-radius:5px;padding:8px 10px;font-family:var(--ax-mono);font-size:11.5px;color:var(--ax-text);margin-top:10px\">' +\n '<span class=\"muted\">POST</span>' +\n '<span style=\"flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\">' + escapeHtml(url) + '</span>' +\n '<button class=\"ax-btn ax-btn--ghost\" style=\"padding:3px 9px;font-size:11px\" data-copy=\"' + escapeHtml(url) + '\">Copy</button>' +\n '</div>' +\n '<div style=\"font-size:11px;color:var(--ax-muted);margin-top:6px\">' + escapeHtml(meta.hint) + '</div>';\n\n card.querySelector('[data-copy]').addEventListener('click', (e) => {\n const u = e.currentTarget.dataset.copy;\n navigator.clipboard.writeText(u).catch(() => {});\n if (window.showToast) window.showToast('URL copied');\n });\n card.querySelector('[data-rm-wh]').addEventListener('click', () => deleteWebhookAction(w.id));\n card.querySelector('[data-toggle-wh]').addEventListener('click', () => toggleWebhook(w.id, !w.enabled));\n list.appendChild(card);\n }\n }\n // Refresh the add-webhook agent picker.\n const sel = $('w-agent');\n const cur = sel.value;\n sel.innerHTML = state.agents.map((a) => '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>').join('');\n if (cur) sel.value = cur;\n}\n\nasync function addWebhook() {\n const body = {\n id: $('w-id').value.trim(),\n source: $('w-source').value,\n agentId: $('w-agent').value,\n secretEnv: $('w-secret').value.trim(),\n description: $('w-desc').value.trim(),\n };\n try {\n const r = await req('POST', '/api/admin/webhooks', body);\n showMsg($('w-msg'), 'ok', r.summary);\n $('w-id').value = ''; $('w-secret').value = ''; $('w-desc').value = '';\n refresh();\n } catch (e) { showMsg($('w-msg'), 'err', e.message); }\n}\n\nasync function deleteWebhookAction(id) {\n if (!confirm('Delete webhook \"' + id + '\"?')) return;\n try { await req('DELETE', '/api/admin/webhooks', { id }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nasync function toggleWebhook(id, enabled) {\n try { await req('PATCH', '/api/admin/webhooks', { id, patch: { enabled } }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nfunction renderMesh() {\n const m = state.mesh || { enabled: false, peers: [] };\n\n // Hero card: status message + enable toggle + SVG network viz.\n const hero = $('mesh-hero');\n if (hero) {\n const count = m.peers.length;\n const nodeName = (state.node && state.node.name) || 'this machine';\n const viz = renderMeshViz(count);\n const toggleCls = m.enabled ? 'ax-mesh-toggle is-on' : 'ax-mesh-toggle';\n const msg = !m.enabled\n ? 'Mesh is off. Turn it on to share work between AgentX machines.'\n : (count === 0\n ? 'Mesh is on, but no peers yet. Add one below to link another machine.'\n : 'Your machine (<b>' + escapeHtml(nodeName) + '</b>) is connected to ' + count + ' peer' + (count === 1 ? '' : 's') + '. Work from any of them can land here — and vice versa.');\n hero.innerHTML =\n '<div>' +\n '<h3>' + (m.enabled ? (count === 0 ? 'Mesh is on' : 'Mesh is active') : 'Mesh is off') + '</h3>' +\n '<p>' + msg + '</p>' +\n '<div class=\"' + toggleCls + '\" onclick=\"toggleMesh(this)\">' +\n '<div class=\"ax-mesh-switch\"></div>' +\n '<span>Mesh networking ' + (m.enabled ? 'enabled' : 'disabled') + '</span>' +\n '</div>' +\n '</div>' +\n '<div class=\"ax-mesh-viz\">' + viz + '</div>';\n }\n\n const peers = $('mesh-peers');\n if (!m.peers.length) {\n peers.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:32px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><p style=\"margin:0;font-size:13px\">No peers yet. Add one below to get started.</p></div>';\n } else {\n peers.innerHTML = '';\n for (const p of m.peers) {\n const statusPill = p.hasToken\n ? '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>authenticated</span>'\n : '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>no token</span>';\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar ax-avatar--' + avatarVariant(p.url) + '\">' + escapeHtml(initialsOf(p.name, p.name)) + '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(p.name) + ' ' + statusPill + '</div>' +\n '<div class=\"ax-sub\"><span class=\"mono muted\">' + escapeHtml(p.url) + '</span></div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n '<button class=\"ax-btn ax-btn--danger\" data-rm-peer=\"' + escapeHtml(p.url) + '\">Remove</button>' +\n '</div>' +\n '</div>';\n card.querySelector('[data-rm-peer]').addEventListener('click', () => removeMeshPeer(p.url));\n peers.appendChild(card);\n }\n }\n}\n\n/** Tiny SVG showing the mesh topology with live dashed wires — YOU in the\n * middle, up to three peers around. Decorative; scales down on narrow\n * viewports via CSS. */\nfunction renderMeshViz(peerCount) {\n // Fixed layout: YOU centered, three peer slots. Extra peers collapse into \"+\".\n const slots = [\n { x: 18, y: 8, label: peerCount >= 1 ? 'A' : '+' },\n { x: 178, y: 8, label: peerCount >= 2 ? 'B' : '+' },\n { x: 18, y: 88, label: peerCount >= 3 ? '+' + (peerCount - 2) : '+' },\n ];\n let lines = '';\n if (peerCount >= 1) lines += '<line x1=\"120\" y1=\"70\" x2=\"40\" y2=\"30\" class=\"live\"/>';\n if (peerCount >= 2) lines += '<line x1=\"120\" y1=\"70\" x2=\"200\" y2=\"30\" class=\"live\"/>';\n if (peerCount >= 3) lines += '<line x1=\"120\" y1=\"70\" x2=\"40\" y2=\"110\" class=\"live\"/>';\n if (peerCount === 0) lines += '<line x1=\"120\" y1=\"70\" x2=\"40\" y2=\"30\"/><line x1=\"120\" y1=\"70\" x2=\"200\" y2=\"30\"/>';\n let dots = '<div class=\"ax-mesh-dot self\" style=\"left:98px;top:48px\">YOU</div>';\n for (const s of slots) {\n const cls = s.label === '+' || /^\\\\+\\\\d+$/.test(s.label) ? 'ax-mesh-dot empty' : 'ax-mesh-dot';\n dots += '<div class=\"' + cls + '\" style=\"left:' + s.x + 'px;top:' + s.y + 'px\">' + s.label + '</div>';\n }\n return '<svg class=\"ax-mesh-wires\" viewBox=\"0 0 240 140\">' + lines + '</svg>' + dots;\n}\n\nasync function toggleMesh(pill) {\n const want = !pill.classList.contains('is-on');\n try {\n await req('POST', '/api/admin/mesh/toggle', { enabled: want });\n refresh();\n } catch (err) { showMsg($('global-msg'), 'err', err.message); }\n}\n\n// Keep the legacy checkbox listener for the hidden input if any calls reach it\n// (the form below the hero still uses #m-url/#m-name/#m-token).\nconst __meshToggle = $('mesh-toggle');\nif (__meshToggle) {\n __meshToggle.addEventListener('change', async (e) => {\n try { await req('POST', '/api/admin/mesh/toggle', { enabled: e.target.checked }); refresh(); }\n catch (err) { showMsg($('global-msg'), 'err', err.message); e.target.checked = !e.target.checked; }\n });\n}\n\nasync function addMeshPeer() {\n const body = { url: $('m-url').value.trim(), name: $('m-name').value.trim(), token: $('m-token').value.trim() };\n try {\n const r = await req('POST', '/api/admin/mesh/peers', body);\n showMsg($('m-msg'), 'ok', r.summary);\n $('m-url').value = ''; $('m-name').value = ''; $('m-token').value = '';\n refresh();\n } catch (e) { showMsg($('m-msg'), 'err', e.message); }\n}\n\nasync function removeMeshPeer(url) {\n if (!confirm('Remove mesh peer at ' + url + '?')) return;\n try { await req('DELETE', '/api/admin/mesh/peers', { url }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nfunction initialsOf(name, id) {\n const src = (name || id || '?').replace(/[^a-zA-Z ]/g, '').trim();\n const parts = src.split(/\\\\s+/).filter(Boolean);\n if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();\n return (parts[0] || id || '?').slice(0, 2).toUpperCase();\n}\n\n/** Pick a stable avatar variant from the string hash. Gives each agent a\n * distinct-looking square without the operator having to configure one. */\nfunction avatarVariant(key) {\n const variants = ['teal', 'amber', 'blue', 'coral'];\n let h = 0;\n for (let i = 0; i < String(key).length; i++) h = (h * 31 + String(key).charCodeAt(i)) >>> 0;\n return variants[h % variants.length];\n}\n\nfunction renderAgents() {\n const list = $('agent-list');\n if (!state.agents.length) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:42px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><h3 style=\"margin:0 0 4px;color:var(--ax-text);font-size:15px;font-weight:600\">No agents yet</h3><p style=\"margin:0 0 16px;font-size:13px\">Click <b>+ New agent</b> below to add your first one.</p></div>';\n // still refresh cron agent picker even when the list is empty\n const selE = $('c-agent'); selE.innerHTML = '';\n return;\n }\n list.innerHTML = '';\n for (const a of state.agents) {\n const accessPill = a.access === 'public'\n ? '<span class=\"ax-pill ax-pill--info\"><span class=\"ax-pill__dot\"></span>public API</span>'\n : '<span class=\"ax-pill ax-pill--off\">private</span>';\n const triggers = (a.mentions || []).map(t =>\n '<span class=\"ax-trigger-pill\">' + escapeHtml(t) + '</span>'\n ).join('') || '<span class=\"muted\">—</span>';\n\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar ax-avatar--' + avatarVariant(a.id) + '\">' + escapeHtml(initialsOf(a.name, a.id)) + '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(a.name) + ' ' + accessPill + '</div>' +\n '<div class=\"ax-sub\">' +\n '<span class=\"mono\">' + escapeHtml(a.id) + '</span>' +\n (a.model ? ' · ' + escapeHtml(a.model) : '') +\n (a.tier ? ' · ' + escapeHtml(a.tier) : '') +\n '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n '<button class=\"ax-btn ax-btn--primary\" data-test=\"' + escapeHtml(a.id) + '\" data-name=\"' + escapeHtml(a.name) + '\">Test drive</button>' +\n '<a class=\"ax-btn\" href=\"/admin/agents/' + encodeURIComponent(a.id) + '\">Manage</a>' +\n '<button class=\"ax-btn ax-btn--ghost\" onclick=\"toggleCard(this)\">Details <svg class=\"ax-chev\" width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\"><path d=\"M2 4l3 3 3-3\"/></svg></button>' +\n '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__details\">' +\n '<dl class=\"ax-detail-grid\">' +\n '<div><dt>AI engine</dt><dd>' + escapeHtml(a.tier || '—') + (a.model ? ' · ' + escapeHtml(a.model) : '') + '</dd></div>' +\n '<div><dt>Max concurrent</dt><dd>' + escapeHtml(String(a.maxConcurrent ?? '—')) + '</dd></div>' +\n '<div style=\"grid-column:1/-1\"><dt>Trigger words</dt><dd><div class=\"ax-triggers\">' + triggers + '</div></dd></div>' +\n '</dl>' +\n (a.systemPrompt ? '<div><dt style=\"font-size:10px;letter-spacing:0.08em;text-transform:uppercase;color:var(--ax-muted);margin-bottom:4px;font-family:var(--ax-mono)\">Personality</dt><dd style=\"margin:0;color:var(--ax-text-2);line-height:1.6;font-size:12.5px;white-space:pre-wrap\">' + escapeHtml(a.systemPrompt) + '</dd></div>' : '') +\n '<div style=\"margin-top:14px;display:flex;gap:6px;flex-wrap:wrap\">' +\n '<button class=\"ax-btn\" data-edit=\"' + escapeHtml(a.id) + '\">Edit</button>' +\n '<button class=\"ax-btn\" data-files=\"' + escapeHtml(a.id) + '\" data-name=\"' + escapeHtml(a.name) + '\">Files</button>' +\n '<button class=\"ax-btn\" data-toggle=\"' + escapeHtml(a.id) + '\" data-access=\"' + escapeHtml(a.access) + '\">' + (a.access === 'public' ? 'Make private' : 'Make public') + '</button>' +\n '<div style=\"flex:1\"></div>' +\n '<button class=\"ax-btn ax-btn--danger\" data-delete=\"' + escapeHtml(a.id) + '\">Delete</button>' +\n '</div>' +\n '</div>';\n\n card.querySelector('[data-test]').addEventListener('click', (e) => {\n const btn = e.currentTarget;\n openTestDrive(btn.dataset.test, btn.dataset.name);\n });\n card.querySelector('[data-edit]').addEventListener('click', () => openAgentEdit(a));\n card.querySelector('[data-files]').addEventListener('click', (e) => {\n const btn = e.currentTarget;\n openFilesModal(btn.dataset.files, btn.dataset.name);\n });\n card.querySelector('[data-toggle]').addEventListener('click', (e) => {\n const btn = e.currentTarget;\n const want = btn.dataset.access === 'public' ? 'private' : 'public';\n setAgentAccess(btn.dataset.toggle, want);\n });\n card.querySelector('[data-delete]').addEventListener('click', () => deleteAgent(a.id));\n list.appendChild(card);\n }\n // Refresh the cron agent picker so new agents show up.\n const sel = $('c-agent');\n const current = sel.value;\n sel.innerHTML = state.agents.map((a) => '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>').join('');\n if (current) sel.value = current;\n}\n\nasync function addAgent() {\n const body = {\n id: $('a-id').value.trim(),\n name: $('a-name').value.trim(),\n triggerWords: $('a-triggers').value.trim(),\n tier: $('a-tier').value,\n model: $('a-model').value.trim() || undefined,\n personality: $('a-personality').value.trim() || undefined,\n access: $('a-public').checked ? 'public' : 'private',\n };\n try {\n const r = await req('POST', '/api/admin/agents', body);\n if (body.access === 'public') {\n await req('POST', '/api/admin/agents/access', { id: body.id, access: 'public' }).catch(() => {});\n }\n showMsg($('a-msg'), 'ok', r.summary || 'Added.');\n $('a-id').value = ''; $('a-name').value = ''; $('a-triggers').value = ''; $('a-personality').value = '';\n $('a-public').checked = false;\n refresh();\n } catch (e) { showMsg($('a-msg'), 'err', e.message); }\n}\n\nasync function setAgentAccess(id, access) {\n try {\n await req('POST', '/api/admin/agents/access', { id, access });\n refresh();\n } catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nasync function deleteAgent(id) {\n if (!confirm('Delete agent \"' + id + '\"? The config entry is removed; the workspace folder on disk is kept.')) return;\n try { await req('DELETE', '/api/admin/agents', { id }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nfunction renderChannels() {\n renderChannelsMenu();\n renderChannelsGrid();\n const t = state.telegram || { enabled: false, accounts: [] };\n const agentOptions = state.agents.map((a) => '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>').join('');\n const accountRows = t.accounts.length === 0\n ? '<div class=\"empty\">No Telegram accounts yet.</div>'\n : t.accounts.map((acc) =>\n '<div class=\"row-card\"><div class=\"info\"><h3>' + escapeHtml(acc.id) + '</h3>' +\n '<div class=\"meta\">' +\n '<span class=\"chip ' + (t.enabled ? '' : 'off') + '\">' + (t.enabled ? 'enabled' : 'disabled') + '</span>' +\n 'bot: <b>' + escapeHtml(acc.botUsername || '—') + '</b> · agent: <b>' + escapeHtml(acc.agentBinding || '—') + '</b> · token ref: <b>' + escapeHtml(acc.botTokenRef || '—') + '</b>' +\n '</div></div>' +\n '<button class=\"ghost\" data-tg-edit=\"' + escapeHtml(acc.id) + '\" style=\"margin-right:6px\">Edit</button>' +\n '<button class=\"danger\" data-id=\"' + escapeHtml(acc.id) + '\">Delete</button></div>').join('');\n $('tg-section').innerHTML =\n '<div class=\"toggle-switch\"><input type=\"checkbox\" id=\"tg-toggle\"' + (t.enabled ? ' checked' : '') + ' /><label for=\"tg-toggle\" style=\"color:var(--text);margin:0\">Telegram connector enabled</label></div>' +\n '<div class=\"list\">' + accountRows + '</div>' +\n '<div class=\"add-form\"><h3>Add a Telegram account</h3>' +\n '<div class=\"rowf\"><div><label>Account id<span class=\"hint\">(e.g. support)</span></label><input id=\"tg-id\" /></div>' +\n '<div><label>Bind to agent</label><select id=\"tg-agent\">' + agentOptions + '</select></div></div>' +\n '<label>Bot username<span class=\"hint\">(optional)</span></label><input id=\"tg-user\" placeholder=\"my_bot\" />' +\n '<label>Bot token env-var<span class=\"hint\">(name of the <code>.env</code> variable holding the token)</span></label><input id=\"tg-env\" placeholder=\"TG_SUPPORT_BOT_TOKEN\" />' +\n '<div class=\"actions\"><button class=\"primary\" onclick=\"addTelegram()\">Add account</button><div id=\"tg-msg\" class=\"msg\"></div></div>' +\n '<div class=\"hint-block\">We reference the token as <code>\\${TG_..._BOT_TOKEN}</code> in <code>agentx.json</code>. The actual value lives in <code>.env</code> — this panel never reads or transmits it.</div>' +\n '</div>';\n $('tg-toggle').addEventListener('change', async (e) => {\n try { await req('POST', '/api/admin/channels/telegram/toggle', { enabled: e.target.checked }); refresh(); }\n catch (err) { showMsg($('global-msg'), 'err', err.message); }\n });\n for (const btn of document.querySelectorAll('#tg-section button.danger')) {\n btn.addEventListener('click', () => deleteTelegram(btn.dataset.id));\n }\n for (const btn of document.querySelectorAll('#tg-section button[data-tg-edit]')) {\n btn.addEventListener('click', () => {\n const id = btn.dataset.tgEdit;\n const acc = (state.telegram.accounts || []).find((a) => a.id === id);\n if (acc) openTelegramEdit(acc);\n });\n }\n renderSlackSection();\n}\n\nconst CHANNEL_DEFS = [\n { id: 'telegram', icon: 'TG', label: 'Telegram' },\n { id: 'whatsapp', icon: 'WA', label: 'WhatsApp' },\n { id: '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\n/** Per-connector card metadata — logo color, long description, setup time.\n * Keeps renderChannelsGrid() focused on wiring rather than content. */\nconst CONNECTOR_META = {\n telegram: { group: 'chat', brand: '#229ED9', desc: 'Replies in Telegram DMs and groups when tagged. Paste your BotFather token; we keep the value in .env.' },\n whatsapp: { group: 'chat', brand: '#25D366', desc: 'Answer on WhatsApp via a local session. Scan a QR once; AgentX pairs as an extra device.' },\n slack: { group: 'chat', brand: '#4A154B', desc: 'Agents can DM or be mentioned in channels. Uses the Events API with socket mode.' },\n discord: { group: 'chat', brand: '#5865F2', desc: 'Works in servers, threads, and DMs. Create a Discord bot, grant Read/Send Messages, paste the token.' },\n gitlab: { group: 'dev', brand: '#FC6D26', desc: 'Pings your agent on MRs, issues, and pipeline events. Works with self-hosted too.' },\n github: { group: 'dev', brand: '#24292e', desc: 'Pings your agent on PRs, issues, and CI runs. Configure per-repo.' },\n};\n\nfunction renderChannelsGrid() {\n const chat = $('ch-chatapps');\n const dev = $('ch-devtools');\n if (!chat || !dev) return;\n chat.innerHTML = ''; dev.innerHTML = '';\n for (const def of CHANNEL_DEFS) {\n const meta = CONNECTOR_META[def.id] || { group: 'chat', brand: 'var(--ax-surface-3)', desc: '' };\n const status = channelStatus(def.id);\n const active = state.__activeChannel === def.id;\n const statusPill = status === 'on'\n ? '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>live</span>'\n : status === 'off'\n ? '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>off</span>'\n : '<span class=\"ax-pill ax-pill--off\">not set up</span>';\n const sub = channelSubtext(def.id, status);\n const card = document.createElement('div');\n card.className = 'ax-connector' + (status === 'on' ? ' is-on' : '') + (active ? ' is-active' : '');\n card.innerHTML =\n '<div class=\"ax-connector__top\">' +\n '<div class=\"ax-connector__logo\" style=\"background:' + meta.brand + ';color:#fff;border-color:' + meta.brand + '\">' + def.icon + '</div>' +\n '<div class=\"ax-connector__meta\">' +\n '<div class=\"ax-connector__name\">' + def.label + '</div>' +\n '<div class=\"ax-connector__sub\">' + sub + '</div>' +\n '</div>' +\n '<div class=\"ax-connector__status\">' + statusPill + '</div>' +\n '</div>' +\n '<div class=\"ax-connector__desc\">' + meta.desc + '</div>' +\n '<div class=\"ax-connector__foot\">' +\n '<span>' + (status === 'none' ? '~5 min setup' : 'Configured') + '</span>' +\n '<span class=\"ax-connector__cta\">' + (status === 'none' ? 'Set up' : 'Manage') + ' <svg viewBox=\"0 0 12 12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\"><path d=\"M4 2l4 4-4 4\"/></svg></span>' +\n '</div>';\n card.addEventListener('click', () => showChannelPane(def.id));\n (meta.group === 'chat' ? chat : dev).appendChild(card);\n }\n if (!state.__activeChannel) {\n // Default to telegram pane showing if nothing picked yet.\n showChannelPane('telegram');\n } else {\n showChannelPane(state.__activeChannel);\n }\n}\n\n/** Human-readable sub-text for the connector card under the name. */\nfunction channelSubtext(id, status) {\n if (id === 'telegram') {\n const t = state.telegram || {};\n const n = (t.accounts || []).length;\n if (n === 0) return 'Not set up';\n return n + ' bot' + (n === 1 ? '' : 's') + ' configured';\n }\n if (id === 'whatsapp') {\n const w = state.whatsapp || {};\n return w.routeCount ? w.routeCount + ' route' + (w.routeCount === 1 ? '' : 's') : 'Not set up';\n }\n if (id === 'slack') {\n const s = state.slack || {};\n return s.botTokenRef ? (s.workspace ? 'Workspace: ' + s.workspace : 'Token set') : 'Not set up';\n }\n if (id === 'discord') return status === 'none' ? 'Not set up' : 'Token set';\n if (id === 'gitlab') return status === 'none' ? 'Not set up' : 'Token set';\n if (id === 'github') {\n const n = ((state.webhooks || []).filter((w) => w.source === 'github')).length;\n return n ? n + ' webhook' + (n === 1 ? '' : 's') : 'Not set up';\n }\n return status === 'none' ? 'Not set up' : 'Configured';\n}\n\n/** Show the pane for the given channel id, hide the others, and highlight\n * the matching connector card. */\nfunction showChannelPane(id) {\n state.__activeChannel = id;\n const panes = ['telegram','whatsapp','slack','discord','gitlab','github'];\n for (const p of panes) {\n const el = document.getElementById('ch-' + p);\n if (el) el.hidden = (p !== id);\n }\n // Reflect active on connector cards\n const chat = $('ch-chatapps');\n const dev = $('ch-devtools');\n [chat, dev].forEach((container) => {\n if (!container) return;\n container.querySelectorAll('.ax-connector').forEach((c, i) => {\n const defs = CHANNEL_DEFS.filter(d => (CONNECTOR_META[d.id]?.group === (container === chat ? 'chat' : 'dev')));\n const def = defs[i];\n if (def) c.classList.toggle('is-active', def.id === id);\n });\n });\n // Lazy-render the panes whose content isn't filled by renderChannels(). TG\n // and Slack are populated unconditionally there; the rest need an explicit\n // call or their panes stay empty when switched via the connector card.\n if (id === 'discord') renderDiscordPane();\n else if (id === 'gitlab') renderGitLabPane();\n else if (id === 'github') renderGitHubPane();\n else if (id === 'whatsapp') renderWhatsAppPane();\n}\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’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’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’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) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:42px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><h3 style=\"margin:0 0 4px;color:var(--ax-text);font-size:15px;font-weight:600\">No schedules yet</h3><p style=\"margin:0;font-size:13px\">Create one below to nudge an agent on a timer.</p></div>';\n return;\n }\n list.innerHTML = '';\n for (const c of state.crons) {\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar ax-avatar--' + avatarVariant(c.id) + '\">' +\n '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>' +\n '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(c.id) + '</div>' +\n '<div class=\"ax-sub\">' +\n '<span class=\"ax-pill\"><span class=\"ax-pill__dot\" style=\"background:var(--ax-accent)\"></span>' + escapeHtml(c.schedule) + '</span>' +\n ' runs <b>' + escapeHtml(c.agent) + '</b>' +\n (c.prompt ? ' · ' + escapeHtml(c.prompt.slice(0, 80) + (c.prompt.length > 80 ? '…' : '')) : '') +\n '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n '<button class=\"ax-btn ax-btn--danger\" data-id=\"' + escapeHtml(c.id) + '\">Delete</button>' +\n '</div>' +\n '</div>';\n card.querySelector('[data-id]').addEventListener('click', () => deleteCron(c.id));\n list.appendChild(card);\n }\n}\n\nasync function addCron() {\n const body = {\n id: $('c-id').value.trim(),\n schedule: $('c-schedule').value.trim(),\n agent: $('c-agent').value,\n prompt: $('c-prompt').value.trim(),\n };\n try {\n const r = await req('POST', '/api/admin/crons', body);\n showMsg($('c-msg'), 'ok', r.summary || 'Added.');\n $('c-id').value = ''; $('c-schedule').value = ''; $('c-prompt').value = '';\n refresh();\n } catch (e) { showMsg($('c-msg'), 'err', e.message); }\n}\n\nasync function deleteCron(id) {\n if (!confirm('Delete schedule \"' + id + '\"?')) return;\n try { await req('DELETE', '/api/admin/crons', { id }); refresh(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\n/* ========== Schedule builder (Guided mode) ==========\n * Translates sentence-builder selections (agent/frequency/days/hour) into a\n * cron expression and populates the hidden #c-schedule input used by\n * addCron(). Also keeps the /preview endpoint happy with the natural-\n * language preview line. */\n\nlet __scheduleState = { agent: '', freq: 'weekly', days: [1], hour: 9 };\n\nfunction scheduleCronFromState() {\n const s = __scheduleState;\n if (s.freq === 'hourly') return '0 * * * *';\n if (s.freq === '15min') return '*/15 * * * *';\n if (s.freq === 'daily') return '0 ' + s.hour + ' * * *';\n if (s.freq === 'weekdays') return '0 ' + s.hour + ' * * 1-5';\n // weekly → days\n const dow = s.days.length ? s.days.slice().sort().join(',') : '1';\n return '0 ' + s.hour + ' * * ' + dow;\n}\n\nfunction schedulePreviewText() {\n const s = __scheduleState;\n const hh = ((s.hour + 11) % 12) + 1;\n const ap = s.hour < 12 ? 'am' : 'pm';\n const time = hh + ':00 ' + ap;\n if (s.freq === 'hourly') return 'Every hour on the hour';\n if (s.freq === '15min') return 'Every 15 minutes';\n if (s.freq === 'daily') return 'Every day at ' + time;\n if (s.freq === 'weekdays') return 'Weekdays at ' + time;\n const names = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];\n const pretty = s.days.slice().sort().map(d => names[d]).join(', ');\n return (pretty || 'No day picked') + ' at ' + time;\n}\n\nfunction updateSchedulePreview() {\n const cron = scheduleCronFromState();\n const $id = (x) => document.getElementById(x);\n const preview = $id('s-preview-text');\n if (preview) preview.innerHTML = '<b>' + schedulePreviewText() + '</b> · cron: <span class=\"mono\" style=\"color:var(--ax-text)\">' + cron + '</span>';\n const echo = $id('s-cron-echo');\n if (echo) echo.textContent = cron;\n // Keep the hidden #c-schedule + #c-agent in sync so addCron() reads them.\n const schedInp = $id('c-schedule');\n if (schedInp) schedInp.value = cron;\n const agentSel = $id('c-agent');\n if (agentSel) agentSel.value = __scheduleState.agent || (agentSel.options[0] && agentSel.options[0].value);\n}\n\nfunction initScheduleBuilder() {\n const freq = document.getElementById('s-freq');\n const hour = document.getElementById('s-hour');\n const agent = document.getElementById('s-agent');\n const sAt = document.getElementById('s-at');\n if (!freq || !hour || !agent) return;\n // Default active day = Monday\n const dayBtns = document.querySelectorAll('#s-days button');\n dayBtns.forEach((b) => {\n if (b.dataset.day === '1') b.classList.add('is-active');\n b.addEventListener('click', () => {\n b.classList.toggle('is-active');\n __scheduleState.days = [...document.querySelectorAll('#s-days button.is-active')]\n .map(el => parseInt(el.dataset.day, 10));\n updateSchedulePreview();\n });\n });\n // Populate agent select from state\n agent.innerHTML = state.agents.map((a) =>\n '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + '</option>'\n ).join('');\n if (agent.options.length) __scheduleState.agent = agent.options[0].value;\n\n // Mode switch\n document.querySelectorAll('#sched-mode button').forEach((b) => {\n b.addEventListener('click', () => {\n document.querySelectorAll('#sched-mode button').forEach((x) => x.classList.toggle('is-active', x === b));\n const simple = document.getElementById('sched-simple');\n const expert = document.getElementById('sched-expert');\n if (!simple || !expert) return;\n const mode = b.dataset.mode;\n simple.style.display = mode === 'simple' ? '' : 'none';\n expert.style.display = mode === 'expert' ? '' : 'none';\n });\n });\n\n // Hide day-picker + \"at\" unless frequency is weekly\n const reflectFreq = () => {\n const days = document.getElementById('s-days');\n const show = __scheduleState.freq === 'weekly';\n if (days) days.style.display = show ? '' : 'none';\n const alwaysTimeless = (__scheduleState.freq === 'hourly' || __scheduleState.freq === '15min');\n if (sAt) sAt.style.display = alwaysTimeless ? 'none' : '';\n hour.style.display = alwaysTimeless ? 'none' : '';\n };\n\n agent.addEventListener('change', () => { __scheduleState.agent = agent.value; updateSchedulePreview(); });\n freq.addEventListener('change', () => { __scheduleState.freq = freq.value; reflectFreq(); updateSchedulePreview(); });\n hour.addEventListener('change', () => {\n // \"9:00 am\" → 9, \"2:00 pm\" → 14\n const text = hour.value.trim().toLowerCase();\n const m = text.match(/^(\\\\d+):\\\\d+\\\\s*(am|pm)/);\n if (m) {\n let h = parseInt(m[1], 10);\n if (m[2] === 'pm' && h !== 12) h += 12;\n if (m[2] === 'am' && h === 12) h = 0;\n __scheduleState.hour = h;\n }\n updateSchedulePreview();\n });\n\n reflectFreq();\n updateSchedulePreview();\n}\n\nasync function addCronExpert() {\n // Expert mode has its own id/agent/prompt inputs; mirror them into\n // the canonical #c-* ids before dispatching.\n const $id = (x) => document.getElementById(x);\n $id('c-id').value = $id('c-id-expert').value.trim();\n const expertAgent = $id('c-agent-expert');\n if (expertAgent && expertAgent.value) {\n $id('c-agent').value = expertAgent.value;\n }\n $id('c-prompt').value = $id('c-prompt-expert').value.trim();\n await addCron();\n}\n\n// Expert-mode agent dropdown mirrors the guided one.\nfunction initScheduleExpertAgentSelect() {\n const sel = document.getElementById('c-agent-expert');\n if (!sel) return;\n sel.innerHTML = state.agents.map((a) =>\n '<option value=\"' + escapeHtml(a.id) + '\">' + escapeHtml(a.name) + ' (' + escapeHtml(a.id) + ')</option>'\n ).join('');\n}\n\n// Live cron preview — debounced so each keystroke doesn't hit the server.\nlet cronPreviewTimer = null;\nfunction scheduleCronPreview() {\n if (cronPreviewTimer) clearTimeout(cronPreviewTimer);\n cronPreviewTimer = setTimeout(runCronPreview, 300);\n}\nasync function runCronPreview() {\n const expr = $('c-schedule').value.trim();\n const box = $('c-preview');\n if (!expr) { box.textContent = ''; return; }\n try {\n const r = await req('POST', '/api/admin/crons/preview', { schedule: expr });\n const fires = r.next.map((iso) => new Date(iso).toLocaleString()).slice(0, 3);\n box.innerHTML =\n '<div style=\"color:var(--text)\"><b>' + escapeHtml(r.human) + '</b></div>' +\n '<div style=\"margin-top:4px\">Next runs (' + escapeHtml(r.timezone) + '): ' + fires.map(escapeHtml).join(' · ') + '</div>';\n } catch (e) {\n box.innerHTML = '<div style=\"color:var(--red)\">' + escapeHtml(e.message) + '</div>';\n }\n}\n$('c-schedule').addEventListener('input', scheduleCronPreview);\n\n// --- Files modal (identity + skills) ---\nconst filesModal = {\n el: $('files-modal'),\n agentId: null,\n agentName: null,\n currentTab: 'identity', // 'identity' | 'skills'\n currentPath: null, // path of file being edited\n originalContent: '', // for revert\n};\n\nasync function openFilesModal(agentId, agentName) {\n filesModal.agentId = agentId;\n filesModal.agentName = agentName;\n $('files-title').textContent = agentName + ' · Files';\n filesModal.el.classList.remove('hidden');\n filesModal.el.setAttribute('aria-hidden', 'false');\n switchFilesTab('identity');\n}\n\nfunction closeFilesModal() {\n filesModal.el.classList.add('hidden');\n filesModal.el.setAttribute('aria-hidden', 'true');\n filesModal.agentId = null;\n filesModal.currentPath = null;\n hideFileEditor();\n}\n\nfunction switchFilesTab(tab) {\n filesModal.currentTab = tab;\n filesModal.currentPath = null;\n $('files-tab-label').textContent = tab === 'skills' ? 'Skills' : 'Identity';\n $('files-tab-identity').style.background = tab === 'identity' ? 'rgba(99,102,241,0.15)' : 'transparent';\n $('files-tab-skills').style.background = tab === 'skills' ? 'rgba(99,102,241,0.15)' : 'transparent';\n $('files-add-skill').style.display = tab === 'skills' ? 'block' : 'none';\n hideFileEditor();\n loadFileList();\n}\n\nasync function loadFileList() {\n const picker = $('files-picker');\n picker.innerHTML = '<div class=\"empty\" style=\"padding:20px 10px;color:var(--muted)\">loading…</div>';\n try {\n const r = await req('GET', '/api/admin/files?agent=' + encodeURIComponent(filesModal.agentId));\n filesModal.overview = r;\n renderFilesPicker(r);\n } catch (e) {\n picker.innerHTML = '<div class=\"empty\" style=\"color:var(--red);padding:14px\">' + escapeHtml(e.message) + '</div>';\n }\n}\n\nfunction renderFilesPicker(overview) {\n const picker = $('files-picker');\n if (filesModal.currentTab === 'identity') {\n const rows = overview.identity.map((f) => {\n const activeCls = filesModal.currentPath === f.path ? ' active' : '';\n const missingCls = f.exists ? '' : ' missing';\n const sizeLabel = f.exists ? formatBytes(f.size) : 'create';\n return '<div class=\"file' + activeCls + missingCls + '\" data-path=\"' + escapeHtml(f.path) + '\">' +\n '<span class=\"info\">' + escapeHtml(f.title) + '</span>' +\n '<span class=\"size\">' + sizeLabel + '</span>' +\n '</div>';\n }).join('');\n picker.innerHTML = '<div class=\"group\">Identity files</div>' + (rows || '<div class=\"empty\">No files.</div>');\n } else {\n if (overview.skills.length === 0) {\n picker.innerHTML = '<div class=\"group\">Skills</div><div class=\"empty\" style=\"padding:14px;color:var(--muted)\">No skills yet.</div>';\n } else {\n const rows = overview.skills.map((s) => {\n const activeCls = filesModal.currentPath === s.path ? ' active' : '';\n return '<div class=\"file' + activeCls + '\" data-path=\"' + escapeHtml(s.path) + '\" data-slug=\"' + escapeHtml(s.slug) + '\">' +\n '<span class=\"info\">' + escapeHtml(s.title) + '<br><span class=\"size\">' + escapeHtml(s.slug) + '</span></span>' +\n '<button class=\"del\" data-rm-skill=\"' + escapeHtml(s.slug) + '\" title=\"Delete skill\">🗑</button>' +\n '</div>';\n }).join('');\n picker.innerHTML = '<div class=\"group\">Skills</div>' + rows;\n }\n }\n for (const row of picker.querySelectorAll('.file')) {\n row.addEventListener('click', (e) => {\n if (e.target.closest('[data-rm-skill]')) return;\n openFileInEditor(row.dataset.path);\n });\n }\n for (const btn of picker.querySelectorAll('[data-rm-skill]')) {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n removeSkill(btn.dataset.rmSkill);\n });\n }\n}\n\nfunction formatBytes(n) {\n if (!n) return '0 B';\n if (n < 1024) return n + ' B';\n if (n < 1024 * 1024) return (n / 1024).toFixed(1).replace(/\\\\.0$/, '') + ' KB';\n return (n / (1024 * 1024)).toFixed(2) + ' MB';\n}\n\nasync function openFileInEditor(path) {\n filesModal.currentPath = path;\n renderFilesPicker(filesModal.overview); // restyle active row\n try {\n const r = await req('GET', '/api/admin/files/read?agent=' + encodeURIComponent(filesModal.agentId) + '&path=' + encodeURIComponent(path));\n filesModal.originalContent = r.content;\n $('files-current-path').textContent = path;\n $('files-editor').value = r.content;\n $('files-editor-empty').style.display = 'none';\n $('files-editor-head').style.display = 'flex';\n $('files-editor-wrap').style.display = 'flex';\n $('files-save-msg').className = 'msg';\n } catch (e) { showMsg($('files-save-msg'), 'err', e.message); }\n}\n\nfunction hideFileEditor() {\n $('files-editor-empty').style.display = 'flex';\n $('files-editor-head').style.display = 'none';\n $('files-editor-wrap').style.display = 'none';\n $('files-save-msg').className = 'msg';\n $('files-editor').value = '';\n}\n\nasync function saveCurrentFile() {\n if (!filesModal.currentPath) return;\n const content = $('files-editor').value;\n try {\n const r = await req('PUT', '/api/admin/files', {\n agent: filesModal.agentId,\n path: filesModal.currentPath,\n content,\n });\n filesModal.originalContent = content;\n showMsg($('files-save-msg'), 'ok', r.summary);\n loadFileList();\n } catch (e) { showMsg($('files-save-msg'), 'err', e.message); }\n}\n\nfunction revertCurrentFile() {\n $('files-editor').value = filesModal.originalContent;\n $('files-save-msg').className = 'msg';\n}\n\nasync function addSkill() {\n const slug = $('skill-slug').value.trim();\n const title = $('skill-title').value.trim();\n const fileInput = $('skill-file');\n const file = fileInput && fileInput.files && fileInput.files[0];\n let content;\n if (file) {\n try { content = await readFileAsText(file); }\n catch (e) { showMsg($('skill-msg'), 'err', 'Could not read file: ' + e.message); return; }\n if (content.length > 200 * 1024) {\n showMsg($('skill-msg'), 'err', 'File too large (>200 KB). Edit in-place after creation instead.');\n return;\n }\n }\n try {\n const body = { agent: filesModal.agentId, slug, title };\n if (content != null) body.content = content;\n const r = await req('POST', '/api/admin/files/skill', body);\n showMsg($('skill-msg'), 'ok', r.summary);\n $('skill-slug').value = ''; $('skill-title').value = '';\n if (fileInput) fileInput.value = '';\n await loadFileList();\n openFileInEditor(r.path);\n } catch (e) { showMsg($('skill-msg'), 'err', e.message); }\n}\n\nfunction readFileAsText(file) {\n return new Promise((resolve, reject) => {\n const r = new FileReader();\n r.onload = () => resolve(String(r.result || ''));\n r.onerror = () => reject(r.error || new Error('read failed'));\n r.readAsText(file);\n });\n}\n\n// Upload-into-editor: replaces the current file's buffer with the uploaded text.\n// User still has to click Save for the write to land on disk, so accidents are reversible.\nfunction wireFilesUpload() {\n const input = $('files-upload');\n if (!input) return;\n input.addEventListener('change', async (e) => {\n const file = e.target.files && e.target.files[0];\n if (!file) return;\n try {\n const text = await readFileAsText(file);\n if (text.length > 200 * 1024) throw new Error('File too large (>200 KB).');\n $('files-editor').value = text;\n showMsg($('files-save-msg'), 'ok', 'Loaded into editor — click Save to persist.');\n } catch (err) {\n showMsg($('files-save-msg'), 'err', err.message);\n }\n e.target.value = '';\n });\n}\nwireFilesUpload();\n\nasync function removeSkill(slug) {\n if (!confirm('Delete skill \"' + slug + '\"? The skill folder and SKILL.md will be removed from disk.')) return;\n try {\n await req('DELETE', '/api/admin/files/skill', { agent: filesModal.agentId, slug });\n if (filesModal.currentPath && filesModal.currentPath.includes('/' + slug + '/')) hideFileEditor();\n loadFileList();\n } catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\n$('files-close').addEventListener('click', closeFilesModal);\n$('files-modal').querySelector('.td-backdrop').addEventListener('click', closeFilesModal);\n$('files-tab-identity').addEventListener('click', () => switchFilesTab('identity'));\n$('files-tab-skills').addEventListener('click', () => switchFilesTab('skills'));\n$('files-save').addEventListener('click', saveCurrentFile);\n$('files-revert').addEventListener('click', revertCurrentFile);\n$('files-editor').addEventListener('keydown', (e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveCurrentFile(); }\n});\ndocument.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && !filesModal.el.classList.contains('hidden')) closeFilesModal();\n});\n\n// --- Edit agent modal ---\nconst editModal = {\n el: $('edit-modal'),\n currentId: null,\n};\n\nfunction openAgentEdit(agent) {\n editModal.currentId = agent.id;\n $('edit-title').textContent = agent.name + ' · ' + agent.id;\n $('e-name').value = agent.name || '';\n $('e-tier').value = agent.tier || 'claude-code';\n $('e-model').value = agent.model || '';\n $('e-triggers').value = (agent.mentions || []).join(', ');\n $('e-personality').value = agent.systemPrompt || '';\n $('e-max-concurrent').value = agent.maxConcurrent || 1;\n $('e-max-exec').value = agent.maxExecutionMinutes || 20;\n $('e-perm').value = agent.permissionMode || 'default';\n $('e-access').checked = agent.access === 'public';\n $('e-msg').className = 'msg';\n editModal.el.classList.remove('hidden');\n editModal.el.setAttribute('aria-hidden', 'false');\n}\n\nfunction closeAgentEdit() {\n editModal.el.classList.add('hidden');\n editModal.el.setAttribute('aria-hidden', 'true');\n editModal.currentId = null;\n}\n\nasync function saveAgentEdit() {\n if (!editModal.currentId) return;\n const id = editModal.currentId;\n const maxConcurrent = parseInt($('e-max-concurrent').value, 10);\n const maxExecutionMinutes = parseInt($('e-max-exec').value, 10);\n const patch = {\n name: $('e-name').value.trim(),\n tier: $('e-tier').value,\n model: $('e-model').value.trim(),\n triggerWords: $('e-triggers').value.trim(),\n systemPrompt: $('e-personality').value,\n maxConcurrent: Number.isFinite(maxConcurrent) ? maxConcurrent : undefined,\n maxExecutionMinutes: Number.isFinite(maxExecutionMinutes) ? maxExecutionMinutes : undefined,\n permissionMode: $('e-perm').value,\n access: $('e-access').checked ? 'public' : 'private',\n };\n try {\n await req('PATCH', '/api/admin/agents', { id, patch });\n showMsg($('e-msg'), 'ok', 'Saved. The change is live (hot-reloaded).');\n setTimeout(() => { closeAgentEdit(); refresh(); }, 600);\n } catch (e) { showMsg($('e-msg'), 'err', e.message); }\n}\n\n// Telegram edit wiring\n$('tg-edit-close').addEventListener('click', closeTelegramEdit);\n$('tg-edit-cancel').addEventListener('click', closeTelegramEdit);\n$('tg-edit-save').addEventListener('click', saveTelegramEdit);\n$('tg-edit-modal').querySelector('.td-backdrop').addEventListener('click', closeTelegramEdit);\n\n$('edit-close').addEventListener('click', closeAgentEdit);\n$('edit-cancel').addEventListener('click', closeAgentEdit);\n$('edit-save').addEventListener('click', saveAgentEdit);\neditModal.el.querySelector('.td-backdrop').addEventListener('click', closeAgentEdit);\ndocument.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && !editModal.el.classList.contains('hidden')) closeAgentEdit();\n});\n\n// --- Test drive chat modal ---\nconst testDrive = {\n modal: $('td-modal'),\n body: $('td-body'),\n title: $('td-title'),\n input: $('td-input'),\n sendBtn: $('td-send'),\n hint: $('td-hint'),\n closeBtn: $('td-close'),\n agentId: null,\n chatId: null, // stable per modal open so follow-ups share session context\n thinkingEl: null,\n busy: false,\n};\n\nfunction openTestDrive(agentId, agentName) {\n testDrive.agentId = agentId;\n testDrive.chatId = 'admin-' + Date.now();\n testDrive.title.textContent = agentName + ' · ' + agentId;\n testDrive.body.innerHTML = '<div class=\"td-empty\">Send a message to sanity-check this agent before wiring it to a real channel.</div>';\n testDrive.hint.textContent = 'Session: ' + testDrive.chatId + ' · channel: test-drive';\n testDrive.input.value = '';\n testDrive.modal.classList.remove('hidden');\n testDrive.modal.setAttribute('aria-hidden', 'false');\n setTimeout(() => testDrive.input.focus(), 50);\n}\n\nfunction closeTestDrive() {\n testDrive.modal.classList.add('hidden');\n testDrive.modal.setAttribute('aria-hidden', 'true');\n testDrive.agentId = null;\n testDrive.chatId = null;\n}\n\nfunction appendChat(kind, text) {\n const empty = testDrive.body.querySelector('.td-empty');\n if (empty) empty.remove();\n const el = document.createElement('div');\n el.className = 'td-msg ' + kind;\n el.textContent = text;\n testDrive.body.appendChild(el);\n testDrive.body.scrollTop = testDrive.body.scrollHeight;\n return el;\n}\n\nasync function sendTestDrive() {\n if (testDrive.busy) return;\n const msg = testDrive.input.value.trim();\n if (!msg || !testDrive.agentId) return;\n appendChat('user', msg);\n testDrive.input.value = '';\n testDrive.input.style.height = 'auto';\n testDrive.thinkingEl = appendChat('thinking', 'thinking…');\n testDrive.busy = true;\n testDrive.sendBtn.disabled = true;\n try {\n const r = await req('POST', '/api/admin/agents/test', {\n agent: testDrive.agentId,\n message: msg,\n chatId: testDrive.chatId,\n });\n testDrive.thinkingEl?.remove();\n testDrive.thinkingEl = null;\n const reply = r?.response?.content || r?.response?.error || '(empty reply)';\n appendChat(r?.response?.error ? 'err' : 'agent', reply);\n } catch (e) {\n testDrive.thinkingEl?.remove();\n testDrive.thinkingEl = null;\n appendChat('err', e.message);\n } finally {\n testDrive.busy = false;\n testDrive.sendBtn.disabled = false;\n testDrive.input.focus();\n }\n}\n\ntestDrive.sendBtn.addEventListener('click', sendTestDrive);\ntestDrive.closeBtn.addEventListener('click', closeTestDrive);\ntestDrive.modal.querySelector('.td-backdrop').addEventListener('click', closeTestDrive);\ntestDrive.input.addEventListener('keydown', (e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); sendTestDrive(); }\n});\ntestDrive.input.addEventListener('input', () => {\n // Autogrow the textarea up to 120px.\n testDrive.input.style.height = 'auto';\n testDrive.input.style.height = Math.min(testDrive.input.scrollHeight, 120) + 'px';\n});\ndocument.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && !testDrive.modal.classList.contains('hidden')) closeTestDrive();\n});\n\n// --- Tokens tab ---\nconst SCOPE_CHOICES = [\n { value: 'dashboard:read', label: 'Read dashboard + live view' },\n { value: 'dashboard:write', label: 'Write (admin + config) — full control' },\n { value: 'agent:*', label: 'Message any public agent' },\n { value: 'mesh:peer', label: 'Mesh peer auth (cross-node)' },\n];\n\nfunction renderScopeChoices() {\n const publicAgents = state.agents.filter((a) => a.access === 'public');\n const agentScopes = publicAgents.map((a) => ({ value: 'agent:' + a.id, label: 'Message agent: ' + a.name + ' (' + a.id + ')' }));\n const all = SCOPE_CHOICES.concat(agentScopes);\n const host = $('t-scopes');\n host.innerHTML = all.map((c) => {\n return '<label style=\"display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text);font-weight:400;margin:0\">' +\n '<input type=\"checkbox\" value=\"' + escapeHtml(c.value) + '\" style=\"width:auto\" /> ' +\n '<code style=\"background:#0e1119;padding:1px 6px;border-radius:3px;font-family:ui-monospace,monospace;font-size:11px;color:var(--accent)\">' + escapeHtml(c.value) + '</code>' +\n '<span style=\"color:var(--muted)\">— ' + escapeHtml(c.label) + '</span>' +\n '</label>';\n }).join('');\n}\n\nasync function loadTokens() {\n renderScopeChoices();\n try {\n const tokens = await req('GET', '/api/admin/tokens');\n const list = $('token-list');\n if (!tokens.length) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:42px 22px;background:var(--ax-surface);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><h3 style=\"margin:0 0 4px;color:var(--ax-text);font-size:15px;font-weight:600\">No tokens yet</h3><p style=\"margin:0;font-size:13px\">Create one below to let a script, a chat bridge, or another AgentX machine talk to this one.</p></div>';\n return;\n }\n list.innerHTML = '';\n for (const t of tokens) {\n const now = Date.now();\n const expired = t.expiresAt && Date.parse(t.expiresAt) < now;\n const stale = t.lastUsedAt && (now - Date.parse(t.lastUsedAt)) > 30 * 24 * 60 * 60 * 1000 && !t.revokedAt;\n const statusPill = t.revokedAt\n ? '<span class=\"ax-pill ax-pill--off\">revoked</span>'\n : (expired\n ? '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>expired</span>'\n : (stale\n ? '<span class=\"ax-pill ax-pill--warn\"><span class=\"ax-pill__dot\"></span>stale</span>'\n : '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>active</span>'));\n const sub = [\n (t.scopes && t.scopes.length) ? 'scopes: ' + t.scopes.map(escapeHtml).join(', ') : 'no scopes',\n t.lastUsedAt ? 'last used ' + new Date(t.lastUsedAt).toLocaleString() : 'never used',\n t.expiresAt ? 'expires ' + new Date(t.expiresAt).toLocaleDateString() : 'no expiry',\n ].join(' · ');\n const avClass = t.revokedAt ? '' : (stale || expired ? 'ax-avatar--amber' : 'ax-avatar--teal');\n const card = document.createElement('div');\n card.className = 'ax-row-card';\n card.innerHTML =\n '<div class=\"ax-row-card__top\">' +\n '<div class=\"ax-avatar ' + avClass + '\">' +\n '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"8\" cy=\"15\" r=\"4\"/><path d=\"M10.85 12.15L19 4M18 5l2 2M15 8l2 2\"/></svg>' +\n '</div>' +\n '<div class=\"ax-row-card__info\">' +\n '<div class=\"ax-name\">' + escapeHtml(t.name) + ' ' + statusPill + '</div>' +\n '<div class=\"ax-sub\"><span class=\"mono\">' + escapeHtml(t.prefix) + '…</span> · ' + sub + '</div>' +\n '</div>' +\n '<div class=\"ax-row-card__actions\">' +\n (t.revokedAt ? '' : '<button class=\"ax-btn ax-btn--danger\" data-revoke=\"' + escapeHtml(t.id) + '\">Revoke</button>') +\n '</div>' +\n '</div>';\n const del = card.querySelector('[data-revoke]');\n if (del) del.addEventListener('click', () => revokeToken(t.id));\n list.appendChild(card);\n }\n } catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\nasync function createToken() {\n const name = $('t-name').value.trim();\n const scopes = [...document.querySelectorAll('#t-scopes input:checked')].map((el) => el.value);\n const expires = $('t-expires').value.trim();\n try {\n const body = { name, scopes, expiresInDays: expires ? parseInt(expires, 10) : undefined };\n const r = await req('POST', '/api/admin/tokens', body);\n $('t-secret').textContent = r.token;\n $('t-reveal').style.display = 'block';\n showMsg($('t-msg'), 'ok', r.summary);\n $('t-name').value = ''; $('t-expires').value = '';\n document.querySelectorAll('#t-scopes input:checked').forEach((el) => { el.checked = false; });\n loadTokens();\n } catch (e) { showMsg($('t-msg'), 'err', e.message); }\n}\n\nasync function revokeToken(id) {\n if (!confirm('Revoke this token? Any callers using it will immediately fail.')) return;\n try { await req('DELETE', '/api/admin/tokens', { id }); loadTokens(); }\n catch (e) { showMsg($('global-msg'), 'err', e.message); }\n}\n\n/* ========== Advanced tab: JSON tree viewer ==========\n *\n * Three modes:\n * tree — collapsible key/value tree with search + inline hints\n * raw — pretty-printed read-only JSON\n * edit — writable textarea (same behavior as the legacy Advanced tab)\n *\n * All three populate from the SAME /api/admin/config payload. When saving,\n * we send the text back to /api/admin/config/raw (only \"edit\" mode has a\n * writable textarea; tree and raw are read-only). */\n\nconst JV_HINTS = {\n 'node.name': 'friendly name of this daemon',\n 'node.id': 'stable identifier, used in logs',\n 'agents[].id': 'lowercase handle, used in logs',\n 'agents[].tier': 'which AI engine runs this agent (claude-code / sdk / orchestrator)',\n 'agents[].mentions': 'trigger words that wake this agent',\n 'channels.telegram.accounts[].botToken': 'reference to an env-var, not the token itself',\n 'channels.telegram.accounts[].agentBinding': 'which agent answers messages to this bot',\n 'crons[].schedule': 'standard five-field cron',\n 'mesh.peers[].url': 'full URL including port',\n 'tokens[].scopes': 'permissions this token grants',\n};\n\nlet __jvMode = 'tree';\nlet __jvRawConfig = null;\n\nfunction renderJvLine(value, path, keyLabel, parent) {\n const isArr = Array.isArray(value);\n const isObj = value && typeof value === 'object' && !isArr;\n const line = document.createElement('div');\n line.className = 'ax-jv-line';\n line.dataset.path = path;\n\n const tog = document.createElement('span');\n tog.className = 'ax-jv-toggle';\n const empty = (isArr ? value.length === 0 : (isObj ? Object.keys(value).length === 0 : true));\n if ((isArr || isObj) && !empty) {\n tog.textContent = '▾';\n tog.addEventListener('click', () => {\n const kids = line.nextElementSibling;\n if (kids && kids.classList.contains('ax-jv-children')) {\n const willCollapse = !kids.classList.contains('is-collapsed');\n kids.classList.toggle('is-collapsed');\n tog.textContent = willCollapse ? '▸' : '▾';\n }\n });\n } else {\n tog.className += ' empty';\n }\n line.appendChild(tog);\n\n if (keyLabel !== null) {\n const k = document.createElement('span');\n k.className = 'ax-jv-key';\n k.textContent = '\"' + keyLabel + '\"';\n line.appendChild(k);\n const colon = document.createElement('span');\n colon.className = 'ax-jv-punc';\n colon.textContent = ': ';\n line.appendChild(colon);\n }\n\n if (isArr || isObj) {\n const open = document.createElement('span');\n open.className = 'ax-jv-punc';\n open.textContent = isArr ? '[' : '{';\n line.appendChild(open);\n if (empty) {\n const close = document.createElement('span');\n close.className = 'ax-jv-empty-brace';\n close.textContent = isArr ? ']' : '}';\n line.appendChild(close);\n }\n } else if (value === null) {\n const v = document.createElement('span'); v.className = 'ax-jv-null'; v.textContent = 'null'; line.appendChild(v);\n } else if (typeof value === 'boolean') {\n const v = document.createElement('span'); v.className = 'ax-jv-bool'; v.textContent = String(value); line.appendChild(v);\n } else if (typeof value === 'number') {\n const v = document.createElement('span'); v.className = 'ax-jv-number'; v.textContent = String(value); line.appendChild(v);\n } else {\n const v = document.createElement('span'); v.className = 'ax-jv-string'; v.textContent = '\"' + String(value) + '\"'; line.appendChild(v);\n }\n\n const hintKey = path.replace(/\\\\[\\\\d+\\\\]/g, '[]');\n const hint = JV_HINTS[hintKey];\n if (hint) {\n const c = document.createElement('span');\n c.className = 'ax-jv-comment';\n c.textContent = '// ' + hint;\n line.appendChild(c);\n }\n\n parent.appendChild(line);\n\n if ((isArr || isObj) && !empty) {\n const wrap = document.createElement('div');\n wrap.className = 'ax-jv-children';\n const entries = isArr ? value.map((v, i) => [i, v]) : Object.entries(value);\n entries.forEach(([k, v]) => {\n const childPath = isArr ? path + '[' + k + ']' : (path ? path + '.' + k : String(k));\n renderJvLine(v, childPath, isArr ? null : k, wrap);\n });\n parent.appendChild(wrap);\n const close = document.createElement('div');\n close.className = 'ax-jv-line';\n const spacer = document.createElement('span');\n spacer.className = 'ax-jv-toggle empty';\n close.appendChild(spacer);\n const b = document.createElement('span');\n b.className = 'ax-jv-punc';\n b.textContent = isArr ? ']' : '}';\n close.appendChild(b);\n parent.appendChild(close);\n }\n}\n\nfunction renderJvTree(cfg) {\n const root = document.getElementById('jv-viewer');\n if (!root) return;\n root.innerHTML = '';\n root.style.whiteSpace = '';\n renderJvLine(cfg, '', null, root);\n}\n\nfunction renderJvRaw(cfg) {\n const root = document.getElementById('jv-viewer');\n if (!root) return;\n root.innerHTML = '<pre style=\"margin:0;font:inherit;color:var(--ax-text-2);white-space:pre-wrap\">' +\n escapeHtml(JSON.stringify(cfg, null, 2)) + '</pre>';\n}\n\nfunction renderJvEdit(cfg) {\n const root = document.getElementById('jv-viewer');\n const ta = document.getElementById('raw-editor');\n if (!root || !ta) return;\n root.innerHTML = '';\n ta.value = JSON.stringify(cfg, null, 2);\n ta.style.cssText = 'display:block;width:100%;min-height:500px;background:var(--ax-bg-elev);color:var(--ax-text);border:1px solid var(--ax-border);border-top:0;border-radius:0 0 var(--ax-radius) var(--ax-radius);font:inherit;font-family:var(--ax-mono);font-size:12.5px;padding:14px 18px;resize:vertical;outline:none';\n root.style.display = 'none';\n}\n\nfunction applyJvMode(mode) {\n __jvMode = mode;\n const root = document.getElementById('jv-viewer');\n const ta = document.getElementById('raw-editor');\n if (!root || !ta) return;\n if (mode !== 'edit') { root.style.display = ''; ta.style.display = 'none'; }\n if (mode === 'tree') renderJvTree(__jvRawConfig);\n if (mode === 'raw') renderJvRaw(__jvRawConfig);\n if (mode === 'edit') renderJvEdit(__jvRawConfig);\n document.querySelectorAll('#jv-mode button').forEach((b) => {\n b.classList.toggle('is-active', b.dataset.jvMode === mode);\n });\n}\n\nasync function loadRaw() {\n try {\n const r = await fetch('/api/admin/config');\n const txt = await r.text();\n // Try parsing; if it fails, fall back to edit mode with the raw text.\n try {\n __jvRawConfig = JSON.parse(txt);\n applyJvMode(__jvMode);\n const meta = document.getElementById('jv-meta');\n if (meta) {\n const kb = (txt.length / 1024).toFixed(1);\n meta.textContent = 'agentx.json · ' + kb + ' KB';\n }\n showMsg($('r-msg'), 'ok', 'Loaded.');\n } catch (pe) {\n __jvRawConfig = null;\n applyJvMode('edit');\n $('raw-editor').value = txt;\n showMsg($('r-msg'), 'err', 'Config is not valid JSON — open Edit mode to fix it.');\n }\n } catch (e) { showMsg($('r-msg'), 'err', e.message); }\n}\n\nasync function saveRaw() {\n // Save from the editor textarea if in edit mode; otherwise serialize the\n // in-memory config (useful after users read the Tree view and just click\n // Save to re-validate).\n const raw = __jvMode === 'edit' ? $('raw-editor').value : JSON.stringify(__jvRawConfig, null, 2);\n try {\n const r = await req('POST', '/api/admin/config/raw', { raw });\n showMsg($('r-msg'), 'ok', (r.summary || 'Saved.') + (r.backupPath ? ' (backup: ' + r.backupPath.split('/').pop() + ')' : ''));\n refresh();\n loadRaw();\n } catch (e) { showMsg($('r-msg'), 'err', e.message); }\n}\n\nfunction initJvControls() {\n const modeWrap = document.getElementById('jv-mode');\n if (modeWrap) {\n modeWrap.querySelectorAll('button').forEach((b) => {\n b.addEventListener('click', () => applyJvMode(b.dataset.jvMode));\n });\n }\n const search = document.getElementById('jv-search');\n if (search) {\n search.addEventListener('input', (e) => {\n const q = String(e.target.value || '').trim().toLowerCase();\n document.querySelectorAll('.ax-jv-line').forEach((line) => {\n const text = line.textContent.toLowerCase();\n line.classList.toggle('is-match', !!q && text.includes(q));\n });\n if (q) {\n document.querySelectorAll('.ax-jv-children.is-collapsed').forEach((c) => {\n if (c.textContent.toLowerCase().includes(q)) {\n c.classList.remove('is-collapsed');\n const sib = c.previousElementSibling;\n const tog = sib && sib.querySelector('.ax-jv-toggle:not(.empty)');\n if (tog) tog.textContent = '▾';\n }\n });\n }\n });\n }\n const expand = document.getElementById('jv-expand-all');\n if (expand) expand.addEventListener('click', () => {\n document.querySelectorAll('.ax-jv-children').forEach((c) => c.classList.remove('is-collapsed'));\n document.querySelectorAll('.ax-jv-toggle').forEach((t) => { if (!t.classList.contains('empty')) t.textContent = '▾'; });\n });\n const collapse = document.getElementById('jv-collapse-all');\n if (collapse) collapse.addEventListener('click', () => {\n document.querySelectorAll('.ax-jv-children').forEach((c) => c.classList.add('is-collapsed'));\n document.querySelectorAll('.ax-jv-toggle').forEach((t) => { if (!t.classList.contains('empty')) t.textContent = '▸'; });\n });\n const save = document.getElementById('jv-save');\n if (save) save.addEventListener('click', () => saveRaw());\n}\n// Wire up once on script load (doesn't depend on /api/admin/state).\ninitJvControls();\n\nfunction escapeHtml(s) {\n return String(s || '').replace(/[&<>\"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[c]));\n}\n\n// Tab switching\nfor (const btn of document.querySelectorAll('nav.tabs button')) {\n btn.addEventListener('click', () => {\n document.querySelectorAll('nav.tabs button').forEach((b) => b.classList.remove('active'));\n document.querySelectorAll('section.tab').forEach((s) => s.classList.remove('active'));\n btn.classList.add('active');\n $('tab-' + btn.dataset.tab).classList.add('active');\n if (btn.dataset.tab === 'advanced') loadRaw();\n if (btn.dataset.tab === 'tokens') loadTokens();\n });\n}\n\n// --- Peer-proxy banner: shown when admin writes target a non-primary peer ---\n(function(){\n var cur = currentPeer();\n if (cur === 'primary') return;\n // Try to resolve a friendly name from the topbar menu (server-rendered).\n var friendly = cur;\n var picked = document.querySelector('.ax-mesh-menu a[data-peer-id=\"' + CSS.escape(cur) + '\"]');\n if (picked) {\n var span = picked.querySelector('.row > span:last-child');\n if (span) friendly = span.textContent.replace(/ · primary/,'').trim();\n }\n $('peer-banner-name').textContent = friendly;\n $('peer-banner').classList.add('is-active');\n $('peer-banner-back').addEventListener('click', function(){\n setCurrentPeer('primary');\n location.reload();\n });\n})();\n\nrefresh();\n`\n","import type { IncomingMessage, ServerResponse } from \"http\"\nimport { resolve } from \"path\"\nimport { loadDaemonConfig } from \"./config\"\nimport { GraphStore, type GraphNode, type Classification } from \"@/graph\"\nimport type { TopbarPeer } from \"./topbar\"\nimport { renderGraphPage } from \"./ui/pages/graph\"\n\n// --- /admin/graph panel: Intent Knowledge Graph management ---\n//\n// Three surfaces:\n// 1. Pending queue — LLM-proposed classifications awaiting human approval.\n// Approve / reject / edit-path inline.\n// 2. Tree view — current nodes grouped by level, editable axes.\n// 3. Schema — raw JSON editor for the level definitions (advanced).\n//\n// Matches admin-panel.ts in chrome + form-driven style. All persistence goes\n// through GraphStore so the disk layout stays canonical.\n\nexport function handleGraphGet(\n _req: IncomingMessage,\n res: ServerResponse,\n peers: TopbarPeer[] = [],\n localToken?: string,\n): void {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderGraphPage({ peers, localToken }))\n}\n\nexport async function handleGraphApi(\n req: IncomingMessage,\n res: ServerResponse,\n path: string,\n): Promise<void> {\n try {\n const body = req.method === \"GET\" ? undefined : await readJsonBody(req)\n const store = openStore()\n const dispatch: Record<string, () => unknown> = {\n \"GET /api/admin/graph/state\": () => getGraphState(store),\n \"POST /api/admin/graph/classifications/approve\": () => approveClassification(store, body),\n \"POST /api/admin/graph/classifications/reject\": () => rejectClassification(store, body),\n \"POST /api/admin/graph/nodes\": () => createNode(store, body),\n \"PATCH /api/admin/graph/nodes\": () => updateNode(store, body),\n \"DELETE /api/admin/graph/nodes\": () => deleteNode(store, body),\n \"PUT /api/admin/graph/schema\": () => saveSchema(store, body),\n }\n const key = `${req.method} ${path}`\n const handler = dispatch[key]\n if (!handler) {\n sendJson(res, 404, { error: `unknown graph endpoint: ${key}` })\n return\n }\n const result = await handler()\n sendJson(res, 200, result)\n } catch (e: any) {\n sendJson(res, 400, { error: e?.message || \"graph op failed\" })\n }\n}\n\n// ========================================================================\n// Store open — reads graph.baseDir out of agentx.json each call (cheap).\n// ========================================================================\n\nfunction openStore(): GraphStore {\n const cfg = loadDaemonConfig()\n const baseDir = resolve(process.cwd(), cfg.graph.baseDir)\n return new GraphStore({ baseDir })\n}\n\n// ========================================================================\n// Read side\n// ========================================================================\n\nfunction getGraphState(store: GraphStore) {\n const cfg = loadDaemonConfig()\n const schema = store.loadSchema()\n const nodes = store.loadNodes().nodes\n const recent = store.readRecentClassifications(100)\n const index = store.loadIndex().entries\n\n // A classification is \"pending\" if its fingerprint hasn't landed in the\n // snap-to-path index yet AND its latest log row isn't rejected. We scan the\n // recent log newest-first so later entries (approved/rejected) win over\n // earlier pending ones for the same msgHash.\n const seen = new Map<string, Classification>()\n for (const c of recent) {\n if (!seen.has(c.msgHash)) seen.set(c.msgHash, c)\n }\n const pending: Classification[] = []\n for (const c of seen.values()) {\n if (c.status === \"pending\" && !index[c.msgHash]) pending.push(c)\n }\n\n return {\n enabled: cfg.graph.enabled,\n schema,\n nodes,\n pending,\n recent: recent.slice(0, 25),\n counts: {\n nodes: nodes.length,\n pending: pending.length,\n fingerprints: Object.keys(index).length,\n },\n }\n}\n\n// ========================================================================\n// Mutations\n// ========================================================================\n\nfunction approveClassification(store: GraphStore, body: any) {\n const msgHash = String(body?.msgHash || \"\").trim()\n if (!msgHash) throw new Error(\"msgHash is required\")\n const c = findPending(store, msgHash)\n // Allow the UI to override the path / axes before approving.\n if (Array.isArray(body?.path) && body.path.length > 0) {\n c.path = body.path.map((s: any) => String(s))\n }\n if (body?.proposedAxes && typeof body.proposedAxes === \"object\") {\n c.proposedAxes = sanitizeAxes(body.proposedAxes)\n }\n store.approveClassification(c)\n return { summary: `Approved ${c.msgHash}` }\n}\n\nfunction rejectClassification(store: GraphStore, body: any) {\n const msgHash = String(body?.msgHash || \"\").trim()\n if (!msgHash) throw new Error(\"msgHash is required\")\n const c = findPending(store, msgHash)\n store.rejectClassification(c)\n return { summary: `Rejected ${c.msgHash}` }\n}\n\nfunction findPending(store: GraphStore, msgHash: string): Classification {\n const recent = store.readRecentClassifications(500)\n const hit = recent.find((c) => c.msgHash === msgHash)\n if (!hit) throw new Error(`No classification found for ${msgHash}`)\n return hit\n}\n\nfunction createNode(store: GraphStore, body: any): { node: GraphNode } {\n const id = slug(String(body?.id || \"\"))\n const level = String(body?.level || \"\").trim()\n const parentId = body?.parentId ? slug(String(body.parentId)) : null\n const axes = sanitizeAxes({ [id]: body?.axes || {} })[id] || {}\n if (!id) throw new Error(\"id is required\")\n if (!level) throw new Error(\"level is required\")\n const node: GraphNode = {\n id,\n level,\n parentId,\n axes,\n createdAt: new Date().toISOString(),\n createdBy: \"admin-ui\",\n }\n store.addNode(node)\n return { node }\n}\n\nfunction updateNode(store: GraphStore, body: any): { node: GraphNode } {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"id is required\")\n const axes = sanitizeAxes({ [id]: body?.axes || {} })[id] || {}\n const node = store.updateNodeAxes(id, axes)\n return { node }\n}\n\nfunction deleteNode(store: GraphStore, body: any) {\n const id = String(body?.id || \"\").trim()\n if (!id) throw new Error(\"id is required\")\n store.deleteNode(id)\n return { summary: `Deleted ${id}` }\n}\n\nfunction saveSchema(store: GraphStore, body: any) {\n if (!body?.schema || typeof body.schema !== \"object\") {\n throw new Error(\"schema object is required\")\n }\n store.saveSchema(body.schema) // GraphStore validates via zod; throws on invalid\n return { summary: \"Schema saved\" }\n}\n\n// ========================================================================\n// Helpers\n// ========================================================================\n\nfunction sanitizeAxes(\n src: Record<string, any>,\n): Record<string, Record<string, string>> {\n const out: Record<string, Record<string, string>> = {}\n for (const [nodeId, axes] of Object.entries(src || {})) {\n if (!axes || typeof axes !== \"object\") continue\n const clean: Record<string, string> = {}\n for (const [k, v] of Object.entries(axes as Record<string, unknown>)) {\n if (typeof v === \"string\") clean[k] = v\n }\n out[nodeId] = clean\n }\n return out\n}\n\nfunction slug(s: string): string {\n return s\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n}\n\nfunction sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(data))\n}\n\nfunction readJsonBody(req: IncomingMessage): Promise<any> {\n return new Promise((ok, err) => {\n let raw = \"\"\n req.on(\"data\", (c) => {\n raw += c\n })\n req.on(\"end\", () => {\n try {\n ok(raw ? JSON.parse(raw) : {})\n } catch (e) {\n err(e)\n }\n })\n req.on(\"error\", err)\n })\n}\n\n","// --- Intent Graph page (/admin/graph) ---\n//\n// Three surfaces inside one page:\n// 1. Pending approval queue — LLM-proposed classifications the operator\n// triages (approve / reject / edit).\n// 2. Taxonomy tree — nodes grouped by level, inline axes editing.\n// 3. Schema editor — raw JSON for advanced level/axis changes.\n//\n// API is in graph-panel.ts (kept there because it's the module that owns\n// GraphStore); this file owns ONLY the HTML/CSS/client JS.\n\nimport { renderShell, type TopbarPeer } from \"..\"\n\nexport interface GraphPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n localToken?: string\n}\n\nexport function renderGraphPage(opts: GraphPageOpts = {}): string {\n const tokenScript = opts.localToken\n ? `<script>window.AX_LOCAL_TOKEN = ${JSON.stringify(opts.localToken)};</script>`\n : \"\"\n\n const body = `<div id=\"msg\" class=\"msg\"></div>\n\n <div id=\"disabled-banner\" class=\"banner warn\" style=\"display:none\">\n <b>Graph is disabled.</b> Set <code class=\"mono\">graph.enabled: true</code>\n in <code class=\"mono\">agentx.json</code> to start classifying traffic.\n The admin UI works either way — nothing classifies until the daemon reloads with graph enabled.\n </div>\n\n <div class=\"counts\">\n <div class=\"count\"><div class=\"n\" id=\"c-nodes\">—</div><div class=\"l\">Nodes</div></div>\n <div class=\"count\"><div class=\"n\" id=\"c-pending\">—</div><div class=\"l\">Pending</div></div>\n <div class=\"count\"><div class=\"n\" id=\"c-fp\">—</div><div class=\"l\">Cached fingerprints</div></div>\n </div>\n\n <div class=\"grid grid-2\">\n <section class=\"card\">\n <h2>Pending approval</h2>\n <p class=\"muted\">The LLM proposed a path for these messages. Approve to add the fingerprint to the snap-to-path cache so similar messages don't hit the LLM again.</p>\n <div id=\"pending-list\"></div>\n </section>\n\n <section class=\"card\">\n <h2>Taxonomy</h2>\n <p class=\"muted\">Current nodes, grouped by level. Edit axes inline; add nodes with the form below.</p>\n <div id=\"tree\"></div>\n\n <details>\n <summary>Add node</summary>\n <div class=\"row\" style=\"margin-top:10px\">\n <select id=\"n-level\"></select>\n <input id=\"n-id\" placeholder=\"node id (e.g. noqta-devops)\" />\n <select id=\"n-parent\"><option value=\"\">— no parent —</option></select>\n </div>\n <div class=\"row\" style=\"margin-top:6px\">\n <input id=\"n-axes\" placeholder='axes JSON, e.g. {\"name\":\"DevOps\",\"orgKind\":\"team\"}' />\n <button class=\"primary\" id=\"btn-add-node\">Add</button>\n </div>\n </details>\n </section>\n </div>\n\n <section class=\"card\" style=\"margin-top:20px\">\n <h2>Schema (advanced)</h2>\n <p class=\"muted\">Levels and their required axes. Edit carefully — changing enum values or removing axes can orphan existing nodes.</p>\n <textarea id=\"schema-editor\" spellcheck=\"false\"></textarea>\n <div class=\"row\" style=\"margin-top:10px;justify-content:flex-end\">\n <button id=\"btn-reload-schema\">Reload</button>\n <button class=\"primary\" id=\"btn-save-schema\">Save schema</button>\n </div>\n </section>\n\n <section class=\"card\" style=\"margin-top:20px\">\n <h2>Recent classifications</h2>\n <p class=\"muted\">Last 25 log rows, newest first — approved / pending / rejected.</p>\n <div id=\"recent\"></div>\n </section>`\n\n return renderShell({\n title: \"AgentX · Intent Graph\",\n activeTab: \"graph\",\n subtitle: \"Intent Graph\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: GRAPH_PAGE_CSS,\n scripts: `${tokenScript}<script>${GRAPH_PAGE_SCRIPT}</script>`,\n })\n}\n\nconst GRAPH_PAGE_CSS = `\nmain { max-width: 1200px; margin: 0 auto; padding: 24px 20px 60px; }\nh2 { margin: 0 0 12px; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }\n.grid { display: grid; gap: 20px; grid-template-columns: 1fr; }\n@media (min-width: 920px) { .grid-2 { grid-template-columns: 3fr 2fr; } }\n.card {\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 8px; padding: 16px;\n}\n.card h2 { margin-top: 0; }\n.muted { color: var(--ax-muted); font-size: 12px; }\n.mono { font-family: var(--ax-mono); }\n.chip {\n display: inline-block; padding: 1px 8px; border-radius: 3px;\n font-size: 11px; background: var(--ax-surface); color: var(--ax-text-2);\n border: 1px solid var(--ax-border); margin-right: 4px;\n}\n.chip.pending {\n background: color-mix(in oklch, var(--ax-warn) 15%, transparent);\n color: var(--ax-warn); border-color: transparent;\n}\n.chip.approved {\n background: color-mix(in oklch, var(--ax-accent) 15%, transparent);\n color: var(--ax-accent); border-color: transparent;\n}\n.chip.rejected {\n background: color-mix(in oklch, var(--ax-err) 15%, transparent);\n color: var(--ax-err); border-color: transparent;\n}\nbutton, .btn {\n font: inherit; font-size: 12px; padding: 6px 12px; border-radius: 4px;\n border: 1px solid var(--ax-border-2); background: var(--ax-surface);\n color: var(--ax-text); cursor: pointer;\n}\nbutton:hover { border-color: var(--ax-accent); color: var(--ax-accent); }\nbutton.primary {\n background: var(--ax-accent); color: var(--ax-bg);\n border-color: var(--ax-accent); font-weight: 600;\n}\nbutton.primary:hover { filter: brightness(1.08); color: var(--ax-bg); }\nbutton.danger { color: var(--ax-err); border-color: var(--ax-err); }\nbutton.danger:hover { background: var(--ax-err); color: #fff; }\ninput, textarea, select {\n font: inherit; font-size: 12px; padding: 6px 10px;\n background: var(--ax-surface); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 4px; width: 100%;\n font-family: var(--ax-mono);\n}\ntextarea { min-height: 220px; white-space: pre; line-height: 1.45; }\n.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n.row > * { flex: 0 0 auto; }\n.row > input, .row > select { flex: 1 1 160px; min-width: 0; }\n.pending-item { border-bottom: 1px solid var(--ax-border); padding: 12px 0; }\n.pending-item:last-child { border-bottom: none; }\n.pending-preview {\n color: var(--ax-text-2); font-size: 12px; margin: 6px 0 8px;\n padding: 8px 10px; background: var(--ax-surface); border-radius: 4px;\n border-left: 2px solid var(--ax-accent); white-space: pre-wrap;\n}\n.path-crumbs {\n font-family: var(--ax-mono); font-size: 12px; color: var(--ax-accent); margin: 4px 0;\n}\n.level-group { margin-bottom: 16px; }\n.level-group h3 {\n font-size: 12px; margin: 0 0 6px; font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.06em; color: var(--ax-muted);\n}\n.node-row {\n display: flex; align-items: center; gap: 10px; padding: 6px 0;\n border-bottom: 1px dotted var(--ax-border);\n}\n.node-row:last-child { border: none; }\n.node-id { font-family: var(--ax-mono); font-weight: 600; min-width: 180px; }\n.axes { display: flex; gap: 6px; flex-wrap: wrap; flex: 1; }\n.axis { font-size: 11px; color: var(--ax-text-2); }\n.axis b { color: var(--ax-text); font-weight: 500; }\n.empty { color: var(--ax-muted); font-style: italic; padding: 16px; text-align: center; }\n.banner { padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 12px; }\n.banner.warn {\n background: color-mix(in oklch, var(--ax-warn) 10%, transparent);\n color: var(--ax-warn);\n border: 1px solid color-mix(in oklch, var(--ax-warn) 30%, transparent);\n}\n.msg { font-size: 11px; padding: 6px 10px; border-radius: 4px; margin-top: 8px; display: none; }\n.msg.ok {\n background: color-mix(in oklch, var(--ax-accent) 10%, transparent);\n color: var(--ax-accent); display: block;\n}\n.msg.err {\n background: color-mix(in oklch, var(--ax-err) 10%, transparent);\n color: var(--ax-err); display: block;\n}\n.counts { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }\n.count {\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 6px; padding: 10px 14px; min-width: 110px;\n}\n.count .n { font-size: 20px; font-weight: 600; line-height: 1; }\n.count .l { font-size: 11px; color: var(--ax-muted); margin-top: 4px; }\ndetails {\n background: var(--ax-surface); border: 1px solid var(--ax-border);\n border-radius: 4px; padding: 8px 12px; margin-top: 8px;\n}\ndetails summary { cursor: pointer; font-size: 11px; color: var(--ax-muted); user-select: none; }\ndetails[open] summary { margin-bottom: 8px; }`\n\n/** Client script — 220-ish lines. Unchanged from the pre-refactor version. */\nconst GRAPH_PAGE_SCRIPT = `\nconst $ = (id) => document.getElementById(id);\nconst state = { schema: null, nodes: [], pending: [], recent: [] };\n\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,c=>({\"&\":\"&\",\"<\":\"<\",\">\":\">\",'\"':\""\",\"'\":\"'\"}[c]));}\n\nasync function req(method, path, body) {\n const headers = { 'Content-Type': 'application/json' };\n if (window.AX_LOCAL_TOKEN) headers['Authorization'] = 'Bearer ' + window.AX_LOCAL_TOKEN;\n const r = await fetch(path, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n const data = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));\n return data;\n}\n\nfunction showMsg(kind, text) {\n const m = $('msg');\n m.className = 'msg ' + kind;\n m.textContent = text;\n setTimeout(() => { m.className = 'msg'; }, 4000);\n}\n\nfunction pathLabel(path) {\n return path.map(id => {\n const n = state.nodes.find(x => x.id === id);\n return n ? (n.axes?.name || n.axes?.what || id) : id;\n }).join(' › ');\n}\n\nfunction renderCounts(counts, enabled) {\n $('c-nodes').textContent = counts.nodes;\n $('c-pending').textContent = counts.pending;\n $('c-fp').textContent = counts.fingerprints;\n $('disabled-banner').style.display = enabled ? 'none' : 'block';\n}\n\nfunction renderTree() {\n const host = $('tree');\n if (!state.schema || state.nodes.length === 0) {\n host.innerHTML = '<div class=\"empty\">No nodes yet. The classifier populates these as new intents arrive.</div>';\n return;\n }\n let html = '';\n for (const level of state.schema.levels) {\n const nodes = state.nodes.filter(n => n.level === level.id);\n html += '<div class=\"level-group\"><h3>' + esc(level.id) + '</h3>';\n if (!nodes.length) {\n html += '<div class=\"muted\" style=\"padding-left:4px\">—</div>';\n } else {\n for (const n of nodes) {\n const axes = Object.entries(n.axes || {}).map(([k, v]) =>\n '<span class=\"axis\"><b>' + esc(k) + ':</b> ' + esc(v) + '</span>'\n ).join(' ');\n html += '<div class=\"node-row\">'\n + '<span class=\"node-id\">' + esc(n.id) + '</span>'\n + '<div class=\"axes\">' + axes + '</div>'\n + '<button data-edit=\"' + esc(n.id) + '\">Edit</button>'\n + '<button class=\"danger\" data-del=\"' + esc(n.id) + '\">Delete</button>'\n + '</div>';\n }\n }\n html += '</div>';\n }\n host.innerHTML = html;\n host.querySelectorAll('[data-edit]').forEach(b =>\n b.addEventListener('click', () => editNode(b.getAttribute('data-edit')))\n );\n host.querySelectorAll('[data-del]').forEach(b =>\n b.addEventListener('click', () => deleteNode(b.getAttribute('data-del')))\n );\n}\n\nfunction renderPending() {\n const host = $('pending-list');\n if (!state.pending.length) {\n host.innerHTML = '<div class=\"empty\">Nothing pending. New traffic will appear here when the classifier proposes a novel path.</div>';\n return;\n }\n host.innerHTML = state.pending.map(c => {\n const conf = c.confidence != null ? ' · confidence ' + (c.confidence * 100).toFixed(0) + '%' : '';\n const preview = c.preview || '(no preview)';\n const path = pathLabel(c.path);\n return '<div class=\"pending-item\" data-hash=\"' + esc(c.msgHash) + '\">'\n + '<div class=\"muted\" style=\"font-size:11px\">' + esc(c.channel || '?') + ' · ' + esc(c.sender || '?') + conf + ' · ' + esc(c.ts) + '</div>'\n + '<div class=\"path-crumbs\">' + esc(path) + '</div>'\n + '<div class=\"pending-preview\">' + esc(preview) + '</div>'\n + '<div class=\"row\">'\n + '<button class=\"primary\" data-approve=\"' + esc(c.msgHash) + '\">Approve</button>'\n + '<button class=\"danger\" data-reject=\"' + esc(c.msgHash) + '\">Reject</button>'\n + '</div>'\n + '</div>';\n }).join('');\n host.querySelectorAll('[data-approve]').forEach(b =>\n b.addEventListener('click', () => approve(b.getAttribute('data-approve')))\n );\n host.querySelectorAll('[data-reject]').forEach(b =>\n b.addEventListener('click', () => reject(b.getAttribute('data-reject')))\n );\n}\n\nfunction renderRecent() {\n const host = $('recent');\n if (!state.recent.length) {\n host.innerHTML = '<div class=\"empty\">No classifications yet.</div>';\n return;\n }\n host.innerHTML = state.recent.map(c =>\n '<div class=\"node-row\">'\n + '<span class=\"chip ' + esc(c.status) + '\">' + esc(c.status) + '</span>'\n + '<span class=\"mono muted\" style=\"font-size:11px\">' + esc(c.ts) + '</span>'\n + '<span style=\"flex:1\">' + esc(pathLabel(c.path)) + '</span>'\n + '<span class=\"muted\" style=\"font-size:11px\">' + esc(c.source) + '</span>'\n + '</div>'\n ).join('');\n}\n\nfunction renderSchemaEditor() {\n $('schema-editor').value = JSON.stringify(state.schema, null, 2);\n const levelSel = $('n-level');\n levelSel.innerHTML = state.schema.levels.map(l =>\n '<option value=\"' + esc(l.id) + '\">' + esc(l.id) + '</option>'\n ).join('');\n const parentSel = $('n-parent');\n parentSel.innerHTML = '<option value=\"\">— no parent —</option>' +\n state.nodes.map(n => '<option value=\"' + esc(n.id) + '\">'\n + esc(n.id) + ' (' + esc(n.level) + ')</option>').join('');\n}\n\nasync function refresh() {\n try {\n const r = await req('GET', '/api/admin/graph/state');\n state.schema = r.schema;\n state.nodes = r.nodes;\n state.pending = r.pending;\n state.recent = r.recent;\n renderCounts(r.counts, r.enabled);\n renderTree();\n renderPending();\n renderRecent();\n renderSchemaEditor();\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function approve(msgHash) {\n try {\n await req('POST', '/api/admin/graph/classifications/approve', { msgHash });\n showMsg('ok', 'Approved.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function reject(msgHash) {\n if (!confirm('Reject this classification? It will not be committed and no fingerprint is cached.')) return;\n try {\n await req('POST', '/api/admin/graph/classifications/reject', { msgHash });\n showMsg('ok', 'Rejected.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function editNode(id) {\n const n = state.nodes.find(x => x.id === id);\n if (!n) return;\n const current = JSON.stringify(n.axes || {}, null, 2);\n const next = prompt('Edit axes for \"' + id + '\" (JSON):', current);\n if (next == null) return;\n let axes;\n try { axes = JSON.parse(next); } catch { showMsg('err', 'Invalid JSON'); return; }\n try {\n await req('PATCH', '/api/admin/graph/nodes', { id, axes });\n showMsg('ok', 'Node updated.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function deleteNode(id) {\n if (!confirm('Delete node \"' + id + '\"? Fails if it has children or references.')) return;\n try {\n await req('DELETE', '/api/admin/graph/nodes', { id });\n showMsg('ok', 'Deleted.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n}\n\n$('btn-add-node').addEventListener('click', async () => {\n const level = $('n-level').value;\n const id = $('n-id').value.trim();\n const parentId = $('n-parent').value || null;\n let axes = {};\n const raw = $('n-axes').value.trim();\n if (raw) {\n try { axes = JSON.parse(raw); }\n catch { showMsg('err', 'Axes JSON invalid.'); return; }\n }\n try {\n await req('POST', '/api/admin/graph/nodes', { id, level, parentId, axes });\n $('n-id').value = ''; $('n-axes').value = '';\n showMsg('ok', 'Node added.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n});\n\n$('btn-save-schema').addEventListener('click', async () => {\n let schema;\n try { schema = JSON.parse($('schema-editor').value); }\n catch { showMsg('err', 'Schema JSON invalid.'); return; }\n try {\n await req('PUT', '/api/admin/graph/schema', { schema });\n showMsg('ok', 'Schema saved.');\n refresh();\n } catch (e) { showMsg('err', e.message); }\n});\n\n$('btn-reload-schema').addEventListener('click', refresh);\n\nrefresh();\n`\n","import type { IncomingMessage, ServerResponse } from \"http\"\nimport { existsSync, readFileSync } from \"fs\"\nimport { resolve } from \"path\"\nimport {\n listAgentFiles,\n readAgentFile,\n writeAgentFile,\n createAgentSkill,\n deleteAgentSkill,\n listSkillTree,\n readSkillFile,\n writeSkillFile,\n detectSkillDeps,\n} from \"./file-ops\"\nimport { HandoverStore } from \"@/channels/handover-store\"\nimport { loadDaemonConfig } from \"./config\"\nimport { mutateAgentxConfig } from \"./config-mutate\"\nimport type { TopbarPeer } from \"./topbar\"\nimport { renderAgentPage } from \"./ui/pages/agent\"\n\n// --- /admin/agents/<id> — one dedicated page per agent -------------------\n//\n// Layout:\n// Header — name, tier, model, mentions, access\n// Overview — editable systemPrompt + metadata form\n// Identity — CLAUDE.md / SOUL.md / IDENTITY.md etc, EasyMDE editor\n// Skills — directory tree per skill, file editor, install/deps actions\n// Channels — read-only list of bindings\n// Activity — recent tasks, today's KPI row\n//\n// \"Install package\" and \"Install deps\" DISPATCH through the daemon's /task\n// endpoint using devops-agent (or node.defaultAgent) as the worker, with a\n// confirm dialog in the UI. That way the user sees the action stream in\n// the normal task view and it's logged like any other task.\n\n// =======================================================================\n// Route handlers\n// =======================================================================\n\nexport function handleAgentPageGet(\n _req: IncomingMessage,\n res: ServerResponse,\n agentId: string,\n peers: TopbarPeer[] = [],\n localToken?: string,\n): void {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(renderAgentPage({ agentId, peers, localToken }))\n}\n\nexport async function handleAgentApi(\n req: IncomingMessage,\n res: ServerResponse,\n path: string,\n): Promise<void> {\n try {\n const body = req.method === \"GET\" ? undefined : await readJsonBody(req)\n const url = new URL(req.url || \"/\", \"http://localhost\")\n const qs = Object.fromEntries(url.searchParams)\n const match = path.match(/^\\/api\\/admin\\/agent\\/([a-z0-9][a-z0-9_-]*)(?:\\/(.+))?$/)\n if (!match) {\n sendJson(res, 404, { error: `unknown agent endpoint: ${path}` })\n return\n }\n const agentId = match[1]\n const sub = match[2] || \"\"\n const key = `${req.method} ${sub}`\n\n const dispatch: Record<string, () => unknown | Promise<unknown>> = {\n \"GET \": () => getAgentState(agentId),\n \"PATCH \": () => patchAgent(agentId, body),\n \"GET identity\": () => getIdentityFiles(agentId),\n \"GET identity/file\": () => getIdentityFile(agentId, String(qs.path || \"\")),\n \"PUT identity/file\": () => putIdentityFile(agentId, body),\n \"GET skills\": () => getSkills(agentId),\n \"POST skills\": () => postCreateSkill(agentId, body),\n \"DELETE skills\": () => deleteSkill(agentId, body),\n \"GET skills/tree\": () => getSkillTree(agentId, String(qs.slug || \"\")),\n \"GET skills/file\": () =>\n getSkillFile(agentId, String(qs.slug || \"\"), String(qs.path || \"\")),\n \"PUT skills/file\": () => putSkillFile(agentId, body),\n \"GET skills/deps\": () => getDepsHint(agentId, String(qs.slug || \"\")),\n \"POST skills/install\": () => dispatchInstallPackage(agentId, body, req),\n \"POST skills/deps\": () => dispatchInstallDeps(agentId, body, req),\n \"GET channels\": () => getChannelsForAgent(agentId),\n \"GET activity\": () => getRecentActivity(agentId, req),\n \"GET handovers\": () => listHandovers(agentId),\n \"POST handovers\": () => createHandover(agentId, body),\n \"DELETE handovers\": () => releaseHandover(body),\n }\n\n const handler = dispatch[key]\n if (!handler) {\n sendJson(res, 404, { error: `unknown agent sub-endpoint: ${key}` })\n return\n }\n const result = await handler()\n sendJson(res, 200, result)\n } catch (e: any) {\n sendJson(res, 400, { error: e?.message || \"agent op failed\" })\n }\n}\n\n// =======================================================================\n// Helpers — reading & mutating agent config\n// =======================================================================\n\nfunction readConfigRaw(): any {\n const file = resolve(process.cwd(), \"agentx.json\")\n if (!existsSync(file)) return null\n return JSON.parse(readFileSync(file, \"utf-8\"))\n}\n\nfunction getAgentDef(agentId: string): any {\n const cfg = readConfigRaw()\n if (!cfg) throw new Error(\"agentx.json not found\")\n const def = cfg.agents?.[agentId]\n if (!def) throw new Error(`Agent \"${agentId}\" not found`)\n return { def, cfg }\n}\n\nfunction getAgentState(agentId: string) {\n const { def, cfg } = getAgentDef(agentId)\n // A minimal workspace snapshot so the page header can render without a\n // second round trip.\n const overview = def.workspace ? listAgentFiles(def.workspace) : null\n return {\n id: agentId,\n name: def.name || agentId,\n tier: def.tier,\n model: def.model,\n workspace: def.workspace,\n systemPrompt: def.systemPrompt || \"\",\n mentions: def.mentions || [],\n access: def.access || \"private\",\n maxConcurrent: def.maxConcurrent ?? 1,\n maxExecutionMinutes: def.maxExecutionMinutes ?? 20,\n permissionMode: def.permissionMode || \"default\",\n overview,\n defaultWorker: cfg.node?.defaultAgent,\n draftAgent: cfg.dashboard?.draftAgent,\n }\n}\n\nfunction patchAgent(agentId: string, body: any) {\n const fields: Record<string, any> = {}\n for (const k of [\n \"name\", \"tier\", \"model\", \"systemPrompt\", \"mentions\",\n \"access\", \"maxConcurrent\", \"maxExecutionMinutes\", \"permissionMode\",\n ]) {\n if (body?.[k] !== undefined) fields[k] = body[k]\n }\n const { summary } = mutateAgentxConfig((cfg) => {\n if (!cfg.agents?.[agentId]) throw new Error(`Agent \"${agentId}\" not found`)\n cfg.agents[agentId] = { ...cfg.agents[agentId], ...fields }\n return `agent \"${agentId}\" updated (${Object.keys(fields).join(\", \")})`\n })\n return { summary, agent: getAgentState(agentId) }\n}\n\n// =======================================================================\n// Identity files\n// =======================================================================\n\nfunction getIdentityFiles(agentId: string) {\n const { def } = getAgentDef(agentId)\n return listAgentFiles(def.workspace)\n}\n\nfunction getIdentityFile(agentId: string, relPath: string) {\n if (!relPath) throw new Error(\"path query param required\")\n const { def } = getAgentDef(agentId)\n return readAgentFile(def.workspace, relPath)\n}\n\nfunction putIdentityFile(agentId: string, body: any) {\n const relPath = String(body?.path || \"\")\n if (!relPath) throw new Error(\"path required\")\n const { def } = getAgentDef(agentId)\n return writeAgentFile(def.workspace, relPath, String(body?.content ?? \"\"))\n}\n\n// =======================================================================\n// Skills — list / tree / file I/O / create / delete\n// =======================================================================\n\nfunction getSkills(agentId: string) {\n const { def } = getAgentDef(agentId)\n const overview = listAgentFiles(def.workspace)\n return { skills: overview.skills, workspace: overview.workspace }\n}\n\nfunction postCreateSkill(agentId: string, body: any) {\n const slug = String(body?.slug || \"\").trim()\n if (!slug) throw new Error(\"slug required\")\n const { def } = getAgentDef(agentId)\n return createAgentSkill(def.workspace, slug, {\n title: body?.title,\n content: body?.content,\n })\n}\n\nfunction deleteSkill(agentId: string, body: any) {\n const slug = String(body?.slug || \"\").trim()\n if (!slug) throw new Error(\"slug required\")\n const { def } = getAgentDef(agentId)\n return deleteAgentSkill(def.workspace, slug)\n}\n\nfunction getSkillTree(agentId: string, slug: string) {\n if (!slug) throw new Error(\"slug query param required\")\n const { def } = getAgentDef(agentId)\n const tree = listSkillTree(def.workspace, slug)\n const deps = detectSkillDeps(def.workspace, slug)\n return { slug, tree, deps }\n}\n\nfunction getSkillFile(agentId: string, slug: string, relPath: string) {\n if (!slug || !relPath) throw new Error(\"slug and path query params required\")\n const { def } = getAgentDef(agentId)\n return readSkillFile(def.workspace, slug, relPath)\n}\n\nfunction putSkillFile(agentId: string, body: any) {\n const slug = String(body?.slug || \"\")\n const relPath = String(body?.path || \"\")\n if (!slug || !relPath) throw new Error(\"slug and path required\")\n const { def } = getAgentDef(agentId)\n return writeSkillFile(def.workspace, slug, relPath, String(body?.content ?? \"\"))\n}\n\nfunction getDepsHint(agentId: string, slug: string) {\n if (!slug) throw new Error(\"slug query param required\")\n const { def } = getAgentDef(agentId)\n return detectSkillDeps(def.workspace, slug)\n}\n\n// =======================================================================\n// Dispatched actions — install package, install deps\n// =======================================================================\n\nasync function dispatchInstallPackage(\n agentId: string,\n body: any,\n req: IncomingMessage,\n): Promise<any> {\n const pkg = String(body?.package || \"\").trim()\n if (!pkg || !/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+(?:\\/[A-Za-z0-9_.-]+)?$/.test(pkg)) {\n throw new Error(\"package must be in 'owner/repo' or 'owner/repo/skill' form\")\n }\n const worker = pickWorker(body?.worker)\n const { def } = getAgentDef(agentId)\n const message = [\n `Install the skills.sh package \"${pkg}\" into agent \"${agentId}\".`,\n `Workspace: ${def.workspace}`,\n `Run: cd ${def.workspace} && npx -y skills add ${pkg}`,\n `If that fails, fall back to: agentx skill install ${pkg}`,\n `Skills should land under ${def.workspace}/.claude/skills/. Report what was installed and any errors.`,\n ].join(\"\\n\")\n return dispatchTask(req, worker, message, `install-${pkg}:${Date.now()}`)\n}\n\nasync function dispatchInstallDeps(\n agentId: string,\n body: any,\n req: IncomingMessage,\n): Promise<any> {\n const slug = String(body?.slug || \"\").trim()\n if (!slug) throw new Error(\"slug required\")\n const worker = pickWorker(body?.worker)\n const { def } = getAgentDef(agentId)\n const hint = detectSkillDeps(def.workspace, slug)\n if (!hint.manager) {\n throw new Error(\n `No dependency manifest detected in .claude/skills/${slug}. Expected one of: package.json, requirements.txt, Gemfile.`,\n )\n }\n const skillPath = resolve(def.workspace, \".claude\", \"skills\", slug)\n const message = [\n `Install dependencies for skill \"${slug}\" in agent \"${agentId}\".`,\n `cd to: ${skillPath}`,\n `Detected manifest: ${hint.file}`,\n `Run: ${hint.command}`,\n `Stream the install output. On success, summarise the packages installed.`,\n `On failure, report the exit code and the last 20 lines of output.`,\n ].join(\"\\n\")\n return dispatchTask(req, worker, message, `deps-${agentId}-${slug}:${Date.now()}`)\n}\n\nfunction pickWorker(explicit?: string): string {\n if (explicit && typeof explicit === \"string\") return explicit\n const cfg = loadDaemonConfig()\n return (\n cfg.dashboard?.draftAgent ||\n cfg.node?.defaultAgent ||\n // Fall back to any agent that looks like devops — we need SOMEONE to run shell\n Object.keys(cfg.agents).find((id) => id.includes(\"devops\")) ||\n Object.keys(cfg.agents)[0]\n )\n}\n\nasync function dispatchTask(\n req: IncomingMessage,\n workerAgent: string,\n message: string,\n chatId: string,\n): Promise<any> {\n const cfg = loadDaemonConfig()\n const daemonUrl = (cfg.dashboard.daemonUrl || \"http://localhost:18800\").replace(/\\/+$/, \"\")\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" }\n // Forward the operator's bearer token to the daemon so the task is\n // attributed + auth'd correctly. The admin auth gate already verified us.\n const auth = req.headers.authorization\n if (auth) headers.Authorization = String(auth)\n else if (cfg.dashboard.token) headers.Authorization = `Bearer ${cfg.dashboard.token}`\n\n const r = await fetch(`${daemonUrl}/task`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n agent: workerAgent,\n message,\n context: { channel: \"admin\", sender: \"admin-ui\", chatId },\n }),\n })\n const text = await r.text()\n let data: any\n try { data = JSON.parse(text) } catch { data = { content: text } }\n if (!r.ok) throw new Error(data?.error || `daemon /task HTTP ${r.status}`)\n return {\n worker: workerAgent,\n taskId: data?.taskId,\n content: data?.content,\n duration: data?.duration,\n error: data?.error,\n }\n}\n\n// =======================================================================\n// Channels & activity — read-only, best-effort from config\n// =======================================================================\n\nfunction getChannelsForAgent(agentId: string) {\n const cfg = readConfigRaw()\n const out: Array<{ channel: string; account?: string; detail: string }> = []\n const tg = cfg?.channels?.telegram?.accounts || {}\n for (const [acct, def] of Object.entries<any>(tg)) {\n if (def?.agentBinding === agentId) {\n out.push({ channel: \"telegram\", account: acct, detail: def?.botUsername || acct })\n }\n }\n if (cfg?.channels?.slack?.agentBinding === agentId) {\n out.push({ channel: \"slack\", detail: \"bound\" })\n }\n if (cfg?.channels?.discord?.agentBinding === agentId) {\n out.push({ channel: \"discord\", detail: \"bound\" })\n }\n const gitlab = cfg?.channels?.gitlab?.agentMappings || []\n for (const m of gitlab) {\n if (m?.agentId === agentId) {\n out.push({\n channel: \"gitlab\",\n detail: (m?.gitlabUsernames || []).join(\", \") || \"mapped\",\n })\n }\n }\n return { bindings: out }\n}\n\nasync function getRecentActivity(agentId: string, _req: IncomingMessage): Promise<any> {\n // Pull from the daemon's /agents snapshot so we don't duplicate task\n // history logic. Keep it best-effort; a failing daemon shouldn't 500 the UI.\n try {\n const cfg = loadDaemonConfig()\n const daemonUrl = (cfg.dashboard.daemonUrl || \"http://localhost:18800\").replace(/\\/+$/, \"\")\n const r = await fetch(`${daemonUrl}/agents`)\n if (!r.ok) throw new Error(`daemon HTTP ${r.status}`)\n const list: any[] = await r.json()\n const agent = list.find((a) => a.id === agentId)\n return {\n activeTasks: agent?.activeTasks ?? 0,\n totalTasks: agent?.totalTasks ?? 0,\n errors: agent?.errors ?? 0,\n lastActive: agent?.lastActive ?? null,\n runningTasks: agent?.runningTasks ?? [],\n lastSummary: agent?.lastSummary ?? null,\n }\n } catch (e: any) {\n return { error: e?.message, activeTasks: 0, totalTasks: 0, errors: 0, runningTasks: [] }\n }\n}\n\n// =======================================================================\n// Handovers\n// =======================================================================\n\nfunction handoverStore(): HandoverStore {\n return new HandoverStore()\n}\n\n/** List active handovers for this agent, split by direction. */\nfunction listHandovers(agentId: string) {\n const store = handoverStore()\n const { incoming, outgoing } = store.listByAgent(agentId)\n return { incoming, outgoing }\n}\n\n/** Create or replace a handover. Body:\n * { channel, chatId, accountId?, toAgent, summary?, expiresAt?, fromAgent? }\n * When `fromAgent` is omitted we default to the agent id in the URL — the\n * common case is \"I'm on agent X's page and handing this chat off from it\n * to agent Y\". */\nfunction createHandover(pageAgentId: string, body: any) {\n const channel = String(body?.channel || \"\").trim()\n const chatId = String(body?.chatId || \"\").trim()\n const toAgent = String(body?.toAgent || \"\").trim()\n if (!channel || !chatId || !toAgent) {\n throw new Error(\"channel, chatId, toAgent required\")\n }\n const fromAgent = String(body?.fromAgent || pageAgentId || \"\").trim() || pageAgentId\n const accountId = body?.accountId ? String(body.accountId) : undefined\n const summary = body?.summary ? String(body.summary) : undefined\n const expiresAt = body?.expiresAt ? String(body.expiresAt) : undefined\n const rec = handoverStore().set({\n channel, chatId, accountId, fromAgent, toAgent, summary, expiresAt,\n createdBy: \"admin-ui\",\n })\n return { summary: `Handed off ${channel}:${chatId} from ${fromAgent} to ${toAgent}`, record: rec }\n}\n\nfunction releaseHandover(body: any) {\n const channel = String(body?.channel || \"\").trim()\n const chatId = String(body?.chatId || \"\").trim()\n if (!channel || !chatId) throw new Error(\"channel and chatId required\")\n const accountId = body?.accountId ? String(body.accountId) : undefined\n const removed = handoverStore().remove(channel, chatId, accountId)\n return { summary: removed ? \"Released\" : \"No active handover for that chat\" }\n}\n\n// =======================================================================\n// Plumbing\n// =======================================================================\n\nfunction sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(data))\n}\n\nfunction readJsonBody(req: IncomingMessage): Promise<any> {\n return new Promise((ok, err) => {\n let raw = \"\"\n req.on(\"data\", (c) => { raw += c })\n req.on(\"end\", () => {\n try { ok(raw ? JSON.parse(raw) : {}) } catch (e) { err(e) }\n })\n req.on(\"error\", err)\n })\n}\n\n// =======================================================================\n// HTML\n// =======================================================================\n\n\n","// --- Agent page (/admin/agents/<id>) ---\n//\n// One dedicated page per agent. Tabs:\n// Overview — editable metadata + system prompt\n// Identity — CLAUDE.md / SOUL.md / IDENTITY.md etc via EasyMDE\n// Skills — per-skill directory tree, file editor, install actions\n// Channels — read-only list of bindings\n// Handovers — manual routing to/from this agent\n// Activity — recent tasks\n//\n// API is in agent-panel.ts (keeps file-ops + handover-store imports in one\n// place); this file owns ONLY the HTML/CSS/client JS.\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\n\nexport interface AgentPageOpts {\n agentId: string\n peers?: TopbarPeer[]\n currentPeerId?: string\n localToken?: string\n}\n\nexport function renderAgentPage(opts: AgentPageOpts): string {\n const agentId = opts.agentId\n const tokenScript = opts.localToken\n ? `<script>window.AX_LOCAL_TOKEN = ${JSON.stringify(opts.localToken)};</script>`\n : \"\"\n const agentIdScript = `<script>window.AGENT_ID = ${JSON.stringify(agentId)};</script>`\n\n // Body HTML — copied verbatim from the old renderAgentHtml, with the\n // ${escapeHtml(agentId)} interpolations re-wired to our esc() helper.\n const body = AGENT_PAGE_BODY.replace(/__AGENT_ID__/g, esc(agentId))\n\n return renderShell({\n title: `AgentX · ${esc(agentId)}`,\n activeTab: \"admin\",\n subtitle: `Agent · ${esc(agentId)}`,\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n // The extracted body already carries its own <main> tag plus the\n // confirm-modal <div> that sits alongside it at the same level.\n noMain: true,\n body,\n css: AGENT_PAGE_CSS,\n headExtras: `<link rel=\"stylesheet\" href=\"https://unpkg.com/easymde/dist/easymde.min.css\">`,\n scripts: `${agentIdScript}${tokenScript}<script src=\"https://unpkg.com/easymde/dist/easymde.min.js\"></script><script>${AGENT_PAGE_SCRIPT}</script>`,\n })\n}\n\nconst AGENT_PAGE_BODY = `\n<!-- Breadcrumb -->\n<div class=\"ax-crumb\">\n <a href=\"/admin\">Settings</a>\n <span class=\"ax-crumb__sep\">/</span>\n <a href=\"/admin#agents\">Agents</a>\n <span class=\"ax-crumb__sep\">/</span>\n <span class=\"ax-crumb__cur\" id=\"a-crumb-name\">__AGENT_ID__</span>\n</div>\n\n<!-- Hero -->\n<div class=\"ax-hero\">\n <div class=\"ax-hero__ava\" id=\"a-ava\">??<span class=\"ax-hero__on\"></span></div>\n <div class=\"ax-hero__title\">\n <h1><span id=\"a-name\">__AGENT_ID__</span></h1>\n <div class=\"ax-hero__sub\">\n <span class=\"mono muted\" id=\"a-id\">__AGENT_ID__</span>\n <span class=\"ax-hero__dot\"></span>\n <span id=\"a-short-desc\">Agent workspace & behavior</span>\n <span class=\"ax-hero__dot\"></span>\n <span id=\"a-chips\"></span>\n </div>\n </div>\n <div class=\"ax-hero__actions\">\n <button class=\"ax-btn ax-btn--ghost\" id=\"btn-test-drive\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg>\n Test drive\n </button>\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-save-meta\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z\"/><polyline points=\"17 21 17 13 7 13 7 21\"/><polyline points=\"7 3 7 8 15 8\"/></svg>\n Save changes\n </button>\n </div>\n</div>\n\n<!-- Inline stats -->\n<div class=\"ax-stats-inline\" id=\"a-stats\">\n <div class=\"ax-stat-inline\">\n <div class=\"ax-stat-inline__icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/></svg></div>\n <div><div class=\"ax-stat-inline__v\" id=\"stat-today\">—</div><div class=\"ax-stat-inline__l\">messages today</div></div>\n </div>\n <div class=\"ax-stat-inline\">\n <div class=\"ax-stat-inline__icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/></svg></div>\n <div><div class=\"ax-stat-inline__v\" id=\"stat-avg\">—</div><div class=\"ax-stat-inline__l\">avg response</div></div>\n </div>\n <div class=\"ax-stat-inline\">\n <div class=\"ax-stat-inline__icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/></svg></div>\n <div><div class=\"ax-stat-inline__v\" id=\"stat-resolved\">—</div><div class=\"ax-stat-inline__l\">resolved today</div></div>\n </div>\n <div class=\"ax-stat-inline\">\n <div class=\"ax-stat-inline__icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"23 6 13.5 15.5 8.5 10.5 1 18\"/><polyline points=\"17 6 23 6 23 12\"/></svg></div>\n <div><div class=\"ax-stat-inline__v\" id=\"stat-handling\">—</div><div class=\"ax-stat-inline__l\">handling right now</div></div>\n </div>\n</div>\n\n<!-- Layout: rail + main + test-drive -->\n<div class=\"ax-layout\">\n\n <!-- LEFT RAIL -->\n <nav class=\"ax-rail\" id=\"a-rail\">\n <div class=\"ax-rail__label\">Configure</div>\n <a data-tab=\"overview\" class=\"is-active\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\"/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"/></svg>\n Overview\n </a>\n <a data-tab=\"identity\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg>\n Personality\n </a>\n <a data-tab=\"skills\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"/></svg>\n Skills\n <span class=\"ax-rail__count\" id=\"rail-skills-count\">0</span>\n </a>\n <a data-tab=\"channels\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z\"/></svg>\n Channels\n <span class=\"ax-rail__count\" id=\"rail-channels-count\">0</span>\n </a>\n <a data-tab=\"handovers\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"17 1 21 5 17 9\"/><path d=\"M3 11V9a4 4 0 0 1 4-4h14\"/><polyline points=\"7 23 3 19 7 15\"/><path d=\"M21 13v2a4 4 0 0 1-4 4H3\"/></svg>\n Handovers\n <span class=\"ax-rail__count\" id=\"rail-handovers-count\">0</span>\n </a>\n\n <div class=\"ax-rail__sep\"></div>\n <div class=\"ax-rail__label\">Observe</div>\n <a data-tab=\"activity\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>\n Activity\n </a>\n\n <div class=\"ax-rail__sep\"></div>\n <a data-tab=\"danger\" style=\"color:var(--ax-text-2)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"color:var(--ax-err)\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>\n Danger zone\n </a>\n </nav>\n\n <!-- MAIN COLUMN -->\n <div>\n <div id=\"msg\" class=\"msg\"></div>\n\n <!-- Overview -->\n <section id=\"tab-overview\" class=\"ax-panel__tab is-active\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Basics</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Give your agent a name and describe what it does. This is how it'll show up in lists and the topbar.</p>\n </div>\n </div>\n <div class=\"ax-grid-2\">\n <div class=\"ax-field\"><label class=\"lbl\">Display name</label><input class=\"inp\" id=\"f-name\" /></div>\n <div class=\"ax-field\"><label class=\"lbl\">Short id <span class=\"opt\">used in URLs & configs</span></label><input class=\"inp mono\" id=\"f-slug\" value=\"__AGENT_ID__\" readonly style=\"font-family:var(--ax-mono)\" /></div>\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Wake words <span class=\"opt\">what gets its attention in a chat</span></label>\n <div class=\"ax-triggers-edit\" id=\"f-triggers\" onclick=\"this.querySelector('input').focus()\">\n <input placeholder=\"Type a word and press Enter…\" onkeydown=\"addTrigger(event)\"/>\n </div>\n <!-- The canonical persisted value: comma-separated mentions. Kept\n hidden so existing saveAgent() wiring still reads it. -->\n <input id=\"f-mentions\" type=\"hidden\" />\n <div class=\"hint\">Messages containing any of these wake the agent. Other messages are ignored — unless the message is a direct reply to the agent.</div>\n </div>\n </div>\n\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Brains</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Which AI does the thinking? <b>Claude Code</b> is what most people want — it runs locally and has access to tools.</p>\n </div>\n </div>\n <div class=\"ax-grid-3\">\n <div class=\"ax-field\">\n <label class=\"lbl\">AI engine</label>\n <select class=\"inp\" id=\"f-tier\">\n <option value=\"claude-code\">Claude Code (recommended)</option>\n <option value=\"sdk\">Anthropic API (SDK)</option>\n <option value=\"orchestrator\">Orchestrator (any LLM)</option>\n </select>\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Model</label>\n <input class=\"inp\" id=\"f-model\" placeholder=\"claude-sonnet-4-6\" />\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Visibility</label>\n <select class=\"inp\" id=\"f-access\">\n <option value=\"private\">Private (only this node)</option>\n <option value=\"public\">Public API</option>\n </select>\n </div>\n </div>\n </div>\n\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Limits & safety</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Reasonable boundaries. Agents that go too long or too wide can burn tokens quickly.</p>\n </div>\n </div>\n <div class=\"ax-slider-row\">\n <label>Conversations at once</label>\n <span class=\"ax-hint\">How many people can this agent talk to in parallel? Lower = more focused, more waits.</span>\n <div class=\"ax-opts\" data-field=\"maxConcurrent\">\n <button data-v=\"1\">1</button><button data-v=\"3\">3</button>\n <button data-v=\"5\">5</button><button data-v=\"10\">10</button>\n </div>\n <input id=\"f-maxc\" type=\"hidden\" />\n </div>\n <div class=\"ax-slider-row\">\n <label>Max task length</label>\n <span class=\"ax-hint\">If a reply takes longer than this, the agent gives up. Protects against runaway loops.</span>\n <div class=\"ax-opts\" data-field=\"maxExecutionMinutes\">\n <button data-v=\"5\">5 min</button><button data-v=\"20\">20 min</button>\n <button data-v=\"60\">1 h</button><button data-v=\"0\">no limit</button>\n </div>\n <input id=\"f-maxt\" type=\"hidden\" />\n </div>\n <div class=\"ax-slider-row\">\n <label>Tool permissions</label>\n <span class=\"ax-hint\"><b>Ask first</b> = you approve each tool call. <b>Trusted</b> = it just goes.</span>\n <div class=\"ax-opts\" data-field=\"permissionMode\">\n <button data-v=\"default\">Ask first</button>\n <button data-v=\"acceptEdits\">Accept edits</button>\n <button data-v=\"plan\">Plan only</button>\n <button data-v=\"bypassPermissions\">Trusted</button>\n </div>\n <input id=\"f-pmode\" type=\"hidden\" />\n </div>\n </div>\n </section>\n\n <!-- Personality — left-docked vertical file list + MD editor -->\n <section id=\"tab-identity\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Personality & instructions</h2>\n <p class=\"ax-lead\" style=\"margin:0\">This is the most important thing about your agent. It's how you tell it who it is, what it can and can't do, and how to speak.</p>\n </div>\n </div>\n\n <div class=\"ax-callout\">\n <span class=\"ax-callout__icon\">?</span>\n <div>These files live in the agent's workspace and are injected at the top of every conversation. Markdown is supported; changes hot-reload on the next turn.</div>\n </div>\n\n <div class=\"ax-identity\">\n <!-- Vertical file list -->\n <aside class=\"ax-identity__files\" id=\"identity-files-tabs\"></aside>\n\n <!-- MD editor for the active file -->\n <div class=\"ax-identity__editor\">\n <div class=\"ax-identity__head\">\n <span class=\"mono muted\" id=\"id-path\" style=\"font-size:11.5px\">—</span>\n <span class=\"ax-pbadge\" id=\"id-chars\">0 chars</span>\n <span style=\"flex:1\"></span>\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-save-id\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z\"/><polyline points=\"17 21 17 13 7 13 7 21\"/><polyline points=\"7 3 7 8 15 8\"/></svg>\n Save\n </button>\n </div>\n <textarea id=\"id-editor\"></textarea>\n <div style=\"margin-top:6px;display:flex;align-items:center;gap:8px\">\n <span class=\"mono muted\" id=\"id-saved-note\" style=\"font-size:11px\"></span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Overview-style system-prompt summary (read + quick-edit) -->\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Quick system prompt</h2>\n <p class=\"ax-lead\" style=\"margin:0\">The <code>systemPrompt</code> field in agentx.json, a short one-liner injected before any CLAUDE.md file loads.</p>\n </div>\n </div>\n <textarea class=\"inp\" id=\"sp-editor\" style=\"min-height:120px;line-height:1.55\" placeholder=\"(Optional) A one-liner that sets the top of every prompt.\"></textarea>\n </div>\n </section>\n\n <!-- Skills -->\n <section id=\"tab-skills\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Skills</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Little plug-ins that give this agent new abilities — look up docs, create tickets, summarise PDFs, you name it.</p>\n </div>\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-install-pkg-open\">+ Install package</button>\n </div>\n\n <div class=\"ax-skill-toolbar\">\n <div class=\"ax-search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/></svg>\n <input class=\"inp\" id=\"skill-search\" placeholder=\"Search skills…\" />\n </div>\n <button class=\"ax-btn\" id=\"btn-new-skill\">+ New skill</button>\n </div>\n\n <div class=\"ax-sec-label-d\"><h3 id=\"skills-count-label\">Installed</h3></div>\n <div class=\"ax-skill-grid\" id=\"skills-list\"></div>\n\n <!-- Selected-skill file tree + editor (inline below the grid). -->\n <div id=\"skill-editor-wrap\" style=\"display:none;margin-top:16px\">\n <div class=\"ax-sec-label-d\">\n <h3 id=\"skill-editor-slug\">Skill files</h3>\n <div style=\"display:flex;gap:6px\">\n <button class=\"ax-btn\" id=\"btn-install-deps\" disabled>Install deps…</button>\n <button class=\"ax-btn ax-btn--danger\" id=\"btn-delete-skill\" disabled>Delete skill</button>\n </div>\n </div>\n <div class=\"ax-grid-2\">\n <div class=\"ax-panel\" style=\"padding:14px\">\n <div id=\"skill-tree\"></div>\n </div>\n <div class=\"ax-panel\" style=\"padding:14px\">\n <div id=\"sk-editor-wrap\" style=\"display:none\">\n <div style=\"display:flex;align-items:center;gap:6px;margin-bottom:8px;font-size:11.5px\">\n <span class=\"mono muted\" id=\"sk-path\">—</span>\n <span style=\"flex:1\"></span>\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-save-sk\" style=\"padding:4px 10px;font-size:11.5px\">Save file</button>\n </div>\n <textarea class=\"inp mono\" id=\"sk-editor\" style=\"min-height:300px;line-height:1.55;font-family:var(--ax-mono);font-size:12px\"></textarea>\n </div>\n <div id=\"sk-editor-empty\" style=\"color:var(--ax-muted);padding:20px;text-align:center;font-size:12px\">Select a file in the tree to edit.</div>\n </div>\n </div>\n </div>\n\n <!-- Install-package dispatcher output + dependencies. -->\n <details id=\"pkg-details\" style=\"margin-top:14px\">\n <summary style=\"cursor:pointer;color:var(--ax-muted);font-size:12px;padding:6px 0\">Install from package (advanced) ▾</summary>\n <div class=\"ax-panel\" style=\"margin-top:10px\">\n <div class=\"ax-grid-2\">\n <div class=\"ax-field\">\n <label class=\"lbl\">Package</label>\n <input class=\"inp mono\" id=\"pkg-input\" placeholder=\"owner/repo or owner/repo/skill\" />\n </div>\n <div class=\"ax-field\" style=\"display:flex;align-items:flex-end\">\n <button class=\"ax-btn ax-btn--primary\" id=\"btn-install-pkg\" style=\"width:100%\">Install</button>\n </div>\n </div>\n <pre id=\"dispatch-output\" style=\"margin:10px 0 0;padding:10px;background:var(--ax-bg-elev);border:1px solid var(--ax-border);border-radius:5px;font-family:var(--ax-mono);font-size:11px;color:var(--ax-text-2);max-height:240px;overflow:auto;white-space:pre-wrap;display:none\"></pre>\n </div>\n </details>\n </div>\n </section>\n\n <!-- Channels -->\n <section id=\"tab-channels\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Where this agent lives</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Places this agent shows up. Connections are configured in the global <a href=\"/admin#channels\">Channels</a> tab — here you see which ones route to this agent.</p>\n </div>\n </div>\n <div id=\"channels-list\"></div>\n </div>\n </section>\n\n <!-- Handovers -->\n <section id=\"tab-handovers\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>Route a conversation somewhere else</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Sometimes you'll want to pass a chat to another agent — say Support has been trying to explain pricing, and Sales should take over. <b>Every message from that chat will go to the new agent until you release the handover.</b></p>\n </div>\n </div>\n\n <div class=\"ax-handover-viz\">\n <div class=\"ax-ho-side ax-ho-side--from\">\n <div class=\"ax-ho-side__av\" id=\"ho-from-av\">??</div>\n <div class=\"ax-ho-side__nm\" id=\"ho-from-nm\">__AGENT_ID__</div>\n <div class=\"ax-ho-side__rl\">from</div>\n </div>\n <div class=\"ax-ho-wire\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"9 18 15 12 9 6\"/></svg>\n </div>\n <div class=\"ax-ho-side ax-ho-side--to\">\n <div class=\"ax-ho-side__av\" id=\"ho-to-av\">—</div>\n <div class=\"ax-ho-side__nm\" id=\"ho-to-nm\">pick one</div>\n <div class=\"ax-ho-side__rl\">to</div>\n </div>\n </div>\n\n <div class=\"ax-grid-3\">\n <div class=\"ax-field\">\n <label class=\"lbl\">Channel</label>\n <select class=\"inp\" id=\"ho-channel\">\n <option value=\"telegram\">Telegram</option>\n <option value=\"whatsapp\">WhatsApp</option>\n </select>\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Chat id <span class=\"opt\">who's the person?</span></label>\n <input class=\"inp mono\" id=\"ho-chatid\" placeholder=\"1816212449 or +21612345678\" />\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Hand off to</label>\n <select class=\"inp\" id=\"ho-target\"></select>\n </div>\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Account <span class=\"opt\">optional, e.g. \"default\" for a multi-bot setup</span></label>\n <input class=\"inp mono\" id=\"ho-account\" placeholder=\"default\" />\n </div>\n <div class=\"ax-field\">\n <label class=\"lbl\">Brief the receiving agent <span class=\"opt\">one-shot note</span></label>\n <textarea class=\"inp\" id=\"ho-summary\" style=\"min-height:100px\" placeholder=\"Customer is asking about enterprise pricing. They've already seen the standard tier...\"></textarea>\n <div class=\"hint\">The note is injected into the next reply's context. The agent will know <i>why</i> it received the handover.</div>\n </div>\n <div class=\"ax-grid-2\">\n <div class=\"ax-field\" style=\"margin-bottom:0\">\n <label class=\"lbl\">Expires <span class=\"opt\">ISO date; blank = never</span></label>\n <input class=\"inp mono\" id=\"ho-expires\" placeholder=\"2026-04-20T00:00:00Z\" />\n </div>\n <div style=\"display:flex;align-items:flex-end;justify-content:flex-end\">\n <button class=\"ax-btn ax-btn--primary lg\" id=\"btn-handover\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"/><polygon points=\"22 2 15 22 11 13 2 9 22 2\"/></svg>\n Hand off\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\"><div><h2>Currently routed elsewhere</h2><p class=\"ax-lead\" style=\"margin:0\">Chats this agent has handed to someone else.</p></div></div>\n <div id=\"ho-outgoing\"></div>\n </div>\n\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\"><div><h2>Taken over from somewhere</h2><p class=\"ax-lead\" style=\"margin:0\">Chats other agents routed <i>to</i> this agent.</p></div></div>\n <div id=\"ho-incoming\"></div>\n </div>\n </section>\n\n <!-- Activity -->\n <section id=\"tab-activity\" class=\"ax-panel__tab\">\n <div class=\"ax-panel\">\n <div class=\"ax-panel__head\">\n <div>\n <h2>How it's doing</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Activity across every channel this agent answers from.</p>\n </div>\n </div>\n <div id=\"activity\"></div>\n </div>\n </section>\n\n <!-- Danger zone -->\n <section id=\"tab-danger\" class=\"ax-panel__tab\">\n <div class=\"ax-panel ax-panel--danger\">\n <div class=\"ax-panel__head\">\n <div>\n <h2 style=\"color:var(--ax-err)\">Danger zone</h2>\n <p class=\"ax-lead\" style=\"margin:0\">Actions here are hard to undo. They affect this agent only — other agents are safe.</p>\n </div>\n </div>\n <div class=\"ax-danger-item\">\n <div class=\"ax-danger-item__info\">\n <div class=\"ax-danger-item__name\" style=\"color:var(--ax-err)\">Delete this agent</div>\n <div class=\"ax-danger-item__desc\">Permanently removes the agent from agentx.json. The workspace folder on disk is preserved; the config entry is gone. You'll need to recreate it from scratch to bring it back.</div>\n </div>\n <button class=\"ax-btn ax-btn--danger\" id=\"btn-delete-agent\">Delete agent…</button>\n </div>\n </div>\n </section>\n\n </div>\n\n\n</div>\n\n<!-- TEST DRIVE SIDE PANEL — docked to the right, toggled by the Test drive\n button in the hero. Closed by default; slides in when opened. -->\n<aside class=\"ax-td-panel\" id=\"td-panel\" aria-hidden=\"true\">\n <div class=\"ax-td-head\">\n <div class=\"ax-td-head__dot\"></div>\n <div>\n <div class=\"ax-td-head__t\">Test drive</div>\n <div class=\"ax-td-head__sub\">not recorded · sandbox</div>\n </div>\n <div style=\"flex:1\"></div>\n <div class=\"ax-td-head__seg\">\n <button class=\"is-active\">Live</button>\n <button>Dry</button>\n </div>\n <button class=\"ax-btn ax-btn--ghost\" id=\"td-close\" aria-label=\"Close\" style=\"padding:4px 9px;font-size:14px;line-height:1;margin-left:4px\">×</button>\n </div>\n\n <div class=\"ax-td-scenarios\">\n <div class=\"ax-td-scenarios__lbl\">Quick scenarios</div>\n <span class=\"ax-td-scenario\" onclick=\"sendScenario(this.textContent)\">Who are you?</span>\n <span class=\"ax-td-scenario\" onclick=\"sendScenario(this.textContent)\">What can you help with?</span>\n <span class=\"ax-td-scenario\" onclick=\"sendScenario(this.textContent)\">Show me your skills</span>\n </div>\n\n <div class=\"ax-td-chat\" id=\"td-chat\">\n <div class=\"ax-td-chat__msg-wrap is-bot\">\n <div class=\"ax-td-chat__bubble is-bot\" id=\"td-greeting\">Hey! I'm ready. Send a message to try me out.</div>\n <div class=\"ax-td-chat__meta\">Sandbox · nothing here is recorded</div>\n </div>\n </div>\n\n <form class=\"ax-td-input\" onsubmit=\"sendTestMessage(event)\">\n <input id=\"td-input-el\" placeholder=\"Test a message…\" autocomplete=\"off\"/>\n <button type=\"submit\">Send</button>\n </form>\n</aside>\n\n<!-- Confirm modal (legacy — used by install/delete flows) -->\n<div class=\"ax-modal-bd\" id=\"confirm-modal-bd\">\n <div class=\"ax-modal\" style=\"width:min(480px,94vw)\">\n <header>\n <h3 id=\"confirm-title\">Confirm</h3>\n <button class=\"ax-modal__close\" onclick=\"closeConfirm()\">×</button>\n </header>\n <div class=\"ax-modal__body\" id=\"confirm-detail\"></div>\n <div class=\"ax-modal__foot\">\n <button class=\"ax-btn\" id=\"confirm-cancel\" onclick=\"closeConfirm()\">Cancel</button>\n <button class=\"ax-btn ax-btn--primary\" id=\"confirm-ok\">Confirm</button>\n </div>\n </div>\n</div>\n`\nconst AGENT_PAGE_CSS = `\n/* Agent-specific layout + legacy styles for bits the migration left in\n * place (EasyMDE theming, skills tree, plain-editor). Shared chrome + tab\n * primitives live in components.css.ts. */\n\n/* Personality tab — vertical file list on the left, MD editor on the right */\n.ax-identity {\n display: grid; grid-template-columns: 220px 1fr; gap: 16px; align-items: flex-start;\n}\n.ax-identity__files {\n display: flex; flex-direction: column; gap: 2px;\n background: var(--ax-bg); border: 1px solid var(--ax-border);\n border-radius: var(--ax-radius-sm); padding: 6px;\n}\n.ax-identity__files button {\n background: transparent; border: 0; color: var(--ax-text-2);\n padding: 8px 10px; font: inherit; font-size: 12.5px; cursor: pointer;\n border-radius: 5px; text-align: left;\n display: flex; align-items: center; gap: 8px;\n font-family: var(--ax-mono);\n}\n.ax-identity__files button:hover { background: var(--ax-surface); color: var(--ax-text); }\n.ax-identity__files button.is-active {\n background: color-mix(in oklch, var(--ax-accent) 14%, var(--ax-surface));\n color: var(--ax-accent); font-weight: 500;\n}\n.ax-identity__files button .ax-f-glyph {\n color: var(--ax-muted); font-size: 11px; width: 14px; text-align: center;\n}\n.ax-identity__files button.is-active .ax-f-glyph { color: var(--ax-accent); }\n.ax-identity__files button .ax-f-new {\n margin-left: auto; font-size: 10px; color: var(--ax-muted);\n letter-spacing: 0.06em; text-transform: uppercase;\n font-family: var(--ax-mono);\n}\n.ax-identity__editor {\n background: var(--ax-bg); border: 1px solid var(--ax-border);\n border-radius: var(--ax-radius-sm); padding: 10px;\n}\n.ax-identity__head {\n display: flex; align-items: center; gap: 10px; margin-bottom: 8px;\n padding-bottom: 8px; border-bottom: 1px solid var(--ax-border);\n}\n\n/* EasyMDE theme tweaks to match AgentX tokens */\n.EasyMDEContainer .editor-toolbar {\n border-color: var(--ax-border) !important;\n background: var(--ax-surface); border-radius: 5px 5px 0 0;\n}\n.EasyMDEContainer .editor-toolbar button {\n color: var(--ax-text-2) !important;\n}\n.EasyMDEContainer .editor-toolbar button:hover,\n.EasyMDEContainer .editor-toolbar button.active {\n background: var(--ax-surface-2) !important; color: var(--ax-accent) !important;\n border-color: var(--ax-border-2) !important;\n}\n.EasyMDEContainer .editor-toolbar i.separator {\n border-color: var(--ax-border) !important;\n}\n.EasyMDEContainer .CodeMirror {\n border-color: var(--ax-border) !important;\n background: var(--ax-bg-elev) !important;\n color: var(--ax-text) !important;\n font-family: var(--ax-mono) !important;\n font-size: 12.5px;\n border-radius: 0 0 5px 5px;\n}\n.EasyMDEContainer .CodeMirror-cursor { border-left-color: var(--ax-accent) !important; }\n.EasyMDEContainer .CodeMirror-selected { background: color-mix(in oklch, var(--ax-accent) 25%, transparent) !important; }\n.EasyMDEContainer .editor-statusbar { color: var(--ax-muted) !important; border-color: var(--ax-border) !important; }\n.EasyMDEContainer .editor-preview,\n.EasyMDEContainer .editor-preview-side {\n background: var(--ax-bg-elev) !important; color: var(--ax-text) !important;\n border-color: var(--ax-border) !important;\n}\n.EasyMDEContainer.sided--no-fullscreen .editor-preview-side {\n border-color: var(--ax-border) !important;\n}\n.cm-s-easymde .cm-header, .cm-s-easymde .cm-hr { color: var(--ax-accent) !important; }\n.cm-s-easymde .cm-keyword, .cm-s-easymde .cm-link { color: var(--ax-info) !important; }\n.cm-s-easymde .cm-string, .cm-s-easymde .cm-strong { color: var(--ax-text) !important; }\n.cm-s-easymde .cm-comment { color: var(--ax-muted) !important; }\n\n@media (max-width: 900px) {\n .ax-identity { grid-template-columns: 1fr; }\n}\n\n.msg { font-size: 12px; padding: 8px 12px; border-radius: 4px; margin-bottom: 12px; display: none; }\n.msg.ok { background: color-mix(in oklch, var(--ax-accent) 12%, transparent); color: var(--ax-accent); display: block; }\n.msg.err { background: color-mix(in oklch, var(--ax-err) 12%, transparent); color: var(--ax-err); display: block; }\n\n/* Form labels + hints used inside panels */\nlabel.lbl, .lbl {\n display: block; font-size: 11px; color: var(--ax-muted);\n letter-spacing: 0.03em; text-transform: uppercase; margin: 0 0 6px;\n font-weight: 500;\n}\nlabel.lbl .opt, .lbl .opt {\n text-transform: none; letter-spacing: 0; font-weight: 400;\n opacity: 0.75; margin-left: 6px;\n}\ninput.inp, textarea.inp, select.inp {\n width: 100%; background: var(--ax-bg); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 5px;\n padding: 8px 11px; font: inherit; font-size: 13px;\n transition: all 120ms;\n}\ninput.inp:focus, textarea.inp:focus, select.inp:focus {\n outline: none; border-color: var(--ax-accent);\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ax-accent) 18%, transparent);\n}\ntextarea.inp { min-height: 100px; line-height: 1.55; resize: vertical; font-family: inherit; }\ntextarea.inp.mono { font-family: var(--ax-mono); }\n.ax-field { margin-bottom: 14px; }\n.hint { font-size: 11.5px; color: var(--ax-muted); margin-top: 5px; line-height: 1.5; }\n\n/* Agent-specific button variant used in rail context (no ax-btn wrapper) */\n.ax-btn.lg { padding: 9px 16px; font-size: 13px; }\n`\nconst AGENT_PAGE_SCRIPT = `\nconst $ = (id) => document.getElementById(id);\nconst state = { agent: null, activeSkill: null, activeSkillFile: null, identityPath: null };\n\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,c=>({\"&\":\"&\",\"<\":\"<\",\">\":\">\",'\"':\""\",\"'\":\"'\"}[c]));}\n\nasync function req(method, path, body) {\n const headers = { 'Content-Type': 'application/json' };\n if (window.AX_LOCAL_TOKEN) headers['Authorization'] = 'Bearer ' + window.AX_LOCAL_TOKEN;\n const r = await fetch(path, { method, headers, body: body ? JSON.stringify(body) : undefined });\n const data = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));\n return data;\n}\n\nfunction showMsg(kind, text){ const m = $('msg'); m.className='msg '+kind; m.textContent=text; setTimeout(()=>{m.className='msg';}, 4000); }\n\n// --- confirm modal ---\nfunction confirmAction(title, detail, onConfirm){\n $('confirm-title').textContent = title;\n $('confirm-detail').textContent = detail;\n const bd = $('confirm-modal-bd');\n if (bd) bd.classList.add('is-show');\n $('confirm-ok').onclick = () => { closeConfirm(); onConfirm(); };\n}\nfunction closeConfirm(){ const bd = $('confirm-modal-bd'); if (bd) bd.classList.remove('is-show'); }\nwindow.closeConfirm = closeConfirm;\n\n// --- Tabs (rail) ---\nfunction switchTab(name) {\n document.querySelectorAll('.ax-rail a').forEach(a => a.classList.toggle('is-active', a.dataset.tab === name));\n document.querySelectorAll('.ax-panel__tab').forEach(s => s.classList.toggle('is-active', s.id === 'tab-' + name));\n try { localStorage.setItem('ax-agent-tab', name); } catch(_) {}\n if (name === 'skills' && !state.skillsLoaded) { loadSkills(); state.skillsLoaded = true; }\n if (name === 'identity' && !state.identityLoaded) { loadIdentity(); state.identityLoaded = true; }\n if (name === 'channels') loadChannels();\n if (name === 'handovers') loadHandovers();\n if (name === 'activity') loadActivity();\n window.scrollTo({ top: 0, behavior: 'smooth' });\n}\ndocument.querySelectorAll('.ax-rail a[data-tab]').forEach(a => {\n a.addEventListener('click', (e) => { e.preventDefault(); switchTab(a.dataset.tab); });\n});\ntry {\n const s = localStorage.getItem('ax-agent-tab');\n if (s && document.querySelector('#tab-' + s)) switchTab(s);\n} catch(_) {}\n\n// --- Hero avatar initials/color mirror the Settings agents list helpers. ---\nfunction initialsOf(name, id) {\n const src = (name || id || '?').replace(/[^a-zA-Z ]/g, '').trim();\n const parts = src.split(/\\\\s+/).filter(Boolean);\n if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();\n return (parts[0] || id || '?').slice(0, 2).toUpperCase();\n}\n\n// --- Trigger-pill editing: render from a string array into #f-triggers.\n// Persists the canonical value as comma-separated into the hidden #f-mentions\n// input so saveAgent() doesn't need new wiring. ---\nfunction renderTriggers(mentions) {\n const host = $('f-triggers');\n if (!host) return;\n // Remove existing pills (keep the <input> at the end).\n host.querySelectorAll('.ax-trig-pill').forEach(n => n.remove());\n const input = host.querySelector('input');\n for (const m of (mentions || [])) {\n const pill = document.createElement('span');\n pill.className = 'ax-trig-pill';\n pill.innerHTML = esc(m) + ' <button type=\"button\">×</button>';\n pill.querySelector('button').addEventListener('click', (e) => {\n e.stopPropagation();\n pill.remove();\n syncTriggersHidden();\n });\n host.insertBefore(pill, input);\n }\n syncTriggersHidden();\n}\nfunction syncTriggersHidden() {\n const pills = [...document.querySelectorAll('#f-triggers .ax-trig-pill')]\n .map(p => p.firstChild?.textContent?.trim() || p.textContent.replace(/×$/, '').trim())\n .filter(Boolean);\n const hidden = $('f-mentions');\n if (hidden) hidden.value = pills.join(', ');\n}\nwindow.addTrigger = function(e) {\n if (e.key !== 'Enter') return;\n e.preventDefault();\n const v = e.target.value.trim();\n if (!v) return;\n const input = e.target;\n const pill = document.createElement('span');\n pill.className = 'ax-trig-pill';\n pill.innerHTML = esc(v) + ' <button type=\"button\">×</button>';\n pill.querySelector('button').addEventListener('click', (ev) => {\n ev.stopPropagation(); pill.remove(); syncTriggersHidden();\n });\n input.parentElement.insertBefore(pill, input);\n input.value = '';\n syncTriggersHidden();\n};\n\n// Slider-row segmented toggles: one button per value. Click writes the\n// value into the sibling hidden input so the existing saveAgent() flow reads\n// it. Call on page load to hydrate from state.\nfunction initOptsToggles() {\n document.querySelectorAll('.ax-opts').forEach((grp) => {\n grp.addEventListener('click', (e) => {\n const b = e.target.closest('button');\n if (!b || !grp.contains(b)) return;\n grp.querySelectorAll('button').forEach(x => x.classList.toggle('is-active', x === b));\n // Find the hidden input that the parent slider-row owns\n const row = grp.closest('.ax-slider-row');\n if (!row) return;\n const hidden = row.querySelector('input[type=hidden]');\n if (hidden) hidden.value = b.dataset.v || '';\n });\n });\n}\nfunction setOpt(field, value) {\n const v = String(value);\n document.querySelectorAll('.ax-opts[data-field=\"' + field + '\"] button').forEach((b) => {\n b.classList.toggle('is-active', String(b.dataset.v || '') === v);\n });\n // also populate the hidden input\n const row = document.querySelector('.ax-opts[data-field=\"' + field + '\"]')?.closest('.ax-slider-row');\n const hidden = row?.querySelector('input[type=hidden]');\n if (hidden) hidden.value = v;\n}\n\n// --- Overview ---\nasync function loadAgent(){\n try {\n const a = await req('GET', '/api/admin/agent/' + AGENT_ID);\n state.agent = a;\n\n // Hero\n const displayName = a.name || AGENT_ID;\n $('a-name').textContent = displayName;\n $('a-id').textContent = AGENT_ID;\n const crumb = $('a-crumb-name'); if (crumb) crumb.textContent = displayName;\n const ava = $('a-ava');\n if (ava) {\n // Strip the pulse dot, rewrite initials, keep the dot.\n ava.textContent = initialsOf(a.name, AGENT_ID);\n const dot = document.createElement('span');\n dot.className = 'ax-hero__on';\n ava.appendChild(dot);\n }\n // Hero chips (tier/access/public-API)\n const chipsParts = [];\n chipsParts.push('<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>online</span>');\n if (a.access === 'public') chipsParts.push('<span class=\"ax-pill ax-pill--info\">public API</span>');\n if (a.tier) chipsParts.push('<span class=\"ax-pill\">' + esc(a.tier) + '</span>');\n $('a-chips').innerHTML = chipsParts.join(' ');\n\n // Overview form\n $('f-name').value = a.name || '';\n $('f-tier').value = a.tier || 'claude-code';\n $('f-model').value = a.model || '';\n $('f-access').value = a.access || 'private';\n setOpt('maxConcurrent', a.maxConcurrent ?? 1);\n setOpt('maxExecutionMinutes', a.maxExecutionMinutes ?? 20);\n setOpt('permissionMode', a.permissionMode || 'default');\n renderTriggers(a.mentions || []);\n\n // Quick system prompt + personality editor share the systemPrompt field.\n const sp = $('sp-editor'); if (sp) sp.value = a.systemPrompt || '';\n\n // Stats placeholders; loadActivity() fills real numbers once the tab is visited.\n const handling = (a.runningTasks?.length) || 0;\n $('stat-handling').textContent = String(handling);\n } catch (e) { showMsg('err', e.message); }\n}\n\n$('btn-save-meta').addEventListener('click', async () => {\n const body = {\n name: $('f-name').value.trim(),\n tier: $('f-tier').value,\n model: $('f-model').value.trim(),\n access: $('f-access').value,\n maxConcurrent: Number($('f-maxc').value) || 1,\n maxExecutionMinutes: Number($('f-maxt').value) || 20,\n permissionMode: $('f-pmode').value,\n mentions: $('f-mentions').value.split(',').map(s=>s.trim()).filter(Boolean),\n systemPrompt: $('sp-editor').value,\n };\n try {\n await req('PATCH', '/api/admin/agent/' + AGENT_ID, body);\n showMsg('ok', 'Saved. Restart the daemon for tier/model changes to take effect.');\n } catch (e) { showMsg('err', e.message); }\n});\n\n// --- Identity (Personality tab) ---\n// Vertical file list + EasyMDE editor. The file list stays docked on the\n// left; clicking a file swaps the editor's contents. Unsaved edits are\n// persisted per-file in state.idDrafts so switching doesn't lose them.\nlet idMde = null;\nstate.idDrafts = {};\n\nasync function loadIdentity(){\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/identity');\n const host = $('identity-files-tabs');\n host.innerHTML = '';\n for (const f of r.identity) {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.dataset.path = f.path;\n // Derive a small 2-3 char glyph from the filename (CLAUDE.md → CL, etc.).\n const base = (f.path.split('/').pop() || f.path).replace(/\\\\.md$/i, '');\n const glyph = base.slice(0, 2).toUpperCase();\n btn.innerHTML =\n '<span class=\"ax-f-glyph\">' + esc(glyph) + '</span>' +\n '<span>' + esc(f.title) + '</span>' +\n (f.exists ? '' : '<span class=\"ax-f-new\">new</span>');\n btn.addEventListener('click', () => {\n host.querySelectorAll('button').forEach(x => x.classList.toggle('is-active', x === btn));\n loadIdentityFile(f.path);\n });\n host.appendChild(btn);\n }\n if (r.identity.length) {\n host.firstElementChild.classList.add('is-active');\n await loadIdentityFile(r.identity[0].path);\n }\n } catch (e) { showMsg('err', e.message); }\n}\n\nfunction ensureIdMde() {\n if (idMde) return idMde;\n if (typeof EasyMDE === 'undefined') return null;\n const el = document.getElementById('id-editor');\n if (!el) return null;\n idMde = new EasyMDE({\n element: el, spellChecker: false, autoDownloadFontAwesome: true,\n status: ['lines','words'], autofocus: false,\n minHeight: '360px',\n toolbar: ['bold','italic','heading','|','quote','code','unordered-list','ordered-list','|','link','table','|','preview','side-by-side','fullscreen','|','guide'],\n });\n idMde.codemirror.on('change', () => {\n updateIdCharCount();\n if (state.identityPath) state.idDrafts[state.identityPath] = idMde.value();\n });\n return idMde;\n}\n\nasync function loadIdentityFile(path){\n // Stash current draft before switching\n if (state.identityPath && idMde) state.idDrafts[state.identityPath] = idMde.value();\n state.identityPath = path;\n $('id-path').textContent = path;\n try {\n const f = await req('GET', '/api/admin/agent/' + AGENT_ID + '/identity/file?path=' + encodeURIComponent(path));\n const mde = ensureIdMde();\n const initial = state.idDrafts[path] !== undefined ? state.idDrafts[path] : (f.content || '');\n if (mde) mde.value(initial);\n else $('id-editor').value = initial;\n updateIdCharCount();\n } catch (e) {\n showMsg('err', e.message);\n if (idMde) idMde.value(''); else $('id-editor').value = '';\n }\n}\n\nfunction updateIdCharCount() {\n const val = idMde ? idMde.value() : ($('id-editor')?.value || '');\n const chars = $('id-chars');\n if (chars) chars.textContent = val.length.toLocaleString() + ' chars';\n}\n\n$('btn-save-id').addEventListener('click', async () => {\n if (!state.identityPath) return;\n const content = idMde ? idMde.value() : $('id-editor').value;\n try {\n await req('PUT', '/api/admin/agent/' + AGENT_ID + '/identity/file', {\n path: state.identityPath, content,\n });\n showMsg('ok', 'Saved ' + state.identityPath);\n const note = $('id-saved-note');\n if (note) note.textContent = 'saved ' + new Date().toLocaleTimeString();\n // Clear the draft now that it's persisted\n delete state.idDrafts[state.identityPath];\n } catch (e) { showMsg('err', e.message); }\n});\n\n// --- Skills ---\nasync function loadSkills(){\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/skills');\n const list = $('skills-list');\n const countLabel = $('skills-count-label');\n const railCount = $('rail-skills-count');\n if (!r.skills.length) {\n list.innerHTML = '<div class=\"ax-empty-card\" style=\"grid-column:1/-1;text-align:center;padding:32px;background:var(--ax-bg-elev);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><p style=\"margin:0;font-size:13px\">No skills installed yet. Install a package below or create a new one.</p></div>';\n if (countLabel) countLabel.textContent = 'Installed';\n if (railCount) railCount.textContent = '0';\n } else {\n list.innerHTML = r.skills.map(s =>\n '<div class=\"ax-skill-card is-on\" data-slug=\"' + esc(s.slug) + '\">' +\n '<div class=\"ax-skill-card__top\">' +\n '<div class=\"ax-skill-card__icon\">' +\n '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2\"/></svg>' +\n '</div>' +\n '<div><div class=\"ax-skill-card__name\">' + esc(s.title || s.slug) + '</div><div class=\"ax-skill-card__slug\">' + esc(s.slug) + '</div></div>' +\n '</div>' +\n '<div class=\"ax-skill-card__desc\">' + esc(s.description || 'No description.') + '</div>' +\n '<div class=\"ax-skill-card__foot\"><span>' + esc(s.source || 'installed') + '</span><span>' + esc(s.version || '') + '</span></div>' +\n '</div>'\n ).join('');\n list.querySelectorAll('[data-slug]').forEach(el =>\n el.addEventListener('click', () => selectSkill(el.dataset.slug))\n );\n if (countLabel) countLabel.textContent = 'Installed — ' + r.skills.length + ' active';\n if (railCount) railCount.textContent = String(r.skills.length);\n }\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function selectSkill(slug){\n state.activeSkill = slug;\n state.activeSkillFile = null;\n document.querySelectorAll('#skills-list [data-slug]').forEach(r=>{\n r.classList.toggle('ax-skill-card--active', r.dataset.slug === slug);\n });\n // Reveal the tree + editor area and set the section label\n const wrap = $('skill-editor-wrap');\n if (wrap) wrap.style.display = 'block';\n const slugLabel = $('skill-editor-slug');\n if (slugLabel) slugLabel.textContent = slug;\n $('btn-delete-skill').disabled = false;\n $('btn-install-deps').disabled = true;\n $('sk-editor-wrap').style.display = 'none';\n $('sk-editor-empty').style.display = 'block';\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/skills/tree?slug=' + encodeURIComponent(slug));\n $('btn-install-deps').disabled = !r.deps?.manager;\n $('btn-install-deps').title = r.deps?.command || 'No package manifest detected';\n const host = $('skill-tree');\n host.innerHTML = r.tree.map(e => {\n const depth = (e.path.match(/\\\\//g) || []).length;\n const pad = 'padding-left:' + (depth * 10) + 'px';\n const icon = e.kind === 'dir' ? '▸' : '·';\n const cls = 'tree-entry ' + e.kind + (e.editable ? '' : ' readonly');\n return '<div class=\"'+cls+'\" style=\"'+pad+'\" data-path=\"'+esc(e.path)+'\" data-editable=\"'+(e.editable?'1':'0')+'\">'\n + '<span>'+icon+' '+esc(e.path.split('/').filter(Boolean).pop() || e.path)+'</span>'\n + '<span style=\"color:var(--ax-muted);font-size:10px\">'+(e.kind==='file'?formatBytes(e.size):'')+'</span>'\n + '</div>';\n }).join('');\n host.querySelectorAll('[data-path]').forEach(r=>r.addEventListener('click', ()=>{\n if (r.dataset.editable === '1') openSkillFile(r.dataset.path);\n }));\n // Auto-open SKILL.md\n const skillMd = r.tree.find(e => e.path === 'SKILL.md' && e.editable);\n if (skillMd) openSkillFile('SKILL.md');\n } catch (e) { showMsg('err', e.message); }\n}\n\nfunction formatBytes(n){ if (!n) return ''; if (n < 1024) return n+'b'; if (n<1048576) return (n/1024).toFixed(1)+'k'; return (n/1048576).toFixed(1)+'M'; }\n\nasync function openSkillFile(path){\n state.activeSkillFile = path;\n $('sk-path').textContent = state.activeSkill + '/' + path;\n $('sk-editor-empty').style.display = 'none';\n $('sk-editor-wrap').style.display = 'block';\n document.querySelectorAll('#skill-tree [data-path]').forEach(r=>r.classList.toggle('active', r.dataset.path === path));\n try {\n const f = await req('GET', '/api/admin/agent/' + AGENT_ID + '/skills/file?slug=' + encodeURIComponent(state.activeSkill) + '&path=' + encodeURIComponent(path));\n $('sk-editor').value = f.content || '';\n } catch (e) { showMsg('err', e.message); }\n}\n\n$('btn-save-sk').addEventListener('click', async () => {\n if (!state.activeSkill || !state.activeSkillFile) return;\n const content = $('sk-editor').value;\n try {\n await req('PUT', '/api/admin/agent/' + AGENT_ID + '/skills/file', {\n slug: state.activeSkill, path: state.activeSkillFile, content,\n });\n showMsg('ok', 'Saved ' + state.activeSkill + '/' + state.activeSkillFile);\n } catch (e) { showMsg('err', e.message); }\n});\n\n$('btn-new-skill').addEventListener('click', async () => {\n const slug = prompt('New skill slug (lowercase, letters/digits/-/_):');\n if (!slug) return;\n try {\n await req('POST', '/api/admin/agent/' + AGENT_ID + '/skills', { slug });\n showMsg('ok', 'Skill created: ' + slug);\n await loadSkills();\n selectSkill(slug);\n } catch (e) { showMsg('err', e.message); }\n});\n\n$('btn-delete-skill').addEventListener('click', () => {\n if (!state.activeSkill) return;\n const slug = state.activeSkill;\n confirmAction(\n 'Delete skill \"' + slug + '\"?',\n 'This removes .claude/skills/' + slug + ' and all files inside it. This cannot be undone.',\n async () => {\n try {\n await req('DELETE', '/api/admin/agent/' + AGENT_ID + '/skills', { slug });\n state.activeSkill = null;\n state.activeSkillFile = null;\n $('skill-tree').innerHTML = '';\n $('sk-editor-wrap').style.display = 'none';\n $('sk-editor-empty').style.display = 'block';\n $('btn-delete-skill').disabled = true;\n $('btn-install-deps').disabled = true;\n showMsg('ok', 'Deleted ' + slug);\n loadSkills();\n } catch (e) { showMsg('err', e.message); }\n }\n );\n});\n\n$('btn-install-pkg').addEventListener('click', () => {\n const pkg = $('pkg-input').value.trim();\n if (!pkg) { showMsg('err', 'Enter a package like \"owner/repo\".'); return; }\n const worker = state.agent?.draftAgent || state.agent?.defaultWorker || 'devops-agent';\n confirmAction(\n 'Install skills.sh package?',\n 'Package: ' + pkg + '\\\\n\\\\nWorker agent: ' + worker + '\\\\n\\\\nThe worker will run: npx skills add ' + pkg + '\\\\n\\\\nInside: ' + (state.agent?.workspace || '(agent workspace)') + '\\\\n\\\\nOutput will appear below.',\n async () => {\n $('dispatch-output').style.display = 'block';\n $('dispatch-output').textContent = 'Dispatching to ' + worker + '…';\n try {\n const r = await req('POST', '/api/admin/agent/' + AGENT_ID + '/skills/install', { package: pkg });\n $('dispatch-output').textContent = (r.content || '(no output)') + '\\\\n\\\\n-- done --';\n loadSkills();\n } catch (e) { $('dispatch-output').textContent = 'Error: ' + e.message; }\n }\n );\n});\n\n$('btn-install-deps').addEventListener('click', async () => {\n if (!state.activeSkill) return;\n const slug = state.activeSkill;\n try {\n const hint = await req('GET', '/api/admin/agent/' + AGENT_ID + '/skills/deps?slug=' + encodeURIComponent(slug));\n if (!hint.manager) { showMsg('err', 'No package manifest in skill dir.'); return; }\n const worker = state.agent?.draftAgent || state.agent?.defaultWorker || 'devops-agent';\n confirmAction(\n 'Install dependencies?',\n 'Skill: ' + slug + '\\\\nManifest: ' + hint.file + '\\\\nCommand: ' + hint.command + '\\\\n\\\\nWorker agent: ' + worker + '\\\\n\\\\nRunning install scripts executes third-party code. Confirm only if you trust this skill\\\\'s source.',\n async () => {\n $('dispatch-output').style.display = 'block';\n $('dispatch-output').textContent = 'Dispatching to ' + worker + '…';\n try {\n const r = await req('POST', '/api/admin/agent/' + AGENT_ID + '/skills/deps', { slug });\n $('dispatch-output').textContent = (r.content || '(no output)') + '\\\\n\\\\n-- done --';\n } catch (e) { $('dispatch-output').textContent = 'Error: ' + e.message; }\n }\n );\n } catch (e) { showMsg('err', e.message); }\n});\n\n// --- Channels + Activity ---\nconst CH_LOGO = {\n telegram: { bg: '#229ED9', init: 'TG' },\n whatsapp: { bg: '#25D366', init: 'WA' },\n slack: { bg: '#4A154B', init: 'SL' },\n discord: { bg: '#5865F2', init: 'DC' },\n gitlab: { bg: '#FC6D26', init: 'GL' },\n github: { bg: '#24292e', init: 'GH' },\n webhook: { bg: 'var(--ax-surface-3)', init: 'WH' },\n};\n\nasync function loadChannels(){\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/channels');\n const host = $('channels-list');\n const railCount = $('rail-channels-count');\n if (railCount) railCount.textContent = String(r.bindings.length);\n if (!r.bindings.length) {\n host.innerHTML = '<div class=\"ax-empty-card\" style=\"text-align:center;padding:32px;background:var(--ax-bg-elev);border:1px dashed var(--ax-border-2);border-radius:var(--ax-radius-lg);color:var(--ax-muted)\"><p style=\"margin:0;font-size:13px\">No channels route to this agent yet. <a href=\"/admin#channels\">Wire one up in Settings → Channels</a>.</p></div>';\n return;\n }\n host.innerHTML = r.bindings.map((b) => {\n const meta = CH_LOGO[b.channel] || { bg: 'var(--ax-surface-3)', init: b.channel.slice(0, 2).toUpperCase() };\n return '<div class=\"ax-chan-row\">' +\n '<div class=\"ax-chan-row__logo\" style=\"background:' + meta.bg + '\">' + meta.init + '</div>' +\n '<div class=\"ax-chan-row__info\">' +\n '<div class=\"ax-chan-row__name\">' + esc(b.channel) + (b.account ? ' · ' + esc(b.account) : '') + '</div>' +\n '<div class=\"ax-chan-row__desc\">' + esc(b.detail || '—') + '</div>' +\n '</div>' +\n '<span class=\"ax-pill ax-pill--ok\"><span class=\"ax-pill__dot\"></span>bound</span>' +\n '</div>';\n }).join('');\n } catch (e) { showMsg('err', e.message); }\n}\n\nasync function loadHandovers(){\n try {\n // Populate the \"Transfer to\" dropdown with every local agent except self.\n const targets = $('ho-target');\n if (!targets.options.length) {\n const raw = await fetch('/api/admin/state', { headers: window.AX_LOCAL_TOKEN ? { Authorization: 'Bearer ' + window.AX_LOCAL_TOKEN } : {} });\n const st = await raw.json();\n targets.innerHTML = '<option value=\"\">Pick a target…</option>' + (st.agents || [])\n .filter(a => a.id !== AGENT_ID)\n .map(a => '<option value=\"'+esc(a.id)+'\" data-name=\"'+esc(a.name || a.id)+'\">'+esc(a.name || a.id)+'</option>')\n .join('');\n targets.addEventListener('change', () => {\n const sel = targets.options[targets.selectedIndex];\n const name = sel?.dataset?.name || sel?.textContent || '—';\n const id = targets.value;\n $('ho-to-nm').textContent = name;\n $('ho-to-av').textContent = id ? initialsOf(name, id) : '—';\n });\n }\n // Hydrate the \"from\" side from the current agent\n const a = state.agent;\n if (a) {\n $('ho-from-av').textContent = initialsOf(a.name, AGENT_ID);\n $('ho-from-nm').textContent = a.name || AGENT_ID;\n }\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/handovers');\n const railCount = $('rail-handovers-count');\n if (railCount) railCount.textContent = String((r.incoming?.length || 0) + (r.outgoing?.length || 0));\n renderHandoverList($('ho-incoming'), r.incoming, 'incoming');\n renderHandoverList($('ho-outgoing'), r.outgoing, 'outgoing');\n } catch (e) { showMsg('err', e.message); }\n}\n\nfunction renderHandoverList(host, list, direction){\n if (!list.length) {\n host.innerHTML = '<div style=\"text-align:center;padding:20px;color:var(--ax-muted);font-size:12.5px;font-style:italic\">' + (direction === 'incoming'\n ? 'No chats are currently routed to this agent.'\n : 'No chats are currently routed away from this agent.') + '</div>';\n return;\n }\n host.innerHTML = list.map(o => {\n const who = direction === 'incoming'\n ? '<b>' + esc(o.chatId) + '</b> from <b>' + esc(o.fromAgent) + '</b>'\n : '<b>' + esc(o.chatId) + '</b> → handed to <b>' + esc(o.toAgent) + '</b>';\n const expires = o.expiresAt ? 'expires ' + esc(o.expiresAt) : 'never expires';\n const consumed = o.summaryConsumedAt ? ' · briefing delivered' : (o.summary ? ' · briefing pending' : '');\n return '<div class=\"ax-ho-list-row\">' +\n '<span class=\"ax-pill ax-pill--info\">' + esc(o.channel) + '</span>' +\n '<span class=\"ax-who\">' + who + consumed + '</span>' +\n '<span class=\"ax-when\">' + esc(o.createdAt) + ' · ' + expires + '</span>' +\n '<button class=\"ax-btn ax-btn--danger\" data-release=\"' + esc(o.channel) + '::' + esc(o.chatId) + '::' + esc(o.accountId || '') + '\">Release</button>' +\n '</div>';\n }).join('');\n host.querySelectorAll('[data-release]').forEach(btn => btn.addEventListener('click', () => {\n const [ch, cid, acct] = btn.dataset.release.split('::');\n releaseHandover(ch, cid, acct || undefined);\n }));\n}\n\nasync function releaseHandover(channel, chatId, accountId){\n if (!confirm('Release the handover for ' + channel + ':' + chatId + '? Routing will revert to the config default on the next message.')) return;\n try {\n await req('DELETE', '/api/admin/agent/' + AGENT_ID + '/handovers', { channel, chatId, accountId });\n showMsg('ok', 'Handover released.');\n loadHandovers();\n } catch (e) { showMsg('err', e.message); }\n}\n\n$('btn-handover').addEventListener('click', () => {\n const channel = $('ho-channel').value;\n const chatId = $('ho-chatid').value.trim();\n const accountId = $('ho-account').value.trim() || undefined;\n const toAgent = $('ho-target').value;\n const summary = $('ho-summary').value.trim() || undefined;\n const expiresAt = $('ho-expires').value.trim() || undefined;\n if (!chatId || !toAgent) { showMsg('err', 'chat id and target agent are required'); return; }\n confirmAction(\n 'Hand off this chat?',\n channel + ':' + chatId + (accountId ? ':'+accountId : '') + '\\\\n\\\\n'\n + 'From: ' + AGENT_ID + '\\\\nTo: ' + toAgent + '\\\\n\\\\n'\n + 'Every next message from this chat will be routed to ' + toAgent + ' until you release the handover. '\n + (summary ? 'The target agent will see your briefing note on its first reply.' : 'No briefing note set — the target agent will not know why.'),\n async () => {\n try {\n await req('POST', '/api/admin/agent/' + AGENT_ID + '/handovers',\n { channel, chatId, accountId, toAgent, summary, expiresAt });\n $('ho-summary').value = '';\n showMsg('ok', 'Handed off.');\n loadHandovers();\n } catch (e) { showMsg('err', e.message); }\n }\n );\n});\n\nasync function loadActivity(){\n try {\n const r = await req('GET', '/api/admin/agent/' + AGENT_ID + '/activity');\n const host = $('activity');\n host.innerHTML =\n '<div class=\"ax-activity-grid\">' +\n '<div class=\"ax-activity-cell\"><div class=\"ax-v\">' + (r.totalTasks ?? 0) + '</div><div class=\"ax-l\">Total tasks</div></div>' +\n '<div class=\"ax-activity-cell\"><div class=\"ax-v\">' + (r.activeTasks ?? 0) + '</div><div class=\"ax-l\">Handling now</div></div>' +\n '<div class=\"ax-activity-cell\"><div class=\"ax-v\">' + (r.errors ?? 0) + '</div><div class=\"ax-l\">Errors</div></div>' +\n '<div class=\"ax-activity-cell\"><div class=\"ax-v\" style=\"font-size:13px;font-family:var(--ax-mono)\">' + esc(r.lastActive || '—') + '</div><div class=\"ax-l\">Last active</div></div>' +\n '</div>' +\n (r.lastSummary\n ? '<div class=\"ax-sec-label-d\"><h3>Last reply</h3></div>' +\n '<div style=\"background:var(--ax-bg-elev);border:1px solid var(--ax-border);border-radius:var(--ax-radius-sm);padding:14px;font-size:12.5px;color:var(--ax-text-2);line-height:1.6\">' +\n esc((r.lastSummary.text || '').slice(0, 400)) +\n '</div>'\n : '');\n // Update hero stats from the activity payload.\n const sToday = $('stat-today'); if (sToday) sToday.textContent = String(r.totalTasks ?? '—');\n const sHand = $('stat-handling'); if (sHand) sHand.textContent = String(r.activeTasks ?? 0);\n } catch (e) { showMsg('err', e.message); }\n}\n\n/* ===== Test drive (sandbox chat) ===== */\nlet __tdBusy = false;\nfunction appendTdBubble(kind, text, meta) {\n const wrap = document.createElement('div');\n wrap.className = 'ax-td-chat__msg-wrap is-' + kind;\n wrap.innerHTML =\n '<div class=\"ax-td-chat__bubble is-' + kind + '\">' + esc(text) + '</div>' +\n '<div class=\"ax-td-chat__meta\">' + esc(meta || '') + '</div>';\n const chat = $('td-chat');\n chat.appendChild(wrap);\n chat.scrollTop = chat.scrollHeight;\n return wrap;\n}\nfunction appendTdTyping() {\n const el = document.createElement('div');\n el.className = 'ax-td-chat__typing';\n el.id = 'td-typing';\n el.innerHTML = '<span></span><span></span><span></span>';\n $('td-chat').appendChild(el);\n $('td-chat').scrollTop = $('td-chat').scrollHeight;\n}\nfunction removeTdTyping() {\n const el = document.getElementById('td-typing');\n if (el) el.remove();\n}\n\nwindow.sendScenario = function(text) {\n const inp = $('td-input-el');\n if (inp) inp.value = text;\n dispatchTd(text);\n};\nwindow.sendTestMessage = function(e) {\n e.preventDefault();\n const inp = $('td-input-el');\n const text = inp.value.trim();\n if (!text) return;\n inp.value = '';\n dispatchTd(text);\n};\nasync function dispatchTd(text) {\n if (__tdBusy) return;\n __tdBusy = true;\n const now = new Date().toLocaleTimeString();\n appendTdBubble('user', text, now + ' · you');\n appendTdTyping();\n const t0 = Date.now();\n try {\n const r = await req('POST', '/task', { agent: AGENT_ID, message: text, context: { channel: 'test-drive', sender: 'admin-ui' } });\n removeTdTyping();\n const elapsed = ((Date.now() - t0) / 1000).toFixed(1) + 's';\n appendTdBubble('bot', r.content || '(empty reply)', new Date().toLocaleTimeString() + ' · ' + elapsed);\n } catch (e) {\n removeTdTyping();\n appendTdBubble('bot', '[error] ' + e.message, 'failed');\n } finally {\n __tdBusy = false;\n }\n}\n\n/* ===== Delete agent ===== */\n$('btn-delete-agent')?.addEventListener('click', () => {\n confirmAction(\n 'Delete this agent?',\n 'Agent \"' + AGENT_ID + '\" will be removed from agentx.json. The workspace folder on disk is preserved; the config entry is gone. You will need to recreate it from scratch to bring it back.',\n async () => {\n try {\n await req('DELETE', '/api/admin/agents', { id: AGENT_ID });\n window.location.href = '/admin';\n } catch (e) { showMsg('err', e.message); }\n }\n );\n});\n\n/* ===== Test-drive side panel: open/close ===== */\nfunction openTestDrive() {\n const p = $('td-panel');\n if (!p) return;\n p.classList.add('is-open');\n p.setAttribute('aria-hidden', 'false');\n setTimeout(() => $('td-input-el')?.focus(), 250);\n}\nfunction closeTestDrive() {\n const p = $('td-panel');\n if (!p) return;\n p.classList.remove('is-open');\n p.setAttribute('aria-hidden', 'true');\n}\n$('btn-test-drive')?.addEventListener('click', openTestDrive);\n$('td-close')?.addEventListener('click', closeTestDrive);\ndocument.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && $('td-panel')?.classList.contains('is-open')) closeTestDrive();\n});\n\n/* ===== Boot ===== */\ninitOptsToggles();\nloadAgent();\n`\n","// Single source of truth for user-facing labels in the dashboard web UIs.\n//\n// AgentX keeps its JSON schema keys (tier, mention, workspace, mesh, heartbeat)\n// because third-party configs depend on them. But the dashboard is for operators\n// who don't care about the schema — they want plain English. This module maps\n// the internal vocabulary to the labels shown on screen and to the glossary.\n//\n// Changes here are SAFE: they only affect what the browser renders. They do NOT\n// change agentx.json, the CLI, or any external API surface.\n\nexport const UI_LABELS = {\n brand: \"AgentX\",\n subtitle: \"Team AI ops\",\n\n // Header summary\n nodes: \"machines\",\n agentsCount: \"agents\",\n activeNow: \"handling now\",\n errorsCount: \"failed\",\n\n // Per-agent card\n stats: {\n active: \"Handling\",\n total: \"Today\",\n errors: \"Failed\",\n },\n idle: \"idle\",\n runningNoPreview: \"working · preparing reply\",\n recentActivities: \"Recent activities →\",\n noAgentsOnNode: \"No agents set up on this machine.\",\n unreachable: \"This machine is unreachable.\",\n neverRan: \"not used yet\",\n\n // Friendly names per tier\n tierLabels: {\n \"claude-code\": \"Claude Code\",\n \"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","// --- Live dashboard page ---\n//\n// Real-time view of every reachable daemon's agents. The HTML here is a\n// skeleton; everything interesting is painted by LIVE_PAGE_SCRIPT after an\n// SSE snapshot lands from /api/live/stream.\n//\n// Ownership split vs the old board-dashboard.ts monolith:\n// ui/tokens.ts — :root / theme tokens (was at the top of LIVE_CSS)\n// ui/components.css.ts — .ax-dot, .ax-badge, .ax-statstrip, .ax-chip,\n// .ax-card (was also at the top of LIVE_CSS)\n// topbar.ts — .ax-topbar, .ax-theme-switch, .ax-mesh-sel\n// HERE — layout + agent cards + history panel + task modal\n//\n// That's ~300 lines of CSS reclaimed from LIVE_CSS onto shared modules.\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\nimport { UI_LABELS } from \"../../ui-labels\"\n\nexport interface LivePageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderLivePage(opts: LivePageOpts = {}): string {\n const labelsScript = `<script>window.UI_LABELS = ${JSON.stringify(UI_LABELS)};</script>`\n const rightExtras = `<span id=\"ts\" class=\"ax-mono\" title=\"Last update\">—</span>\n <span id=\"conn-dot\" class=\"ax-dot ax-dot--ok ax-dot--pulse\" title=\"connected\"></span>\n <span id=\"conn-label\">live</span>`\n\n const body = `<div class=\"ax-app-live\">\n <section id=\"statstrip\" class=\"ax-statstrip\"></section>\n <main id=\"grid\" class=\"ax-live__body\"></main>\n</div>\n<aside id=\"history-panel\" class=\"history-panel hidden\" aria-hidden=\"true\">\n <header>\n <h2 id=\"history-panel-title\">${esc(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\">${esc(UI_LABELS.taskModalTitle)}</h2>\n <span class=\"task-modal-status\" id=\"task-modal-status\">${esc(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\n return renderShell({\n title: `${UI_LABELS.brand} · ${UI_LABELS.subtitle}`,\n activeTab: \"live\",\n subtitle: UI_LABELS.subtitle,\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n // Paste rightExtras manually since renderShell doesn't forward them to\n // renderTopbar — the Live page is the only one that uses them (ticking\n // timestamp + connection dot), not worth a config knob.\n customHeader: undefined, // let shell emit the standard topbar\n noMain: true,\n body,\n css: LIVE_PAGE_CSS,\n scripts: labelsScript + `<script>${LIVE_PAGE_SCRIPT}</script>`,\n headExtras: `<script>\n // rightExtras for the topbar are injected at runtime — they reference\n // IDs (#ts, #conn-dot) that the script below addresses directly.\n document.addEventListener('DOMContentLoaded', () => {\n const right = document.querySelector('.ax-topbar__right');\n if (!right) return;\n const extras = document.createElement('div');\n extras.style.cssText = 'display:flex;gap:10px;align-items:center;font-size:var(--ax-fs-xs);color:var(--ax-muted)';\n extras.innerHTML = ${JSON.stringify(rightExtras)};\n right.insertBefore(extras, right.firstChild);\n });\n </script>`,\n })\n}\n\n/** CSS specific to the Live page. Tokens + .ax-badge/.ax-dot/.ax-statstrip/\n * .ax-chip/.ax-card / topbar all live in shared modules — this file owns\n * only layout (app flex column, node sections, agent grid) + the agent\n * card internals + history panel + task modal + event timeline. */\nconst LIVE_PAGE_CSS = `\n/* --- Page frame --- */\n.ax-app-live { min-height: 100vh; display: flex; flex-direction: column; }\n\n/* --- Live body (list of node sections) --- */\n.ax-live__body { display: flex; flex-direction: column; gap: var(--ax-gap); padding: var(--ax-pad); }\n.ax-node {\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 8px; overflow: hidden;\n}\n.ax-node > header {\n background: transparent; padding: 12px 16px;\n border-bottom: 1px solid var(--ax-border);\n display: flex; align-items: center; gap: 10px;\n}\n.ax-node__name { font-weight: 600; font-size: 14px; }\n.ax-node__url { color: var(--ax-muted); font-family: var(--ax-mono); font-size: 11px; }\n.ax-node__tag {\n font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;\n padding: 2px 7px; border-radius: 3px; border: 1px solid var(--ax-border-2);\n color: var(--ax-text-2); font-family: var(--ax-mono);\n}\n.ax-node__tag--up { color: var(--ax-accent); border-color: color-mix(in oklch, var(--ax-accent) 50%, transparent); }\n.ax-node__tag--down { color: var(--ax-err); border-color: color-mix(in oklch, var(--ax-err) 50%, transparent); }\n.ax-grid--agents {\n display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));\n gap: var(--ax-gap); padding: 12px 14px;\n}\n\n/* --- Agent cards --- */\n.ax-agent {\n display: flex; flex-direction: column; gap: 10px;\n transition: border-color 200ms ease, box-shadow 200ms ease, background 200ms ease;\n}\n.ax-agent.is-handling {\n border-color: color-mix(in oklch, var(--ax-accent) 75%, var(--ax-border));\n background: linear-gradient(180deg,\n color-mix(in oklch, var(--ax-accent) 6%, var(--ax-surface)) 0%,\n var(--ax-surface) 38%);\n animation: ax-agent-breathe 2.2s ease-in-out infinite;\n}\n.ax-agent.is-errored {\n border-color: color-mix(in oklch, var(--ax-err) 55%, var(--ax-border));\n box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--ax-err) 25%, transparent);\n}\n@keyframes ax-agent-breathe {\n 0%, 100% {\n box-shadow:\n 0 0 0 1px color-mix(in oklch, var(--ax-accent) 45%, transparent),\n 0 0 20px -6px color-mix(in oklch, var(--ax-accent) 30%, transparent);\n }\n 50% {\n box-shadow:\n 0 0 0 1px color-mix(in oklch, var(--ax-accent) 70%, transparent),\n 0 0 32px -2px color-mix(in oklch, var(--ax-accent) 55%, transparent);\n }\n}\n.ax-agent__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }\n.ax-agent__id { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex: 1; }\n.ax-mention {\n color: var(--ax-accent); font-size: 15px; font-weight: 500;\n font-family: var(--ax-mono); letter-spacing: -0.01em;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.ax-agent__name {\n font-size: var(--ax-fs-sm); color: var(--ax-muted);\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.ax-agent__tier { display: flex; gap: 4px; flex-shrink: 0; }\n.ax-agent__model {\n font-size: var(--ax-fs-xs); color: var(--ax-muted);\n font-family: var(--ax-mono); margin-top: -4px;\n}\n.ax-agent__stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }\n.ax-ministat {\n padding: 8px 10px; background: var(--ax-bg);\n border: 1px solid var(--ax-border); border-radius: 4px;\n}\n.ax-ministat__label {\n display: flex; align-items: center; gap: 4px; font-size: 10px;\n color: var(--ax-muted); text-transform: uppercase; letter-spacing: 0.06em;\n}\n.ax-ministat__value {\n font-size: 18px; font-weight: 600; margin-top: 2px;\n letter-spacing: -0.02em; font-family: var(--ax-mono);\n font-variant-numeric: tabular-nums;\n}\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 {\n border-top: 1px dashed var(--ax-border); padding-top: 8px;\n color: var(--ax-accent);\n}\n.ax-agent__spark-caption {\n display: flex; justify-content: space-between; font-size: 10px;\n text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--ax-muted); margin-top: 2px;\n}\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 {\n text-align: left; background: var(--ax-bg);\n border: 1px solid color-mix(in oklch, var(--ax-accent) 30%, var(--ax-border));\n padding: 8px 10px; border-radius: 4px; cursor: pointer;\n font: inherit; color: var(--ax-text);\n}\n.ax-agent__task:hover { border-color: var(--ax-accent); }\n.ax-agent__task-head {\n display: flex; align-items: center; gap: 6px;\n font-size: var(--ax-fs-xs); color: var(--ax-accent);\n}\n.ax-agent__task-head .elapsed { margin-left: auto; font-family: var(--ax-mono); color: var(--ax-muted); }\n.ax-agent__task-body {\n font-size: var(--ax-fs-sm); margin-top: 4px;\n display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;\n overflow: hidden; line-height: 1.35;\n}\n.ax-agent__summary { border-top: 1px dashed var(--ax-border); padding-top: 8px; }\n.ax-agent__summary-caption {\n font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--ax-muted);\n}\n.ax-agent__summary-text {\n font-size: var(--ax-fs-sm); color: var(--ax-text-2);\n line-height: 1.45; margin-top: 4px; text-wrap: pretty;\n display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;\n}\n.ax-agent__summary.is-fail .ax-agent__summary-caption { color: var(--ax-err); }\n.ax-agent__foot {\n display: flex; justify-content: space-between; align-items: center;\n font-size: var(--ax-fs-xs); color: var(--ax-muted);\n border-top: 1px solid var(--ax-border); padding-top: 8px;\n}\n.ax-linkbtn {\n background: transparent; border: none; color: var(--ax-text-2);\n padding: 2px 6px; font: inherit; cursor: pointer;\n font-size: var(--ax-fs-sm); text-decoration: none;\n}\n.ax-linkbtn:hover { color: var(--ax-accent); }\n\n.ax-empty {\n padding: 32px 10px; text-align: center; color: var(--ax-muted);\n font-size: var(--ax-fs-sm); border: 1px dashed var(--ax-border);\n border-radius: 4px; margin: 12px 14px;\n}\n\n/* --- History panel (right-docked sheet) --- */\n.history-panel {\n 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}\n.history-panel.hidden { transform: translateX(100%); pointer-events: none; }\n.history-panel > header {\n display: flex; align-items: center; gap: 10px; padding: 12px 14px;\n background: var(--ax-bg-elev); border-bottom: 1px solid var(--border);\n}\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 {\n background: transparent; border: none; color: var(--muted); font-size: 20px;\n cursor: pointer; padding: 0 6px; line-height: 1;\n}\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 {\n 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}\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 {\n background: color-mix(in oklch, var(--ax-accent) 16%, transparent); color: var(--accent);\n font-size: 9px; text-transform: uppercase; padding: 1px 6px; border-radius: 3px;\n letter-spacing: 0.5px;\n}\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; 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 (full transcript view) --- */\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 {\n 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}\n.task-modal-card > header {\n display: flex; align-items: center; gap: 10px; padding: 12px 16px;\n border-bottom: 1px solid var(--ax-border); background: var(--ax-bg-elev);\n}\n.task-modal-card > header h2 {\n margin: 0; font-size: 14px; font-weight: 600; flex: 1;\n color: var(--ax-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n letter-spacing: -0.005em;\n}\n.task-modal-channel {\n 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}\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 {\n background: transparent; border: none; color: var(--ax-muted); font-size: 22px;\n cursor: pointer; padding: 0 6px; line-height: 1;\n}\n.task-modal-close:hover { color: var(--ax-text); }\n\n/* --- Event timeline (inside task modal) --- */\n.task-modal-output {\n margin: 0; flex: 1; overflow: auto; padding: 16px 20px;\n background: var(--ax-bg); display: flex; flex-direction: column; gap: 14px;\n}\n.ax-ev { border-left: 2px solid var(--ax-border-2); padding: 2px 0 2px 12px; }\n.ax-ev--tool { border-color: var(--ax-info); }\n.ax-ev--tool-result { border-color: var(--ax-border-2); }\n.ax-ev--tool-result.is-err { border-color: var(--ax-err); }\n.ax-ev--thought { border-color: var(--ax-muted); }\n.ax-ev--text { border-color: var(--ax-accent); }\n.ax-ev--system {\n border-color: var(--ax-border-2); color: var(--ax-muted); font-size: var(--ax-fs-xs);\n font-family: var(--ax-mono); padding-top: 4px; padding-bottom: 4px;\n}\n.ax-ev--system.is-done { border-color: var(--ax-accent); }\n.ax-ev--error { border-color: var(--ax-err); }\n.ax-ev__head { display: flex; align-items: center; gap: 8px; font-size: var(--ax-fs-xs); color: var(--ax-text-2); }\n.ax-ev__label { text-transform: uppercase; letter-spacing: 0.06em; font-size: 10px; font-family: var(--ax-mono); }\n.ax-ev__label--soft { color: var(--ax-muted); }\n.ax-ev__label--tool { color: var(--ax-info); }\n.ax-ev__label--result { color: var(--ax-muted); }\n.ax-ev__label--text { color: var(--ax-accent); }\n.ax-ev__label--error { color: var(--ax-err); }\n.ax-ev__time { margin-left: auto; font-family: var(--ax-mono); color: var(--ax-muted); }\n.ax-ev__tool { font-family: var(--ax-mono); color: var(--ax-info); font-size: var(--ax-fs-xs); }\n.ax-ev__code {\n margin: 4px 0 0; padding: 8px 10px; background: var(--ax-bg-elev);\n border: 1px solid var(--ax-border); border-radius: 3px; font-family: var(--ax-mono);\n font-size: var(--ax-fs-xs); white-space: pre-wrap; word-break: break-word;\n line-height: 1.5; color: var(--ax-text-2); max-height: 200px; overflow: auto;\n}\n.ax-ev__code--muted { color: var(--ax-muted); }\n.ax-ev__code--err { color: var(--ax-err); border-color: color-mix(in oklch, var(--ax-err) 35%, var(--ax-border)); }\n.ax-ev__thought {\n margin-top: 4px; color: var(--ax-text-2); font-style: italic;\n font-size: var(--ax-fs-sm); line-height: 1.55; text-wrap: pretty;\n}\n.ax-ev__text {\n margin-top: 4px; color: var(--ax-text); font-size: var(--ax-fs-sm);\n line-height: 1.55; text-wrap: pretty; white-space: pre-wrap; word-break: break-word;\n}\n.ax-ev--thinking {\n display: flex; align-items: center; gap: 8px; padding: 8px 0 0 12px;\n color: var(--ax-muted); font-size: var(--ax-fs-sm);\n}\n.ax-ev__spinner {\n width: 10px; height: 10px; border: 1.5px solid var(--ax-border-2);\n border-top-color: var(--ax-accent); border-radius: 50%;\n animation: ax-spin 800ms linear infinite; display: inline-block;\n}\n@keyframes ax-spin { to { transform: rotate(360deg); } }`\n\n/** Live-page client script — SSE snapshot handler, agent card renderer,\n * task modal + history panel. 650-ish lines of straight JS; moving it\n * here doesn't change it. */\nconst LIVE_PAGE_SCRIPT = `\n'use strict';\n\nconst ui = { nodes: new Map(), summary: { nodes: 0, agents: 0, busy: 0, errors: 0 } };\n\nfunction escapeHtml(s) {\n return String(s).replace(/[&<>\"']/g, c => ({ '&':'&', '<':'<', '>':'>', '\"':'"', \"'\":''' }[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 // Error border reflects the MOST RECENT task's status, not the cumulative\n // error count. A red border drops the moment a new task succeeds — it should\n // not persist forever because of a failure an hour ago. The cumulative\n // a.errors count stays visible in the \"failed\" ministat below as an\n // informational value.\n const errored = a.lastSummary && a.lastSummary.ok === false;\n card.className = 'ax-card ax-agent' + (busy ? ' is-handling' : '') + (errored ? ' is-errored' : '');\n const nodeUrl = (node && node.url) || '';\n const L = window.UI_LABELS || {};\n const tierLabels = (L.tierLabels) || {};\n const tierDisplay = tierLabels[a.tier] || a.tier || '';\n\n // Running task card (clickable → opens live stream modal)\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 + ' data-started-at=\"' + escapeHtml(t.startedAt || '') + '\" title=\"' + escapeHtml(t.messagePreview || '') + '\">' +\n '<div class=\"ax-agent__task-head\">' +\n '<span class=\"ax-dot ax-dot--live ax-dot--pulse\"></span>' +\n '<span>running · ' + escapeHtml(t.channel || '') + '</span>' +\n '<span class=\"elapsed\">' + elapsed + '</span>' +\n '</div>' +\n '<div class=\"ax-agent__task-body\">' + escapeHtml(t.messagePreview || '(no preview)') + '</div>' +\n '</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 lastActiveAttr = a.lastActive ? ' data-last-active=\"' + escapeHtml(a.lastActive) + '\"' : '';\n const recentLink = nodeUrl\n ? '<button class=\"ax-linkbtn\" data-agent-id=\"' + escapeHtml(a.id) + '\" data-agent-name=\"' + escapeHtml(a.name || a.id) + '\" data-node-url=\"' + escapeHtml(nodeUrl) + '\" data-recent=\"1\">history →</button>'\n : '';\n\n // Head: mention (trigger) + human name + tier badge + live/idle badge\n const mention = (a.mentions && a.mentions.length) ? '@' + a.mentions[0].replace(/^@/, '') : '@' + a.id;\n const liveBadge = busy\n ? '<span class=\"ax-badge ax-badge--mono ax-badge--live\"><span class=\"ax-dot ax-dot--live ax-dot--pulse\"></span> live</span>'\n : (errored ? '<span class=\"ax-badge ax-badge--mono ax-badge--warn\">errored</span>' : '<span class=\"ax-badge ax-badge--mono ax-badge--ghost\">idle</span>');\n const tierBadge = tierDisplay ? '<span class=\"ax-badge ax-badge--mono ax-badge--ghost\" title=\"AI engine\">' + escapeHtml(tierDisplay) + '</span>' : '';\n\n 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\"' + lastActiveAttr + '><span class=\"last-active\">' + 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 // #conn exists on the Kanban page; the Live page only has #conn-dot/#conn-label\n // in the topbar. Guard it or the snapshot handler throws before render runs,\n // which silently freezes the live grid between page reloads.\n const conn = document.getElementById('conn');\n let es;\n const open = () => {\n try { es = new EventSource('/api/live/stream'); } catch (e) { setTimeout(open, 2000); return; }\n es.addEventListener('snapshot', (ev) => {\n if (conn) { conn.className = 'conn ok'; conn.title = 'connected'; }\n try { render(JSON.parse(ev.data)); } catch (e) { console.error('live render failed', e); }\n });\n es.addEventListener('error', () => {\n if (conn) { conn.className = 'conn err'; conn.title = 'reconnecting…'; }\n es.close(); setTimeout(open, 2000);\n });\n };\n open();\n}\n\n// Tick elapsed / last-active counters every second from DOM data attributes,\n// so they advance smoothly between 2s server snapshots instead of jumping.\nlet lastSnap = null;\nfetch('/api/live').then(r => r.json()).then(s => { lastSnap = s; render(s); }).catch(() => {});\nsetInterval(() => {\n const now = Date.now();\n // Running-task elapsed counters.\n document.querySelectorAll('.ax-agent__task[data-started-at]').forEach((btn) => {\n const startedAt = btn.getAttribute('data-started-at');\n if (!startedAt) return;\n const t = new Date(startedAt).getTime();\n if (!t) return;\n const el = btn.querySelector('.elapsed');\n if (el) el.textContent = fmtElapsed(now - t);\n });\n // Idle-card \"last active\" text — smooths from \"5s ago\" to \"6s ago\".\n document.querySelectorAll('.ax-agent__foot[data-last-active]').forEach((foot) => {\n const iso = foot.getAttribute('data-last-active');\n if (!iso) return;\n const el = foot.querySelector('.last-active');\n if (el) el.textContent = 'last active ' + fmtAgo(iso);\n });\n}, 1000);\nconnect();\n\n// --- 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\n/** Flush any trailing partial line left in lineBuf. Called at the end of an\n * archived-task render so transcripts that don't end with a newline still\n * surface their last line. */\nfunction flushOutput() {\n if (!taskModal.output) return;\n if (taskModal.lineBuf && taskModal.lineBuf.length) {\n processStreamLine(taskModal.lineBuf);\n taskModal.lineBuf = '';\n }\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 flushOutput();\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 const tx = rec.transcript || '';\n if (tx) appendOutput(tx);\n // Only append the \"Final reply\" block if the transcript didn't already\n // carry it. Guard against a missing transcript (indexOf would throw).\n if (rec.responseText && (!tx || tx.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 flushOutput();\n // If the record returned nothing renderable, show an explicit empty\n // state so the modal doesn't look broken.\n if (!taskModal.output.children.length) {\n taskModal.output.innerHTML =\n '<div class=\"ax-ev ax-ev--system\">' +\n '<div class=\"ax-ev__head\"><span class=\"ax-ev__label ax-ev__label--soft\">empty</span>' +\n '<span>This task has no recorded transcript. It may have finished before history capture kicked in, or the archive file was pruned.</span></div>' +\n '</div>';\n }\n } catch (e) {\n setStatus(L.taskModalLoadFailed || \"couldn't load\", 'err');\n appendOutput('Error: ' + e.message);\n flushOutput();\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`\n","// --- Boards (Kanban) page ---\n//\n// Column-based issue/task view over configured boards (GitLab/GitHub/local).\n// Subheader owns board picker + search + filters; body holds the kanban\n// columns + issue modal + agent activity drawer. Client JS drives sortable\n// drag-drop, live updates, and optimistic writes.\n//\n// Deduplicated from the old board-dashboard.ts monolith:\n// ui/tokens.ts — :root, theme overrides, font/color vars\n// ui/components.css.ts — .ax-badge / .ax-card / .ax-field reusable bits\n// topbar.ts — .ax-topbar / .ax-subheader / .ax-theme-switch\n// HERE — subheader inputs + kanban columns + issue modal +\n// activity drawer + client JS\n//\n// The page accepts renderBoardPage({ peers }) — daemon-config awareness\n// stays in board-dashboard.ts where the routing happens.\n\nimport { renderShell, type TopbarPeer } from \"..\"\n\nexport interface BoardsPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderBoardsPage(opts: BoardsPageOpts = {}): string {\n const subheader = `\n <select id=\"boardPicker\" title=\"Switch board\"></select>\n <div class=\"search\">\n <span class=\"icon\">⌕</span>\n <input id=\"filterSearch\" type=\"search\" placeholder=\"Search or filter…\" autocomplete=\"off\" />\n </div>\n <select id=\"filterAssignee\" title=\"Assignee\"><option value=\"\">All assignees</option></select>\n <select id=\"filterMilestone\" title=\"Milestone\"><option value=\"\">Any milestone</option></select>\n <div id=\"filterLabels\" class=\"chips\"></div>\n <div class=\"spacer\"></div>\n <button id=\"activityBtn\" title=\"Agent activity (a)\">◉ Activity</button>\n <button id=\"newIssueBtn\" title=\"New issue\">+ New</button>\n <button id=\"refreshBtn\" title=\"Refresh (r)\">↻</button>`\n\n const body = `<aside id=\"activity-panel\" class=\"activity-panel hidden\" aria-hidden=\"true\">\n <header>\n <h2>Agent Activity</h2>\n <span id=\"activity-source\" class=\"source\"></span>\n <button id=\"activity-close\" class=\"x\" title=\"Close\">✕</button>\n </header>\n <div id=\"activity-body\">Loading…</div>\n <footer id=\"activity-events\" class=\"events\"></footer>\n</aside>\n<main id=\"columns\"></main>\n<div id=\"toast\"></div>\n<div id=\"modal\" class=\"modal\" aria-hidden=\"true\">\n <div class=\"modal-backdrop\"></div>\n <div class=\"modal-panel\" role=\"dialog\" aria-modal=\"true\">\n <header class=\"modal-h\">\n <div class=\"modal-title-row\">\n <span id=\"m-iid\" class=\"iid\"></span>\n <input id=\"m-title\" type=\"text\" placeholder=\"Issue title\" />\n </div>\n <button id=\"m-close\" class=\"x\" title=\"Close (esc)\">✕</button>\n </header>\n <div class=\"modal-body\">\n <div class=\"m-main\">\n <div id=\"m-draft\" class=\"draft-box\" hidden>\n <label class=\"fld\">\n <span>✨ Describe the issue (the agent will draft a proper title + description)</span>\n <textarea id=\"m-rough\" rows=\"3\" placeholder=\"e.g. when I upload a CSV over 500 rows in Receiving Records the import silently fails\"></textarea>\n </label>\n <div class=\"draft-row\">\n <select id=\"m-draft-agent\" title=\"Drafting agent\"><option value=\"\">Agent…</option></select>\n <button id=\"m-draft-go\" class=\"btn-primary\" type=\"button\">✨ Draft</button>\n <span id=\"m-draft-hint\" class=\"hint\"></span>\n </div>\n </div>\n <div class=\"fld\">\n <div class=\"desc-head\">\n <span>Description</span>\n <div class=\"desc-tabs\" role=\"tablist\">\n <button type=\"button\" class=\"desc-tab\" data-tab=\"preview\" role=\"tab\">Preview</button>\n <button type=\"button\" class=\"desc-tab active\" data-tab=\"write\" role=\"tab\">Write</button>\n </div>\n </div>\n <div id=\"m-desc-preview\" class=\"desc-preview\" hidden></div>\n <textarea id=\"m-desc\" rows=\"10\" placeholder=\"Markdown supported (GitLab flavored)\"></textarea>\n </div>\n <div class=\"fld\">\n <span>Labels</span>\n <div id=\"m-labels\" class=\"chip-picker\"></div>\n </div>\n </div>\n <aside class=\"m-side\">\n <div class=\"fld\">\n <span>State</span>\n <div id=\"m-state\" class=\"state-toggle\">\n <button data-state=\"opened\">Open</button>\n <button data-state=\"closed\">Closed</button>\n </div>\n </div>\n <div class=\"fld\">\n <span>Assignees</span>\n <div id=\"m-assignees\" class=\"chip-picker\"></div>\n </div>\n <div class=\"fld\">\n <span>Milestone</span>\n <select id=\"m-milestone\"><option value=\"\">None</option></select>\n </div>\n <div class=\"fld muted\">\n <span>Author</span>\n <div id=\"m-author\"></div>\n </div>\n <div class=\"fld muted\">\n <span>Created</span>\n <div id=\"m-created\"></div>\n </div>\n <div class=\"fld muted\">\n <span>Updated</span>\n <div id=\"m-updated\"></div>\n </div>\n <a id=\"m-link\" class=\"ext-link\" href=\"#\" target=\"_blank\" rel=\"noopener\">Open in GitLab ↗</a>\n </aside>\n </div>\n <footer class=\"modal-f\">\n <div id=\"m-hint\" class=\"hint\"></div>\n <div class=\"spacer\"></div>\n <button id=\"m-cancel\" class=\"btn-ghost\">Cancel</button>\n <button id=\"m-save\" class=\"btn-primary\">Save</button>\n </footer>\n </div>\n</div>\n<script src=\"/sortable.min.js\"></script>`\n\n return renderShell({\n title: \"AgentX · Boards\",\n activeTab: \"boards\",\n subtitle: \"Boards\",\n subheader,\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n noMain: true,\n body,\n css: BOARDS_PAGE_CSS,\n scripts: `<script>${BOARDS_PAGE_SCRIPT}</script>`,\n headExtras: `<script>\n // The boards page has its own connection dot + wants body height:100%\n // for the full-viewport kanban layout. Inject the dot into the topbar\n // and override body height at runtime so the shared shell doesn't\n // need a dedicated opt-in.\n document.addEventListener('DOMContentLoaded', () => {\n document.documentElement.style.height = '100%';\n document.body.style.height = '100%';\n const right = document.querySelector('.ax-topbar__right');\n if (right && !right.querySelector('.conn')) {\n const dot = document.createElement('div');\n dot.id = 'conn'; dot.className = 'conn ok'; dot.title = 'ready';\n dot.textContent = '●';\n right.insertBefore(dot, right.firstChild);\n }\n });\n </script>`,\n })\n}\n\nconst BOARDS_PAGE_CSS = `\n/* Kanban subheader — replaces the legacy toolbar that used to live in <header>. */\n.ax-subheader select, .ax-subheader input, .ax-subheader button {\n background: var(--col); color: var(--text); border: 1px solid var(--border);\n padding: 6px 10px; border-radius: 6px; font: inherit; outline: none; }\n.ax-subheader select:focus, .ax-subheader input:focus { border-color: var(--accent); }\n.ax-subheader .search { position: relative; }\n.ax-subheader .search .icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%);\n color: var(--muted); pointer-events: none; font-size: 14px; }\n.ax-subheader .search input { padding-left: 28px; width: 260px; }\n.ax-subheader button { cursor: pointer; }\n.ax-subheader button:hover { border-color: var(--accent); }\n.ax-subheader .chips { display: flex; gap: 4px; flex-wrap: wrap; }\n.ax-subheader #newIssueBtn { background: var(--accent); color: #fff; border-color: var(--accent); }\n.ax-subheader #newIssueBtn:hover { filter: brightness(1.1); }\n.ax-topbar__right .conn { font-size: 14px; line-height: 1; }\n.ax-topbar__right .conn.ok { color: var(--green); }\n.ax-topbar__right .conn.warn { color: var(--yellow); }\n.ax-topbar__right .conn.err { color: var(--red); }\n\nmain#columns { display: flex; gap: 10px; padding: 12px; overflow-x: auto; align-items: stretch;\n min-height: calc(100vh - 60px); }\n.col { background: var(--col); border: 1px solid var(--border); border-radius: 8px;\n min-width: 280px; max-width: 320px; flex: 1 0 280px; display: flex; flex-direction: column;\n max-height: calc(100vh - 84px); }\n.col .accent { height: 3px; border-top-left-radius: 7px; border-top-right-radius: 7px; background: var(--gray); }\n.col header.col-h { margin: 0; padding: 10px 12px; background: var(--col-hdr);\n border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px;\n position: sticky; top: 0; z-index: 1; border-radius: 0; }\n.col header.col-h h3 { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); flex: 1; }\n.col header.col-h .count { background: rgba(255,255,255,0.08); color: var(--muted);\n font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }\n.stack { padding: 8px; display: flex; flex-direction: column; gap: 8px; overflow-y: auto;\n flex: 1; min-height: 40px; }\n\n.card { background: var(--card); border: 1px solid var(--border); border-radius: 6px;\n padding: 10px 12px; cursor: grab; transition: border-color .15s, transform .15s;\n display: flex; flex-direction: column; gap: 6px; }\n.card:hover { border-color: var(--accent); }\n.card.closed { opacity: 0.75; }\n.card .title { font-weight: 500; font-size: 13px; line-height: 1.4; color: var(--text);\n overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; word-break: break-word; }\n.card .labels { display: flex; gap: 4px; flex-wrap: wrap; }\n.card .label { font-size: 10px; padding: 2px 8px; border-radius: 10px; font-weight: 500;\n background: rgba(99, 102, 241, 0.25); color: #e6e8ef; white-space: nowrap; line-height: 1.4; }\n.card .foot { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted);\n flex-wrap: wrap; }\n.card .foot .iid { font-family: ui-monospace, monospace; }\n.card .foot .milestone { background: rgba(99,102,241,0.15); color: var(--accent);\n padding: 1px 6px; border-radius: 3px; font-size: 10px; }\n.card .foot .est { background: rgba(255,255,255,0.08); padding: 1px 6px; border-radius: 3px; font-size: 10px; }\n.card .foot .date { margin-left: auto; }\n.card .foot a.link { color: var(--muted); text-decoration: none; }\n.card .foot a.link:hover { color: var(--accent); }\n\n.avatar { display: inline-flex; align-items: center; justify-content: center;\n width: 22px; height: 22px; border-radius: 50%; font-size: 10px; font-weight: 600;\n color: white; flex-shrink: 0; }\n.avatar.unassigned { background: var(--gray); }\n\n.sortable-ghost { opacity: 0.4; }\n.sortable-drag { cursor: grabbing; }\n.sortable-chosen { border-color: var(--accent); }\n.empty { color: var(--muted); font-style: italic; text-align: center; padding: 16px; font-size: 12px; }\n\n#toast { position: fixed; right: 16px; bottom: 16px; padding: 10px 14px;\n background: var(--red); color: white; border-radius: 6px; font-size: 13px;\n max-width: 360px; display: none; z-index: 200; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }\n#toast.show { display: block; }\n\n/* Multi-assignee stack on cards */\n.avatar-stack { display: inline-flex; }\n.avatar-stack .avatar { margin-left: -6px; border: 2px solid var(--card); }\n.avatar-stack .avatar:first-child { margin-left: 0; }\n.avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }\n\n/* Column add button */\n.col header.col-h .add { background: transparent; color: var(--muted); border: 1px solid var(--border);\n padding: 0 8px; line-height: 22px; border-radius: 4px; cursor: pointer; font-size: 14px; }\n.col header.col-h .add:hover { color: var(--accent); border-color: var(--accent); }\n\n/* --- Modal --- */\n.modal { position: fixed; inset: 0; z-index: 150; display: none; }\n.modal.open { display: block; }\n.modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }\n.modal-panel { position: relative; max-width: 940px; width: calc(100% - 40px); height: calc(100vh - 60px);\n margin: 30px auto; background: var(--card); border: 1px solid var(--border); border-radius: 10px;\n display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,0.55); overflow: hidden; }\n.modal-h { display: flex; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--border); align-items: center; }\n.modal-h .modal-title-row { flex: 1; display: flex; gap: 10px; align-items: center; }\n.modal-h .iid { font-family: ui-monospace, monospace; color: var(--muted); font-size: 13px; }\n.modal-h input[type=\"text\"] { flex: 1; background: var(--col); border: 1px solid var(--border); color: var(--text);\n padding: 8px 10px; border-radius: 6px; font-size: 15px; font-weight: 500; outline: none; }\n.modal-h input[type=\"text\"]:focus { border-color: var(--accent); }\n.modal-h .x { background: transparent; color: var(--muted); border: none; font-size: 18px; cursor: pointer; padding: 6px 8px; }\n.modal-h .x:hover { color: var(--text); }\n\n.modal-body { flex: 1; display: grid; grid-template-columns: 1fr 300px; gap: 0;\n overflow: hidden; min-height: 0; }\n.modal-body .m-main { padding: 16px; overflow-y: auto; min-height: 0; }\n.modal-body .m-side { padding: 16px; border-left: 1px solid var(--border); background: var(--col);\n overflow-y: auto; min-height: 0; display: flex; flex-direction: column; gap: 14px; }\n\n.fld { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }\n.fld > span { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }\n.fld.muted > div { font-size: 12px; color: var(--muted); }\n.fld textarea { background: var(--col); border: 1px solid var(--border); color: var(--text);\n padding: 10px; border-radius: 6px; font: 13px/1.5 ui-monospace, \"SF Mono\", monospace; outline: none; resize: vertical; }\n.fld textarea:focus { border-color: var(--accent); }\n.fld select { background: var(--col); border: 1px solid var(--border); color: var(--text);\n padding: 6px 10px; border-radius: 6px; font: inherit; outline: none; }\n\n/* Description edit/preview tabs */\n.desc-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.desc-tabs { display: inline-flex; background: var(--col); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }\n.desc-tabs button { background: transparent; border: none; color: var(--muted); padding: 4px 10px; cursor: pointer; font: inherit; font-size: 11px; }\n.desc-tabs button.active { background: var(--accent); color: white; }\n.desc-preview { background: var(--col); border: 1px solid var(--border); border-radius: 6px;\n padding: 12px 14px; min-height: 120px; max-height: 420px; overflow-y: auto; line-height: 1.55; }\n.desc-preview.empty { color: var(--muted); font-style: italic; }\n.desc-preview h1 { font-size: 18px; margin: 16px 0 8px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }\n.desc-preview h2 { font-size: 16px; margin: 14px 0 6px; }\n.desc-preview h3 { font-size: 14px; margin: 12px 0 4px; color: var(--text); }\n.desc-preview p { margin: 6px 0; }\n.desc-preview ul, .desc-preview ol { margin: 6px 0; padding-left: 20px; }\n.desc-preview li { margin: 2px 0; }\n.desc-preview li.task { list-style: none; margin-left: -16px; }\n.desc-preview li.task input { margin-right: 6px; vertical-align: middle; }\n.desc-preview code { background: rgba(255,255,255,0.08); padding: 1px 5px; border-radius: 3px;\n font: 12px ui-monospace, \"SF Mono\", monospace; color: #e6e8ef; }\n.desc-preview pre { background: #0a0c11; border: 1px solid var(--border); border-radius: 6px;\n padding: 10px; overflow-x: auto; margin: 8px 0; }\n.desc-preview pre code { background: transparent; padding: 0; font-size: 12px; line-height: 1.45; color: #d8dce6; }\n.desc-preview blockquote { border-left: 3px solid var(--accent); padding: 2px 12px; margin: 8px 0;\n color: var(--muted); background: rgba(99,102,241,0.08); border-radius: 0 4px 4px 0; }\n.desc-preview a { color: var(--accent); text-decoration: none; }\n.desc-preview a:hover { text-decoration: underline; }\n.desc-preview hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; }\n.desc-preview table { border-collapse: collapse; margin: 8px 0; font-size: 12px; }\n.desc-preview th, .desc-preview td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }\n.desc-preview th { background: var(--col-hdr); font-weight: 600; }\n.desc-preview img { max-width: 100%; border-radius: 4px; margin: 6px 0; }\n.desc-preview del { color: var(--muted); }\n.desc-preview .ref { background: rgba(99,102,241,0.15); padding: 0 4px; border-radius: 3px; font-family: ui-monospace, monospace; font-size: 12px; }\n\n.chip-picker { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 6px;\n background: var(--col); border: 1px solid var(--border); border-radius: 6px; min-height: 36px; }\n.chip-picker .chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px;\n border-radius: 10px; font-size: 11px; background: rgba(99,102,241,0.25); color: #e6e8ef; font-weight: 500; }\n.chip-picker .chip .remove { cursor: pointer; color: rgba(255,255,255,0.7); margin-left: 2px; }\n.chip-picker .chip .remove:hover { color: white; }\n.chip-picker .add-chip { background: transparent; border: 1px dashed var(--border); color: var(--muted);\n padding: 3px 10px; border-radius: 10px; cursor: pointer; font: inherit; font-size: 11px; }\n.chip-picker .add-chip:hover { border-color: var(--accent); color: var(--accent); }\n\n.chip-picker .dropdown { position: relative; }\n.chip-picker .dropdown .menu { position: absolute; top: 100%; left: 0; margin-top: 4px;\n background: var(--card); border: 1px solid var(--border); border-radius: 6px; min-width: 240px;\n max-height: 280px; overflow-y: auto; box-shadow: 0 8px 20px rgba(0,0,0,0.4); z-index: 10; padding: 4px; }\n.chip-picker .dropdown .menu input.search { width: 100%; background: var(--col); border: 1px solid var(--border);\n color: var(--text); padding: 6px 10px; border-radius: 4px; font: inherit; margin-bottom: 4px; outline: none; }\n.chip-picker .dropdown .menu .opt { padding: 6px 10px; cursor: pointer; border-radius: 4px;\n font-size: 12px; display: flex; align-items: center; gap: 8px; }\n.chip-picker .dropdown .menu .opt:hover { background: var(--col); }\n.chip-picker .dropdown .menu .opt.selected { background: rgba(99,102,241,0.15); color: var(--accent); }\n.chip-picker .dropdown .menu .empty { padding: 10px; color: var(--muted); font-size: 12px; text-align: center; }\n\n.state-toggle { display: inline-flex; background: var(--col); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }\n.state-toggle button { background: transparent; border: none; color: var(--muted); padding: 6px 12px;\n cursor: pointer; font: inherit; font-size: 12px; }\n.state-toggle button.active[data-state=\"opened\"] { background: var(--green); color: white; }\n.state-toggle button.active[data-state=\"closed\"] { background: var(--gray); color: white; }\n\n.ext-link { color: var(--muted); text-decoration: none; font-size: 12px; margin-top: auto; padding-top: 10px;\n border-top: 1px solid var(--border); }\n.ext-link:hover { color: var(--accent); }\n\n.modal-f { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--border); }\n.modal-f .spacer { flex: 1; }\n.modal-f .hint { color: var(--muted); font-size: 12px; }\n.btn-primary { background: var(--accent); color: white; border: 1px solid var(--accent);\n padding: 7px 16px; border-radius: 6px; cursor: pointer; font: inherit; font-weight: 500; }\n.btn-primary:hover { filter: brightness(1.1); }\n.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }\n.btn-ghost { background: transparent; color: var(--text); border: 1px solid var(--border);\n padding: 7px 14px; border-radius: 6px; cursor: pointer; font: inherit; }\n.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }\n\n@media (max-width: 720px) {\n .modal-body { grid-template-columns: 1fr; }\n .modal-body .m-side { border-left: none; border-top: 1px solid var(--border); }\n}\n\n/* --- Activity panel --- */\n.activity-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 340px;\n background: var(--card); border-left: 1px solid var(--border); z-index: 140;\n display: flex; flex-direction: column; box-shadow: -8px 0 24px rgba(0,0,0,0.35);\n transition: transform 0.2s ease; }\n.activity-panel.hidden { transform: translateX(100%); pointer-events: none; }\n.activity-panel > header { display: flex; align-items: center; gap: 8px; padding: 12px 14px;\n border-bottom: 1px solid var(--border); background: var(--col-hdr); }\n.activity-panel > header h2 { margin: 0; font-size: 13px; font-weight: 600; flex: 1; }\n.activity-panel > header .source { font-size: 10px; color: var(--muted); font-family: ui-monospace, monospace; }\n.activity-panel > header .x { background: transparent; border: none; color: var(--muted); font-size: 16px;\n cursor: pointer; padding: 4px 8px; }\n.activity-panel > header .x:hover { color: var(--text); }\n#activity-body { flex: 1; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 8px; }\n.agent-card { background: var(--col); border: 1px solid var(--border); border-radius: 6px;\n padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }\n.agent-card .top { display: flex; align-items: center; gap: 8px; }\n.agent-card .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--gray); flex-shrink: 0; }\n.agent-card .dot.active { background: var(--green); box-shadow: 0 0 6px var(--green); animation: pulse 1.2s ease-in-out infinite; }\n.agent-card .dot.errored { background: var(--red); }\n.agent-card .name { font-weight: 600; font-size: 13px; flex: 1; }\n.agent-card .tier { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }\n.agent-card .meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.agent-card .model { font-family: ui-monospace, monospace; font-size: 10px; color: var(--accent);\n background: rgba(99,102,241,0.12); padding: 1px 7px; border-radius: 3px; letter-spacing: 0.2px; }\n.agent-card .last { font-size: 10px; color: var(--muted); }\n.agent-card .last.muted { font-style: italic; opacity: 0.7; }\n.agent-card .stats { display: flex; gap: 10px; font-size: 11px; color: var(--muted); }\n.agent-card .stats b { color: var(--text); font-weight: 600; }\n.agent-card .stats .errors b { color: var(--red); }\n.agent-card .current { font-size: 11px; color: var(--muted); padding-top: 4px; border-top: 1px dashed var(--border);\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.agent-card .current b { color: var(--text); font-weight: 500; }\n@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.4 } }\n\n.activity-panel .events { padding: 10px 14px; max-height: 200px; overflow-y: auto;\n border-top: 1px solid var(--border); background: #0a0c11; font: 11px/1.5 ui-monospace, monospace; }\n.activity-panel .events .evt { color: var(--muted); margin-bottom: 4px; }\n.activity-panel .events .evt .ts { color: #4a4e5a; }\n.activity-panel .events .evt .who { color: var(--accent); }\n.activity-panel .events .evt .what { color: var(--text); }\n.activity-panel .events .evt.err .what { color: var(--red); }\n\n/* Shift the main grid left when activity panel is open so cards aren't hidden */\nbody.panel-open main#columns { padding-right: 352px; }\nbody.panel-open header { padding-right: 356px; }\n\nheader #activityBtn.on { background: var(--accent); color: white; border-color: var(--accent); }\nheader a.link { color: var(--text); text-decoration: none; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px; }\nheader a.link:hover { color: var(--accent); border-color: var(--accent); }\n\n/* AI draft row inside the create modal */\n.draft-box { background: var(--col); border: 1px dashed var(--accent); border-radius: 8px;\n padding: 10px 12px; margin-bottom: 14px; }\n.draft-box .fld { margin-bottom: 8px; }\n.draft-box .draft-row { display: flex; gap: 8px; align-items: center; }\n.draft-box .draft-row select { flex: 0 0 160px; }\n.draft-box .draft-row .hint { color: var(--muted); font-size: 12px; flex: 1; }\n.draft-box .draft-row .hint.err { color: var(--red); }\n.draft-box .draft-row .hint.ok { color: var(--green); }\n`\nconst BOARDS_PAGE_SCRIPT = `\n'use strict';\nconst state = {\n boards: [], current: null, items: {},\n filter: { q: '', assignee: '', label: '', milestone: '' },\n // Per-board caches — members + milestones + known labels.\n meta: { members: [], milestones: [], labels: [] },\n modal: { mode: null, id: null, original: null, draft: null }\n};\n\nfunction csrfHeaders(extra) {\n return Object.assign({ 'Content-Type': 'application/json', 'X-Requested-With': 'agentx-board' }, extra || {});\n}\nfunction toast(msg, kind) {\n const el = document.getElementById('toast');\n el.textContent = msg;\n el.style.background = kind === 'ok' ? 'var(--green)' : 'var(--red)';\n el.classList.add('show');\n clearTimeout(toast._t);\n toast._t = setTimeout(() => el.classList.remove('show'), 4000);\n}\n\nasync function loadBoards() {\n const r = await fetch('/api/boards');\n if (!r.ok) throw new Error('failed to load boards');\n state.boards = await r.json();\n const picker = document.getElementById('boardPicker');\n picker.innerHTML = '';\n for (const b of state.boards) {\n const opt = document.createElement('option');\n opt.value = b.id; opt.textContent = b.name || b.id;\n picker.appendChild(opt);\n }\n picker.onchange = () => switchBoard(picker.value);\n if (state.boards.length) switchBoard(state.boards[0].id);\n}\n\nasync function switchBoard(id) {\n state.current = state.boards.find(b => b.id === id);\n state.meta = { members: [], milestones: [], labels: [] };\n renderColumns();\n populateFilters();\n await loadItems();\n // Fetch members + milestones in the background for pickers.\n loadMeta();\n}\n\nasync function loadMeta() {\n if (!state.current) return;\n try {\n const [mem, ms] = await Promise.all([\n fetch(\\`/api/boards/\\${state.current.id}/members\\`).then(r => r.ok ? r.json() : []),\n fetch(\\`/api/boards/\\${state.current.id}/milestones\\`).then(r => r.ok ? r.json() : []),\n ]);\n state.meta.members = mem;\n state.meta.milestones = ms;\n state.meta.labels = collectKnownLabels();\n } catch (e) { /* non-fatal */ }\n}\n\nfunction collectKnownLabels() {\n const set = new Set();\n for (const list of Object.values(state.items)) for (const i of list) {\n for (const l of (i.labels || [])) set.add(l);\n }\n return [...set].sort();\n}\n\nfunction populateFilters() {\n if (!state.current) return;\n const labelsEl = document.getElementById('filterLabels');\n labelsEl.innerHTML = '';\n for (const lbl of state.current.labels || []) {\n const chip = document.createElement('span');\n chip.className = 'label';\n chip.style.cursor = 'pointer';\n chip.style.background = lbl.color || labelColor(lbl.name);\n chip.style.color = pickTextColor(lbl.color);\n chip.textContent = lbl.name;\n chip.onclick = () => {\n state.filter.label = state.filter.label === lbl.name ? '' : lbl.name;\n renderChipStates(); loadItems();\n };\n labelsEl.appendChild(chip);\n }\n}\nfunction renderChipStates() {\n document.querySelectorAll('#filterLabels .label').forEach(chip => {\n chip.style.opacity = (state.filter.label && chip.textContent !== state.filter.label) ? '0.35' : '1';\n });\n}\n\nfunction renderColumns() {\n const main = document.getElementById('columns');\n main.innerHTML = '';\n if (!state.current) return;\n for (const col of state.current.columns) {\n const section = document.createElement('section');\n section.className = 'col';\n section.dataset.colId = col.id;\n const canAdd = col.kind !== 'closed';\n section.innerHTML = \\`\n <div class=\"accent\" style=\"background:\\${col.accent || 'var(--gray)'}\"></div>\n <header class=\"col-h\">\n <h3>\\${escapeHtml(col.title)}</h3>\n <span class=\"count\">0</span>\n \\${canAdd ? \\`<button class=\"add\" data-col-id=\"\\${col.id}\" title=\"New issue in this column\">+</button>\\` : ''}\n </header>\n <div class=\"stack\" data-col-id=\"\\${col.id}\"></div>\n \\`;\n main.appendChild(section);\n }\n document.querySelectorAll('.col .add').forEach(btn => {\n btn.onclick = () => openCreateModal(btn.dataset.colId);\n });\n document.querySelectorAll('.stack').forEach(el => {\n Sortable.create(el, {\n group: 'board', animation: 150,\n onEnd: async (ev) => {\n const from = ev.from.dataset.colId;\n const to = ev.to.dataset.colId;\n if (from === to) return;\n const itemId = ev.item.dataset.id;\n try {\n const r = await fetch(\\`/api/boards/\\${state.current.id}/items/\\${encodeURIComponent(itemId)}/move\\`, {\n method: 'PATCH', headers: csrfHeaders(), body: JSON.stringify({ from, to })\n });\n if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || r.statusText);\n toast('Moved ✓', 'ok');\n updateCounts();\n } catch (e) {\n ev.from.insertBefore(ev.item, ev.from.children[ev.oldIndex] || null);\n updateCounts();\n toast('Move failed: ' + (e.message || 'unknown'));\n }\n }\n });\n });\n}\n\nfunction updateCounts() {\n document.querySelectorAll('.col').forEach(col => {\n const n = col.querySelector('.stack').children.length;\n const empty = col.querySelector('.stack .empty');\n const real = empty ? 0 : n;\n col.querySelector('.count').textContent = real;\n });\n}\n\nasync function loadItems() {\n const conn = document.getElementById('conn');\n conn.className = 'conn warn'; conn.title = 'loading…';\n if (!state.current) return;\n const params = new URLSearchParams();\n if (state.filter.q) params.set('q', state.filter.q);\n if (state.filter.assignee) params.set('assignee', state.filter.assignee);\n if (state.filter.label) params.set('label', state.filter.label);\n if (state.filter.milestone) params.set('milestone', state.filter.milestone);\n try {\n const r = await fetch(\\`/api/boards/\\${state.current.id}/items?\\${params.toString()}\\`);\n if (!r.ok) throw new Error(await r.text());\n const data = await r.json();\n state.items = data.columns;\n paintItems();\n populateAssigneeFilter();\n populateMilestoneFilter();\n conn.className = 'conn ok'; conn.title = 'ok';\n } catch (e) {\n conn.className = 'conn err'; conn.title = 'load failed';\n toast('Load failed: ' + (e.message || 'unknown'));\n }\n}\n\nfunction populateAssigneeFilter() {\n const set = new Set();\n for (const list of Object.values(state.items)) for (const i of list) if (i.assignee) set.add(i.assignee);\n const sel = document.getElementById('filterAssignee');\n const current = sel.value;\n sel.innerHTML = '<option value=\"\">All assignees</option>';\n for (const a of [...set].sort()) {\n const o = document.createElement('option'); o.value = a; o.textContent = a;\n sel.appendChild(o);\n }\n sel.value = current;\n}\nfunction populateMilestoneFilter() {\n const set = new Set();\n for (const list of Object.values(state.items)) for (const i of list) if (i.milestone) set.add(i.milestone);\n const sel = document.getElementById('filterMilestone');\n const current = sel.value;\n sel.innerHTML = '<option value=\"\">Any milestone</option>';\n for (const m of [...set].sort()) {\n const o = document.createElement('option'); o.value = m; o.textContent = m;\n sel.appendChild(o);\n }\n sel.value = current;\n}\n\nfunction paintItems() {\n if (!state.current) return;\n for (const col of state.current.columns) {\n const stack = document.querySelector(\\`.stack[data-col-id=\"\\${col.id}\"]\\`);\n if (!stack) continue;\n stack.innerHTML = '';\n const list = state.items[col.id] || [];\n if (list.length === 0) {\n const empty = document.createElement('div'); empty.className = 'empty'; empty.textContent = 'Empty';\n stack.appendChild(empty);\n } else {\n for (const item of list) stack.appendChild(renderCard(item, col));\n }\n }\n updateCounts();\n}\n\nfunction renderCard(item, col) {\n const card = document.createElement('div');\n card.className = 'card' + (item.state === 'closed' ? ' closed' : '');\n card.dataset.id = item.id;\n const iid = item.id.split(':').slice(-1)[0];\n\n // Filter out Status::* scoped labels from display (represented by the column).\n const details = (item.labelDetails || (item.labels || []).map(n => ({ name: n })))\n .filter(l => !/^Status::/i.test(l.name));\n const labelChips = details.slice(0, 5).map(l => {\n const bg = l.color || labelColor(l.name);\n const fg = l.text_color || pickTextColor(bg);\n return \\`<span class=\"label\" title=\"\\${escapeHtml(l.name)}\" style=\"background:\\${bg};color:\\${fg}\">\\${escapeHtml(l.name)}</span>\\`;\n }).join('');\n\n const assignees = (item.assignees && item.assignees.length)\n ? item.assignees\n : (item.assignee ? [{ username: item.assignee }] : []);\n const avatarStack = assignees.length\n ? \\`<span class=\"avatar-stack\">\\${assignees.slice(0, 3).map(renderAvatar).join('')}</span>\\`\n : \\`<span class=\"avatar unassigned\" title=\"unassigned\">?</span>\\`;\n\n const milestone = item.milestone ? \\`<span class=\"milestone\" title=\"Milestone\">🏁 \\${escapeHtml(item.milestone)}</span>\\` : '';\n const est = item.estimatedSeconds ? \\`<span class=\"est\" title=\"Estimate\">⏱ \\${fmtDuration(item.estimatedSeconds)}</span>\\` : '';\n const date = item.updatedAt ? \\`<span class=\"date\" title=\"Updated \\${escapeHtml(item.updatedAt)}\">\\${fmtDate(item.updatedAt)}</span>\\` : '';\n const link = item.url ? \\`<a class=\"link\" href=\"\\${escapeHtml(item.url)}\" target=\"_blank\" rel=\"noopener\" title=\"Open in GitLab\" onclick=\"event.stopPropagation()\">↗</a>\\` : '';\n\n card.innerHTML = \\`\n <div class=\"title\">\\${escapeHtml(item.title || '(untitled)')}</div>\n \\${labelChips ? \\`<div class=\"labels\">\\${labelChips}</div>\\` : ''}\n <div class=\"foot\">\n \\${avatarStack}\n <span class=\"iid\">#\\${escapeHtml(iid)}</span>\n \\${milestone}\n \\${est}\n \\${date}\n \\${link}\n </div>\n \\`;\n card.addEventListener('click', (e) => {\n if (e.target.closest('a,button')) return;\n openDetailModal(item.id);\n });\n return card;\n}\n\nfunction renderAvatar(a) {\n const name = a.name || a.username || '?';\n const img = a.avatarUrl ? \\`<img src=\"\\${escapeHtml(a.avatarUrl)}\" alt=\"\">\\` : '';\n const bg = labelColor(a.username || name);\n return \\`<span class=\"avatar\" title=\"\\${escapeHtml(name)}\" style=\"background:\\${bg}\">\\${img || escapeHtml(initials(name))}</span>\\`;\n}\n\nfunction escapeHtml(s) {\n return String(s).replace(/[&<>\"']/g, c => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[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","// --- Glossary page ---\n//\n// Plain-English definitions of the terms that appear on every dashboard\n// surface. Static; sourced from ui-labels.GLOSSARY.\n//\n// Small surface — could have lived inline in board-dashboard.ts, but\n// having it as its own file keeps the \"one route, one page module\"\n// convention and lets the `esc` helper come from ui/util instead of a\n// local duplicate.\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\nimport { UI_LABELS, GLOSSARY } from \"../../ui-labels\"\n\nexport interface GlossaryPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderGlossaryPage(opts: GlossaryPageOpts = {}): string {\n const items = GLOSSARY.map((g) => {\n const alias = g.alias\n ? `<span class=\"alias\" title=\"Schema key\">${esc(g.alias)}</span>`\n : \"\"\n return `<article class=\"term\"><h3>${esc(g.term)}${alias}</h3><p>${esc(g.definition)}</p></article>`\n }).join(\"\")\n\n const body = `<h1>Plain-English glossary</h1>\n <p class=\"lead\">What the terms on the dashboard mean. Schema keys (shown in the pill on the right of each term) are what you'd write in <code>agentx.json</code> — the dashboard just relabels them for readability.</p>\n ${items}`\n\n return renderShell({\n title: `${UI_LABELS.brand} · Glossary`,\n activeTab: \"glossary\",\n subtitle: \"Glossary\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: GLOSSARY_PAGE_CSS,\n })\n}\n\nconst GLOSSARY_PAGE_CSS = `\nmain { max-width: 760px; margin: 0 auto; padding: 32px 24px 80px; }\nh1 { font-size: 22px; margin: 0 0 6px; font-weight: 600; letter-spacing: -0.01em; }\n.lead {\n color: var(--ax-muted); margin: 0 0 28px;\n font-size: 13px; line-height: 1.6;\n}\n.lead code {\n font-family: var(--ax-mono); font-size: 12px;\n background: var(--ax-surface); padding: 1px 6px; border-radius: 3px;\n border: 1px solid var(--ax-border);\n}\narticle.term {\n background: var(--ax-surface); border: 1px solid var(--ax-border);\n border-radius: 6px; padding: 16px 20px; margin: 10px 0;\n}\narticle.term h3 {\n font-size: 14px; margin: 0 0 6px;\n display: flex; align-items: center; gap: 10px;\n font-weight: 600; letter-spacing: -0.005em;\n}\narticle.term .alias {\n font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--ax-muted); background: var(--ax-bg);\n padding: 2px 7px; border-radius: 3px;\n font-weight: 500; font-family: var(--ax-mono);\n border: 1px solid var(--ax-border);\n}\narticle.term p {\n margin: 0; color: var(--ax-text-2); font-size: 13px; line-height: 1.6;\n}`\n","// --- Workflows page (Phase 2: observability) ---\n//\n// Read-only view over the workflow definitions + runs under .agentx/workflows/.\n// Scope is deliberately tight: list workflows, show per-workflow status,\n// inspect recent runs. No visual editor, no manual-run button — those land\n// in later phases once the observability surface has proven useful.\n//\n// Renders a server skeleton; the client JS fetches /api/workflows and\n// /api/workflows/runs, paints cards, and streams the selected run's history\n// via SSE at /api/workflows/runs/<runId>/stream.\n//\n// Tab highlight note: the TopbarTab enum does not yet contain \"workflows\".\n// Using \"admin\" as placeholder — the integration-seam commit adds\n// \"workflows\" to src/daemon/topbar.ts:TopbarTab and this switches over.\n\nimport { renderShell, esc, type TopbarPeer } from \"..\"\n\nexport interface WorkflowsPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderWorkflowsPage(opts: WorkflowsPageOpts = {}): string {\n const body = `<div class=\"ax-wf__root\">\n <aside class=\"ax-wf__list\">\n <header>\n <h2>Workflows</h2>\n <div class=\"ax-wf__header-actions\">\n <a id=\"wf-new\" class=\"ax-wf__icon\" href=\"/workflows/editor?new=1\" title=\"New workflow\">+</a>\n <button id=\"wf-refresh\" class=\"ax-wf__icon\" title=\"Refresh (r)\">↻</button>\n </div>\n </header>\n <div class=\"ax-wf__search\">\n <input id=\"wf-filter\" type=\"search\" placeholder=\"Filter by id or title…\" autocomplete=\"off\" />\n </div>\n <ul id=\"wf-list\" class=\"ax-wf__cards\" aria-live=\"polite\"></ul>\n <div id=\"wf-empty\" class=\"ax-wf__empty\" hidden>\n <p>No workflows defined.</p>\n <p class=\"hint\">Add a definition under <code>.agentx/workflows/*.json</code>, validate with <code>agentx workflow validate</code>, and reload.</p>\n </div>\n </aside>\n\n <section class=\"ax-wf__detail\">\n <header id=\"wf-detail-head\">\n <span class=\"hint\">Select a workflow to inspect.</span>\n </header>\n <div id=\"wf-detail-body\" class=\"ax-wf__detail-body\"></div>\n </section>\n\n <aside id=\"wf-run-panel\" class=\"ax-wf__run-panel hidden\" aria-hidden=\"true\">\n <header>\n <div>\n <h3 id=\"wf-run-title\">Run</h3>\n <span id=\"wf-run-meta\" class=\"hint\"></span>\n </div>\n <button id=\"wf-run-close\" class=\"ax-wf__icon\" title=\"Close (esc)\">×</button>\n </header>\n <div id=\"wf-run-timeline\" class=\"ax-wf__timeline\"></div>\n <footer>\n <span id=\"wf-run-status\" class=\"ax-wf__status-pill\">—</span>\n <span id=\"wf-run-live\" class=\"hint\">—</span>\n </footer>\n </aside>\n</div>\n\n<div id=\"wf-toast\" class=\"ax-wf__toast\" hidden></div>`\n\n return renderShell({\n title: \"AgentX · Workflows\",\n activeTab: \"workflows\",\n subtitle: \"Workflows\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: WORKFLOWS_PAGE_CSS,\n scripts: `<script>${WORKFLOWS_PAGE_SCRIPT}</script>`,\n })\n}\n\n// ───────────────────────── CSS ─────────────────────────\n\nconst WORKFLOWS_PAGE_CSS = `\n.ax-wf__root {\n display: grid;\n grid-template-columns: 340px 1fr;\n grid-template-rows: 100%;\n height: calc(100vh - var(--ax-topbar-h, 48px));\n background: var(--ax-bg);\n}\n.ax-wf__list {\n border-right: 1px solid var(--ax-border);\n display: flex; flex-direction: column;\n background: var(--ax-bg-elev);\n}\n.ax-wf__list > header {\n display: flex; align-items: center; justify-content: space-between;\n padding: 14px 18px; border-bottom: 1px solid var(--ax-border);\n}\n.ax-wf__header-actions {\n display: flex; gap: 2px; align-items: center;\n}\n.ax-wf__header-actions a.ax-wf__icon {\n text-decoration: none;\n}\n.ax-wf__detail-title { flex: 1; }\n.ax-wf__detail-actions { display: flex; gap: 6px; }\n.ax-wf__btn {\n font-family: inherit; font-size: 12px;\n background: var(--ax-bg); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 4px;\n padding: 5px 10px; cursor: pointer;\n text-decoration: none; display: inline-flex; gap: 4px; align-items: center;\n transition: background 120ms ease, border-color 120ms ease;\n}\n.ax-wf__btn:hover { background: var(--ax-surface); border-color: var(--ax-border-2); }\n.ax-wf__list > header h2 {\n font-size: 14px; font-weight: 600; margin: 0; letter-spacing: -0.005em;\n}\n.ax-wf__search { padding: 10px 14px; border-bottom: 1px solid var(--ax-border); }\n.ax-wf__search input {\n width: 100%; padding: 7px 10px; font-size: 12px;\n background: var(--ax-bg); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 4px;\n font-family: inherit;\n}\n.ax-wf__cards {\n list-style: none; margin: 0; padding: 8px 0;\n overflow-y: auto; flex: 1;\n}\n.ax-wf__card {\n padding: 10px 18px; border-bottom: 1px solid var(--ax-border);\n cursor: pointer; transition: background 120ms ease;\n}\n.ax-wf__card:hover { background: var(--ax-surface); }\n.ax-wf__card.is-active { background: var(--ax-surface); box-shadow: inset 3px 0 0 var(--ax-accent); }\n.ax-wf__card-title {\n font-size: 13px; font-weight: 600; letter-spacing: -0.005em;\n color: var(--ax-text); margin-bottom: 3px;\n}\n.ax-wf__card-id {\n font-family: var(--ax-mono); font-size: 10px; color: var(--ax-muted);\n}\n.ax-wf__card-meta {\n display: flex; gap: 8px; flex-wrap: wrap;\n margin-top: 6px; font-size: 11px; color: var(--ax-text-2);\n}\n.ax-wf__card-meta .tag {\n padding: 1px 7px; border-radius: 3px; background: var(--ax-bg);\n border: 1px solid var(--ax-border); font-size: 10px;\n font-family: var(--ax-mono);\n}\n.ax-wf__card-meta .live {\n color: var(--ax-accent);\n border-color: color-mix(in oklch, var(--ax-accent) 50%, transparent);\n}\n\n.ax-wf__detail {\n display: flex; flex-direction: column;\n overflow: hidden;\n}\n.ax-wf__detail > header {\n padding: 16px 22px; border-bottom: 1px solid var(--ax-border);\n display: flex; align-items: center; gap: 12px;\n background: var(--ax-bg-elev);\n}\n.ax-wf__detail > header h2 {\n font-size: 15px; font-weight: 600; margin: 0; letter-spacing: -0.01em;\n}\n.ax-wf__detail-body {\n padding: 22px; overflow: auto;\n display: grid; gap: 18px;\n grid-template-columns: 1fr 1fr;\n align-content: start;\n}\n.ax-wf__panel {\n background: var(--ax-bg-elev); border: 1px solid var(--ax-border);\n border-radius: 6px; padding: 14px 16px;\n}\n.ax-wf__panel h3 {\n font-size: 11px; font-weight: 600; letter-spacing: 0.04em;\n text-transform: uppercase; color: var(--ax-muted);\n margin: 0 0 10px;\n}\n.ax-wf__panel--full { grid-column: 1 / -1; }\n.ax-wf__states {\n display: flex; flex-wrap: wrap; gap: 6px;\n font-size: 11px;\n}\n.ax-wf__state {\n font-family: var(--ax-mono); padding: 2px 8px;\n border-radius: 3px; background: var(--ax-surface);\n border: 1px solid var(--ax-border);\n}\n.ax-wf__state.is-terminal { color: var(--ax-muted); opacity: 0.75; }\n.ax-wf__trans {\n font-family: var(--ax-mono); font-size: 11px;\n color: var(--ax-text-2); line-height: 1.8;\n}\n.ax-wf__trans .arr { color: var(--ax-muted); }\n.ax-wf__trans .cond { color: var(--ax-accent); font-size: 10px; }\n.ax-wf__runs {\n display: flex; flex-direction: column; gap: 4px;\n}\n.ax-wf__run-row {\n display: grid;\n grid-template-columns: 76px 1fr 110px 110px 24px;\n gap: 10px; align-items: center;\n padding: 7px 8px; border-radius: 4px;\n font-size: 12px; cursor: pointer;\n}\n.ax-wf__run-row:hover { background: var(--ax-surface); }\n.ax-wf__run-row .id { font-family: var(--ax-mono); color: var(--ax-muted); font-size: 11px; }\n.ax-wf__run-row .status { font-size: 10px; text-align: center; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; font-family: var(--ax-mono); }\n.ax-wf__run-row .status.running { background: color-mix(in oklch, var(--ax-accent) 15%, transparent); color: var(--ax-accent); }\n.ax-wf__run-row .status.completed { color: var(--ax-muted); }\n.ax-wf__run-row .status.failed { color: var(--ax-err); }\n.ax-wf__run-row .status.paused { color: var(--ax-warn); }\n.ax-wf__run-row .status.canceled { color: var(--ax-muted); opacity: 0.7; }\n.ax-wf__json {\n font-family: var(--ax-mono); font-size: 11px;\n background: var(--ax-bg); color: var(--ax-text);\n border: 1px solid var(--ax-border); border-radius: 4px;\n padding: 10px 12px; max-height: 280px; overflow: auto;\n white-space: pre-wrap; word-break: break-word;\n}\n\n/* run detail drawer */\n.ax-wf__run-panel {\n position: fixed; top: var(--ax-topbar-h, 48px); right: 0; bottom: 0;\n width: 420px; background: var(--ax-bg-elev); border-left: 1px solid var(--ax-border);\n transform: translateX(100%); transition: transform 180ms ease;\n display: flex; flex-direction: column; z-index: 40;\n}\n.ax-wf__run-panel:not(.hidden) { transform: translateX(0); }\n.ax-wf__run-panel > header {\n display: flex; justify-content: space-between; align-items: center;\n padding: 14px 18px; border-bottom: 1px solid var(--ax-border);\n}\n.ax-wf__run-panel h3 {\n font-size: 13px; font-weight: 600; margin: 0 0 2px;\n}\n.ax-wf__timeline {\n flex: 1; overflow-y: auto; padding: 12px 18px;\n display: flex; flex-direction: column; gap: 8px;\n}\n.ax-wf__timeline-entry {\n border-left: 2px solid var(--ax-border);\n padding: 6px 0 10px 12px; position: relative;\n}\n.ax-wf__timeline-entry::before {\n content: \"\"; position: absolute; left: -5px; top: 10px;\n width: 8px; height: 8px; background: var(--ax-accent);\n border-radius: 50%;\n}\n.ax-wf__timeline-entry.result-failed::before { background: var(--ax-err); }\n.ax-wf__timeline-entry .time {\n font-family: var(--ax-mono); font-size: 10px; color: var(--ax-muted);\n}\n.ax-wf__timeline-entry .move {\n font-size: 12px; margin: 2px 0;\n}\n.ax-wf__timeline-entry .agent {\n font-family: var(--ax-mono); font-size: 10px; color: var(--ax-text-2);\n}\n.ax-wf__run-panel > footer {\n padding: 12px 18px; border-top: 1px solid var(--ax-border);\n display: flex; justify-content: space-between; align-items: center;\n}\n.ax-wf__status-pill {\n font-family: var(--ax-mono); font-size: 10px; text-transform: uppercase;\n padding: 2px 8px; border-radius: 3px;\n}\n.ax-wf__status-pill.running { background: color-mix(in oklch, var(--ax-accent) 15%, transparent); color: var(--ax-accent); }\n.ax-wf__status-pill.completed { color: var(--ax-muted); background: var(--ax-surface); }\n.ax-wf__status-pill.failed { color: var(--ax-err); background: color-mix(in oklch, var(--ax-err) 15%, transparent); }\n\n.ax-wf__icon {\n background: none; border: none; color: var(--ax-muted);\n cursor: pointer; font-size: 18px; padding: 4px 8px;\n}\n.ax-wf__icon:hover { color: var(--ax-text); }\n.ax-wf__empty {\n padding: 40px 22px; color: var(--ax-muted); font-size: 13px;\n text-align: center;\n}\n.ax-wf__empty .hint { font-size: 11px; margin-top: 6px; }\n.ax-wf__empty code {\n font-family: var(--ax-mono); background: var(--ax-surface);\n padding: 1px 6px; border-radius: 3px;\n border: 1px solid var(--ax-border);\n}\n.ax-wf__toast {\n position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);\n padding: 8px 16px; background: var(--ax-bg-elev);\n border: 1px solid var(--ax-border); border-radius: 4px;\n font-size: 12px; z-index: 50;\n}\n.hint { color: var(--ax-muted); font-size: 11px; }\n`\n\n// ───────────────────────── CLIENT JS ─────────────────────────\n//\n// Kept inline in the module per the existing page convention. All endpoints\n// assumed by this script are wired by daemon/workflows-api.ts.\n\nconst WORKFLOWS_PAGE_SCRIPT = `\n(() => {\n const $ = (sel) => document.querySelector(sel)\n const $$ = (sel) => Array.from(document.querySelectorAll(sel))\n\n const state = {\n workflows: [],\n runs: [],\n selectedId: null,\n filter: \"\",\n sse: null,\n }\n\n const token = (typeof localStorage !== \"undefined\" && localStorage.getItem(\"ax_token\")) || \"\"\n const headers = () => token ? { \"Authorization\": \"Bearer \" + token } : {}\n\n async function fetchJSON(url) {\n const res = await fetch(url, { headers: headers() })\n if (!res.ok) throw new Error(url + \": \" + res.status)\n return res.json()\n }\n\n // --- Rendering -------------------------------------------------------\n\n function renderList() {\n const needle = state.filter.toLowerCase()\n const filtered = state.workflows.filter(wf =>\n !needle || wf.id.toLowerCase().includes(needle) || (wf.title || \"\").toLowerCase().includes(needle))\n\n const runsByWorkflow = state.runs.reduce((acc, r) => {\n (acc[r.workflowId] ||= []).push(r); return acc\n }, {})\n\n if (filtered.length === 0) {\n $(\"#wf-list\").innerHTML = \"\"\n $(\"#wf-empty\").hidden = state.workflows.length > 0\n return\n }\n $(\"#wf-empty\").hidden = true\n\n $(\"#wf-list\").innerHTML = filtered.map(wf => {\n const myRuns = runsByWorkflow[wf.id] || []\n const live = myRuns.filter(r => r.status === \"running\").length\n const lastRun = myRuns[0]\n const isActive = state.selectedId === wf.id\n // V2: trigger is a node inside wf.nodes, not a workflow-level field.\n const triggerNode = (wf.nodes || []).find(n => n && n.type && n.type.indexOf(\"trigger.\") === 0)\n const triggerSource = triggerNode ? (triggerNode.config && triggerNode.config.source) || triggerNode.type.replace(\"trigger.\", \"\") : \"?\"\n return \\`\n <li class=\"ax-wf__card \\${isActive ? \"is-active\" : \"\"}\" data-id=\"\\${wf.id}\">\n <div class=\"ax-wf__card-title\">\\${esc(wf.title || wf.id)}</div>\n <div class=\"ax-wf__card-id\">\\${esc(wf.id)} · v\\${wf.version}</div>\n <div class=\"ax-wf__card-meta\">\n <span class=\"tag\">\\${esc(triggerSource)}</span>\n \\${live > 0 ? \\`<span class=\"tag live\">\\${live} running</span>\\` : \"\"}\n \\${lastRun ? \\`<span class=\"tag\">last: \\${esc(lastRun.status)}</span>\\` : \\`<span class=\"tag\">no runs</span>\\`}\n </div>\n </li>\\`\n }).join(\"\")\n\n $$(\".ax-wf__card\").forEach(el => {\n el.addEventListener(\"click\", () => select(el.dataset.id))\n })\n }\n\n function renderDetail() {\n const wf = state.workflows.find(w => w.id === state.selectedId)\n if (!wf) {\n $(\"#wf-detail-head\").innerHTML = \\`<span class=\"hint\">Select a workflow to inspect.</span>\\`\n $(\"#wf-detail-body\").innerHTML = \"\"\n return\n }\n // V2: trigger is a node. Pull source + filter from its config.\n const triggerNode = (wf.nodes || []).find(n => n && n.type && n.type.indexOf(\"trigger.\") === 0)\n const triggerCfg = (triggerNode && triggerNode.config) || {}\n const filter = triggerCfg.filter || {}\n const trigFilter = [filter.project, filter.repo, filter.chat].filter(Boolean).join(\" / \")\n const triggerSource = triggerNode ? (triggerCfg.source || triggerNode.type.replace(\"trigger.\", \"\")) : \"?\"\n\n $(\"#wf-detail-head\").innerHTML = \\`\n <div class=\"ax-wf__detail-title\">\n <h2>\\${esc(wf.title || wf.id)}</h2>\n <span class=\"hint\">\\${esc(wf.id)} · v\\${wf.version} · trigger: \\${esc(triggerSource)}\\${trigFilter ? \" · \" + esc(trigFilter) : \"\"}</span>\n </div>\n <div class=\"ax-wf__detail-actions\">\n <a class=\"ax-wf__btn\" href=\"/workflows/editor?id=\\${encodeURIComponent(wf.id)}\" title=\"Open in visual editor\">✎ Edit</a>\n </div>\\`\n\n // V2: list nodes (with brief type + any agent id) and edges. No more\n // states/transitions — the DAG structure lives in nodes + edges.\n const nodeCount = (wf.nodes || []).length\n const edgeCount = (wf.edges || []).length\n const nodeEntries = (wf.nodes || []).map((n) => {\n const cfg = n.config || {}\n const badge = n.type === \"agent\" ? (cfg.agentId || \"(no agent)\")\n : n.type.indexOf(\"action.\") === 0 ? n.type.replace(\"action.\", \"\")\n : n.type.indexOf(\"trigger.\") === 0 ? (cfg.source || n.type.replace(\"trigger.\", \"\"))\n : n.type\n return \\`<span class=\"ax-wf__state\" title=\"\\${esc(n.type)}\">\\${esc(n.id)} <span class=\"hint\" style=\"margin-left:4px\">\\${esc(badge)}</span></span>\\`\n }).join(\" \")\n\n const edgesHtml = (wf.edges || []).map((e) => {\n const label = e.label ? \\` <span class=\"cond\">\\${esc(e.label)}</span>\\` : \"\"\n const port = e.fromPort ? \\` :\\${esc(e.fromPort)}\\` : \"\"\n return \\`<div><span>\\${esc(e.from)}\\${port}</span> <span class=\"arr\">→</span> <span>\\${esc(e.to)}</span>\\${label}</div>\\`\n }).join(\"\") || \\`<span class=\"hint\">(no edges)</span>\\`\n\n const runs = state.runs.filter(r => r.workflowId === wf.id).slice(0, 50)\n const runsHtml = runs.length ? runs.map(r => {\n // V2: no more run.state. Show the next pending node (if running) or\n // the last executed node (if terminal).\n const last = (r.history && r.history.length) ? r.history[r.history.length - 1] : null\n const position = r.status === \"running\" && r.pending && r.pending.length\n ? r.pending[0] + \" (next)\"\n : last ? last.nodeId + \" (\" + last.status + \")\"\n : \"—\"\n return \\`<div class=\"ax-wf__run-row\" data-runid=\"\\${r.id}\">\n <span class=\"id\">\\${r.id.slice(0,8)}</span>\n <span>\\${esc(position)} <span class=\"hint\">· \\${esc(r.entityRef.id)}</span></span>\n <span class=\"hint\">\\${relTime(r.updatedAt)}</span>\n <span class=\"status \\${r.status}\">\\${r.status}</span>\n <span>›</span>\n </div>\\`\n }).join(\"\") : \\`<span class=\"hint\">No runs yet for this workflow.</span>\\`\n\n $(\"#wf-detail-body\").innerHTML = \\`\n <div class=\"ax-wf__panel\">\n <h3>Nodes (\\${nodeCount})</h3>\n <div class=\"ax-wf__states\">\\${nodeEntries}</div>\n </div>\n <div class=\"ax-wf__panel\">\n <h3>Edges (\\${edgeCount})</h3>\n <div class=\"ax-wf__trans\">\\${edgesHtml}</div>\n </div>\n <div class=\"ax-wf__panel ax-wf__panel--full\">\n <h3>Recent runs (\\${runs.length})</h3>\n <div class=\"ax-wf__runs\">\\${runsHtml}</div>\n </div>\n <div class=\"ax-wf__panel ax-wf__panel--full\">\n <h3>Definition</h3>\n <pre class=\"ax-wf__json\">\\${esc(JSON.stringify(wf, null, 2))}</pre>\n </div>\\`\n\n $$(\".ax-wf__run-row\").forEach(el => {\n el.addEventListener(\"click\", () => openRun(el.dataset.runid))\n })\n }\n\n // --- Run drawer ------------------------------------------------------\n\n async function openRun(runId) {\n const res = await fetchJSON(\"/api/workflows/runs/\" + encodeURIComponent(runId))\n paintRun(res.run || res)\n connectRunStream(runId)\n const panel = $(\"#wf-run-panel\")\n panel.classList.remove(\"hidden\")\n panel.setAttribute(\"aria-hidden\", \"false\")\n }\n\n function closeRun() {\n if (state.sse) { state.sse.close(); state.sse = null }\n const panel = $(\"#wf-run-panel\")\n panel.classList.add(\"hidden\")\n panel.setAttribute(\"aria-hidden\", \"true\")\n }\n\n function paintRun(run) {\n $(\"#wf-run-title\").textContent = run.workflowId + \" run\"\n $(\"#wf-run-meta\").textContent = run.id.slice(0, 8) + \" · \" + run.entityRef.backend + \":\" + run.entityRef.id + \" · home=\" + run.homeNode\n const pill = $(\"#wf-run-status\")\n pill.textContent = run.status\n pill.className = \"ax-wf__status-pill \" + run.status\n // V2: history entries are per-node executions, not state transitions.\n // Each entry has { at, nodeId, status, inputKeys, output?, note? }.\n $(\"#wf-run-timeline\").innerHTML = (run.history || []).map(h => {\n const failed = h.status === \"failed\" || h.status === \"timeout\"\n const outputHint = h.output ? Object.keys(h.output).slice(0, 3).map(k => k + \"=\" + briefVal(h.output[k])).join(\" · \") : \"\"\n return \\`<div class=\"ax-wf__timeline-entry \\${failed ? \"result-failed\" : \"\"}\">\n <div class=\"time\">\\${new Date(h.at).toLocaleString()}</div>\n <div class=\"move\"><strong>\\${esc(h.nodeId)}</strong> · \\${esc(h.status)}</div>\n \\${h.inputKeys && h.inputKeys.length ? \\`<div class=\"agent\">inputs: \\${h.inputKeys.map(esc).join(\", \")}</div>\\` : \"\"}\n \\${outputHint ? \\`<div class=\"agent\">output: \\${esc(outputHint)}</div>\\` : \"\"}\n \\${h.note ? \\`<div class=\"agent\">\\${esc(h.note)}</div>\\` : \"\"}\n </div>\\`\n }).join(\"\")\n }\n\n function connectRunStream(runId) {\n if (state.sse) state.sse.close()\n try {\n state.sse = new EventSource(\"/api/workflows/runs/\" + encodeURIComponent(runId) + \"/stream\")\n $(\"#wf-run-live\").textContent = \"live\"\n state.sse.onmessage = (evt) => {\n try {\n const run = JSON.parse(evt.data)\n paintRun(run)\n } catch { /* ignore */ }\n }\n state.sse.onerror = () => { $(\"#wf-run-live\").textContent = \"disconnected\" }\n } catch { $(\"#wf-run-live\").textContent = \"sse unavailable\" }\n }\n\n // --- Actions ---------------------------------------------------------\n\n async function select(id) {\n state.selectedId = id\n // Keep URL hash in sync so refreshes and editor deep-links stay on the\n // same workflow. Use replaceState so the browser's back button doesn't\n // accumulate selection history.\n try { history.replaceState(null, \"\", \"#\" + encodeURIComponent(id)) } catch (_) { /* */ }\n renderList()\n renderDetail()\n }\n\n async function refresh() {\n try {\n const [workflows, runs] = await Promise.all([\n fetchJSON(\"/api/workflows\"),\n fetchJSON(\"/api/workflows/runs?limit=100\"),\n ])\n state.workflows = workflows.workflows || workflows\n state.runs = runs.runs || runs\n // If the URL hash names a workflow (e.g. /workflows#my-workflow)\n // and nothing is selected yet, auto-select it. Editor's \"History\"\n // button links here with the hash, so users land on the right detail.\n if (!state.selectedId && location.hash) {\n const fromHash = decodeURIComponent(location.hash.slice(1))\n if (state.workflows.some(w => w.id === fromHash)) state.selectedId = fromHash\n }\n renderList()\n renderDetail()\n } catch (e) {\n toast(\"Failed to load workflows: \" + e.message)\n }\n }\n\n function toast(msg) {\n const el = $(\"#wf-toast\")\n el.textContent = msg\n el.hidden = false\n setTimeout(() => { el.hidden = true }, 2400)\n }\n\n function esc(s) {\n return String(s ?? \"\")\n .replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")\n .replace(/\"/g, \""\").replace(/'/g, \"'\")\n }\n\n function relTime(iso) {\n const d = new Date(iso).getTime()\n const s = Math.floor((Date.now() - d) / 1000)\n if (s < 60) return s + \"s ago\"\n if (s < 3600) return Math.floor(s/60) + \"m ago\"\n if (s < 86400) return Math.floor(s/3600) + \"h ago\"\n return Math.floor(s/86400) + \"d ago\"\n }\n\n /** Compact single-line preview of a node-output value for the timeline. */\n function briefVal(v) {\n if (v === null || v === undefined) return \"\"\n if (typeof v === \"string\") return v.length > 48 ? v.slice(0, 45) + \"…\" : v\n if (typeof v === \"number\" || typeof v === \"boolean\") return String(v)\n try {\n const s = JSON.stringify(v)\n return s.length > 48 ? s.slice(0, 45) + \"…\" : s\n } catch { return \"?\" }\n }\n\n // --- Bindings --------------------------------------------------------\n\n document.addEventListener(\"DOMContentLoaded\", () => {\n $(\"#wf-refresh\").addEventListener(\"click\", refresh)\n $(\"#wf-run-close\").addEventListener(\"click\", closeRun)\n $(\"#wf-filter\").addEventListener(\"input\", (e) => {\n state.filter = e.target.value\n renderList()\n })\n document.addEventListener(\"keydown\", (e) => {\n if (e.key === \"r\" && !e.metaKey && document.activeElement?.tagName !== \"INPUT\") refresh()\n if (e.key === \"Escape\") closeRun()\n })\n refresh()\n // Light polling keeps run counts fresh without needing a global SSE.\n setInterval(refresh, 15000)\n })\n})();\n`\n","// --- Workflow visual editor — host page ---\n//\n// Thin shell that loads the React Flow-based editor bundle served from\n// /assets/workflow-editor.js. All interaction logic lives in\n// src/web/workflow-editor/ (see main.tsx). This page is deliberately\n// minimal — just a root div the bundle mounts into, plus a reset stylesheet\n// so the bundle's CSS-in-TS can take over without inheriting dashboard\n// component styles we don't want on the canvas.\n\nimport { renderShell, type TopbarPeer } from \"..\"\n\nexport interface WorkflowEditorPageOpts {\n peers?: TopbarPeer[]\n currentPeerId?: string\n}\n\nexport function renderWorkflowEditorPage(opts: WorkflowEditorPageOpts = {}): string {\n const body = `<div id=\"wfe-root\" style=\"height:100%\"></div>`\n\n return renderShell({\n title: \"AgentX · Workflow editor\",\n activeTab: \"workflows\",\n subtitle: \"Workflow editor\",\n peers: opts.peers,\n currentPeerId: opts.currentPeerId,\n body,\n css: HOST_CSS,\n // Bundle is served as an IIFE from /assets/workflow-editor.js — built\n // by `tsup --config tsup.web.config.ts` to dist/web/workflow-editor.js.\n // `defer` keeps the script out of the head-blocking path; the bundle\n // itself waits for DOMContentLoaded before mounting.\n scripts: `<script src=\"/assets/workflow-editor.global.js\" defer></script>`,\n noMain: true,\n })\n}\n\nconst HOST_CSS = `\n/* Preload the fonts the redesign's CSS references. Placed in the shell so\n * they start fetching while the React bundle is still parsing. */\n@import url(\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Instrument+Serif&display=swap\");\n\nhtml, body { height: 100%; }\nbody { margin: 0; overflow: hidden; }\n/* The editor takes over the whole viewport except the dashboard topbar. The\n * topbar still renders via the shell so the Workflows tab highlights and\n * global nav works. */\n.ax-topbar { position: relative; z-index: 50; }\n#wfe-root { width: 100%; height: calc(100vh - 48px); }\n/* Tiny fallback while the bundle loads — prevents an empty flash. */\n#wfe-root:empty::before {\n content: \"Loading editor…\";\n display: block;\n padding: 40px 24px;\n color: #94a3b8;\n font: 13px/1 -apple-system, BlinkMacSystemFont, sans-serif;\n text-align: center;\n}\n`\n","import type { IncomingMessage, ServerResponse } from \"http\"\nimport {\n LayoutStore,\n RunStore,\n WorkflowStore,\n lintWorkflow,\n workflowSchema,\n type WorkflowLayout,\n type WorkflowRun,\n} from \"@/workflows\"\nimport type { WorkflowDispatcher } from \"@/workflows/dispatcher\"\nimport type { TaskStore } from \"@/workflows/task-store\"\nimport type { ActorStore } from \"@/actors/store\"\nimport { formSubmissionSchema } from \"@/forms/types\"\n\n// --- Workflows HTTP API ---\n//\n// Powers the /workflows dashboard (observability) + /workflows/editor pages.\n// Reads require `dashboard:read`; writes require `dashboard:write`. The\n// caller passes in a `requireScope(req, res, [...])` shim so this module\n// stays decoupled from src/daemon/token-store.ts. The daemon wires the real\n// middleware at boot.\n//\n// Endpoints:\n// GET /api/workflows — list of workflow definitions\n// POST /api/workflows — create a new workflow (body = Workflow JSON)\n// GET /api/workflows/:id — one workflow\n// PUT /api/workflows/:id — full replace (body = Workflow JSON)\n// DELETE /api/workflows/:id — delete definition + its layout\n// POST /api/workflows/:id/validate — schema + lint on the submitted body\n// (read-only \"would this save?\" check)\n// GET /api/workflows/:id/layout — node positions for the editor\n// PUT /api/workflows/:id/layout — save node positions\n// GET /api/workflows/runs?limit=N — recent runs across all workflows\n// GET /api/workflows/runs/:id — single run detail\n// GET /api/workflows/runs/:id/stream — SSE stream of that run's latest snapshot\n//\n// The server is forgiving on authoring errors: save endpoints return a\n// structured `{ error, issues: [...] }` body so the editor can highlight\n// which field broke, rather than a bare 400.\n\nexport interface WorkflowsApiDeps {\n store: WorkflowStore\n runs: RunStore\n layouts: LayoutStore\n /** Optional task store + dispatcher for the BPM /tasks + /inbox endpoints.\n * Passing the live dispatcher lets the API drive run resumes from form\n * submissions. */\n tasks?: TaskStore\n actors?: ActorStore\n dispatcher?: WorkflowDispatcher\n /** Gate the request; write the 401/403 response and return falsy on failure. */\n requireScope: (req: IncomingMessage, res: ServerResponse, scopes: string[]) => unknown\n /** Called when a run mutates — daemon exposes its broadcaster so the SSE\n * stream can be woken rather than polled. Optional; the stream falls back\n * to polling if not provided. */\n runEvents?: {\n subscribe: (runId: string, handler: (run: WorkflowRun) => void) => () => void\n }\n}\n\n/** Router the daemon calls from its HTTP handler. Returns true if the\n * request was handled (response written), false if the path isn't ours. */\nexport function handleWorkflowsApi(req: IncomingMessage, res: ServerResponse, deps: WorkflowsApiDeps): boolean {\n const url = req.url || \"\"\n if (!url.startsWith(\"/api/workflows\")) return false\n\n const method = req.method || \"GET\"\n\n // /api/workflows (GET list, POST create)\n if (url === \"/api/workflows\") {\n if (method === \"GET\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n return sendJson(res, 200, { workflows: deps.store.list() })\n }\n if (method === \"POST\") {\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n return withBody(req, res, (body) => handleSave(res, deps, body, { mode: \"create\" }))\n }\n }\n\n // /api/workflows/runs[...] (list / one / stream)\n if (url.startsWith(\"/api/workflows/runs\") && method === \"GET\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n return handleRuns(req, res, deps, url)\n }\n\n // /api/workflows/:id[/layout|/validate] (various)\n const defMatch = url.match(/^\\/api\\/workflows\\/([^\\/?]+)(\\/layout|\\/validate)?(\\?.*)?$/)\n if (defMatch) {\n const id = decodeURIComponent(defMatch[1])\n const suffix = defMatch[2]\n\n if (suffix === \"/layout\") {\n if (method === \"GET\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n return sendJson(res, 200, { layout: deps.layouts.get(id) })\n }\n if (method === \"PUT\") {\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n return withBody(req, res, (body) => {\n try {\n const saved = deps.layouts.save(id, body as WorkflowLayout)\n return sendJson(res, 200, { layout: saved })\n } catch (e: any) {\n return sendJson(res, 400, { error: \"invalid layout\", message: e.message })\n }\n })\n }\n }\n\n if (suffix === \"/validate\" && method === \"POST\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n return withBody(req, res, (body) => {\n const parsed = workflowSchema.safeParse(body)\n if (!parsed.success) {\n return sendJson(res, 200, {\n ok: false,\n issues: parsed.error.issues.map((i) => ({ path: i.path.join(\".\"), message: i.message })),\n })\n }\n const lintIssues = lintWorkflow(parsed.data).map((m) => ({ path: \"\", message: m }))\n return sendJson(res, 200, { ok: lintIssues.length === 0, issues: lintIssues, workflow: parsed.data })\n })\n }\n\n if (!suffix) {\n if (method === \"GET\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n const wf = deps.store.get(id)\n if (!wf) return sendJson(res, 404, { error: \"workflow not found\" })\n return sendJson(res, 200, { workflow: wf })\n }\n if (method === \"PUT\") {\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n return withBody(req, res, (body) => handleSave(res, deps, body, { mode: \"update\", id }))\n }\n if (method === \"DELETE\") {\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n const existed = deps.store.delete(id)\n deps.layouts.delete(id)\n return sendJson(res, existed ? 200 : 404, existed ? { ok: true } : { error: \"workflow not found\" })\n }\n }\n }\n\n // /api/workflows/tasks — GET list (inbox); ?actor=<id> filter\n // /api/workflows/tasks/:id — GET task detail\n // /api/workflows/tasks/:id/submit — POST submission\n if (url.startsWith(\"/api/workflows/tasks\")) {\n if (!deps.tasks || !deps.dispatcher) return sendJson(res, 501, { error: \"task engine not enabled\" })\n const trail = url.replace(/^\\/api\\/workflows\\/tasks/, \"\")\n if (trail === \"\" || trail.startsWith(\"?\")) {\n if (method !== \"GET\") return sendJson(res, 405, { error: \"method not allowed\" })\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n const q = new URL(url, \"http://_\").searchParams\n const actor = q.get(\"actor\") || undefined\n const tasks = actor ? deps.tasks.listForActor(actor) : deps.tasks.listOpen()\n return sendJson(res, 200, { tasks })\n }\n const match = trail.match(/^\\/([^\\/?]+)(\\/submit)?$/)\n if (match) {\n const taskId = decodeURIComponent(match[1])\n if (match[2]) {\n if (method !== \"POST\") return sendJson(res, 405, { error: \"method not allowed\" })\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n return withBody(req, res, async (body) => {\n const parsed = formSubmissionSchema.safeParse(body && typeof body === \"object\" && \"submission\" in (body as object)\n ? (body as { submission: unknown }).submission\n : body)\n if (!parsed.success) {\n return sendJson(res, 400, {\n error: \"invalid submission\",\n issues: parsed.error.issues.map((i) => ({ path: i.path.join(\".\"), message: i.message })),\n })\n }\n const submittedBy = typeof (body as any)?.submittedBy === \"string\" ? (body as any).submittedBy : \"anonymous\"\n const result = await deps.dispatcher!.submitTask(taskId, parsed.data, submittedBy)\n if (!result.ok) return sendJson(res, 400, { error: result.error, fieldErrors: result.fieldErrors })\n return sendJson(res, 200, { ok: true, runId: result.runId })\n })\n }\n if (method === \"GET\") {\n if (!deps.requireScope(req, res, [\"dashboard:read\"])) return true\n const task = deps.tasks.get(taskId)\n if (!task) return sendJson(res, 404, { error: \"task not found\" })\n return sendJson(res, 200, { task })\n }\n }\n }\n\n // /api/workflows/signal/:name — POST manual signal emission.\n // Body: { scope?: \"workflow\" | \"global\"; workflowId?: string; payload?: object }\n if (url.startsWith(\"/api/workflows/signal/\")) {\n if (method !== \"POST\") return sendJson(res, 405, { error: \"method not allowed\" })\n if (!deps.requireScope(req, res, [\"dashboard:write\"])) return true\n if (!deps.dispatcher) return sendJson(res, 501, { error: \"dispatcher not enabled\" })\n const name = decodeURIComponent(url.replace(/^\\/api\\/workflows\\/signal\\//, \"\").split(\"?\")[0])\n if (!name) return sendJson(res, 400, { error: \"signal name required\" })\n return withBody(req, res, (body) => {\n const b = body as { scope?: \"workflow\" | \"global\"; workflowId?: string; payload?: Record<string, unknown> } | undefined\n const emission = deps.dispatcher!.emitSignal({\n name,\n scope: b?.scope,\n workflowId: b?.workflowId,\n payload: b?.payload,\n })\n return sendJson(res, 200, { ok: true, emission })\n })\n }\n\n return sendJson(res, 404, { error: \"unknown workflows endpoint\", path: url })\n}\n\n// -------------------- save (create/update) --------------------\n\n// Top-level workflow keys the editor doesn't yet have UI controls for. When\n// the editor saves a workflow without these keys present in the raw payload\n// (typical when an older cached IIFE bundle is in the user's browser), we\n// preserve the on-disk value instead of letting Zod's default silently flip\n// the field back. Without this guard, server-only behaviours like\n// `mesh.allowRemote: true` get reset on every Save until the user\n// hard-refreshes — see 2026-04-23 incident where a cached editor repeatedly\n// stripped mesh and broke the cross-mesh broadcast.\n//\n// Add new keys here when you ship a server-side workflow feature ahead of\n// matching editor UI; remove a key once the editor sends it.\nconst EDITOR_BLIND_KEYS = [\"mesh\"] as const\n\nfunction handleSave(\n res: ServerResponse,\n deps: WorkflowsApiDeps,\n body: unknown,\n opts: { mode: \"create\" } | { mode: \"update\"; id: string },\n): true {\n const parsed = workflowSchema.safeParse(body)\n if (!parsed.success) {\n return sendJson(res, 400, {\n error: \"invalid workflow\",\n issues: parsed.error.issues.map((i) => ({ path: i.path.join(\".\"), message: i.message })),\n })\n }\n const wf = parsed.data\n const lintIssues = lintWorkflow(wf)\n if (lintIssues.length > 0) {\n return sendJson(res, 400, {\n error: \"lint errors\",\n issues: lintIssues.map((m) => ({ path: \"\", message: m })),\n })\n }\n if (opts.mode === \"update\" && wf.id !== opts.id) {\n return sendJson(res, 400, { error: \"workflow id does not match path\" })\n }\n if (opts.mode === \"create\" && deps.store.get(wf.id)) {\n return sendJson(res, 409, { error: `workflow \"${wf.id}\" already exists` })\n }\n\n // Preserve editor-blind keys from disk. We check the RAW payload (not the\n // parsed one — Zod has already filled defaults) to distinguish \"editor\n // omitted this field\" from \"editor explicitly set the default value\".\n if (opts.mode === \"update\" && body && typeof body === \"object\") {\n const raw = body as Record<string, unknown>\n const existing = deps.store.get(opts.id)\n if (existing) {\n for (const key of EDITOR_BLIND_KEYS) {\n if (!(key in raw) && key in existing) {\n ;(wf as Record<string, unknown>)[key] = (existing as Record<string, unknown>)[key]\n }\n }\n }\n }\n\n const saved = deps.store.save(wf)\n // Prune stale layout entries if any nodes were removed since the last save.\n deps.layouts.sync(saved.id, saved.nodes.map((n) => n.id))\n return sendJson(res, 200, { workflow: saved })\n}\n\n// -------------------- runs --------------------\n\nfunction handleRuns(req: IncomingMessage, res: ServerResponse, deps: WorkflowsApiDeps, url: string): boolean {\n const q = new URL(url, \"http://_\").searchParams\n const trail = url.replace(/^\\/api\\/workflows\\/runs/, \"\")\n if (trail === \"\" || trail.startsWith(\"?\")) {\n const limit = Math.max(1, Math.min(500, Number(q.get(\"limit\") || 50)))\n const workflowId = q.get(\"workflowId\") || undefined\n return sendJson(res, 200, { runs: deps.runs.list({ workflowId, limit }) })\n }\n const runMatch = trail.match(/^\\/([^\\/?]+)(\\/stream)?$/)\n if (runMatch) {\n const runId = decodeURIComponent(runMatch[1])\n if (runMatch[2]) return streamRun(req, res, deps, runId)\n const run = deps.runs.get(runId)\n if (!run) return sendJson(res, 404, { error: \"run not found\" })\n return sendJson(res, 200, { run })\n }\n return sendJson(res, 404, { error: \"unknown runs endpoint\" })\n}\n\n/** SSE run-detail stream. When `deps.runEvents` is provided, the stream is\n * push-driven; otherwise it polls the run-store every 2s. Either way the\n * client sees a complete snapshot per message. */\nfunction streamRun(req: IncomingMessage, res: ServerResponse, deps: WorkflowsApiDeps, runId: string): boolean {\n const run = deps.runs.get(runId)\n if (!run) { sendJson(res, 404, { error: \"run not found\" }); return true }\n\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n \"Connection\": \"keep-alive\",\n \"X-Accel-Buffering\": \"no\",\n })\n res.write(`data: ${JSON.stringify(run)}\\n\\n`)\n\n let closed = false\n const onEnd = () => { closed = true; unsubscribe?.(); clearInterval(timer); try { res.end() } catch { /* */ } }\n req.on(\"close\", onEnd)\n\n const emit = (r: WorkflowRun) => { if (closed) return; try { res.write(`data: ${JSON.stringify(r)}\\n\\n`) } catch { onEnd() } }\n\n const unsubscribe = deps.runEvents?.subscribe(runId, emit)\n\n // Fallback poll — also acts as the source when `runEvents` is missing,\n // and as a safety net when an event is missed during a fs flush.\n let lastUpdated = run.updatedAt\n const timer = setInterval(() => {\n if (closed) return\n const latest = deps.runs.get(runId)\n if (latest && latest.updatedAt !== lastUpdated) {\n lastUpdated = latest.updatedAt\n emit(latest)\n }\n }, 2000)\n\n return true\n}\n\n// -------------------- helpers --------------------\n\nfunction withBody(req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => boolean | true | Promise<boolean | true>): true {\n let raw = \"\"\n req.setEncoding(\"utf8\")\n req.on(\"data\", (chunk) => { raw += chunk })\n req.on(\"end\", () => {\n let parsed: unknown\n try { parsed = raw ? JSON.parse(raw) : {} } catch (e: any) {\n sendJson(res, 400, { error: \"invalid JSON body\", message: e.message })\n return\n }\n void Promise.resolve(handler(parsed)).catch((e: any) => {\n try { sendJson(res, 500, { error: \"handler threw\", message: e?.message ?? String(e) }) }\n catch { /* response already sent */ }\n })\n })\n req.on(\"error\", (e: any) => sendJson(res, 400, { error: \"body read failed\", message: e.message }))\n return true\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): true {\n res.writeHead(status, { \"Content-Type\": \"application/json\" })\n res.end(JSON.stringify(body))\n return true\n}\n"],"mappings":"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AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiNjBI,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8elBG,GAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwHjBC,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0DAkRhBC,GAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ECjVlB,SAASC,GAAiBC,EAAuB,CAAC,EAAW,CAClE,IAAMC,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iEAcZC,EAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0CA2Fb,OAAOC,EAAY,CACjB,MAAO,qBACP,UAAW,SACX,SAAU,SACV,UAAAF,EACA,MAAOD,EAAK,MACZ,cAAeA,EAAK,cACpB,OAAQ,GACR,KAAAE,EACA,IAAKE,GACL,QAAS,WAAWC,cACpB,WAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAiBd,CAAC,CACH,CAEA,IAAMD,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8PlBC,GAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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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,EAgOrBC,GAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":["init_config","createServer","appendFileSync","existsSync","mkdirSync","readFile","fileURLToPath","dirname","resolve","SORTABLE_JS","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","SKILL_EDITABLE_EXTENSIONS","SKILL_TREE_MAX_DEPTH","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","listSkillTree","skillRoot","out","walk","absDir","relDir","depth","acc","entries","rel","allowedByName","readSkillFile","writeSkillFile","detectSkillDeps","check","title","ICONS","renderAdminPage","opts","tokenScript","chrome","pageHead","healthStrip","renderShell","ADMIN_PAGE_BODY","TOAST_HTML","ADMIN_PAGE_CSS","TOAST_SCRIPT","ROW_CARD_SCRIPT","ADMIN_HEALTH_SCRIPT","ADMIN_PAGE_SCRIPT","sectionHead","witBanner","secLabel","handleAdminGet","_req","res","peers","localToken","renderAdminPage","handleAdminConfigGet","cfg","readConfigRaw","handleAdminApi","req","path","proxyDaemonSvg","body","readJsonBody","dispatch","getAdminState","addAgent","editAgent","deleteAgent","setAgentAccess","addTelegramAccount","deleteTelegramAccount","toggleTelegram","addCron","deleteCron","replaceConfigRaw","listTokens","createToken","revokeToken","testDriveAgent","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","schedule","agent","prompt","backupPath","s","i","init_config","resolve","renderGraphPage","opts","tokenScript","body","renderShell","GRAPH_PAGE_CSS","GRAPH_PAGE_SCRIPT","handleGraphGet","_req","res","peers","localToken","renderGraphPage","handleGraphApi","req","path","body","readJsonBody","store","openStore","dispatch","getGraphState","approveClassification","rejectClassification","createNode","updateNode","deleteNode","saveSchema","key","handler","sendJson","result","e","cfg","loadDaemonConfig","baseDir","resolve","GraphStore","schema","nodes","recent","index","seen","c","pending","msgHash","findPending","s","sanitizeAxes","hit","id","slug","level","parentId","axes","node","src","out","nodeId","clean","k","v","status","data","ok","err","raw","existsSync","readFileSync","resolve","init_config","renderAgentPage","opts","agentId","tokenScript","agentIdScript","body","AGENT_PAGE_BODY","esc","renderShell","AGENT_PAGE_CSS","AGENT_PAGE_SCRIPT","handleAgentPageGet","_req","res","agentId","peers","localToken","renderAgentPage","handleAgentApi","req","path","body","readJsonBody","url","qs","match","sendJson","sub","key","handler","getAgentState","patchAgent","getIdentityFiles","getIdentityFile","putIdentityFile","getSkills","postCreateSkill","deleteSkill","getSkillTree","getSkillFile","putSkillFile","getDepsHint","dispatchInstallPackage","dispatchInstallDeps","getChannelsForAgent","getRecentActivity","listHandovers","createHandover","releaseHandover","result","e","readConfigRaw","file","resolve","existsSync","readFileSync","getAgentDef","cfg","def","overview","listAgentFiles","fields","k","summary","mutateAgentxConfig","relPath","readAgentFile","writeAgentFile","slug","createAgentSkill","deleteAgentSkill","tree","listSkillTree","deps","detectSkillDeps","readSkillFile","writeSkillFile","pkg","worker","pickWorker","message","dispatchTask","hint","skillPath","explicit","loadDaemonConfig","id","workerAgent","chatId","daemonUrl","headers","auth","r","text","data","out","tg","acct","gitlab","m","agent","a","handoverStore","HandoverStore","store","incoming","outgoing","pageAgentId","channel","toAgent","fromAgent","accountId","expiresAt","rec","status","ok","err","raw","c","UI_LABELS","GLOSSARY","renderLivePage","opts","labelsScript","UI_LABELS","rightExtras","body","esc","renderShell","LIVE_PAGE_CSS","LIVE_PAGE_SCRIPT","renderBoardsPage","opts","subheader","body","renderShell","BOARDS_PAGE_CSS","BOARDS_PAGE_SCRIPT","renderGlossaryPage","opts","body","GLOSSARY","g","alias","esc","renderShell","UI_LABELS","GLOSSARY_PAGE_CSS","renderWorkflowsPage","opts","body","renderShell","WORKFLOWS_PAGE_CSS","WORKFLOWS_PAGE_SCRIPT","renderWorkflowEditorPage","opts","body","renderShell","HOST_CSS","handleWorkflowsApi","req","res","deps","url","method","sendJson","withBody","body","handleSave","handleRuns","defMatch","id","suffix","saved","e","parsed","workflowSchema","i","lintIssues","lintWorkflow","m","wf","existed","trail","actor","tasks","match","taskId","formSubmissionSchema","submittedBy","result","task","name","b","emission","EDITOR_BLIND_KEYS","opts","raw","existing","key","n","q","limit","workflowId","runMatch","runId","streamRun","run","closed","onEnd","unsubscribe","timer","emit","r","lastUpdated","latest","handler","chunk","status","startBoardDashboard","config","dashboard","boards","port","bind","token","sources","board","buildWorkSource","e","wfNodeId","wfDir","resolve","workflowStore","WorkflowStore","workflowRuns","RunStore","workflowLayouts","LayoutStore","createServer","req","res","handleRequest","sendJson","displayHost","b","ctx","url","path","method","wz","wizardState","peers","buildTopbarPeers","html","renderLivePage","renderBoardsPage","renderGlossaryPage","renderWorkflowsPage","renderWorkflowEditorPage","handleWizardGet","handleWizardPost","handleAdminGet","handleGraphGet","agentPageMatch","handleAgentPageGet","peerId","peer","findPeer","proxyAdminToPeer","authHeader","got","tokStore","TokenStore","rec","recordHasScope","handleAdminConfigGet","handleGraphApi","handleAgentApi","handleAdminApi","SORTABLE_JS","rel","assetPath","resolveFromHere","buf","readAsset","ct","snap","buildLiveSnapshot","startLiveStream","u","nodeUrl","agentId","taskId","proxyTaskStream","limit","proxyTaskHistory","headers","r","list","body","readJson","drafted","draftIssue","publicAgentMatch","requireScopedToken","proxyPublicAgentMessage","workflowId","local","qs","targets","d","remoteLists","t","data","byId","merged","a","runDetailMatch","runId","daemonUrl","handleWorkflowsApi","s","scopes","presented","extractToken","scope","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","p","diff","transitionDiff","gl","daemon","agentUsernames","mapping","GitLabWorkSource","args","entry","dir","existsSync","mkdirSync","line","appendFileSync","fwd","fetchDaemonAgents","signal","base","healthRes","agentsRes","meshRes","agents","h","meshResSafe","_meshJsonCache","j","fetchMeshPeers","primaryUrl","dash","primaryToken","seen","key","ac","timeout","meshPeers","nodes","send","ev","tick","timer","target","allowed","tokenForNode","upstreamCtl","upstreamRes","reader","value","done","required","agentDef","message","upstream","text","upstreamPath","DRAFT_CONVENTIONS","rough","boardCtx","userPrompt","content","parsed","extractJson","labels","x","fenced","start","depth","here","dirname","fileURLToPath","readFile","status","reject","chunks","raw","resolvePeerDashboardUrl","ctxConfig","cfg","primary","extras","pathWithQuery","xrw","respBody","id","match"]}
|