myaiforone 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/README.md +113 -0
  2. package/agents/_template/CLAUDE.md +18 -0
  3. package/agents/_template/agent.json +7 -0
  4. package/agents/platform/agentcreator/CLAUDE.md +300 -0
  5. package/agents/platform/appcreator/CLAUDE.md +158 -0
  6. package/agents/platform/gym/CLAUDE.md +486 -0
  7. package/agents/platform/gym/agent.json +40 -0
  8. package/agents/platform/gym/programs/agent-building/program.json +160 -0
  9. package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
  10. package/agents/platform/gym/programs/getting-started/program.json +124 -0
  11. package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
  12. package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
  13. package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
  14. package/agents/platform/gym/souls/alex.md +12 -0
  15. package/agents/platform/gym/souls/jordan.md +12 -0
  16. package/agents/platform/gym/souls/morgan.md +12 -0
  17. package/agents/platform/gym/souls/riley.md +12 -0
  18. package/agents/platform/gym/souls/sam.md +12 -0
  19. package/agents/platform/hub/CLAUDE.md +372 -0
  20. package/agents/platform/promptcreator/CLAUDE.md +130 -0
  21. package/agents/platform/skillcreator/CLAUDE.md +163 -0
  22. package/bin/cli.js +566 -0
  23. package/config.example.json +310 -0
  24. package/dist/agent-registry.d.ts +32 -0
  25. package/dist/agent-registry.d.ts.map +1 -0
  26. package/dist/agent-registry.js +144 -0
  27. package/dist/agent-registry.js.map +1 -0
  28. package/dist/channels/discord.d.ts +17 -0
  29. package/dist/channels/discord.d.ts.map +1 -0
  30. package/dist/channels/discord.js +114 -0
  31. package/dist/channels/discord.js.map +1 -0
  32. package/dist/channels/imessage.d.ts +23 -0
  33. package/dist/channels/imessage.d.ts.map +1 -0
  34. package/dist/channels/imessage.js +214 -0
  35. package/dist/channels/imessage.js.map +1 -0
  36. package/dist/channels/slack.d.ts +19 -0
  37. package/dist/channels/slack.d.ts.map +1 -0
  38. package/dist/channels/slack.js +167 -0
  39. package/dist/channels/slack.js.map +1 -0
  40. package/dist/channels/telegram.d.ts +19 -0
  41. package/dist/channels/telegram.d.ts.map +1 -0
  42. package/dist/channels/telegram.js +274 -0
  43. package/dist/channels/telegram.js.map +1 -0
  44. package/dist/channels/types.d.ts +44 -0
  45. package/dist/channels/types.d.ts.map +1 -0
  46. package/dist/channels/types.js +18 -0
  47. package/dist/channels/types.js.map +1 -0
  48. package/dist/channels/whatsapp.d.ts +23 -0
  49. package/dist/channels/whatsapp.d.ts.map +1 -0
  50. package/dist/channels/whatsapp.js +189 -0
  51. package/dist/channels/whatsapp.js.map +1 -0
  52. package/dist/config.d.ts +134 -0
  53. package/dist/config.d.ts.map +1 -0
  54. package/dist/config.js +127 -0
  55. package/dist/config.js.map +1 -0
  56. package/dist/cron.d.ts +8 -0
  57. package/dist/cron.d.ts.map +1 -0
  58. package/dist/cron.js +35 -0
  59. package/dist/cron.js.map +1 -0
  60. package/dist/decrypt-keys.d.ts +7 -0
  61. package/dist/decrypt-keys.d.ts.map +1 -0
  62. package/dist/decrypt-keys.js +53 -0
  63. package/dist/decrypt-keys.js.map +1 -0
  64. package/dist/encrypt-keys.d.ts +8 -0
  65. package/dist/encrypt-keys.d.ts.map +1 -0
  66. package/dist/encrypt-keys.js +62 -0
  67. package/dist/encrypt-keys.js.map +1 -0
  68. package/dist/executor.d.ts +31 -0
  69. package/dist/executor.d.ts.map +1 -0
  70. package/dist/executor.js +2009 -0
  71. package/dist/executor.js.map +1 -0
  72. package/dist/gemini-executor.d.ts +27 -0
  73. package/dist/gemini-executor.d.ts.map +1 -0
  74. package/dist/gemini-executor.js +160 -0
  75. package/dist/gemini-executor.js.map +1 -0
  76. package/dist/goals.d.ts +24 -0
  77. package/dist/goals.d.ts.map +1 -0
  78. package/dist/goals.js +189 -0
  79. package/dist/goals.js.map +1 -0
  80. package/dist/gym/activity-digest.d.ts +30 -0
  81. package/dist/gym/activity-digest.d.ts.map +1 -0
  82. package/dist/gym/activity-digest.js +506 -0
  83. package/dist/gym/activity-digest.js.map +1 -0
  84. package/dist/gym/dimension-scorer.d.ts +76 -0
  85. package/dist/gym/dimension-scorer.d.ts.map +1 -0
  86. package/dist/gym/dimension-scorer.js +236 -0
  87. package/dist/gym/dimension-scorer.js.map +1 -0
  88. package/dist/gym/gym-router.d.ts +7 -0
  89. package/dist/gym/gym-router.d.ts.map +1 -0
  90. package/dist/gym/gym-router.js +718 -0
  91. package/dist/gym/gym-router.js.map +1 -0
  92. package/dist/gym/index.d.ts +11 -0
  93. package/dist/gym/index.d.ts.map +1 -0
  94. package/dist/gym/index.js +11 -0
  95. package/dist/gym/index.js.map +1 -0
  96. package/dist/heartbeat.d.ts +21 -0
  97. package/dist/heartbeat.d.ts.map +1 -0
  98. package/dist/heartbeat.js +163 -0
  99. package/dist/heartbeat.js.map +1 -0
  100. package/dist/index.d.ts +2 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +254 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/keystore.d.ts +22 -0
  105. package/dist/keystore.d.ts.map +1 -0
  106. package/dist/keystore.js +178 -0
  107. package/dist/keystore.js.map +1 -0
  108. package/dist/logger.d.ts +9 -0
  109. package/dist/logger.d.ts.map +1 -0
  110. package/dist/logger.js +45 -0
  111. package/dist/logger.js.map +1 -0
  112. package/dist/memory/daily.d.ts +22 -0
  113. package/dist/memory/daily.d.ts.map +1 -0
  114. package/dist/memory/daily.js +82 -0
  115. package/dist/memory/daily.js.map +1 -0
  116. package/dist/memory/embeddings.d.ts +15 -0
  117. package/dist/memory/embeddings.d.ts.map +1 -0
  118. package/dist/memory/embeddings.js +154 -0
  119. package/dist/memory/embeddings.js.map +1 -0
  120. package/dist/memory/index.d.ts +32 -0
  121. package/dist/memory/index.d.ts.map +1 -0
  122. package/dist/memory/index.js +159 -0
  123. package/dist/memory/index.js.map +1 -0
  124. package/dist/memory/search.d.ts +21 -0
  125. package/dist/memory/search.d.ts.map +1 -0
  126. package/dist/memory/search.js +77 -0
  127. package/dist/memory/search.js.map +1 -0
  128. package/dist/memory/store.d.ts +23 -0
  129. package/dist/memory/store.d.ts.map +1 -0
  130. package/dist/memory/store.js +144 -0
  131. package/dist/memory/store.js.map +1 -0
  132. package/dist/ollama-executor.d.ts +17 -0
  133. package/dist/ollama-executor.d.ts.map +1 -0
  134. package/dist/ollama-executor.js +112 -0
  135. package/dist/ollama-executor.js.map +1 -0
  136. package/dist/openai-executor.d.ts +38 -0
  137. package/dist/openai-executor.d.ts.map +1 -0
  138. package/dist/openai-executor.js +197 -0
  139. package/dist/openai-executor.js.map +1 -0
  140. package/dist/router.d.ts +11 -0
  141. package/dist/router.d.ts.map +1 -0
  142. package/dist/router.js +185 -0
  143. package/dist/router.js.map +1 -0
  144. package/dist/test-message.d.ts +2 -0
  145. package/dist/test-message.d.ts.map +1 -0
  146. package/dist/test-message.js +60 -0
  147. package/dist/test-message.js.map +1 -0
  148. package/dist/utils/imsg-db-reader.d.ts +24 -0
  149. package/dist/utils/imsg-db-reader.d.ts.map +1 -0
  150. package/dist/utils/imsg-db-reader.js +92 -0
  151. package/dist/utils/imsg-db-reader.js.map +1 -0
  152. package/dist/utils/imsg-rpc.d.ts +25 -0
  153. package/dist/utils/imsg-rpc.d.ts.map +1 -0
  154. package/dist/utils/imsg-rpc.js +149 -0
  155. package/dist/utils/imsg-rpc.js.map +1 -0
  156. package/dist/utils/message-formatter.d.ts +3 -0
  157. package/dist/utils/message-formatter.d.ts.map +1 -0
  158. package/dist/utils/message-formatter.js +69 -0
  159. package/dist/utils/message-formatter.js.map +1 -0
  160. package/dist/web-ui.d.ts +12 -0
  161. package/dist/web-ui.d.ts.map +1 -0
  162. package/dist/web-ui.js +5784 -0
  163. package/dist/web-ui.js.map +1 -0
  164. package/dist/whatsapp-chats.d.ts +2 -0
  165. package/dist/whatsapp-chats.d.ts.map +1 -0
  166. package/dist/whatsapp-chats.js +76 -0
  167. package/dist/whatsapp-chats.js.map +1 -0
  168. package/dist/whatsapp-login.d.ts +2 -0
  169. package/dist/whatsapp-login.d.ts.map +1 -0
  170. package/dist/whatsapp-login.js +90 -0
  171. package/dist/whatsapp-login.js.map +1 -0
  172. package/dist/wiki-sync.d.ts +21 -0
  173. package/dist/wiki-sync.d.ts.map +1 -0
  174. package/dist/wiki-sync.js +147 -0
  175. package/dist/wiki-sync.js.map +1 -0
  176. package/docs/AddNewAgentGuide.md +100 -0
  177. package/docs/AddNewMcpGuide.md +72 -0
  178. package/docs/Architecture.md +795 -0
  179. package/docs/CLAUDE-AI-SETUP.md +166 -0
  180. package/docs/Setup.md +297 -0
  181. package/docs/ai-gym-architecture.md +1040 -0
  182. package/docs/ai-gym-build-plan.md +343 -0
  183. package/docs/ai-gym-onboarding.md +122 -0
  184. package/docs/appcreator_plan.md +348 -0
  185. package/docs/platform-mcp-audit.md +320 -0
  186. package/docs/server-deployment-plan.md +503 -0
  187. package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
  188. package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
  189. package/docs/user-guide.md +2016 -0
  190. package/mcp-catalog.json +628 -0
  191. package/package.json +63 -0
  192. package/public/MyAIforOne-logomark-512.svg +16 -0
  193. package/public/MyAIforOne-logomark-transparent.svg +15 -0
  194. package/public/activity.html +314 -0
  195. package/public/admin.html +1674 -0
  196. package/public/agent-dashboard.html +670 -0
  197. package/public/api-docs.html +1106 -0
  198. package/public/automations.html +722 -0
  199. package/public/canvas.css +223 -0
  200. package/public/canvas.js +588 -0
  201. package/public/changelog.html +231 -0
  202. package/public/gym.html +2766 -0
  203. package/public/home.html +1930 -0
  204. package/public/index.html +2809 -0
  205. package/public/lab.html +1643 -0
  206. package/public/library.html +1442 -0
  207. package/public/marketplace.html +1101 -0
  208. package/public/mcp-docs.html +441 -0
  209. package/public/mini.html +390 -0
  210. package/public/monitor.html +584 -0
  211. package/public/org.html +4304 -0
  212. package/public/projects.html +734 -0
  213. package/public/settings.html +645 -0
  214. package/public/tasks.html +932 -0
  215. package/public/trainers/alex.svg +12 -0
  216. package/public/trainers/jordan.svg +12 -0
  217. package/public/trainers/morgan.svg +12 -0
  218. package/public/trainers/riley.svg +12 -0
  219. package/public/trainers/sam.svg +12 -0
  220. package/public/user-guide.html +218 -0
  221. package/registry/agents.json +3 -0
  222. package/registry/apps.json +20 -0
  223. package/registry/installed-drafts.json +3 -0
  224. package/registry/mcps.json +1084 -0
  225. package/registry/prompts/personal/mcp-test-prompt.md +6 -0
  226. package/registry/prompts/personal/memory-recall.md +6 -0
  227. package/registry/prompts/platform/brainstorm.md +15 -0
  228. package/registry/prompts/platform/code-review.md +16 -0
  229. package/registry/prompts/platform/explain.md +16 -0
  230. package/registry/prompts.json +58 -0
  231. package/registry/skills/external/brainstorming.md +5 -0
  232. package/registry/skills/external/code-review.md +40 -0
  233. package/registry/skills/external/frontend-patterns.md +642 -0
  234. package/registry/skills/external/frontend-slides.md +184 -0
  235. package/registry/skills/external/systematic-debugging.md +5 -0
  236. package/registry/skills/external/tdd.md +328 -0
  237. package/registry/skills/external/verification-before-completion.md +5 -0
  238. package/registry/skills/external/writing-plans.md +5 -0
  239. package/registry/skills/platform/ai41_app_build.md +930 -0
  240. package/registry/skills/platform/ai41_app_deploy.md +168 -0
  241. package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
  242. package/registry/skills/platform/ai41_app_patterns.md +359 -0
  243. package/registry/skills/platform/ai41_app_register.md +85 -0
  244. package/registry/skills/platform/ai41_app_scaffold.md +421 -0
  245. package/registry/skills/platform/ai41_app_verify.md +107 -0
  246. package/registry/skills/platform/opProjectCreate.md +239 -0
  247. package/registry/skills/platform/op_devbrowser.md +136 -0
  248. package/registry/skills/platform/sop_brandguidelines.md +103 -0
  249. package/registry/skills/platform/sop_docx.md +117 -0
  250. package/registry/skills/platform/sop_frontenddesign.md +44 -0
  251. package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
  252. package/registry/skills/platform/sop_mcpbuilder.md +133 -0
  253. package/registry/skills/platform/sop_pdf.md +172 -0
  254. package/registry/skills/platform/sop_pptx.md +133 -0
  255. package/registry/skills/platform/sop_skillcreator.md +104 -0
  256. package/registry/skills/platform/sop_themefactory.md +128 -0
  257. package/registry/skills/platform/sop_webapptesting.md +75 -0
  258. package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
  259. package/registry/skills/platform/sop_xlsx.md +134 -0
  260. package/registry/skills.json +1055 -0
  261. package/scripts/discover-chats.sh +11 -0
  262. package/scripts/install-service-windows.ps1 +87 -0
  263. package/scripts/install-service.sh +52 -0
  264. package/scripts/seed-registry.ts +195 -0
  265. package/scripts/test-send.sh +5 -0
  266. package/scripts/tray-indicator.ps1 +35 -0
  267. package/scripts/uninstall-service-windows.ps1 +23 -0
  268. package/scripts/uninstall-service.sh +15 -0
  269. package/scripts/xbar-myagent.5s.sh +32 -0
  270. package/server/mcp-server/dist/index.d.ts +11 -0
  271. package/server/mcp-server/dist/index.js +1332 -0
  272. package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
  273. package/server/mcp-server/dist/lib/api-client.js +241 -0
  274. package/server/mcp-server/index.ts +1545 -0
  275. package/server/mcp-server/lib/api-client.ts +366 -0
  276. package/server/mcp-server/tsconfig.json +14 -0
  277. package/src/agent-registry.ts +180 -0
  278. package/src/channels/discord.ts +129 -0
  279. package/src/channels/imessage.ts +261 -0
  280. package/src/channels/slack.ts +208 -0
  281. package/src/channels/telegram.ts +307 -0
  282. package/src/channels/types.ts +62 -0
  283. package/src/channels/whatsapp.ts +227 -0
  284. package/src/config.ts +281 -0
  285. package/src/cron.ts +43 -0
  286. package/src/decrypt-keys.ts +60 -0
  287. package/src/encrypt-keys.ts +70 -0
  288. package/src/executor.ts +2190 -0
  289. package/src/gemini-executor.ts +212 -0
  290. package/src/goals.ts +240 -0
  291. package/src/gym/activity-digest.ts +546 -0
  292. package/src/gym/dimension-scorer.ts +297 -0
  293. package/src/gym/gym-router.ts +801 -0
  294. package/src/gym/index.ts +19 -0
  295. package/src/heartbeat.ts +220 -0
  296. package/src/index.ts +275 -0
  297. package/src/keystore.ts +190 -0
  298. package/src/logger.ts +51 -0
  299. package/src/memory/daily.ts +101 -0
  300. package/src/memory/embeddings.ts +185 -0
  301. package/src/memory/index.ts +218 -0
  302. package/src/memory/search.ts +124 -0
  303. package/src/memory/store.ts +189 -0
  304. package/src/ollama-executor.ts +126 -0
  305. package/src/openai-executor.ts +259 -0
  306. package/src/router.ts +230 -0
  307. package/src/test-message.ts +72 -0
  308. package/src/utils/imsg-db-reader.ts +109 -0
  309. package/src/utils/imsg-rpc.ts +178 -0
  310. package/src/utils/message-formatter.ts +90 -0
  311. package/src/web-ui.ts +5778 -0
  312. package/src/whatsapp-chats.ts +91 -0
  313. package/src/whatsapp-login.ts +110 -0
  314. package/src/wiki-sync.ts +199 -0
  315. package/tsconfig.json +19 -0
@@ -0,0 +1,1674 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>MyAIforOne — Admin</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
8
+ <style>
9
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
10
+ :root{
11
+ --bg-deep:#060a13;--bg-surface:rgba(12,18,33,0.92);--bg-card:rgba(16,22,40,0.85);
12
+ --bg-input:rgba(0,0,0,0.35);--border-dim:rgba(56,189,248,0.08);
13
+ --border-glow:rgba(56,189,248,0.18);--border-active:rgba(56,189,248,0.45);
14
+ --text-primary:rgba(255,255,255,0.92);--text-secondary:rgba(255,255,255,0.68);
15
+ --text-muted:rgba(148,163,184,0.55);--accent:#22d3ee;--accent-bg:rgba(6,182,212,0.15);
16
+ --purple:rgba(139,92,246,0.7);--purple-bg:rgba(139,92,246,0.12);
17
+ --green:#4ade80;--green-bg:rgba(74,222,128,0.1);
18
+ --red:#f87171;--red-bg:rgba(248,113,113,0.1);--amber:#fbbf24;--amber-bg:rgba(251,191,36,0.1);
19
+ --shadow:0 2px 12px rgba(0,0,0,0.3);--shadow-glow:0 0 20px rgba(34,211,238,0.08);--radius:12px;
20
+ --font-sans:'DM Sans',system-ui,sans-serif;--font-mono:'IBM Plex Mono',monospace;
21
+ --font-display:'Syne',sans-serif;
22
+ }
23
+ [data-theme="light"]{
24
+ --bg-deep:#f4f6f9;--bg-surface:rgba(255,255,255,0.95);--bg-card:rgba(255,255,255,0.9);
25
+ --bg-input:rgba(0,0,0,0.04);--border-dim:rgba(0,0,0,0.08);
26
+ --border-glow:rgba(14,116,144,0.18);--border-active:rgba(14,116,144,0.45);
27
+ --text-primary:rgba(15,23,42,0.92);--text-secondary:rgba(51,65,85,0.8);
28
+ --text-muted:rgba(100,116,139,0.6);--accent:#0891b2;--accent-bg:rgba(14,116,144,0.08);
29
+ --purple:rgba(109,40,217,0.75);--purple-bg:rgba(139,92,246,0.08);
30
+ --green:#16a34a;--green-bg:rgba(22,163,74,0.08);--red:#dc2626;--red-bg:rgba(220,38,38,0.08);
31
+ --amber:#d97706;--amber-bg:rgba(217,119,6,0.08);--shadow:0 1px 8px rgba(0,0,0,0.06);--shadow-glow:none;
32
+ }
33
+ html,body{width:100%;height:100%;max-width:100vw;background:var(--bg-deep);font-family:var(--font-sans);color:var(--text-primary);transition:background .3s,color .3s}
34
+
35
+ /* ── Topbar ── */
36
+ .topbar{height:56px;display:flex;align-items:center;padding:0 24px;background:var(--bg-surface);border-bottom:1px solid var(--border-dim);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);position:fixed;top:0;left:0;right:0;z-index:100;overflow:hidden;max-width:100vw}
37
+ .topbar-logo{display:flex;align-items:center;gap:10px;margin-right:32px;flex-shrink:0}
38
+ .logo-mark{width:28px;height:28px;border-radius:8px;object-fit:contain}
39
+ .logo-text{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent)}
40
+ .tab-group{display:flex;gap:0;flex-shrink:1;min-width:0;overflow-x:auto}
41
+ .tab-btn{font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--text-muted);background:none;border:none;padding:16px 14px;cursor:pointer;position:relative;transition:color .2s;text-decoration:none;display:block;white-space:nowrap;flex-shrink:0}
42
+ .tab-btn:hover{color:var(--text-secondary)}
43
+ .tab-btn.active{color:var(--accent)}
44
+ .tab-btn.active::after{content:'';position:absolute;bottom:0;left:12px;right:12px;height:2px;background:var(--accent);border-radius:1px}
45
+ .topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px;flex-shrink:0}
46
+ .gym-nav-btn{font-family:var(--font-sans);font-size:12px;font-weight:700;color:#fff;background:var(--accent);border:none;border-radius:8px;padding:6px 14px;cursor:pointer;text-decoration:none;letter-spacing:.02em;transition:opacity .2s,transform .2s;white-space:nowrap}
47
+ .gym-nav-btn:hover{opacity:.88;transform:translateY(-1px)}
48
+ .theme-toggle{width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .2s}
49
+ .theme-toggle:hover{border-color:var(--border-glow);color:var(--text-secondary)}
50
+ .gear-btn{height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;letter-spacing:.03em;padding:0 10px;transition:all .2s;text-decoration:none;white-space:nowrap}
51
+ .gear-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
52
+ .docs-dropdown{position:relative;display:inline-block}
53
+ .docs-menu{display:none;position:absolute;top:40px;right:0;min-width:160px;background:var(--bg-surface);border:1px solid var(--border-glow);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.4);z-index:200;padding:4px;backdrop-filter:blur(20px)}
54
+ .docs-menu.open{display:block}
55
+ .docs-menu a{display:flex;align-items:center;gap:8px;padding:7px 12px;font-size:12px;font-weight:500;color:var(--text-secondary);text-decoration:none;border-radius:6px;transition:all .15s}
56
+ .docs-menu a:hover{background:var(--accent-bg);color:var(--accent)}
57
+ .docs-menu a .dm-icon{font-size:14px;width:18px;text-align:center}
58
+
59
+ /* ── Admin tab bar (below topbar) ── */
60
+ .admin-nav{position:fixed;top:56px;left:0;right:0;z-index:99;height:44px;display:flex;align-items:center;gap:2px;padding:0 24px;background:var(--bg-surface);border-bottom:1px solid var(--border-dim);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}
61
+ .type-tab{font-family:var(--font-sans);font-size:12px;font-weight:600;padding:6px 18px;border-radius:7px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s}
62
+ .type-tab:hover{color:var(--text-secondary)}
63
+ .type-tab.active{background:var(--accent-bg);color:var(--accent);border:1px solid var(--border-active)}
64
+
65
+ /* ── Canvas ── */
66
+ .canvas{position:fixed;top:100px;left:0;right:0;bottom:0;overflow:auto}
67
+
68
+ /* ── Tab panels ── */
69
+ .tab-panel{display:none}
70
+ .tab-panel.active{display:block}
71
+
72
+ /* ─────────────────────────────────────────────────────────────── */
73
+ /* ── CHANNELS TAB ── */
74
+ /* ─────────────────────────────────────────────────────────────── */
75
+ .channels-grid{display:flex;flex-wrap:wrap;justify-content:center;gap:24px;padding:24px 40px}
76
+ .org-title-banner{font-family:var(--font-display);font-size:20px;font-weight:800;padding:24px 40px 0;color:var(--text-primary)}
77
+ .empty-org{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 20px;color:var(--text-muted)}
78
+ .empty-org-icon{font-size:48px;opacity:.3;margin-bottom:12px}
79
+ .empty-org-text{font-size:14px;font-weight:500;margin-bottom:4px}
80
+
81
+ .channel-card{width:480px;max-width:94vw;background:var(--bg-card);border:1px solid var(--border-glow);border-radius:16px;overflow:hidden;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);box-shadow:var(--shadow);transition:all .3s}
82
+ .channel-card:hover{box-shadow:var(--shadow),var(--shadow-glow)}
83
+ .channel-card-header{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border-dim)}
84
+ .channel-card-icon{width:36px;height:36px;border-radius:10px;background:var(--accent-bg);border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
85
+ .channel-card-name{font-family:var(--font-display);font-size:15px;font-weight:700;text-transform:capitalize}
86
+ .channel-status-pill{margin-left:auto;font-family:var(--font-mono);font-size:10px;font-weight:600;padding:3px 10px;border-radius:20px;text-transform:uppercase;letter-spacing:.06em}
87
+ .channel-status-pill.enabled{background:var(--green-bg);color:var(--green);border:1px solid rgba(74,222,128,0.2)}
88
+ .channel-status-pill.disabled{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(251,191,36,0.2)}
89
+
90
+ .channel-card-body{padding:16px 20px}
91
+ .channel-setting-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
92
+ .channel-setting-label{font-family:var(--font-mono);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);min-width:100px;flex-shrink:0}
93
+ .channel-setting-input{padding:6px 10px;border-radius:8px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);font-family:var(--font-mono);font-size:12px;outline:none;transition:border-color .2s}
94
+ .channel-setting-input:focus{border-color:var(--accent)}
95
+ .channel-setting-select{padding:6px 10px;border-radius:8px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);font-family:var(--font-mono);font-size:12px;outline:none;cursor:pointer}
96
+ .channel-save-btn{font-family:var(--font-sans);font-size:11px;font-weight:600;padding:6px 16px;border-radius:8px;border:none;background:var(--accent);color:#fff;cursor:pointer;transition:all .2s;margin-top:4px}
97
+ .channel-save-btn:hover{filter:brightness(1.1)}
98
+ .channel-save-btn:disabled{opacity:.5;cursor:not-allowed}
99
+ .channel-section-title{font-family:var(--font-mono);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.1em;color:var(--accent);margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid var(--border-dim)}
100
+ .channel-agent-row{display:flex;align-items:center;gap:8px;padding:6px 0;font-size:12px}
101
+ .channel-agent-name{font-family:var(--font-sans);font-weight:600;color:var(--text-primary)}
102
+ .channel-agent-alias{font-family:var(--font-mono);font-size:10px;color:var(--text-muted)}
103
+ .channel-agent-chatid{font-family:var(--font-mono);font-size:10px;color:var(--text-secondary);margin-left:auto}
104
+ .channel-agent-remove{width:22px;height:22px;border-radius:5px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:12px;transition:all .2s}
105
+ .channel-agent-remove:hover{border-color:rgba(239,68,68,.3);color:#ef4444}
106
+ .channel-add-row{display:flex;gap:6px;align-items:center;margin-top:8px}
107
+ .channel-add-select,.channel-add-input{padding:6px 8px;border-radius:7px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);font-family:var(--font-mono);font-size:11px;outline:none}
108
+ .channel-add-select{width:140px}
109
+ .channel-add-input{flex:1;min-width:80px}
110
+ .channel-add-btn{font-family:var(--font-mono);font-size:11px;color:var(--accent);background:none;border:1px solid var(--accent);border-radius:7px;padding:5px 10px;cursor:pointer;white-space:nowrap;transition:all .2s}
111
+ .channel-add-btn:hover{background:var(--accent-bg)}
112
+ .monitored-ids{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}
113
+ .monitored-pill{font-family:var(--font-mono);font-size:10px;padding:3px 8px;border-radius:5px;background:var(--bg-input);border:1px solid var(--border-dim);color:var(--text-secondary);display:flex;align-items:center;gap:4px}
114
+ .monitored-pill button{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:11px;padding:0 2px;transition:color .2s}
115
+ .monitored-pill button:hover{color:#ef4444}
116
+ .channel-restart-note{font-family:var(--font-mono);font-size:9px;color:var(--text-muted);margin-top:8px;opacity:.7}
117
+
118
+ /* ─────────────────────────────────────────────────────────────── */
119
+ /* ── ACTIVITY TAB ── */
120
+ /* ─────────────────────────────────────────────────────────────── */
121
+ .activity-toolbar{padding:14px 40px;display:flex;align-items:center;gap:12px;flex-shrink:0;flex-wrap:wrap;border-bottom:1px solid var(--border-dim);background:var(--bg-surface)}
122
+ .filter-select{padding:6px 12px;border-radius:8px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);font-family:var(--font-sans);font-size:12px;cursor:pointer;transition:border-color .2s;outline:none}
123
+ .filter-select:focus{border-color:var(--border-active)}
124
+ .search-input{flex:1;min-width:200px;padding:6px 14px;border-radius:8px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);font-family:var(--font-sans);font-size:13px;transition:border-color .2s;outline:none}
125
+ .search-input:focus{border-color:var(--border-active)}
126
+ .search-input::placeholder{color:var(--text-muted)}
127
+ .result-count{font-size:11px;color:var(--text-muted)}
128
+ .activity-list{padding:12px 40px}
129
+ .activity-list::-webkit-scrollbar{width:4px}
130
+ .activity-list::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:2px}
131
+ .activity-entry{padding:12px 16px;margin-bottom:6px;border-radius:10px;background:var(--bg-card);border:1px solid var(--border-dim);transition:all .2s;cursor:pointer}
132
+ .activity-entry:hover{border-color:var(--border-glow)}
133
+ .activity-top{display:flex;align-items:center;gap:10px;margin-bottom:6px}
134
+ .activity-avatar{width:28px;height:28px;border-radius:7px;flex-shrink:0;background:var(--accent-bg);border:1px solid rgba(34,211,238,0.3);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-size:9px;font-weight:600;color:var(--accent)}
135
+ .activity-agent{font-size:13px;font-weight:600;color:var(--accent)}
136
+ .activity-channel{font-size:10px;padding:2px 6px;border-radius:4px;background:var(--purple-bg);color:var(--purple);font-weight:500}
137
+ .activity-time{font-size:11px;color:var(--text-muted);margin-left:auto}
138
+ .activity-query{font-size:13px;color:var(--text-primary);margin-bottom:4px;line-height:1.5}
139
+ .activity-response{font-size:12px;color:var(--text-secondary);line-height:1.5;max-height:0;overflow:hidden;transition:max-height .3s ease;border-top:1px solid transparent}
140
+ .activity-entry.expanded .activity-response{max-height:500px;overflow-y:auto;padding-top:8px;margin-top:8px;border-top-color:var(--border-dim)}
141
+ .activity-expand{font-size:10px;color:var(--text-muted);cursor:pointer}
142
+ .no-activity{text-align:center;padding:40px;color:var(--text-muted);font-size:14px}
143
+ .loading-msg{text-align:center;padding:40px;color:var(--text-muted);font-size:13px}
144
+
145
+ /* ─────────────────────────────────────────────────────────────── */
146
+ /* ── SETTINGS TAB ── */
147
+ /* ─────────────────────────────────────────────────────────────── */
148
+ .settings-page{padding:28px 40px 60px;max-width:820px;margin:0 auto}
149
+ .page-title{font-family:var(--font-display);font-size:22px;font-weight:800;margin-bottom:4px}
150
+ .page-subtitle{font-size:13px;color:var(--text-muted);margin-bottom:36px}
151
+ .section{background:var(--bg-card);border:1px solid var(--border-glow);border-radius:var(--radius);margin-bottom:24px;overflow:hidden}
152
+ .section-header{padding:16px 20px;border-bottom:1px solid var(--border-dim);display:flex;align-items:center;gap:10px}
153
+ .section-icon{width:28px;height:28px;border-radius:7px;background:var(--accent-bg);border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:13px;color:var(--accent);flex-shrink:0}
154
+ .section-title{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--accent)}
155
+ .section-body{padding:16px 20px}
156
+ .account-row{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--border-dim)}
157
+ .account-row:last-of-type{border-bottom:none}
158
+ .account-name{font-family:var(--font-mono);font-size:12px;font-weight:600;color:var(--accent);min-width:90px}
159
+ .account-path{font-family:var(--font-mono);font-size:11px;color:var(--text-secondary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
160
+ .status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
161
+ .status-dot.ok{background:var(--green);box-shadow:0 0 6px rgba(74,222,128,0.5)}
162
+ /* Feature flag toggle switch */
163
+ #svcGymEnabled:checked ~ span:first-of-type{background:#a855f7}
164
+ #svcGymEnabled:checked ~ .flag-knob{transform:translateX(18px)}
165
+ .status-dot.unknown{background:var(--text-muted)}
166
+ .status-dot.err{background:var(--red)}
167
+ .btn-icon{width:26px;height:26px;border-radius:6px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}
168
+ .btn-icon:hover{border-color:var(--red);color:var(--red);background:var(--red-bg)}
169
+ .btn-icon.relogin:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
170
+ .divider{border:none;border-top:1px solid var(--border-dim);margin:16px 0}
171
+ .form-label{font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);margin-bottom:12px}
172
+ .field-row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap}
173
+ .field{display:flex;flex-direction:column;gap:4px;flex:1;min-width:120px}
174
+ .field label{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}
175
+ .field input{padding:8px 10px;border-radius:8px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);font-family:var(--font-mono);font-size:12px;outline:none;transition:border-color .2s;width:100%}
176
+ .field input:focus{border-color:var(--border-active)}
177
+ .field input::placeholder{color:var(--text-muted)}
178
+ .btn-primary{padding:8px 18px;border-radius:8px;border:1px solid var(--accent);background:var(--accent-bg);color:var(--accent);font-family:var(--font-sans);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;transition:all .2s;align-self:flex-end;flex-shrink:0}
179
+ .btn-primary:hover{background:var(--accent);color:#000}
180
+ .btn-primary:disabled{opacity:.45;cursor:not-allowed}
181
+ .hint{font-size:11px;color:var(--text-muted);margin-top:10px;line-height:1.6}
182
+ .hint code{font-family:var(--font-mono);font-size:10px;background:var(--bg-input);padding:1px 5px;border-radius:4px}
183
+ .login-panel{margin-top:14px;padding:16px;border-radius:10px;border:1px solid var(--border-dim);background:var(--bg-input);display:none}
184
+ .login-panel.visible{display:block}
185
+ .login-panel-title{font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);margin-bottom:8px}
186
+ .login-url-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:4px}
187
+ .login-url{font-family:var(--font-mono);font-size:11px;color:var(--accent);word-break:break-all;flex:1;min-width:0;user-select:all}
188
+ .btn-copy{padding:5px 12px;border-radius:7px;border:1px solid var(--border-glow);background:transparent;color:var(--text-muted);font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;transition:all .2s;flex-shrink:0}
189
+ .btn-copy:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
190
+ .btn-open{padding:5px 12px;border-radius:7px;border:1px solid var(--accent);background:var(--accent-bg);color:var(--accent);font-size:11px;font-weight:600;cursor:pointer;text-decoration:none;white-space:nowrap;transition:all .2s;flex-shrink:0}
191
+ .btn-open:hover{background:var(--accent);color:#000}
192
+ .code-input-row{display:flex;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap}
193
+ .code-input-row input{flex:1;min-width:160px;padding:8px 10px;border-radius:8px;border:1px solid var(--border-dim);background:var(--bg-deep);color:var(--text-primary);font-family:var(--font-mono);font-size:12px;outline:none;transition:border-color .2s}
194
+ .code-input-row input:focus{border-color:var(--border-active)}
195
+ .code-input-row input::placeholder{color:var(--text-muted)}
196
+ .login-status-msg{font-size:12px;color:var(--text-muted);min-height:16px}
197
+ .btn-verify{padding:7px 16px;border-radius:7px;border:1px solid var(--green);background:var(--green-bg);color:var(--green);font-size:12px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap}
198
+ .btn-verify:hover{background:var(--green);color:#000}
199
+ .btn-verify:disabled{opacity:.45;cursor:not-allowed}
200
+ .service-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
201
+ @media(max-width:560px){.service-grid{grid-template-columns:1fr}}
202
+ .btn-save{padding:8px 20px;border-radius:8px;border:1px solid var(--accent);background:var(--accent-bg);color:var(--accent);font-family:var(--font-sans);font-size:12px;font-weight:600;cursor:pointer;transition:all .2s;margin-top:14px}
203
+ .btn-save:hover{background:var(--accent);color:#000}
204
+ .empty{padding:20px 0;text-align:center;color:var(--text-muted);font-size:12px}
205
+
206
+ /* ─────────────────────────────────────────────────────────────── */
207
+ /* ── DOCS TAB ── */
208
+ /* ─────────────────────────────────────────────────────────────── */
209
+ .docs-page{padding:28px 40px 60px;max-width:820px;margin:0 auto}
210
+ .docs-cards{display:flex;flex-direction:column;gap:14px;margin-top:20px}
211
+ .docs-card{display:flex;align-items:center;gap:16px;padding:20px 24px;background:var(--bg-card);border:1px solid var(--border-glow);border-radius:var(--radius);text-decoration:none;color:var(--text-primary);transition:all .25s}
212
+ .docs-card:hover{border-color:var(--border-active);box-shadow:var(--shadow),var(--shadow-glow);transform:translateY(-1px)}
213
+ .docs-card-icon{width:40px;height:40px;border-radius:10px;background:var(--accent-bg);border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--accent);flex-shrink:0}
214
+ .docs-card-body{flex:1}
215
+ .docs-card-title{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent);margin-bottom:3px}
216
+ .docs-card-desc{font-size:12px;color:var(--text-muted)}
217
+ .docs-card-arrow{font-size:18px;color:var(--text-muted);flex-shrink:0}
218
+
219
+ /* ── Toast ── */
220
+ .toast{position:fixed;bottom:24px;right:24px;padding:10px 18px;border-radius:8px;font-size:12px;font-weight:600;z-index:9999;opacity:0;transform:translateY(8px);transition:all .25s;pointer-events:none}
221
+ .toast.show{opacity:1;transform:translateY(0)}
222
+ .toast.ok{background:var(--green-bg);border:1px solid var(--green);color:var(--green)}
223
+ .toast.err{background:var(--red-bg);border:1px solid var(--red);color:var(--red)}
224
+ .toast.info{background:var(--accent-bg);border:1px solid var(--accent);color:var(--accent)}
225
+
226
+ ::-webkit-scrollbar{width:6px}
227
+ ::-webkit-scrollbar-track{background:transparent}
228
+ ::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:3px}
229
+ </style>
230
+ </head>
231
+ <body>
232
+
233
+ <!-- ── Topbar ── -->
234
+ <div class="topbar">
235
+ <a href="/" style="display:flex;align-items:center;gap:10px;margin-right:28px;text-decoration:none">
236
+ <img class="logo-mark" src="/MyAIforOne-logomark-transparent.svg" alt="MyAIforOne">
237
+ <span class="logo-text">MyAIforOne</span>
238
+ </a>
239
+ <nav class="tab-group">
240
+ <a class="tab-btn" href="/">Home</a>
241
+ <a class="tab-btn" href="/org">Agents</a>
242
+ <a class="tab-btn" href="/ui">Chat</a>
243
+ <a class="tab-btn" href="/library">Library</a>
244
+ <a class="tab-btn" href="/lab">Lab</a>
245
+ </nav>
246
+ <div class="topbar-right">
247
+ <a class="gym-nav-btn gym-tab-link" href="/gym" style="display:none">Gym</a>
248
+ <a href="/marketplace" class="gear-btn" title="Marketplace" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Marketplace</a>
249
+ <a href="/monitor" class="gear-btn" title="Monitor" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Monitor</a>
250
+ <a href="/admin" class="gear-btn" title="Admin" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Admin</a>
251
+ <a href="/user-guide" class="gear-btn" title="User Guide" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">User Guide</a>
252
+ <button onclick="window.open('/mini','minibar','width=440,height=460,resizable=yes,scrollbars=no')" title="Open Mini Bar" style="display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;font-size:15px;transition:all .2s" onmouseover="this.style.borderColor='var(--border-glow)'" onmouseout="this.style.borderColor='var(--border-dim)'">⊡</button>
253
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn" title="Toggle theme">&#9728;</button>
254
+ </div>
255
+ </div>
256
+
257
+ <!-- ── Admin tab nav ── -->
258
+ <div class="admin-nav">
259
+ <button class="type-tab" data-tab="channels" onclick="switchTab('channels')">Channels</button>
260
+ <button class="type-tab" data-tab="activity" onclick="switchTab('activity')">Activity</button>
261
+ <button class="type-tab active" data-tab="settings" onclick="switchTab('settings')">Settings</button>
262
+ <button class="type-tab" data-tab="docs" onclick="switchTab('docs')">Docs</button>
263
+ </div>
264
+
265
+ <!-- ── Canvas ── -->
266
+ <div class="canvas">
267
+
268
+ <!-- ═══════════════════════════ CHANNELS TAB ═══════════════════════════ -->
269
+ <div class="tab-panel" id="panel-channels">
270
+ <div id="channelsPanelRoot"><div class="loading-msg">Loading channels...</div></div>
271
+ </div>
272
+
273
+ <!-- ═══════════════════════════ ACTIVITY TAB ═══════════════════════════ -->
274
+ <div class="tab-panel" id="panel-activity">
275
+ <div class="activity-toolbar">
276
+ <select class="filter-select" id="agentFilter" onchange="fetchActivity()">
277
+ <option value="">All Agents</option>
278
+ </select>
279
+ <select class="filter-select" id="channelFilter" onchange="renderActivity()">
280
+ <option value="">All Channels</option>
281
+ <option value="web">Web</option>
282
+ <option value="telegram">Telegram</option>
283
+ <option value="slack">Slack</option>
284
+ <option value="imessage">iMessage</option>
285
+ <option value="goals">Goals</option>
286
+ <option value="delegate">Delegate</option>
287
+ </select>
288
+ <input class="search-input" id="searchInput" placeholder="Search messages..." oninput="debounceSearch()">
289
+ <span class="result-count" id="resultCount"></span>
290
+ </div>
291
+ <div class="activity-list" id="activityList">
292
+ <div class="loading-msg">Loading activity...</div>
293
+ </div>
294
+ </div>
295
+
296
+ <!-- ═══════════════════════════ SETTINGS TAB ═══════════════════════════ -->
297
+ <div class="tab-panel active" id="panel-settings">
298
+ <div class="settings-page">
299
+ <div class="page-title">Settings</div>
300
+ <div class="page-subtitle">Manage Claude accounts, service options, and global defaults.</div>
301
+
302
+ <!-- ── Profile ── -->
303
+ <div class="section">
304
+ <div class="section-header">
305
+ <div class="section-icon" style="background:var(--cyan-bg,rgba(0,188,212,.12));border-color:var(--cyan,#00bcd4);color:var(--cyan,#00bcd4)">&#9786;</div>
306
+ <div>
307
+ <div class="section-title" style="color:var(--cyan,#00bcd4)">Profile</div>
308
+ <div style="font-size:11px;color:var(--text-muted);margin-top:2px">Your identity — agents can use this to personalize responses</div>
309
+ </div>
310
+ </div>
311
+ <div class="section-body">
312
+ <div class="service-grid">
313
+ <div class="field">
314
+ <label>Name</label>
315
+ <input id="profileName" placeholder="Your name" />
316
+ </div>
317
+ <div class="field">
318
+ <label>Role / Title</label>
319
+ <input id="profileRole" placeholder="Product manager, developer, student…" />
320
+ </div>
321
+ <div class="field">
322
+ <label>Industry</label>
323
+ <input id="profileIndustry" placeholder="Fintech, healthcare, education…" />
324
+ </div>
325
+ <div class="field">
326
+ <label>AI Experience</label>
327
+ <select id="profileAiExperience">
328
+ <option value="">— select —</option>
329
+ <option value="beginner">Beginner</option>
330
+ <option value="intermediate">Intermediate</option>
331
+ <option value="advanced">Advanced</option>
332
+ </select>
333
+ </div>
334
+ </div>
335
+ <div class="field" style="margin-top:10px">
336
+ <label>Interests <span style="font-size:10px;color:var(--text-muted)">(comma-separated)</span></label>
337
+ <input id="profileInterests" placeholder="automation, coding, writing, research…" />
338
+ </div>
339
+ <button class="btn-save" onclick="saveProfile()">Save</button>
340
+ </div>
341
+ </div>
342
+
343
+ <!-- ── Claude Accounts ── -->
344
+ <div class="section">
345
+ <div class="section-header">
346
+ <div class="section-icon">&#9881;</div>
347
+ <div><div class="section-title">Claude Accounts</div></div>
348
+ </div>
349
+ <div class="section-body">
350
+ <div id="accountsList"><div class="empty">Loading...</div></div>
351
+ <hr class="divider">
352
+ <div class="form-label">Add Account</div>
353
+ <div class="field-row">
354
+ <div class="field" style="max-width:200px">
355
+ <label>Account Name</label>
356
+ <input id="newAccName" placeholder="main, work, personal…" />
357
+ </div>
358
+ <button class="btn-primary" id="startLoginBtn" onclick="startLogin()">&#x2192; Login &amp; Add</button>
359
+ </div>
360
+ <div class="login-panel" id="loginPanel">
361
+ <div class="login-panel-title">Authorize Claude Account</div>
362
+ <div id="loginPanelBody"></div>
363
+ </div>
364
+ <div class="hint">
365
+ The config directory is derived automatically from the account name — <code>main</code> uses <code>~/.claude</code>, any other name uses <code>~/.claude-{name}</code>.
366
+ </div>
367
+ </div>
368
+ </div>
369
+
370
+ <!-- ── Service ── -->
371
+ <div class="section">
372
+ <div class="section-header">
373
+ <div class="section-icon" style="background:var(--purple-bg);border-color:var(--purple);color:var(--purple)">&#9670;</div>
374
+ <div><div class="section-title" style="color:var(--purple)">Service</div></div>
375
+ <span style="margin-left:auto;font-size:11px;color:var(--text-muted)">Restart required for most changes</span>
376
+ </div>
377
+ <div class="section-body">
378
+ <div class="service-grid">
379
+ <div class="field">
380
+ <label>Personal Agents Directory</label>
381
+ <input id="svcAgentsDir" placeholder="~/Desktop/MyAIforOne Drive/PersonalAgents" />
382
+ </div>
383
+ <div class="field">
384
+ <label>Personal Registry Directory</label>
385
+ <input id="svcRegistryDir" placeholder="~/Desktop/MyAIforOne Drive/PersonalRegistry" />
386
+ </div>
387
+ <div class="field">
388
+ <label>Web UI Port</label>
389
+ <input id="svcPort" type="number" placeholder="4888" />
390
+ </div>
391
+ <div class="field">
392
+ <label>Log Level</label>
393
+ <input id="svcLogLevel" placeholder="info" />
394
+ </div>
395
+ <div class="field">
396
+ <label>Default Claude Account</label>
397
+ <select id="svcDefaultAccount" style="padding:6px 10px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:13px">
398
+ <option value="">System default (~/.claude)</option>
399
+ </select>
400
+ <div style="font-size:11px;color:var(--text-muted);margin-top:2px">Used for agents without a per-agent account override. Switch when credits run out.</div>
401
+ </div>
402
+ </div>
403
+ <button class="btn-save" onclick="saveService()">Save</button>
404
+ </div>
405
+ </div>
406
+
407
+ <!-- ── Feature Flags ── -->
408
+ <div class="section">
409
+ <div class="section-header">
410
+ <div class="section-icon" style="background:rgba(168,85,247,0.15);border-color:#a855f7;color:#a855f7">&#9873;</div>
411
+ <div>
412
+ <div class="section-title" style="color:#a855f7">Feature Flags</div>
413
+ <div style="font-size:11px;color:var(--text-muted);margin-top:2px">Toggle platform modules on or off</div>
414
+ </div>
415
+ </div>
416
+ <div class="section-body">
417
+ <div style="display:flex;flex-direction:column;gap:16px">
418
+ <!-- AI Gym -->
419
+ <div style="padding:14px 16px;border-radius:10px;background:var(--bg-secondary);border:1px solid var(--border)">
420
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:12px">
421
+ <div>
422
+ <div style="font-weight:600;font-size:13px;color:var(--text-primary)">AI Gym</div>
423
+ <div style="font-size:11px;color:var(--text-muted);margin-top:3px">Personal AI coaching, training programs, and skill dimension tracking</div>
424
+ </div>
425
+ <label style="position:relative;display:inline-block;width:42px;height:24px;flex-shrink:0">
426
+ <input type="checkbox" id="svcGymEnabled" onchange="saveGymSettings()" style="opacity:0;width:0;height:0" />
427
+ <span style="position:absolute;cursor:pointer;inset:0;background:var(--bg-tertiary,#333);border-radius:24px;transition:.2s"></span>
428
+ <span class="flag-knob" style="position:absolute;top:3px;left:3px;width:18px;height:18px;background:#fff;border-radius:50%;transition:.2s;pointer-events:none"></span>
429
+ </label>
430
+ </div>
431
+ <!-- Sub-flags (only visible when Gym is on) -->
432
+ <div id="gymSubFlags" style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border);display:none">
433
+ <div style="display:flex;flex-direction:column;gap:10px">
434
+ <label style="display:flex;align-items:center;gap:10px;cursor:pointer">
435
+ <input type="checkbox" id="svcGymOnlyMode" style="width:14px;height:14px;accent-color:#a855f7;cursor:pointer" onchange="saveGymSettings()" />
436
+ <span style="font-size:12px;color:var(--text-secondary)">Gym-Only Mode</span>
437
+ <span style="font-size:10px;color:var(--text-muted)">— landing page becomes Gym; nav shows only Gym, Agents, Settings</span>
438
+ </label>
439
+ <label style="display:flex;align-items:center;gap:10px;cursor:pointer">
440
+ <input type="checkbox" id="svcAibriefingEnabled" style="width:14px;height:14px;accent-color:var(--accent);cursor:pointer" onchange="saveGymSettings()" />
441
+ <span style="font-size:12px;color:var(--text-secondary)">AI Briefing Feed</span>
442
+ <span style="font-size:10px;color:var(--text-muted)">— weekly AI news digest (requires web search MCP)</span>
443
+ </label>
444
+ <div style="margin-top:10px;padding-top:10px;border-top:1px solid var(--border)">
445
+ <button onclick="reconfigureGym()" style="font-size:12px;font-weight:600;padding:6px 14px;border-radius:8px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);cursor:pointer;transition:all .2s"
446
+ onmouseover="this.style.borderColor='var(--accent)';this.style.color='var(--accent)'"
447
+ onmouseout="this.style.borderColor='var(--border)';this.style.color='var(--text-secondary)'">Reconfigure Gym</button>
448
+ <span style="font-size:10px;color:var(--text-muted);margin-left:8px">Re-run onboarding (pick trainer, update profile)</span>
449
+ </div>
450
+ </div>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ </div>
455
+ </div>
456
+
457
+ <div class="settings-section">
458
+ <div class="section-header" style="display:flex;align-items:center;gap:12px">
459
+ <div>
460
+ <div class="section-title" style="color:var(--amber)">AI Models</div>
461
+ <div style="font-size:11px;color:var(--text-muted);margin-top:2px">Multi-model support via Ollama for open-source models</div>
462
+ </div>
463
+ </div>
464
+ <div class="section-body">
465
+ <div class="field" style="margin-bottom:14px">
466
+ <label style="display:flex;align-items:center;gap:10px;cursor:pointer">
467
+ <input type="checkbox" id="svcMultiModelEnabled" style="width:16px;height:16px;accent-color:var(--accent);cursor:pointer" />
468
+ <span>Enable Multi-Model</span>
469
+ </label>
470
+ <div class="hint" style="margin-top:4px">When enabled, agents can use alternative models via Ollama. When disabled, all agents use Claude.</div>
471
+ </div>
472
+ <!-- Setup guide — shown when toggle is ON -->
473
+ <div id="ollamaSetupGuide" style="display:none;margin-bottom:16px;padding:14px 16px;border-radius:10px;background:rgba(255,191,0,0.06);border:1px solid rgba(255,191,0,0.18)">
474
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
475
+ <span style="font-size:18px">&#9888;</span>
476
+ <span style="font-weight:600;font-size:13px;color:var(--amber)">Requires Ollama + a downloaded model</span>
477
+ </div>
478
+ <div style="font-size:12px;color:var(--text-muted);line-height:1.7">
479
+ <div style="margin-bottom:10px"><strong style="color:var(--text)">Step 1 — Install Ollama</strong></div>
480
+ <div style="display:flex;flex-direction:column;gap:6px;margin-bottom:14px">
481
+ <div style="display:flex;align-items:center;gap:8px">
482
+ <span style="font-size:14px">&#63743;</span>
483
+ <span><strong>macOS:</strong></span>
484
+ <code style="background:var(--bg-secondary);padding:2px 8px;border-radius:4px;font-size:11px;user-select:all">curl -fsSL https://ollama.com/install.sh | sh</code>
485
+ </div>
486
+ <div style="display:flex;align-items:center;gap:8px">
487
+ <span style="font-size:14px">&#8862;</span>
488
+ <span><strong>Windows:</strong></span>
489
+ <span>Download from <a href="https://ollama.com/download" target="_blank" style="color:var(--accent)">ollama.com/download</a></span>
490
+ </div>
491
+ <div style="display:flex;align-items:center;gap:8px">
492
+ <span style="font-size:14px">&#128039;</span>
493
+ <span><strong>Linux:</strong></span>
494
+ <code style="background:var(--bg-secondary);padding:2px 8px;border-radius:4px;font-size:11px;user-select:all">curl -fsSL https://ollama.com/install.sh | sh</code>
495
+ </div>
496
+ </div>
497
+ <div style="margin-bottom:10px"><strong style="color:var(--text)">Step 2 — Pull a model</strong></div>
498
+ <div style="display:flex;flex-direction:column;gap:4px;margin-bottom:14px">
499
+ <code style="background:var(--bg-secondary);padding:4px 10px;border-radius:4px;font-size:11px;user-select:all">ollama pull gemma2</code>
500
+ <code style="background:var(--bg-secondary);padding:4px 10px;border-radius:4px;font-size:11px;user-select:all">ollama pull llama3.1</code>
501
+ <code style="background:var(--bg-secondary);padding:4px 10px;border-radius:4px;font-size:11px;user-select:all">ollama pull mistral</code>
502
+ </div>
503
+ <div style="margin-bottom:8px"><strong style="color:var(--text)">Step 3 — Set executor on an agent</strong></div>
504
+ <div style="font-size:11px;color:var(--text-muted)">
505
+ In the agent's config, set <code style="background:var(--bg-secondary);padding:1px 6px;border-radius:3px">executor</code> to <code style="background:var(--bg-secondary);padding:1px 6px;border-radius:3px">ollama:gemma2</code> (or whichever model you pulled).
506
+ Agents without an executor field will use the Platform Default below.
507
+ </div>
508
+ </div>
509
+ <div id="ollamaHealthStatus" style="margin-top:8px;padding:8px 12px;border-radius:6px;font-size:11px;display:none"></div>
510
+ <button class="btn-save" style="margin-top:6px;background:transparent;border:1px solid var(--amber);color:var(--amber);font-size:11px;padding:4px 12px" onclick="checkOllamaConnection()">Test Connection</button>
511
+ </div>
512
+
513
+ <div class="service-grid" id="multiModelFields">
514
+ <div class="field">
515
+ <label>Platform Default Executor</label>
516
+ <select id="svcPlatformDefaultExecutor">
517
+ <option value="claude">Claude (default)</option>
518
+ </select>
519
+ </div>
520
+ <div class="field">
521
+ <label>Ollama Base URL</label>
522
+ <input id="svcOllamaBaseUrl" placeholder="http://localhost:11434" />
523
+ </div>
524
+ </div>
525
+
526
+ <div style="margin:16px 0 8px;font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em">Provider API Keys</div>
527
+ <div class="service-grid" id="providerKeyFields">
528
+ <div class="field">
529
+ <label>OpenAI <span style="font-size:9px;color:var(--text-muted)">(openai:gpt-4o)</span></label>
530
+ <input id="keyOpenai" type="password" placeholder="sk-..." autocomplete="off" />
531
+ <button class="btn-save" style="margin-top:4px;background:transparent;border:1px solid var(--accent);color:var(--accent);font-size:10px;padding:3px 10px" onclick="testProvider('openai')">Test</button>
532
+ </div>
533
+ <div class="field">
534
+ <label>xAI / Grok <span style="font-size:9px;color:var(--text-muted)">(grok:grok-3)</span></label>
535
+ <input id="keyXai" type="password" placeholder="xai-..." autocomplete="off" />
536
+ <button class="btn-save" style="margin-top:4px;background:transparent;border:1px solid var(--accent);color:var(--accent);font-size:10px;padding:3px 10px" onclick="testProvider('grok')">Test</button>
537
+ </div>
538
+ <div class="field">
539
+ <label>Google Gemini <span style="font-size:9px;color:var(--text-muted)">(gemini:gemini-2.5-flash)</span></label>
540
+ <input id="keyGoogle" type="password" placeholder="AIza..." autocomplete="off" />
541
+ <button class="btn-save" style="margin-top:4px;background:transparent;border:1px solid var(--accent);color:var(--accent);font-size:10px;padding:3px 10px" onclick="testProvider('gemini')">Test</button>
542
+ </div>
543
+ <div class="field">
544
+ <label>Groq <span style="font-size:9px;color:var(--text-muted)">(groq:llama-3.3-70b-versatile)</span></label>
545
+ <input id="keyGroq" type="password" placeholder="gsk_..." autocomplete="off" />
546
+ <button class="btn-save" style="margin-top:4px;background:transparent;border:1px solid var(--accent);color:var(--accent);font-size:10px;padding:3px 10px" onclick="testProvider('groq')">Test</button>
547
+ </div>
548
+ <div class="field">
549
+ <label>Together <span style="font-size:9px;color:var(--text-muted)">(together:meta-llama/...)</span></label>
550
+ <input id="keyTogether" type="password" placeholder="..." autocomplete="off" />
551
+ <button class="btn-save" style="margin-top:4px;background:transparent;border:1px solid var(--accent);color:var(--accent);font-size:10px;padding:3px 10px" onclick="testProvider('together')">Test</button>
552
+ </div>
553
+ <div class="field">
554
+ <label>Mistral <span style="font-size:9px;color:var(--text-muted)">(mistral:mistral-large-latest)</span></label>
555
+ <input id="keyMistral" type="password" placeholder="..." autocomplete="off" />
556
+ <button class="btn-save" style="margin-top:4px;background:transparent;border:1px solid var(--accent);color:var(--accent);font-size:10px;padding:3px 10px" onclick="testProvider('mistral')">Test</button>
557
+ </div>
558
+ </div>
559
+ <button class="btn-save" onclick="saveMultiModel()">Save</button>
560
+ </div>
561
+ </div>
562
+
563
+ <!-- ── Deployment ── -->
564
+ <div class="section">
565
+ <div class="section-header">
566
+ <div class="section-icon" style="background:var(--green-bg);border-color:var(--green);color:var(--green)">&#9652;</div>
567
+ <div><div class="section-title" style="color:var(--green)">Deployment</div></div>
568
+ </div>
569
+ <div class="section-body">
570
+ <div class="service-grid">
571
+ <div class="field">
572
+ <label>Provider</label>
573
+ <input id="depProvider" placeholder="railway" />
574
+ </div>
575
+ <div class="field">
576
+ <label>Deploy Token <span id="depTokenReveal" onclick="toggleReveal('depToken','depTokenReveal')" style="cursor:pointer;font-size:10px;color:var(--accent);margin-left:4px">show</span></label>
577
+ <input id="depToken" type="password" placeholder="••••••••" autocomplete="off" />
578
+ </div>
579
+ <div class="field">
580
+ <label>GitHub Org / User</label>
581
+ <input id="depGithubOrg" placeholder="myorg" />
582
+ </div>
583
+ <div class="field">
584
+ <label>GitHub Token <span id="depGithubTokenReveal" onclick="toggleReveal('depGithubToken','depGithubTokenReveal')" style="cursor:pointer;font-size:10px;color:var(--accent);margin-left:4px">show</span></label>
585
+ <input id="depGithubToken" type="password" placeholder="••••••••" autocomplete="off" />
586
+ </div>
587
+ </div>
588
+ <button class="btn-save" onclick="saveDeployment()">Save</button>
589
+ </div>
590
+ </div>
591
+
592
+ <!-- ── SaaS Publishing ── -->
593
+ <div class="section">
594
+ <div class="section-header">
595
+ <div class="section-icon" style="background:var(--accent-bg);border-color:var(--accent);color:var(--accent)">&#8599;</div>
596
+ <div>
597
+ <div class="section-title">SaaS Publishing</div>
598
+ <div style="font-size:11px;color:var(--text-muted);margin-top:2px">Publish skills, prompts, agents, and apps from your Library to a shared SaaS workspace</div>
599
+ </div>
600
+ <span id="saas-status-dot" style="margin-left:auto;display:none;width:8px;height:8px;border-radius:50%;background:var(--green);flex-shrink:0" title="Connected"></span>
601
+ </div>
602
+ <div class="section-body">
603
+ <div class="service-grid">
604
+ <div class="field">
605
+ <label>SaaS Base URL</label>
606
+ <input id="saasBaseUrl" placeholder="https://myagent-app.up.railway.app" />
607
+ </div>
608
+ <div class="field">
609
+ <label>API Key <span id="saasKeyReveal" onclick="toggleReveal('saasKey','saasKeyReveal')" style="cursor:pointer;font-size:10px;color:var(--accent);margin-left:4px">show</span></label>
610
+ <input id="saasKey" type="password" placeholder="mak_..." autocomplete="off" />
611
+ </div>
612
+ </div>
613
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
614
+ <button class="btn-save" onclick="saveSaas()">Save</button>
615
+ <button class="btn-save" style="background:transparent;border:1px solid var(--border-glow);color:var(--text-secondary)" onclick="testSaas(this)">Test Connection</button>
616
+ </div>
617
+ </div>
618
+ </div>
619
+
620
+ <!-- ── Status Indicator ── -->
621
+ <div class="section">
622
+ <div class="section-header">
623
+ <div class="section-icon" style="background:rgba(168,85,247,0.15);border-color:#a855f7;color:#a855f7">&#9679;</div>
624
+ <div>
625
+ <div class="section-title">Status Indicator</div>
626
+ <div style="font-size:11px;color:var(--text-muted);margin-top:2px">Show a live status dot in your menu bar (Mac) or system tray (Windows)</div>
627
+ </div>
628
+ </div>
629
+ <div class="section-body">
630
+ <p style="font-size:13px;color:var(--text-muted);margin:0 0 16px;line-height:1.5">
631
+ Adds a small green/red indicator that shows if MyAgent is running. Click it to see agent count, uptime, and quick links. You can restart or stop the service from the menu.
632
+ </p>
633
+ <div id="indicatorInstallArea"></div>
634
+ </div>
635
+ </div>
636
+ </div>
637
+ </div>
638
+
639
+ <!-- ═══════════════════════════ DOCS TAB ═══════════════════════════ -->
640
+ <div class="tab-panel" id="panel-docs">
641
+ <div class="docs-page">
642
+ <div class="page-title">Docs</div>
643
+ <div class="page-subtitle">Reference documentation for the MyAgent API, MCP tools, and changelog.</div>
644
+ <div class="docs-cards">
645
+ <a class="docs-card" href="/api-docs#api" target="_blank">
646
+ <div class="docs-card-icon">&#9881;</div>
647
+ <div class="docs-card-body">
648
+ <div class="docs-card-title">API Docs</div>
649
+ <div class="docs-card-desc">Full REST API reference — endpoints, request/response schemas, authentication.</div>
650
+ </div>
651
+ <span class="docs-card-arrow">&#x2197;</span>
652
+ </a>
653
+ <a class="docs-card" href="/api-docs#mcp" target="_blank">
654
+ <div class="docs-card-icon">&#9830;</div>
655
+ <div class="docs-card-body">
656
+ <div class="docs-card-title">MCP Tools</div>
657
+ <div class="docs-card-desc">Model Context Protocol tool definitions — available actions agents can invoke via MCP.</div>
658
+ </div>
659
+ <span class="docs-card-arrow">&#x2197;</span>
660
+ </a>
661
+ <a class="docs-card" href="/changelog">
662
+ <div class="docs-card-icon">&#x29C9;</div>
663
+ <div class="docs-card-body">
664
+ <div class="docs-card-title">Changelog</div>
665
+ <div class="docs-card-desc">Release history — feature additions, fixes, and breaking changes by version.</div>
666
+ </div>
667
+ <span class="docs-card-arrow">&#x2192;</span>
668
+ </a>
669
+ <a class="docs-card" href="/user-guide">
670
+ <div class="docs-card-icon">&#x2709;</div>
671
+ <div class="docs-card-body">
672
+ <div class="docs-card-title">User Guide</div>
673
+ <div class="docs-card-desc">Comprehensive platform walkthrough — every page, button, action, API endpoint, and MCP tool.</div>
674
+ </div>
675
+ <span class="docs-card-arrow">&#x2192;</span>
676
+ </a>
677
+ </div>
678
+ </div>
679
+ </div>
680
+
681
+ </div><!-- /canvas -->
682
+
683
+ <div class="toast" id="toast"></div>
684
+
685
+ <script>
686
+ // ── Theme ──────────────────────────────────────────────────────────────
687
+ function applyTheme(t){
688
+ document.documentElement.setAttribute('data-theme',t);
689
+ localStorage.setItem('theme',t);
690
+ document.getElementById('themeBtn').textContent = t === 'light' ? '🌙' : '☀️';
691
+ }
692
+ function toggleTheme(){applyTheme(document.documentElement.getAttribute('data-theme')==='light'?'dark':'light')}
693
+ (function(){
694
+ const saved = localStorage.getItem('theme') || 'dark';
695
+ document.documentElement.setAttribute('data-theme', saved);
696
+ setTimeout(() => { document.getElementById('themeBtn').textContent = saved === 'light' ? '🌙' : '☀️'; }, 0);
697
+ })();
698
+
699
+ // ── Toast ──────────────────────────────────────────────────────────────
700
+ let toastTimer;
701
+ function showToast(msg, type='ok', html=false) {
702
+ const t = document.getElementById('toast');
703
+ if (html) { t.innerHTML = msg; } else { t.textContent = msg; }
704
+ t.className = `toast ${type} show`;
705
+ clearTimeout(toastTimer);
706
+ toastTimer = setTimeout(() => t.classList.remove('show'), html ? 10000 : 3500);
707
+ }
708
+
709
+ // ── Tab switching ──────────────────────────────────────────────────────
710
+ const TABS = ['channels','activity','settings','docs'];
711
+ let channelsLoaded = false;
712
+ let activityLoaded = false;
713
+ let settingsLoaded = false;
714
+
715
+ function switchTab(name) {
716
+ if (!TABS.includes(name)) name = 'settings';
717
+ TABS.forEach(t => {
718
+ document.getElementById('panel-' + t).classList.toggle('active', t === name);
719
+ document.querySelector(`.type-tab[data-tab="${t}"]`).classList.toggle('active', t === name);
720
+ });
721
+ // Lazy-load tab data
722
+ if (name === 'channels' && !channelsLoaded) loadChannels();
723
+ if (name === 'activity' && !activityLoaded) initActivity();
724
+ if (name === 'settings' && !settingsLoaded) { loadProfile(); loadAccounts(); loadService(); loadSaas(); settingsLoaded = true; }
725
+ // Update URL param
726
+ const url = new URL(location.href);
727
+ url.searchParams.set('tab', name);
728
+ history.replaceState(null, '', url.toString());
729
+ }
730
+
731
+ // Read tab from URL on load
732
+ (function(){
733
+ const params = new URLSearchParams(location.search);
734
+ const tab = params.get('tab');
735
+ switchTab(TABS.includes(tab) ? tab : 'settings');
736
+ })();
737
+
738
+ // ──────────────────────────────────────────────────────────────────────
739
+ // ── CHANNELS ──────────────────────────────────────────────────────────
740
+ // ──────────────────────────────────────────────────────────────────────
741
+ let channelsData = [];
742
+ let channelAgents = [];
743
+
744
+ const channelIcons = {
745
+ telegram: '✈️', slack: '💬', discord: '🎮', imessage: '💬', whatsapp: '📲',
746
+ };
747
+
748
+ async function loadChannels() {
749
+ channelsLoaded = true;
750
+ const root = document.getElementById('channelsPanelRoot');
751
+ try {
752
+ const [chRes, dashRes] = await Promise.all([
753
+ fetch('/api/channels'),
754
+ fetch('/api/dashboard'),
755
+ ]);
756
+ channelsData = (await chRes.json()).channels || [];
757
+ const dash = await dashRes.json();
758
+ channelAgents = dash.agents || [];
759
+ renderChannelsPanel(root);
760
+ } catch(e) {
761
+ root.innerHTML = '<div class="no-activity">Failed to load channels: ' + escHtml(e.message) + '</div>';
762
+ }
763
+ }
764
+
765
+ async function refreshChannels() {
766
+ const root = document.getElementById('channelsPanelRoot');
767
+ try {
768
+ const [chRes, dashRes] = await Promise.all([fetch('/api/channels'), fetch('/api/dashboard')]);
769
+ channelsData = (await chRes.json()).channels || [];
770
+ channelAgents = (await dashRes.json()).agents || [];
771
+ renderChannelsPanel(root);
772
+ } catch {}
773
+ }
774
+
775
+ function renderChannelsPanel(canvas) {
776
+ if (!channelsData || channelsData.length === 0) {
777
+ canvas.innerHTML = '<div class="empty-org"><div class="empty-org-icon">📡</div><div class="empty-org-text">No channels configured</div></div>';
778
+ return;
779
+ }
780
+
781
+ let html = '<div class="org-title-banner">Channels</div><div class="channels-grid">';
782
+
783
+ for (const ch of channelsData) {
784
+ const icon = channelIcons[ch.name] || '🔗';
785
+ const statusClass = ch.enabled ? 'enabled' : 'disabled';
786
+ const statusText = ch.enabled ? 'Connected' : 'Disabled';
787
+ const timeoutMin = Math.round((ch.stickyTimeoutMs || 300000) / 60000);
788
+
789
+ html += `<div class="channel-card" id="ch-card-${ch.name}">
790
+ <div class="channel-card-header">
791
+ <div class="channel-card-icon">${icon}</div>
792
+ <div class="channel-card-name">${escHtml(ch.name)}</div>
793
+ <span class="channel-status-pill ${statusClass}">${statusText}</span>
794
+ </div>
795
+ <div class="channel-card-body">
796
+ <div class="channel-setting-row">
797
+ <span class="channel-setting-label">Sticky Mode</span>
798
+ <select class="channel-setting-select" id="ch-sticky-${ch.name}" onchange="togglePrefixVisibility('${ch.name}')">
799
+ <option value="none" ${ch.stickyRouting==='none'?'selected':''}>None</option>
800
+ <option value="sticky" ${ch.stickyRouting==='sticky'?'selected':''}>Sticky</option>
801
+ <option value="prefix" ${ch.stickyRouting==='prefix'?'selected':''}>Prefix</option>
802
+ </select>
803
+ </div>
804
+ <div class="channel-setting-row" id="ch-prefix-row-${ch.name}" style="${ch.stickyRouting==='prefix'?'':'display:none'}">
805
+ <span class="channel-setting-label">Prefix</span>
806
+ <input class="channel-setting-input" id="ch-prefix-${ch.name}" value="${escHtml(ch.stickyPrefix || '!')}" style="width:60px">
807
+ </div>
808
+ <div class="channel-setting-row">
809
+ <span class="channel-setting-label">Timeout (min)</span>
810
+ <input class="channel-setting-input" id="ch-timeout-${ch.name}" type="number" min="1" value="${timeoutMin}" style="width:80px">
811
+ </div>
812
+ <button class="channel-save-btn" onclick="saveChannelSettings('${ch.name}')">Save Settings</button>
813
+ <div class="channel-restart-note">Changes require service restart to take effect</div>`;
814
+
815
+ // iMessage monitored chat IDs
816
+ if (ch.name === 'imessage' && ch.monitoredChatIds) {
817
+ html += `<div class="channel-section-title">Monitored Chat IDs</div>
818
+ <div class="monitored-ids" id="ch-monitored-${ch.name}">`;
819
+ for (const mid of ch.monitoredChatIds) {
820
+ html += `<div class="monitored-pill">${mid}<button onclick="removeMonitoredId(${mid})">&times;</button></div>`;
821
+ }
822
+ html += `</div>
823
+ <div class="channel-add-row" style="margin-top:6px">
824
+ <input class="channel-add-input" id="ch-addmon-${ch.name}" type="number" placeholder="Chat ID (number)" style="width:140px">
825
+ <button class="channel-add-btn" onclick="addMonitoredId('${ch.name}')">+ Add</button>
826
+ </div>`;
827
+ }
828
+
829
+ // Agents grouped by chat ID
830
+ const chatGroups = {};
831
+ for (const ag of ch.agents) {
832
+ if (!chatGroups[ag.chatId]) chatGroups[ag.chatId] = [];
833
+ chatGroups[ag.chatId].push(ag);
834
+ }
835
+ const chatIds = Object.keys(chatGroups).sort();
836
+ html += `<div class="channel-section-title" style="display:flex;align-items:center;gap:8px">
837
+ Chat Groups
838
+ <select class="channel-setting-select" id="ch-chatfilter-${ch.name}" onchange="filterChatGroup('${ch.name}')" style="font-size:11px;padding:2px 6px">
839
+ <option value="">All (${chatIds.length} chats, ${ch.agents.length} routes)</option>
840
+ ${chatIds.map(cid => `<option value="${cid}">Chat ${escHtml(cid)} (${chatGroups[cid].length})</option>`).join('')}
841
+ </select>
842
+ </div>`;
843
+ if (chatIds.length === 0) {
844
+ html += '<div style="font-size:12px;color:var(--text-muted);padding:4px 0">No agents on this channel</div>';
845
+ } else {
846
+ for (const cid of chatIds) {
847
+ const agentsInChat = chatGroups[cid];
848
+ html += `<div class="ch-chatgroup-${ch.name}" data-chatid="${escHtml(cid)}" style="margin:8px 0 4px;padding:6px 10px;background:rgba(139,92,246,0.05);border:1px solid var(--border-dim);border-radius:8px">
849
+ <div style="font-family:var(--font-mono);font-size:11px;font-weight:600;color:var(--accent);margin-bottom:6px">Chat ${escHtml(cid)} <span style="font-weight:400;color:var(--text-muted)">(${agentsInChat.length} agent${agentsInChat.length>1?'s':''})</span></div>`;
850
+ for (const ag of agentsInChat) {
851
+ html += `<div class="channel-agent-row">
852
+ <span class="channel-agent-name">${escHtml(ag.agentName)}</span>
853
+ <span class="channel-agent-alias">${escHtml(ag.alias)}</span>
854
+ <button class="channel-agent-remove" onclick="removeChannelAgent('${ch.name}','${escHtml(ag.agentId)}','${escHtml(ag.chatId)}')" title="Remove route">&times;</button>
855
+ </div>`;
856
+ }
857
+ html += `</div>`;
858
+ }
859
+ }
860
+
861
+ // Add agent
862
+ const agentIdsOnChannel = new Set(ch.agents.map(a => a.agentId));
863
+ const availableAgents = channelAgents.filter(a => !agentIdsOnChannel.has(a.id));
864
+ html += `<div class="channel-add-row">
865
+ <select class="channel-add-select" id="ch-addagent-${ch.name}">
866
+ ${availableAgents.length === 0 ? '<option value="">All agents added</option>' :
867
+ '<option value="">Select agent...</option>' + availableAgents.map(a => `<option value="${a.id}">${escHtml(a.name)} (${a.aliases?.[0] || a.id})</option>`).join('')}
868
+ </select>
869
+ <input class="channel-add-input" id="ch-addchatid-${ch.name}" placeholder="Chat/Channel ID">
870
+ <button class="channel-add-btn" onclick="addChannelAgent('${ch.name}')" ${availableAgents.length===0?'disabled':''}>+ Add Agent</button>
871
+ </div>`;
872
+
873
+ html += '</div></div>';
874
+ }
875
+
876
+ html += '</div>';
877
+ canvas.innerHTML = html;
878
+ }
879
+
880
+ function togglePrefixVisibility(channelName) {
881
+ const mode = document.getElementById('ch-sticky-' + channelName).value;
882
+ document.getElementById('ch-prefix-row-' + channelName).style.display = mode === 'prefix' ? '' : 'none';
883
+ }
884
+
885
+ async function saveChannelSettings(channelName) {
886
+ const mode = document.getElementById('ch-sticky-' + channelName).value;
887
+ const prefix = document.getElementById('ch-prefix-' + channelName)?.value || '!';
888
+ const timeoutMin = parseInt(document.getElementById('ch-timeout-' + channelName).value) || 5;
889
+ const timeoutMs = timeoutMin * 60000;
890
+ try {
891
+ const res = await fetch('/api/channels/' + channelName, {
892
+ method: 'PUT', headers: {'Content-Type':'application/json'},
893
+ body: JSON.stringify({ stickyRouting: mode, stickyPrefix: prefix, stickyTimeoutMs: timeoutMs }),
894
+ });
895
+ const data = await res.json();
896
+ if (data.ok) {
897
+ const ch = channelsData.find(c => c.name === channelName);
898
+ if (ch) { ch.stickyRouting = mode; ch.stickyPrefix = prefix; ch.stickyTimeoutMs = timeoutMs; }
899
+ showToast('Saved. Restart service for full effect.', 'ok');
900
+ } else { showToast('Error: ' + (data.error || 'Unknown'), 'err'); }
901
+ } catch(err) { showToast('Error: ' + err.message, 'err'); }
902
+ }
903
+
904
+ async function addChannelAgent(channelName) {
905
+ const selectEl = document.getElementById('ch-addagent-' + channelName);
906
+ const chatIdEl = document.getElementById('ch-addchatid-' + channelName);
907
+ const agentId = selectEl.value;
908
+ const chatId = chatIdEl.value.trim();
909
+ if (!agentId) { showToast('Select an agent', 'err'); return; }
910
+ if (!chatId) { showToast('Enter a chat/channel ID', 'err'); return; }
911
+ try {
912
+ const res = await fetch('/api/channels/' + channelName + '/agents', {
913
+ method: 'POST', headers: {'Content-Type':'application/json'},
914
+ body: JSON.stringify({ agentId, chatId, requireMention: true }),
915
+ });
916
+ const data = await res.json();
917
+ if (data.ok) { await refreshChannels(); }
918
+ else { showToast('Error: ' + (data.error || 'Unknown'), 'err'); }
919
+ } catch(err) { showToast('Error: ' + err.message, 'err'); }
920
+ }
921
+
922
+ function filterChatGroup(channelName) {
923
+ const filter = document.getElementById('ch-chatfilter-' + channelName)?.value || '';
924
+ const groups = document.querySelectorAll('.ch-chatgroup-' + channelName);
925
+ for (const g of groups) {
926
+ g.style.display = (!filter || g.dataset.chatid === filter) ? '' : 'none';
927
+ }
928
+ }
929
+
930
+ async function removeChannelAgent(channelName, agentId, chatId) {
931
+ if (!confirm('Remove ' + agentId + ' from ' + channelName + ':' + chatId + '?')) return;
932
+ try {
933
+ const res = await fetch('/api/channels/' + channelName + '/agents/' + agentId, {
934
+ method: 'DELETE', headers: {'Content-Type':'application/json'},
935
+ body: JSON.stringify({ chatId }),
936
+ });
937
+ const data = await res.json();
938
+ if (data.ok) { await refreshChannels(); }
939
+ else { showToast('Error: ' + (data.error || 'Unknown'), 'err'); }
940
+ } catch(err) { showToast('Error: ' + err.message, 'err'); }
941
+ }
942
+
943
+ async function addMonitoredId(channelName) {
944
+ const input = document.getElementById('ch-addmon-' + channelName);
945
+ const chatId = parseInt(input.value);
946
+ if (isNaN(chatId)) { showToast('Enter a valid numeric chat ID', 'err'); return; }
947
+ try {
948
+ const res = await fetch('/api/channels/' + channelName + '/monitored', {
949
+ method: 'POST', headers: {'Content-Type':'application/json'},
950
+ body: JSON.stringify({ chatId }),
951
+ });
952
+ const data = await res.json();
953
+ if (data.ok) { input.value = ''; await refreshChannels(); }
954
+ else { showToast('Error: ' + (data.error || 'Unknown'), 'err'); }
955
+ } catch(err) { showToast('Error: ' + err.message, 'err'); }
956
+ }
957
+
958
+ async function removeMonitoredId(chatId) {
959
+ if (!confirm('Remove monitored chat ID ' + chatId + '?')) return;
960
+ try {
961
+ const res = await fetch('/api/channels/imessage/monitored', {
962
+ method: 'DELETE', headers: {'Content-Type':'application/json'},
963
+ body: JSON.stringify({ chatId }),
964
+ });
965
+ const data = await res.json();
966
+ if (data.ok) { await refreshChannels(); }
967
+ else { showToast('Error: ' + (data.error || 'Unknown'), 'err'); }
968
+ } catch(err) { showToast('Error: ' + err.message, 'err'); }
969
+ }
970
+
971
+ // ──────────────────────────────────────────────────────────────────────
972
+ // ── ACTIVITY ──────────────────────────────────────────────────────────
973
+ // ──────────────────────────────────────────────────────────────────────
974
+ let activityAgents = [];
975
+ let allEntries = [];
976
+ let searchTimeout = null;
977
+
978
+ async function initActivity() {
979
+ activityLoaded = true;
980
+ try {
981
+ const res = await fetch('/api/dashboard');
982
+ const data = await res.json();
983
+ activityAgents = data.agents || [];
984
+ const select = document.getElementById('agentFilter');
985
+ const sorted = activityAgents.slice().sort((a, b) => a.name.localeCompare(b.name));
986
+ for (const a of sorted) {
987
+ const opt = document.createElement('option');
988
+ opt.value = a.id;
989
+ opt.textContent = `${a.name} (${a.aliases?.[0] || a.id})`;
990
+ select.appendChild(opt);
991
+ }
992
+ } catch { /* ignore */ }
993
+ fetchActivity();
994
+ }
995
+
996
+ async function fetchActivity() {
997
+ const agentId = document.getElementById('agentFilter').value;
998
+ const search = document.getElementById('searchInput').value.trim();
999
+ const params = new URLSearchParams();
1000
+ if (agentId) params.set('agent', agentId);
1001
+ if (search) params.set('q', search);
1002
+ params.set('limit', '300');
1003
+ try {
1004
+ const res = await fetch('/api/activity?' + params.toString());
1005
+ const data = await res.json();
1006
+ allEntries = data.entries || [];
1007
+ renderActivity();
1008
+ } catch {
1009
+ document.getElementById('activityList').innerHTML = '<div class="no-activity">Failed to load activity</div>';
1010
+ }
1011
+ }
1012
+
1013
+ function renderActivity() {
1014
+ const channelFilter = document.getElementById('channelFilter').value;
1015
+ let entries = allEntries;
1016
+ if (channelFilter) entries = entries.filter(e => e.channel === channelFilter);
1017
+ document.getElementById('resultCount').textContent = `${entries.length} entries`;
1018
+ if (entries.length === 0) {
1019
+ document.getElementById('activityList').innerHTML = '<div class="no-activity">No activity found</div>';
1020
+ return;
1021
+ }
1022
+ const html = entries.map((e) => {
1023
+ const agent = activityAgents.find(a => a.id === e.agentId);
1024
+ const initials = (agent?.aliases?.[0] || e.agentId || '??').replace('@','').slice(0,2).toUpperCase();
1025
+ const name = agent?.name || e.agentId;
1026
+ const time = formatTime(e.ts);
1027
+ const date = formatDate(e.ts);
1028
+ const channel = e.channel || '';
1029
+ const query = escHtml((e.text || '').slice(0, 200));
1030
+ const response = escHtml((e.response || '').slice(0, 800));
1031
+ const agentLink = e.agentId ? `/ui#${e.agentId}${e.ts ? '?ts=' + encodeURIComponent(e.ts) : ''}` : '#';
1032
+ return `<div class="activity-entry" onclick="this.classList.toggle('expanded')">
1033
+ <div class="activity-top">
1034
+ <div class="activity-avatar">${initials}</div>
1035
+ <a class="activity-agent" href="${agentLink}" onclick="event.stopPropagation()" style="text-decoration:none">${escHtml(name)}</a>
1036
+ ${channel ? `<span class="activity-channel">${channel}</span>` : ''}
1037
+ ${e.sessionId ? `<span class="activity-channel" style="background:var(--green-bg);color:var(--green)">session</span>` : ''}
1038
+ <span class="activity-time">${date} ${time}</span>
1039
+ <a href="${agentLink}" onclick="event.stopPropagation()" style="font-size:10px;color:var(--text-muted);text-decoration:none;padding:2px 8px;border-radius:4px;border:1px solid var(--border-dim)" onmouseover="this.style.borderColor='var(--border-glow)';this.style.color='var(--accent)'" onmouseout="this.style.borderColor='var(--border-dim)';this.style.color='var(--text-muted)'">Chat &#x2192;</a>
1040
+ </div>
1041
+ <div class="activity-query">${query}</div>
1042
+ <div class="activity-response">${response}</div>
1043
+ <span class="activity-expand">Click to expand response</span>
1044
+ </div>`;
1045
+ }).join('');
1046
+ document.getElementById('activityList').innerHTML = html;
1047
+ }
1048
+
1049
+ function debounceSearch() {
1050
+ clearTimeout(searchTimeout);
1051
+ searchTimeout = setTimeout(fetchActivity, 400);
1052
+ }
1053
+
1054
+ function formatTime(ts) {
1055
+ if (!ts) return '';
1056
+ try { return new Date(ts).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}); } catch { return ''; }
1057
+ }
1058
+
1059
+ function formatDate(ts) {
1060
+ if (!ts) return '';
1061
+ try {
1062
+ const d = new Date(ts);
1063
+ const now = new Date();
1064
+ if (d.toDateString() === now.toDateString()) return 'Today';
1065
+ const yesterday = new Date(now);
1066
+ yesterday.setDate(yesterday.getDate() - 1);
1067
+ if (d.toDateString() === yesterday.toDateString()) return 'Yesterday';
1068
+ return d.toLocaleDateString([], {month:'short',day:'numeric'});
1069
+ } catch { return ''; }
1070
+ }
1071
+
1072
+ // ──────────────────────────────────────────────────────────────────────
1073
+ // ── SETTINGS ──────────────────────────────────────────────────────────
1074
+ // ──────────────────────────────────────────────────────────────────────
1075
+ let accounts = {};
1076
+
1077
+ async function loadAccounts() {
1078
+ const el = document.getElementById('accountsList');
1079
+ try {
1080
+ const res = await fetch('/api/config/accounts');
1081
+ accounts = await res.json();
1082
+ renderAccounts();
1083
+ for (const name of Object.keys(accounts)) checkStatus(name);
1084
+ } catch(e) {
1085
+ if (el) el.innerHTML = `<div class="empty" style="color:var(--red)">Failed to load accounts: ${escHtml(e.message)}</div>`;
1086
+ }
1087
+ }
1088
+
1089
+ function renderAccounts() {
1090
+ const el = document.getElementById('accountsList');
1091
+ const keys = Object.keys(accounts);
1092
+ if (!keys.length) { el.innerHTML = '<div class="empty">No accounts yet. Add one below.</div>'; return; }
1093
+ el.innerHTML = keys.map(name => `
1094
+ <div class="account-row" id="acc-${escId(name)}">
1095
+ <span class="status-dot unknown" id="dot-${escId(name)}" title="Checking..."></span>
1096
+ <span class="account-name">${escHtml(name)}</span>
1097
+ <span id="email-${escId(name)}" style="font-size:12px;color:var(--text-muted);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
1098
+ <span class="account-path" title="${escHtml(accounts[name])}">${escHtml(accounts[name])}</span>
1099
+ <button class="btn-icon relogin" onclick="reloginAccount('${escHtml(name)}')" title="Re-authorize">&#x21BA;</button>
1100
+ <button class="btn-icon" onclick="deleteAccount('${escHtml(name)}')" title="Remove">&#x2715;</button>
1101
+ </div>
1102
+ `).join('');
1103
+ }
1104
+
1105
+ async function checkStatus(name) {
1106
+ const dot = document.getElementById(`dot-${escId(name)}`);
1107
+ if (!dot) return;
1108
+ try {
1109
+ const res = await fetch(`/api/config/accounts/${encodeURIComponent(name)}/status`);
1110
+ const d = await res.json();
1111
+ dot.className = `status-dot ${d.loggedIn ? 'ok' : 'err'}`;
1112
+ dot.title = d.loggedIn ? 'Logged in' : 'Not logged in';
1113
+ const emailEl = document.getElementById(`email-${escId(name)}`);
1114
+ if (emailEl && d.email) emailEl.textContent = d.email;
1115
+ } catch { dot.className = 'status-dot unknown'; }
1116
+ }
1117
+
1118
+ async function deleteAccount(name) {
1119
+ if (!confirm(`Remove account "${name}"?`)) return;
1120
+ const res = await fetch(`/api/config/accounts/${encodeURIComponent(name)}`, { method: 'DELETE' });
1121
+ if (!res.ok) { const e = await res.json(); showToast(e.error, 'err'); return; }
1122
+ delete accounts[name];
1123
+ renderAccounts();
1124
+ showToast(`Account "${name}" removed`);
1125
+ }
1126
+
1127
+ let pendingLogin = null;
1128
+
1129
+ async function startLogin() {
1130
+ const name = document.getElementById('newAccName').value.trim();
1131
+ if (!name) { showToast('Account name required', 'err'); return; }
1132
+ const path = (name === 'main' || name === 'default') ? '~/.claude' : `~/.claude-${name}`;
1133
+ const btn = document.getElementById('startLoginBtn');
1134
+ btn.disabled = true; btn.textContent = 'Connecting…';
1135
+ const panel = document.getElementById('loginPanel');
1136
+ const body = document.getElementById('loginPanelBody');
1137
+ panel.classList.remove('visible');
1138
+ body.innerHTML = `<div style="color:var(--text-muted);font-size:12px">Starting login process…</div>`;
1139
+ panel.classList.add('visible');
1140
+ try {
1141
+ const res = await fetch('/api/config/accounts/login', {
1142
+ method: 'POST', headers: {'Content-Type':'application/json'},
1143
+ body: JSON.stringify({ name, path })
1144
+ });
1145
+ const data = await res.json();
1146
+ if (data.alreadyLoggedIn) {
1147
+ await saveAccount(name, path);
1148
+ panel.classList.remove('visible');
1149
+ showToast(`Account "${name}" added (already logged in)`);
1150
+ clearAddForm();
1151
+ } else if (data.url) {
1152
+ pendingLogin = { name, path, sessionId: data.sessionId };
1153
+ showLoginPanel(data.url, data.sessionId);
1154
+ showToast('Authorization URL ready', 'info');
1155
+ } else {
1156
+ body.innerHTML = `<div style="color:var(--red);font-size:12px">${escHtml(data.error || 'Could not start login')}</div>`;
1157
+ }
1158
+ } catch(e) {
1159
+ body.innerHTML = `<div style="color:var(--red);font-size:12px">${escHtml(e.message)}</div>`;
1160
+ } finally {
1161
+ btn.disabled = false; btn.textContent = '→ Login & Add';
1162
+ }
1163
+ }
1164
+
1165
+ function showLoginPanel(url) {
1166
+ const body = document.getElementById('loginPanelBody');
1167
+ body.innerHTML = `
1168
+ <div class="login-panel-title">Step 1 — Open this URL in your browser</div>
1169
+ <div class="login-url-row">
1170
+ <span class="login-url" id="loginUrlText">${escHtml(url)}</span>
1171
+ <button class="btn-copy" onclick="copyUrl('${escHtml(url)}')" title="Copy URL">&#x2398; Copy</button>
1172
+ <a class="btn-open" href="${escHtml(url)}" target="_blank">Open &#x2197;</a>
1173
+ </div>
1174
+ <div class="login-panel-title" style="margin-top:14px">Step 2 — Paste the code Anthropic gives you</div>
1175
+ <div class="code-input-row">
1176
+ <input id="authCodeInput" placeholder="Paste code here…" autocomplete="off" spellcheck="false" />
1177
+ <button class="btn-verify" id="submitCodeBtn" onclick="submitCode()">Submit Code</button>
1178
+ </div>
1179
+ <div id="codeStatus" class="login-status-msg"></div>
1180
+ <div style="margin-top:12px;font-size:11px;color:var(--text-muted)">
1181
+ If the browser flow completes automatically (no code shown), just click
1182
+ <button class="btn-verify" style="padding:4px 12px;font-size:11px" onclick="verifyLogin()">Verify &amp; Save</button>
1183
+ </div>
1184
+ `;
1185
+ }
1186
+
1187
+ async function copyUrl(url) {
1188
+ try {
1189
+ await navigator.clipboard.writeText(url);
1190
+ showToast('URL copied to clipboard', 'info');
1191
+ } catch {
1192
+ const el = document.getElementById('loginUrlText');
1193
+ if (el) { const r = document.createRange(); r.selectNode(el); window.getSelection().removeAllRanges(); window.getSelection().addRange(r); }
1194
+ showToast('Select + copy the URL above', 'info');
1195
+ }
1196
+ }
1197
+
1198
+ async function submitCode() {
1199
+ if (!pendingLogin?.sessionId) return;
1200
+ const code = document.getElementById('authCodeInput')?.value.trim();
1201
+ if (!code) { showToast('Paste the code first', 'err'); return; }
1202
+ const btn = document.getElementById('submitCodeBtn');
1203
+ if (btn) btn.disabled = true;
1204
+ document.getElementById('codeStatus').textContent = 'Submitting code…';
1205
+ try {
1206
+ const res = await fetch('/api/config/accounts/login/code', {
1207
+ method: 'POST', headers: {'Content-Type':'application/json'},
1208
+ body: JSON.stringify({ sessionId: pendingLogin.sessionId, code })
1209
+ });
1210
+ const d = await res.json();
1211
+ if (!res.ok) throw new Error(d.error);
1212
+ document.getElementById('codeStatus').textContent = 'Code submitted — verifying…';
1213
+ setTimeout(() => verifyLogin(), 2000);
1214
+ } catch(e) {
1215
+ document.getElementById('codeStatus').textContent = `Error: ${e.message}`;
1216
+ if (btn) btn.disabled = false;
1217
+ }
1218
+ }
1219
+
1220
+ async function verifyLogin() {
1221
+ if (!pendingLogin) return;
1222
+ const { name, path } = pendingLogin;
1223
+ try {
1224
+ await saveAccount(name, path);
1225
+ const res = await fetch(`/api/config/accounts/${encodeURIComponent(name)}/status`);
1226
+ const d = await res.json();
1227
+ if (d.loggedIn) {
1228
+ showToast(`Account "${name}" added and verified`);
1229
+ pendingLogin = null;
1230
+ document.getElementById('loginPanel').classList.remove('visible');
1231
+ clearAddForm();
1232
+ } else {
1233
+ const status = document.getElementById('codeStatus');
1234
+ if (status) status.textContent = 'Not authorized yet — finish the browser flow then try again.';
1235
+ showToast('Not authorized yet', 'info');
1236
+ }
1237
+ } catch(e) { showToast(e.message, 'err'); }
1238
+ }
1239
+
1240
+ function clearAddForm() { document.getElementById('newAccName').value = ''; }
1241
+
1242
+ async function reloginAccount(name) {
1243
+ const path = accounts[name];
1244
+ if (!path) return;
1245
+ const btn = event.target;
1246
+ btn.disabled = true;
1247
+ try {
1248
+ const res = await fetch('/api/config/accounts/login', {
1249
+ method: 'POST', headers: {'Content-Type':'application/json'},
1250
+ body: JSON.stringify({ name, path })
1251
+ });
1252
+ const data = await res.json();
1253
+ if (data.alreadyLoggedIn) {
1254
+ showToast(`"${name}" is already logged in`);
1255
+ checkStatus(name);
1256
+ } else if (data.url) {
1257
+ const w = window.open(data.url, '_blank');
1258
+ if (!w || w.closed) {
1259
+ showToast(`<a href="${data.url}" target="_blank" style="color:var(--accent);text-decoration:underline">Click here to authorize</a> (popup was blocked)`, 'info', true);
1260
+ } else {
1261
+ showToast('Opened auth URL — authorize then refresh status', 'info');
1262
+ }
1263
+ setTimeout(() => checkStatus(name), 8000);
1264
+ } else { showToast(data.error || 'Could not get login URL', 'err'); }
1265
+ } finally { btn.disabled = false; }
1266
+ }
1267
+
1268
+ async function saveAccount(name, path) {
1269
+ const res = await fetch('/api/config/accounts', {
1270
+ method: 'POST', headers: {'Content-Type':'application/json'},
1271
+ body: JSON.stringify({ name, path })
1272
+ });
1273
+ if (!res.ok) { const e = await res.json(); throw new Error(e.error); }
1274
+ accounts[name] = path;
1275
+ renderAccounts();
1276
+ checkStatus(name);
1277
+ }
1278
+
1279
+ async function loadProfile() {
1280
+ try {
1281
+ const res = await fetch('/api/profile');
1282
+ const p = await res.json();
1283
+ document.getElementById('profileName').value = p.name || '';
1284
+ document.getElementById('profileRole').value = p.role || '';
1285
+ document.getElementById('profileIndustry').value = p.industry || '';
1286
+ document.getElementById('profileAiExperience').value = p.aiExperience || '';
1287
+ document.getElementById('profileInterests').value = (p.interests || []).join(', ');
1288
+ } catch {}
1289
+ }
1290
+
1291
+ async function saveProfile() {
1292
+ const name = document.getElementById('profileName').value.trim();
1293
+ const role = document.getElementById('profileRole').value.trim();
1294
+ const industry = document.getElementById('profileIndustry').value.trim();
1295
+ const aiExperience = document.getElementById('profileAiExperience').value;
1296
+ const interestsRaw = document.getElementById('profileInterests').value.trim();
1297
+ const interests = interestsRaw ? interestsRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
1298
+ const res = await fetch('/api/profile', {
1299
+ method: 'PUT', headers: {'Content-Type':'application/json'},
1300
+ body: JSON.stringify({ name, role, industry, aiExperience: aiExperience || undefined, interests })
1301
+ });
1302
+ const data = await res.json();
1303
+ if (!res.ok) { showToast(data.error, 'err'); return; }
1304
+ showToast('Profile saved');
1305
+ }
1306
+
1307
+ async function loadService() {
1308
+ const res = await fetch('/api/config/service');
1309
+ const s = await res.json();
1310
+ document.getElementById('svcAgentsDir').value = s.personalAgentsDir || '';
1311
+ document.getElementById('svcRegistryDir').value = s.personalRegistryDir || '';
1312
+ document.getElementById('svcPort').value = s.webUIPort || '';
1313
+ document.getElementById('svcLogLevel').value = s.logLevel || '';
1314
+ // Populate default account dropdown
1315
+ const acctSelect = document.getElementById('svcDefaultAccount');
1316
+ acctSelect.innerHTML = '<option value="">System default (~/.claude)</option>';
1317
+ try {
1318
+ const acctRes = await fetch('/api/config/accounts');
1319
+ const accountsMap = await acctRes.json();
1320
+ for (const name of Object.keys(accountsMap)) {
1321
+ const opt = document.createElement('option');
1322
+ opt.value = name;
1323
+ opt.textContent = name;
1324
+ if (name === s.defaultClaudeAccount) opt.selected = true;
1325
+ acctSelect.appendChild(opt);
1326
+ }
1327
+ } catch (e) { /* accounts endpoint may not exist yet */ }
1328
+ const d = s.deployment || {};
1329
+ document.getElementById('depProvider').value = d.provider || '';
1330
+ document.getElementById('depToken').value = d.deployToken || '';
1331
+ document.getElementById('depGithubOrg').value = d.githubOrg || '';
1332
+ document.getElementById('depGithubToken').value = d.githubToken || '';
1333
+ // Feature flags
1334
+ document.getElementById('svcGymEnabled').checked = !!s.gymEnabled;
1335
+ document.getElementById('svcGymOnlyMode').checked = !!s.gymOnlyMode;
1336
+ document.getElementById('svcAibriefingEnabled').checked = !!s.aibriefingEnabled;
1337
+ updateGymSubFlags();
1338
+ // Multi-model
1339
+ document.getElementById('svcMultiModelEnabled').checked = !!s.multiModelEnabled;
1340
+ await loadPlatformDefaultExecutorOptions(s.platformDefaultExecutor || 'claude', s);
1341
+ document.getElementById('svcOllamaBaseUrl').value = s.ollamaBaseUrl || '';
1342
+ // Provider keys (masked values from API)
1343
+ const pk = s.providerKeys || {};
1344
+ document.getElementById('keyOpenai').value = pk.openai || '';
1345
+ document.getElementById('keyXai').value = pk.xai || '';
1346
+ document.getElementById('keyGoogle').value = pk.google || '';
1347
+ document.getElementById('keyGroq').value = pk.groq || '';
1348
+ document.getElementById('keyTogether').value = pk.together || '';
1349
+ document.getElementById('keyMistral').value = pk.mistral || '';
1350
+ updateMultiModelFieldsVisibility();
1351
+ }
1352
+
1353
+ async function saveService() {
1354
+ const personalAgentsDir = document.getElementById('svcAgentsDir').value.trim();
1355
+ const personalRegistryDir = document.getElementById('svcRegistryDir').value.trim();
1356
+ const webUIPort = document.getElementById('svcPort').value.trim();
1357
+ const logLevel = document.getElementById('svcLogLevel').value.trim();
1358
+ const defaultClaudeAccount = document.getElementById('svcDefaultAccount').value;
1359
+ const res = await fetch('/api/config/service', {
1360
+ method: 'PUT', headers: {'Content-Type':'application/json'},
1361
+ body: JSON.stringify({ personalAgentsDir, personalRegistryDir, webUIPort: webUIPort ? Number(webUIPort) : undefined, logLevel, defaultClaudeAccount })
1362
+ });
1363
+ const data = await res.json();
1364
+ if (!res.ok) { showToast(data.error, 'err'); return; }
1365
+ showToast(data.note || 'Saved');
1366
+ }
1367
+
1368
+ function updateMultiModelFieldsVisibility() {
1369
+ const on = document.getElementById('svcMultiModelEnabled').checked;
1370
+ document.getElementById('multiModelFields').style.opacity = on ? '1' : '0.4';
1371
+ document.getElementById('multiModelFields').style.pointerEvents = on ? 'auto' : 'none';
1372
+ document.getElementById('ollamaSetupGuide').style.display = on ? 'block' : 'none';
1373
+ const pkf = document.getElementById('providerKeyFields');
1374
+ if (pkf) { pkf.style.opacity = on ? '1' : '0.4'; pkf.style.pointerEvents = on ? 'auto' : 'none'; }
1375
+ }
1376
+ document.addEventListener('DOMContentLoaded', () => {
1377
+ const cb = document.getElementById('svcMultiModelEnabled');
1378
+ if (cb) cb.addEventListener('change', updateMultiModelFieldsVisibility);
1379
+ });
1380
+
1381
+ async function checkOllamaConnection() {
1382
+ const statusEl = document.getElementById('ollamaHealthStatus');
1383
+ statusEl.style.display = 'block';
1384
+ statusEl.style.background = 'rgba(255,255,255,0.04)';
1385
+ statusEl.style.color = 'var(--text-muted)';
1386
+ statusEl.textContent = 'Checking connection...';
1387
+ const baseUrl = document.getElementById('svcOllamaBaseUrl').value.trim() || 'http://localhost:11434';
1388
+ try {
1389
+ const res = await fetch(baseUrl + '/api/tags');
1390
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1391
+ const data = await res.json();
1392
+ const models = (data.models || []).map(m => m.name);
1393
+ if (models.length === 0) {
1394
+ statusEl.style.background = 'rgba(255,191,0,0.1)';
1395
+ statusEl.style.color = 'var(--amber)';
1396
+ statusEl.innerHTML = '&#9888; Connected to Ollama but no models found. Run <code style="background:var(--bg-secondary);padding:1px 6px;border-radius:3px">ollama pull gemma2</code> to get started.';
1397
+ } else {
1398
+ statusEl.style.background = 'rgba(0,200,100,0.08)';
1399
+ statusEl.style.color = 'var(--green)';
1400
+ statusEl.innerHTML = '&#10003; Connected — available models: <strong>' + models.join(', ') + '</strong>';
1401
+ }
1402
+ } catch (e) {
1403
+ statusEl.style.background = 'rgba(255,80,80,0.08)';
1404
+ statusEl.style.color = 'var(--red, #f55)';
1405
+ statusEl.innerHTML = '&#10007; Cannot reach Ollama at <strong>' + baseUrl + '</strong>. Is it running?';
1406
+ }
1407
+ }
1408
+
1409
+ function updateGymSubFlags() {
1410
+ const on = document.getElementById('svcGymEnabled').checked;
1411
+ const sub = document.getElementById('gymSubFlags');
1412
+ if (sub) sub.style.display = on ? 'block' : 'none';
1413
+ }
1414
+
1415
+ async function reconfigureGym() {
1416
+ if (!confirm('This will reset your gym onboarding so you can re-pick your trainer and update your profile. Continue?')) return;
1417
+ try {
1418
+ await fetch('/api/gym/learner-profile', {
1419
+ method: 'PUT', headers: {'Content-Type':'application/json'},
1420
+ body: JSON.stringify({ onboardingStep: 0, onboardingComplete: false })
1421
+ });
1422
+ showToast('Gym onboarding reset — redirecting...');
1423
+ setTimeout(() => { window.location.href = '/gym'; }, 800);
1424
+ } catch (e) {
1425
+ showToast('Failed to reset gym: ' + e.message);
1426
+ }
1427
+ }
1428
+
1429
+ async function saveGymSettings() {
1430
+ const gymEnabled = document.getElementById('svcGymEnabled').checked;
1431
+ const gymOnlyMode = document.getElementById('svcGymOnlyMode').checked;
1432
+ const aibriefingEnabled = document.getElementById('svcAibriefingEnabled').checked;
1433
+ updateGymSubFlags();
1434
+ const res = await fetch('/api/config/service', {
1435
+ method: 'PUT', headers: {'Content-Type':'application/json'},
1436
+ body: JSON.stringify({ gymEnabled, gymOnlyMode: gymEnabled ? gymOnlyMode : false, aibriefingEnabled })
1437
+ });
1438
+ const data = await res.json();
1439
+ if (data.ok) {
1440
+ document.querySelectorAll('.gym-tab-link').forEach(el => { el.style.display = gymEnabled ? '' : 'none'; });
1441
+ showToast(gymEnabled ? 'AI Gym enabled' : 'AI Gym disabled');
1442
+ }
1443
+ }
1444
+
1445
+ async function loadPlatformDefaultExecutorOptions(currentValue, svc) {
1446
+ const sel = document.getElementById('svcPlatformDefaultExecutor');
1447
+ if (!sel) return;
1448
+ sel.innerHTML = '<option value="claude">Claude (default)</option>';
1449
+ if (svc && svc.multiModelEnabled) {
1450
+ const pk = svc.providerKeys || {};
1451
+ const cloudModels = [
1452
+ { key: 'openai', prefix: 'openai', label: 'OpenAI', models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o3-mini'] },
1453
+ { key: 'xai', prefix: 'grok', label: 'Grok', models: ['grok-3', 'grok-3-mini', 'grok-2'] },
1454
+ { key: 'google',prefix: 'gemini', label: 'Gemini', models: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'] },
1455
+ { key: 'groq', prefix: 'groq', label: 'Groq', models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'] },
1456
+ { key: 'together', prefix: 'together', label: 'Together', models: ['meta-llama/Llama-3.3-70B-Instruct', 'mistralai/Mixtral-8x22B-Instruct-v0.1'] },
1457
+ { key: 'mistral', prefix: 'mistral', label: 'Mistral', models: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'] },
1458
+ ];
1459
+ for (const provider of cloudModels) {
1460
+ if (pk[provider.key]) {
1461
+ const group = document.createElement('optgroup');
1462
+ group.label = provider.label;
1463
+ for (const model of provider.models) {
1464
+ const opt = document.createElement('option');
1465
+ opt.value = provider.prefix + ':' + model;
1466
+ opt.textContent = model;
1467
+ group.appendChild(opt);
1468
+ }
1469
+ sel.appendChild(group);
1470
+ }
1471
+ }
1472
+ try {
1473
+ const ollamaUrl = (svc.ollamaBaseUrl || 'http://localhost:11434') + '/api/tags';
1474
+ const mr = await fetch('/api/ollama-proxy?url=' + encodeURIComponent(ollamaUrl));
1475
+ if (mr.ok) {
1476
+ const data = await mr.json();
1477
+ const models = data.models || [];
1478
+ if (models.length > 0) {
1479
+ const group = document.createElement('optgroup');
1480
+ group.label = 'Ollama (local)';
1481
+ models.forEach(m => {
1482
+ const opt = document.createElement('option');
1483
+ opt.value = 'ollama:' + m.name;
1484
+ opt.textContent = m.name;
1485
+ group.appendChild(opt);
1486
+ });
1487
+ sel.appendChild(group);
1488
+ }
1489
+ }
1490
+ } catch(e) { /* Ollama not reachable */ }
1491
+ }
1492
+ if (currentValue && currentValue !== 'claude' && !sel.querySelector(`option[value="${CSS.escape(currentValue)}"]`)) {
1493
+ const opt = document.createElement('option');
1494
+ opt.value = currentValue;
1495
+ opt.textContent = currentValue;
1496
+ sel.appendChild(opt);
1497
+ }
1498
+ sel.value = currentValue || 'claude';
1499
+ }
1500
+
1501
+ async function saveMultiModel() {
1502
+ const multiModelEnabled = document.getElementById('svcMultiModelEnabled').checked;
1503
+ const platformDefaultExecutor = document.getElementById('svcPlatformDefaultExecutor').value || 'claude';
1504
+ const ollamaBaseUrl = document.getElementById('svcOllamaBaseUrl').value.trim() || 'http://localhost:11434';
1505
+ // Collect provider keys — only send non-masked values
1506
+ const providerKeys = {};
1507
+ const keyMap = { keyOpenai: 'openai', keyXai: 'xai', keyGoogle: 'google', keyGroq: 'groq', keyTogether: 'together', keyMistral: 'mistral' };
1508
+ for (const [elId, field] of Object.entries(keyMap)) {
1509
+ const val = document.getElementById(elId).value.trim();
1510
+ if (val && val !== '••••••••') providerKeys[field] = val;
1511
+ }
1512
+ const res = await fetch('/api/config/service', {
1513
+ method: 'PUT', headers: {'Content-Type':'application/json'},
1514
+ body: JSON.stringify({ multiModelEnabled, platformDefaultExecutor, ollamaBaseUrl, providerKeys })
1515
+ });
1516
+ const data = await res.json();
1517
+ if (!res.ok) { showToast(data.error, 'err'); return; }
1518
+ showToast('AI Models settings saved');
1519
+ }
1520
+
1521
+ async function testProvider(provider) {
1522
+ const btn = event.target;
1523
+ const origText = btn.textContent;
1524
+ btn.disabled = true;
1525
+ btn.textContent = 'Testing...';
1526
+ try {
1527
+ const res = await fetch('/api/config/provider-test', {
1528
+ method: 'POST', headers: {'Content-Type':'application/json'},
1529
+ body: JSON.stringify({ provider })
1530
+ });
1531
+ const data = await res.json();
1532
+ if (data.ok) {
1533
+ showToast(`${provider}: Connected! ${data.models ? data.models.length + ' models available' : ''}`, 'ok');
1534
+ } else {
1535
+ showToast(`${provider}: ${data.error}`, 'err');
1536
+ }
1537
+ } catch (e) {
1538
+ showToast(`${provider}: Connection failed`, 'err');
1539
+ } finally {
1540
+ btn.disabled = false;
1541
+ btn.textContent = origText;
1542
+ }
1543
+ }
1544
+
1545
+ async function saveDeployment() {
1546
+ const deployment = {
1547
+ provider: document.getElementById('depProvider').value.trim(),
1548
+ deployToken: document.getElementById('depToken').value,
1549
+ githubOrg: document.getElementById('depGithubOrg').value.trim(),
1550
+ githubToken: document.getElementById('depGithubToken').value,
1551
+ };
1552
+ const res = await fetch('/api/config/service', {
1553
+ method: 'PUT', headers: {'Content-Type':'application/json'},
1554
+ body: JSON.stringify({ deployment })
1555
+ });
1556
+ const data = await res.json();
1557
+ if (!res.ok) { showToast(data.error, 'err'); return; }
1558
+ showToast('Deployment settings saved');
1559
+ }
1560
+
1561
+ async function loadSaas() {
1562
+ try {
1563
+ const d = await (await fetch('/api/saas/config')).json();
1564
+ document.getElementById('saasBaseUrl').value = d.baseUrl || '';
1565
+ document.getElementById('saasKey').value = d.hasKey ? '••••••••' : '';
1566
+ if (d.connected) document.getElementById('saas-status-dot').style.display = 'block';
1567
+ } catch { /* ignore */ }
1568
+ }
1569
+
1570
+ async function saveSaas() {
1571
+ const baseUrl = document.getElementById('saasBaseUrl').value.trim();
1572
+ const apiKey = document.getElementById('saasKey').value;
1573
+ const res = await fetch('/api/saas/config', {
1574
+ method: 'PUT', headers: {'Content-Type':'application/json'},
1575
+ body: JSON.stringify({ baseUrl, apiKey })
1576
+ });
1577
+ const data = await res.json();
1578
+ if (!res.ok) { showToast(data.error, 'err'); return; }
1579
+ showToast('SaaS settings saved');
1580
+ await loadSaas();
1581
+ }
1582
+
1583
+ async function testSaas(btn) {
1584
+ const orig = btn.textContent;
1585
+ btn.textContent = 'Testing…'; btn.disabled = true;
1586
+ // Use current form values so you can test before saving
1587
+ const baseUrl = document.getElementById('saasBaseUrl').value.trim();
1588
+ const apiKey = document.getElementById('saasKey').value;
1589
+ try {
1590
+ const res = await fetch('/api/saas/test', {
1591
+ method: 'POST', headers: {'Content-Type':'application/json'},
1592
+ body: JSON.stringify({ baseUrl, apiKey })
1593
+ });
1594
+ const data = await res.json();
1595
+ if (!res.ok) { showToast(data.error || 'Connection failed', 'err'); }
1596
+ else { showToast('Connected to SaaS!', 'ok'); document.getElementById('saas-status-dot').style.display = 'block'; }
1597
+ } catch { showToast('Could not reach SaaS', 'err'); }
1598
+ btn.textContent = orig; btn.disabled = false;
1599
+ }
1600
+
1601
+ function toggleReveal(inputId, labelId) {
1602
+ const inp = document.getElementById(inputId);
1603
+ const lbl = document.getElementById(labelId);
1604
+ if (inp.type === 'password') { inp.type = 'text'; lbl.textContent = 'hide'; }
1605
+ else { inp.type = 'password'; lbl.textContent = 'show'; }
1606
+ }
1607
+
1608
+ // Status indicator install
1609
+ (function(){
1610
+ const area = document.getElementById('indicatorInstallArea');
1611
+ if (!area) return;
1612
+ const isMac = navigator.platform.toUpperCase().includes('MAC');
1613
+ const isWin = navigator.platform.toUpperCase().includes('WIN');
1614
+ if (isMac) {
1615
+ area.innerHTML = `
1616
+ <div style="font-size:13px;line-height:1.6">
1617
+ <strong>macOS — xbar menu bar plugin</strong>
1618
+ <ol style="margin:8px 0;padding-left:20px;color:var(--text-muted)">
1619
+ <li>Install xbar: <code style="background:var(--bg-card);padding:2px 6px;border-radius:4px">brew install --cask xbar</code></li>
1620
+ <li>Click the button below to copy the plugin into place</li>
1621
+ <li>Open xbar — a green dot with agent count appears in your menu bar</li>
1622
+ </ol>
1623
+ <button class="btn-save" onclick="installXbar()" id="xbarInstallBtn" style="margin-top:4px">Install xbar Plugin</button>
1624
+ <span id="xbarStatus" style="margin-left:12px;font-size:12px;color:var(--text-muted)"></span>
1625
+ </div>`;
1626
+ } else if (isWin) {
1627
+ area.innerHTML = `
1628
+ <div style="font-size:13px;line-height:1.6">
1629
+ <strong>Windows — System Tray indicator</strong>
1630
+ <ol style="margin:8px 0;padding-left:20px;color:var(--text-muted)">
1631
+ <li>The tray indicator script is at <code style="background:var(--bg-card);padding:2px 6px;border-radius:4px">scripts/tray-indicator.ps1</code></li>
1632
+ <li>Run it: <code style="background:var(--bg-card);padding:2px 6px;border-radius:4px">powershell -WindowStyle Hidden -File scripts\\tray-indicator.ps1</code></li>
1633
+ <li>A tray icon appears — right-click for Web UI, Restart, and Exit</li>
1634
+ </ol>
1635
+ <p style="color:var(--text-muted);font-size:11px;margin-top:8px">To auto-start: add the script to your Windows Startup folder or Task Scheduler.</p>
1636
+ </div>`;
1637
+ } else {
1638
+ area.innerHTML = '<p style="font-size:12px;color:var(--text-muted)">Status indicator is available for macOS (xbar) and Windows (system tray).</p>';
1639
+ }
1640
+ })();
1641
+
1642
+ async function installXbar() {
1643
+ const btn = document.getElementById('xbarInstallBtn');
1644
+ const status = document.getElementById('xbarStatus');
1645
+ btn.disabled = true; btn.textContent = 'Installing...';
1646
+ try {
1647
+ const res = await fetch('/api/install-xbar', { method: 'POST' });
1648
+ const data = await res.json();
1649
+ if (data.ok) {
1650
+ status.textContent = 'Installed! Open xbar to see the status dot.';
1651
+ status.style.color = '#22c55e'; btn.textContent = 'Installed';
1652
+ } else {
1653
+ status.textContent = data.error || 'Install failed';
1654
+ status.style.color = '#f87171'; btn.textContent = 'Install xbar Plugin'; btn.disabled = false;
1655
+ }
1656
+ } catch(e) {
1657
+ status.textContent = e.message; status.style.color = '#f87171';
1658
+ btn.textContent = 'Install xbar Plugin'; btn.disabled = false;
1659
+ }
1660
+ }
1661
+
1662
+ // ── Utils ──────────────────────────────────────────────────────────────
1663
+ function escHtml(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') }
1664
+ function escId(s){ return String(s||'').replace(/[^a-z0-9]/gi,'_') }
1665
+
1666
+ // ── Close docs dropdown on outside click ──────────────────────────────
1667
+ document.addEventListener('click', function(e){
1668
+ const dd = document.getElementById('docsDropdown');
1669
+ if (dd && !dd.contains(e.target)) document.getElementById('docsMenu')?.classList.remove('open');
1670
+ });
1671
+ </script>
1672
+ <script>(function(){fetch("/api/config/service").then(function(r){return r.json()}).then(function(d){if(d.gymEnabled)document.querySelectorAll(".gym-tab-link").forEach(function(el){el.style.display=""});if(d.gymOnlyMode){var keep=["/gym","/org","/admin"];document.querySelectorAll(".tab-group .tab-btn").forEach(function(el){var h=el.getAttribute("href");if(h&&keep.indexOf(h)===-1&&!el.classList.contains("gym-tab-link"))el.style.display="none"});document.querySelectorAll(".topbar-right .gear-btn").forEach(function(el){var h=el.getAttribute("href");if(h&&["/marketplace","/monitor","/user-guide"].indexOf(h)!==-1)el.style.display="none"})}}).catch(function(){})})()</script>
1673
+ </body>
1674
+ </html>