@useconductor/conductor 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 (504) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +23 -0
  3. package/.eslintrc.json +23 -0
  4. package/.gitattributes +6 -0
  5. package/.github/FUNDING.yml +15 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +91 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  9. package/.github/ISSUE_TEMPLATE/plugin_request.yml +71 -0
  10. package/.github/README.md +13 -0
  11. package/.github/workflows/README.md +22 -0
  12. package/.github/workflows/auto-release.yml +112 -0
  13. package/.github/workflows/ci.yml +49 -0
  14. package/.github/workflows/claude-code-review.yml +44 -0
  15. package/.github/workflows/claude.yml +36 -0
  16. package/.github/workflows/sync-install.yml +47 -0
  17. package/.mcp.json +9 -0
  18. package/.prettierrc.json +7 -0
  19. package/C.png +0 -0
  20. package/CHANGELOG.md +74 -0
  21. package/CLAUDE.md +118 -0
  22. package/CONTRIBUTING.md +231 -0
  23. package/LICENSE +201 -0
  24. package/README.md +179 -0
  25. package/SECURITY.md +47 -0
  26. package/commands/conductor-setup.md +11 -0
  27. package/commands/conductor-status.md +7 -0
  28. package/dist/ai/base.d.ts +44 -0
  29. package/dist/ai/base.d.ts.map +1 -0
  30. package/dist/ai/base.js +47 -0
  31. package/dist/ai/base.js.map +1 -0
  32. package/dist/ai/claude.d.ts +11 -0
  33. package/dist/ai/claude.d.ts.map +1 -0
  34. package/dist/ai/claude.js +149 -0
  35. package/dist/ai/claude.js.map +1 -0
  36. package/dist/ai/gemini.d.ts +15 -0
  37. package/dist/ai/gemini.d.ts.map +1 -0
  38. package/dist/ai/gemini.js +156 -0
  39. package/dist/ai/gemini.js.map +1 -0
  40. package/dist/ai/maestro.d.ts +22 -0
  41. package/dist/ai/maestro.d.ts.map +1 -0
  42. package/dist/ai/maestro.js +142 -0
  43. package/dist/ai/maestro.js.map +1 -0
  44. package/dist/ai/manager.d.ts +47 -0
  45. package/dist/ai/manager.d.ts.map +1 -0
  46. package/dist/ai/manager.js +450 -0
  47. package/dist/ai/manager.js.map +1 -0
  48. package/dist/ai/ollama.d.ts +16 -0
  49. package/dist/ai/ollama.d.ts.map +1 -0
  50. package/dist/ai/ollama.js +151 -0
  51. package/dist/ai/ollama.js.map +1 -0
  52. package/dist/ai/openai.d.ts +11 -0
  53. package/dist/ai/openai.d.ts.map +1 -0
  54. package/dist/ai/openai.js +132 -0
  55. package/dist/ai/openai.js.map +1 -0
  56. package/dist/ai/openrouter.d.ts +11 -0
  57. package/dist/ai/openrouter.d.ts.map +1 -0
  58. package/dist/ai/openrouter.js +139 -0
  59. package/dist/ai/openrouter.js.map +1 -0
  60. package/dist/bot/slack.d.ts +17 -0
  61. package/dist/bot/slack.d.ts.map +1 -0
  62. package/dist/bot/slack.js +144 -0
  63. package/dist/bot/slack.js.map +1 -0
  64. package/dist/bot/telegram.d.ts +19 -0
  65. package/dist/bot/telegram.d.ts.map +1 -0
  66. package/dist/bot/telegram.js +157 -0
  67. package/dist/bot/telegram.js.map +1 -0
  68. package/dist/cli/commands/ai.d.ts +4 -0
  69. package/dist/cli/commands/ai.d.ts.map +1 -0
  70. package/dist/cli/commands/ai.js +161 -0
  71. package/dist/cli/commands/ai.js.map +1 -0
  72. package/dist/cli/commands/doctor.d.ts +18 -0
  73. package/dist/cli/commands/doctor.d.ts.map +1 -0
  74. package/dist/cli/commands/doctor.js +213 -0
  75. package/dist/cli/commands/doctor.js.map +1 -0
  76. package/dist/cli/commands/init.d.ts +15 -0
  77. package/dist/cli/commands/init.d.ts.map +1 -0
  78. package/dist/cli/commands/init.js +281 -0
  79. package/dist/cli/commands/init.js.map +1 -0
  80. package/dist/cli/commands/install.d.ts +16 -0
  81. package/dist/cli/commands/install.d.ts.map +1 -0
  82. package/dist/cli/commands/install.js +750 -0
  83. package/dist/cli/commands/install.js.map +1 -0
  84. package/dist/cli/commands/lifecycle.d.ts +4 -0
  85. package/dist/cli/commands/lifecycle.d.ts.map +1 -0
  86. package/dist/cli/commands/lifecycle.js +84 -0
  87. package/dist/cli/commands/lifecycle.js.map +1 -0
  88. package/dist/cli/commands/marketplace.d.ts +13 -0
  89. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  90. package/dist/cli/commands/marketplace.js +197 -0
  91. package/dist/cli/commands/marketplace.js.map +1 -0
  92. package/dist/cli/commands/mcp.d.ts +6 -0
  93. package/dist/cli/commands/mcp.d.ts.map +1 -0
  94. package/dist/cli/commands/mcp.js +83 -0
  95. package/dist/cli/commands/mcp.js.map +1 -0
  96. package/dist/cli/commands/onboard.d.ts +10 -0
  97. package/dist/cli/commands/onboard.d.ts.map +1 -0
  98. package/dist/cli/commands/onboard.js +207 -0
  99. package/dist/cli/commands/onboard.js.map +1 -0
  100. package/dist/cli/commands/plugin-create.d.ts +13 -0
  101. package/dist/cli/commands/plugin-create.d.ts.map +1 -0
  102. package/dist/cli/commands/plugin-create.js +122 -0
  103. package/dist/cli/commands/plugin-create.js.map +1 -0
  104. package/dist/cli/commands/plugins.d.ts +5 -0
  105. package/dist/cli/commands/plugins.d.ts.map +1 -0
  106. package/dist/cli/commands/plugins.js +30 -0
  107. package/dist/cli/commands/plugins.js.map +1 -0
  108. package/dist/cli/commands/release.d.ts +13 -0
  109. package/dist/cli/commands/release.d.ts.map +1 -0
  110. package/dist/cli/commands/release.js +243 -0
  111. package/dist/cli/commands/release.js.map +1 -0
  112. package/dist/cli/commands/telegram.d.ts +3 -0
  113. package/dist/cli/commands/telegram.d.ts.map +1 -0
  114. package/dist/cli/commands/telegram.js +20 -0
  115. package/dist/cli/commands/telegram.js.map +1 -0
  116. package/dist/cli/index.d.ts +3 -0
  117. package/dist/cli/index.d.ts.map +1 -0
  118. package/dist/cli/index.js +402 -0
  119. package/dist/cli/index.js.map +1 -0
  120. package/dist/config/oauth.d.ts +8 -0
  121. package/dist/config/oauth.d.ts.map +1 -0
  122. package/dist/config/oauth.js +13 -0
  123. package/dist/config/oauth.js.map +1 -0
  124. package/dist/core/audit.d.ts +91 -0
  125. package/dist/core/audit.d.ts.map +1 -0
  126. package/dist/core/audit.js +233 -0
  127. package/dist/core/audit.js.map +1 -0
  128. package/dist/core/circuit-breaker.d.ts +56 -0
  129. package/dist/core/circuit-breaker.d.ts.map +1 -0
  130. package/dist/core/circuit-breaker.js +107 -0
  131. package/dist/core/circuit-breaker.js.map +1 -0
  132. package/dist/core/conductor.d.ts +44 -0
  133. package/dist/core/conductor.d.ts.map +1 -0
  134. package/dist/core/conductor.js +200 -0
  135. package/dist/core/conductor.js.map +1 -0
  136. package/dist/core/config.d.ts +66 -0
  137. package/dist/core/config.d.ts.map +1 -0
  138. package/dist/core/config.js +86 -0
  139. package/dist/core/config.js.map +1 -0
  140. package/dist/core/database.d.ts +59 -0
  141. package/dist/core/database.d.ts.map +1 -0
  142. package/dist/core/database.js +342 -0
  143. package/dist/core/database.js.map +1 -0
  144. package/dist/core/errors.d.ts +231 -0
  145. package/dist/core/errors.d.ts.map +1 -0
  146. package/dist/core/errors.js +254 -0
  147. package/dist/core/errors.js.map +1 -0
  148. package/dist/core/health.d.ts +72 -0
  149. package/dist/core/health.d.ts.map +1 -0
  150. package/dist/core/health.js +116 -0
  151. package/dist/core/health.js.map +1 -0
  152. package/dist/core/interfaces.d.ts +62 -0
  153. package/dist/core/interfaces.d.ts.map +1 -0
  154. package/dist/core/interfaces.js +8 -0
  155. package/dist/core/interfaces.js.map +1 -0
  156. package/dist/core/logger.d.ts +15 -0
  157. package/dist/core/logger.d.ts.map +1 -0
  158. package/dist/core/logger.js +30 -0
  159. package/dist/core/logger.js.map +1 -0
  160. package/dist/core/rbac.d.ts +132 -0
  161. package/dist/core/rbac.d.ts.map +1 -0
  162. package/dist/core/rbac.js +230 -0
  163. package/dist/core/rbac.js.map +1 -0
  164. package/dist/core/retry.d.ts +22 -0
  165. package/dist/core/retry.d.ts.map +1 -0
  166. package/dist/core/retry.js +41 -0
  167. package/dist/core/retry.js.map +1 -0
  168. package/dist/core/webhooks.d.ts +92 -0
  169. package/dist/core/webhooks.d.ts.map +1 -0
  170. package/dist/core/webhooks.js +176 -0
  171. package/dist/core/webhooks.js.map +1 -0
  172. package/dist/core/zero-config.d.ts +22 -0
  173. package/dist/core/zero-config.d.ts.map +1 -0
  174. package/dist/core/zero-config.js +59 -0
  175. package/dist/core/zero-config.js.map +1 -0
  176. package/dist/dashboard/cli.d.ts +6 -0
  177. package/dist/dashboard/cli.d.ts.map +1 -0
  178. package/dist/dashboard/cli.js +42 -0
  179. package/dist/dashboard/cli.js.map +1 -0
  180. package/dist/dashboard/index.html +3426 -0
  181. package/dist/dashboard/server.d.ts +7 -0
  182. package/dist/dashboard/server.d.ts.map +1 -0
  183. package/dist/dashboard/server.js +1427 -0
  184. package/dist/dashboard/server.js.map +1 -0
  185. package/dist/mcp/server.d.ts +27 -0
  186. package/dist/mcp/server.d.ts.map +1 -0
  187. package/dist/mcp/server.js +380 -0
  188. package/dist/mcp/server.js.map +1 -0
  189. package/dist/mcp/tools/misc.d.ts +15 -0
  190. package/dist/mcp/tools/misc.d.ts.map +1 -0
  191. package/dist/mcp/tools/misc.js +49 -0
  192. package/dist/mcp/tools/misc.js.map +1 -0
  193. package/dist/plugins/builtin/calculator.d.ts +11 -0
  194. package/dist/plugins/builtin/calculator.d.ts.map +1 -0
  195. package/dist/plugins/builtin/calculator.js +166 -0
  196. package/dist/plugins/builtin/calculator.js.map +1 -0
  197. package/dist/plugins/builtin/colors.d.ts +15 -0
  198. package/dist/plugins/builtin/colors.d.ts.map +1 -0
  199. package/dist/plugins/builtin/colors.js +193 -0
  200. package/dist/plugins/builtin/colors.js.map +1 -0
  201. package/dist/plugins/builtin/cron.d.ts +40 -0
  202. package/dist/plugins/builtin/cron.d.ts.map +1 -0
  203. package/dist/plugins/builtin/cron.js +578 -0
  204. package/dist/plugins/builtin/cron.js.map +1 -0
  205. package/dist/plugins/builtin/crypto.d.ts +11 -0
  206. package/dist/plugins/builtin/crypto.d.ts.map +1 -0
  207. package/dist/plugins/builtin/crypto.js +83 -0
  208. package/dist/plugins/builtin/crypto.js.map +1 -0
  209. package/dist/plugins/builtin/database.d.ts +29 -0
  210. package/dist/plugins/builtin/database.d.ts.map +1 -0
  211. package/dist/plugins/builtin/database.js +230 -0
  212. package/dist/plugins/builtin/database.js.map +1 -0
  213. package/dist/plugins/builtin/docker.d.ts +12 -0
  214. package/dist/plugins/builtin/docker.d.ts.map +1 -0
  215. package/dist/plugins/builtin/docker.js +436 -0
  216. package/dist/plugins/builtin/docker.js.map +1 -0
  217. package/dist/plugins/builtin/fun.d.ts +11 -0
  218. package/dist/plugins/builtin/fun.d.ts.map +1 -0
  219. package/dist/plugins/builtin/fun.js +114 -0
  220. package/dist/plugins/builtin/fun.js.map +1 -0
  221. package/dist/plugins/builtin/gcal.d.ts +38 -0
  222. package/dist/plugins/builtin/gcal.d.ts.map +1 -0
  223. package/dist/plugins/builtin/gcal.js +280 -0
  224. package/dist/plugins/builtin/gcal.js.map +1 -0
  225. package/dist/plugins/builtin/gdrive.d.ts +26 -0
  226. package/dist/plugins/builtin/gdrive.d.ts.map +1 -0
  227. package/dist/plugins/builtin/gdrive.js +295 -0
  228. package/dist/plugins/builtin/gdrive.js.map +1 -0
  229. package/dist/plugins/builtin/github-actions.d.ts +38 -0
  230. package/dist/plugins/builtin/github-actions.d.ts.map +1 -0
  231. package/dist/plugins/builtin/github-actions.js +629 -0
  232. package/dist/plugins/builtin/github-actions.js.map +1 -0
  233. package/dist/plugins/builtin/github.d.ts +26 -0
  234. package/dist/plugins/builtin/github.d.ts.map +1 -0
  235. package/dist/plugins/builtin/github.js +800 -0
  236. package/dist/plugins/builtin/github.js.map +1 -0
  237. package/dist/plugins/builtin/gmail.d.ts +50 -0
  238. package/dist/plugins/builtin/gmail.d.ts.map +1 -0
  239. package/dist/plugins/builtin/gmail.js +445 -0
  240. package/dist/plugins/builtin/gmail.js.map +1 -0
  241. package/dist/plugins/builtin/hash.d.ts +11 -0
  242. package/dist/plugins/builtin/hash.d.ts.map +1 -0
  243. package/dist/plugins/builtin/hash.js +95 -0
  244. package/dist/plugins/builtin/hash.js.map +1 -0
  245. package/dist/plugins/builtin/homekit.d.ts +53 -0
  246. package/dist/plugins/builtin/homekit.d.ts.map +1 -0
  247. package/dist/plugins/builtin/homekit.js +341 -0
  248. package/dist/plugins/builtin/homekit.js.map +1 -0
  249. package/dist/plugins/builtin/index.d.ts +4 -0
  250. package/dist/plugins/builtin/index.d.ts.map +1 -0
  251. package/dist/plugins/builtin/index.js +96 -0
  252. package/dist/plugins/builtin/index.js.map +1 -0
  253. package/dist/plugins/builtin/jira.d.ts +50 -0
  254. package/dist/plugins/builtin/jira.d.ts.map +1 -0
  255. package/dist/plugins/builtin/jira.js +353 -0
  256. package/dist/plugins/builtin/jira.js.map +1 -0
  257. package/dist/plugins/builtin/linear.d.ts +35 -0
  258. package/dist/plugins/builtin/linear.d.ts.map +1 -0
  259. package/dist/plugins/builtin/linear.js +397 -0
  260. package/dist/plugins/builtin/linear.js.map +1 -0
  261. package/dist/plugins/builtin/lumen.d.ts +21 -0
  262. package/dist/plugins/builtin/lumen.d.ts.map +1 -0
  263. package/dist/plugins/builtin/lumen.js +404 -0
  264. package/dist/plugins/builtin/lumen.js.map +1 -0
  265. package/dist/plugins/builtin/memory.d.ts +22 -0
  266. package/dist/plugins/builtin/memory.d.ts.map +1 -0
  267. package/dist/plugins/builtin/memory.js +184 -0
  268. package/dist/plugins/builtin/memory.js.map +1 -0
  269. package/dist/plugins/builtin/n8n.d.ts +60 -0
  270. package/dist/plugins/builtin/n8n.d.ts.map +1 -0
  271. package/dist/plugins/builtin/n8n.js +519 -0
  272. package/dist/plugins/builtin/n8n.js.map +1 -0
  273. package/dist/plugins/builtin/network.d.ts +11 -0
  274. package/dist/plugins/builtin/network.d.ts.map +1 -0
  275. package/dist/plugins/builtin/network.js +88 -0
  276. package/dist/plugins/builtin/network.js.map +1 -0
  277. package/dist/plugins/builtin/notes.d.ts +47 -0
  278. package/dist/plugins/builtin/notes.d.ts.map +1 -0
  279. package/dist/plugins/builtin/notes.js +641 -0
  280. package/dist/plugins/builtin/notes.js.map +1 -0
  281. package/dist/plugins/builtin/notion.d.ts +47 -0
  282. package/dist/plugins/builtin/notion.d.ts.map +1 -0
  283. package/dist/plugins/builtin/notion.js +317 -0
  284. package/dist/plugins/builtin/notion.js.map +1 -0
  285. package/dist/plugins/builtin/shell.d.ts +12 -0
  286. package/dist/plugins/builtin/shell.d.ts.map +1 -0
  287. package/dist/plugins/builtin/shell.js +310 -0
  288. package/dist/plugins/builtin/shell.js.map +1 -0
  289. package/dist/plugins/builtin/slack.d.ts +31 -0
  290. package/dist/plugins/builtin/slack.d.ts.map +1 -0
  291. package/dist/plugins/builtin/slack.js +295 -0
  292. package/dist/plugins/builtin/slack.js.map +1 -0
  293. package/dist/plugins/builtin/spotify.d.ts +55 -0
  294. package/dist/plugins/builtin/spotify.d.ts.map +1 -0
  295. package/dist/plugins/builtin/spotify.js +623 -0
  296. package/dist/plugins/builtin/spotify.js.map +1 -0
  297. package/dist/plugins/builtin/stripe.d.ts +35 -0
  298. package/dist/plugins/builtin/stripe.d.ts.map +1 -0
  299. package/dist/plugins/builtin/stripe.js +376 -0
  300. package/dist/plugins/builtin/stripe.js.map +1 -0
  301. package/dist/plugins/builtin/system.d.ts +11 -0
  302. package/dist/plugins/builtin/system.d.ts.map +1 -0
  303. package/dist/plugins/builtin/system.js +91 -0
  304. package/dist/plugins/builtin/system.js.map +1 -0
  305. package/dist/plugins/builtin/text-tools.d.ts +11 -0
  306. package/dist/plugins/builtin/text-tools.d.ts.map +1 -0
  307. package/dist/plugins/builtin/text-tools.js +146 -0
  308. package/dist/plugins/builtin/text-tools.js.map +1 -0
  309. package/dist/plugins/builtin/timezone.d.ts +13 -0
  310. package/dist/plugins/builtin/timezone.d.ts.map +1 -0
  311. package/dist/plugins/builtin/timezone.js +164 -0
  312. package/dist/plugins/builtin/timezone.js.map +1 -0
  313. package/dist/plugins/builtin/todoist.d.ts +49 -0
  314. package/dist/plugins/builtin/todoist.d.ts.map +1 -0
  315. package/dist/plugins/builtin/todoist.js +540 -0
  316. package/dist/plugins/builtin/todoist.js.map +1 -0
  317. package/dist/plugins/builtin/translate.d.ts +11 -0
  318. package/dist/plugins/builtin/translate.d.ts.map +1 -0
  319. package/dist/plugins/builtin/translate.js +42 -0
  320. package/dist/plugins/builtin/translate.js.map +1 -0
  321. package/dist/plugins/builtin/url-tools.d.ts +11 -0
  322. package/dist/plugins/builtin/url-tools.d.ts.map +1 -0
  323. package/dist/plugins/builtin/url-tools.js +70 -0
  324. package/dist/plugins/builtin/url-tools.js.map +1 -0
  325. package/dist/plugins/builtin/vercel.d.ts +55 -0
  326. package/dist/plugins/builtin/vercel.d.ts.map +1 -0
  327. package/dist/plugins/builtin/vercel.js +514 -0
  328. package/dist/plugins/builtin/vercel.js.map +1 -0
  329. package/dist/plugins/builtin/weather.d.ts +13 -0
  330. package/dist/plugins/builtin/weather.d.ts.map +1 -0
  331. package/dist/plugins/builtin/weather.js +103 -0
  332. package/dist/plugins/builtin/weather.js.map +1 -0
  333. package/dist/plugins/builtin/x.d.ts +54 -0
  334. package/dist/plugins/builtin/x.d.ts.map +1 -0
  335. package/dist/plugins/builtin/x.js +402 -0
  336. package/dist/plugins/builtin/x.js.map +1 -0
  337. package/dist/plugins/manager.d.ts +77 -0
  338. package/dist/plugins/manager.d.ts.map +1 -0
  339. package/dist/plugins/manager.js +141 -0
  340. package/dist/plugins/manager.js.map +1 -0
  341. package/dist/plugins/validation.d.ts +18 -0
  342. package/dist/plugins/validation.d.ts.map +1 -0
  343. package/dist/plugins/validation.js +81 -0
  344. package/dist/plugins/validation.js.map +1 -0
  345. package/dist/security/auth.d.ts +23 -0
  346. package/dist/security/auth.d.ts.map +1 -0
  347. package/dist/security/auth.js +56 -0
  348. package/dist/security/auth.js.map +1 -0
  349. package/dist/security/keychain.d.ts +60 -0
  350. package/dist/security/keychain.d.ts.map +1 -0
  351. package/dist/security/keychain.js +213 -0
  352. package/dist/security/keychain.js.map +1 -0
  353. package/dist/utils/google-auth.d.ts +21 -0
  354. package/dist/utils/google-auth.d.ts.map +1 -0
  355. package/dist/utils/google-auth.js +135 -0
  356. package/dist/utils/google-auth.js.map +1 -0
  357. package/dist/utils/retry.d.ts +5 -0
  358. package/dist/utils/retry.d.ts.map +1 -0
  359. package/dist/utils/retry.js +34 -0
  360. package/dist/utils/retry.js.map +1 -0
  361. package/docs/README.md +13 -0
  362. package/docs/api.md +210 -0
  363. package/docs/getting-started.md +100 -0
  364. package/docs/plugins.md +306 -0
  365. package/docs-site/.vitepress/config.ts +59 -0
  366. package/docs-site/README.md +12 -0
  367. package/docs-site/index.md +30 -0
  368. package/eslint.config.js +29 -0
  369. package/install.ps1 +334 -0
  370. package/install.sh +1119 -0
  371. package/local-install.sh +304 -0
  372. package/package.json +90 -0
  373. package/packages/README.md +11 -0
  374. package/packages/plugin-sdk/README.md +12 -0
  375. package/packages/plugin-sdk/package.json +22 -0
  376. package/packages/plugin-sdk/src/README.md +11 -0
  377. package/packages/plugin-sdk/src/index.ts +191 -0
  378. package/sdks/README.md +26 -0
  379. package/sdks/csharp/ConductorClient.cs +65 -0
  380. package/sdks/csharp/README.md +11 -0
  381. package/sdks/go/README.md +11 -0
  382. package/sdks/go/conductor.go +257 -0
  383. package/sdks/java/ConductorClient.java +27 -0
  384. package/sdks/java/README.md +11 -0
  385. package/sdks/php/README.md +11 -0
  386. package/sdks/php/src/Client.php +72 -0
  387. package/sdks/python/README.md +12 -0
  388. package/sdks/python/conductor/__init__.py +227 -0
  389. package/sdks/python/pyproject.toml +30 -0
  390. package/sdks/ruby/README.md +11 -0
  391. package/sdks/ruby/lib/conductor.rb +46 -0
  392. package/sdks/rust/Cargo.toml +14 -0
  393. package/sdks/rust/README.md +11 -0
  394. package/sdks/swift/README.md +11 -0
  395. package/sdks/swift/Sources/Conductor/ConductorClient.swift +65 -0
  396. package/skills/conductor-mcp/SKILL.md +38 -0
  397. package/src/README.md +20 -0
  398. package/src/ai/README.md +18 -0
  399. package/src/ai/base.ts +93 -0
  400. package/src/ai/claude.ts +162 -0
  401. package/src/ai/gemini.ts +188 -0
  402. package/src/ai/maestro.ts +168 -0
  403. package/src/ai/manager.ts +537 -0
  404. package/src/ai/ollama.ts +186 -0
  405. package/src/ai/openai.ts +147 -0
  406. package/src/ai/openrouter.ts +152 -0
  407. package/src/bot/README.md +12 -0
  408. package/src/bot/slack.ts +164 -0
  409. package/src/bot/telegram.ts +185 -0
  410. package/src/cli/README.md +24 -0
  411. package/src/cli/commands/README.md +20 -0
  412. package/src/cli/commands/ai.ts +170 -0
  413. package/src/cli/commands/doctor.ts +221 -0
  414. package/src/cli/commands/init.ts +348 -0
  415. package/src/cli/commands/install.ts +792 -0
  416. package/src/cli/commands/lifecycle.ts +95 -0
  417. package/src/cli/commands/marketplace.ts +253 -0
  418. package/src/cli/commands/mcp.ts +92 -0
  419. package/src/cli/commands/onboard.ts +248 -0
  420. package/src/cli/commands/plugin-create.ts +130 -0
  421. package/src/cli/commands/plugins.ts +36 -0
  422. package/src/cli/commands/release.ts +251 -0
  423. package/src/cli/commands/telegram.ts +25 -0
  424. package/src/cli/index.ts +450 -0
  425. package/src/config/README.md +11 -0
  426. package/src/config/oauth.ts +26 -0
  427. package/src/core/README.md +22 -0
  428. package/src/core/audit.ts +291 -0
  429. package/src/core/circuit-breaker.ts +129 -0
  430. package/src/core/conductor.ts +240 -0
  431. package/src/core/config.ts +149 -0
  432. package/src/core/database.ts +411 -0
  433. package/src/core/errors.ts +275 -0
  434. package/src/core/health.ts +159 -0
  435. package/src/core/interfaces.ts +75 -0
  436. package/src/core/logger.ts +33 -0
  437. package/src/core/rbac.ts +321 -0
  438. package/src/core/retry.ts +61 -0
  439. package/src/core/webhooks.ts +234 -0
  440. package/src/core/zero-config.ts +72 -0
  441. package/src/dashboard/README.md +15 -0
  442. package/src/dashboard/cli.ts +48 -0
  443. package/src/dashboard/index.html +3426 -0
  444. package/src/dashboard/server.ts +1544 -0
  445. package/src/mcp/README.md +20 -0
  446. package/src/mcp/server.ts +475 -0
  447. package/src/mcp/tools/README.md +11 -0
  448. package/src/mcp/tools/misc.ts +61 -0
  449. package/src/plugins/README.md +28 -0
  450. package/src/plugins/builtin/README.md +23 -0
  451. package/src/plugins/builtin/calculator.ts +178 -0
  452. package/src/plugins/builtin/colors.ts +201 -0
  453. package/src/plugins/builtin/cron.ts +649 -0
  454. package/src/plugins/builtin/crypto.ts +85 -0
  455. package/src/plugins/builtin/database.ts +235 -0
  456. package/src/plugins/builtin/docker.ts +426 -0
  457. package/src/plugins/builtin/fun.ts +118 -0
  458. package/src/plugins/builtin/gcal.ts +305 -0
  459. package/src/plugins/builtin/gdrive.ts +326 -0
  460. package/src/plugins/builtin/github-actions.ts +666 -0
  461. package/src/plugins/builtin/github.ts +912 -0
  462. package/src/plugins/builtin/gmail.ts +492 -0
  463. package/src/plugins/builtin/hash.ts +98 -0
  464. package/src/plugins/builtin/homekit.ts +389 -0
  465. package/src/plugins/builtin/index.ts +116 -0
  466. package/src/plugins/builtin/jira.ts +380 -0
  467. package/src/plugins/builtin/linear.ts +448 -0
  468. package/src/plugins/builtin/lumen.ts +497 -0
  469. package/src/plugins/builtin/memory.ts +200 -0
  470. package/src/plugins/builtin/n8n.ts +565 -0
  471. package/src/plugins/builtin/network.ts +92 -0
  472. package/src/plugins/builtin/notes.ts +689 -0
  473. package/src/plugins/builtin/notion.ts +348 -0
  474. package/src/plugins/builtin/shell.ts +334 -0
  475. package/src/plugins/builtin/slack.ts +327 -0
  476. package/src/plugins/builtin/spotify.ts +665 -0
  477. package/src/plugins/builtin/stripe.ts +388 -0
  478. package/src/plugins/builtin/system.ts +93 -0
  479. package/src/plugins/builtin/text-tools.ts +150 -0
  480. package/src/plugins/builtin/timezone.ts +173 -0
  481. package/src/plugins/builtin/todoist.ts +625 -0
  482. package/src/plugins/builtin/translate.ts +47 -0
  483. package/src/plugins/builtin/url-tools.ts +73 -0
  484. package/src/plugins/builtin/vercel.ts +546 -0
  485. package/src/plugins/builtin/weather.ts +112 -0
  486. package/src/plugins/builtin/x.ts +440 -0
  487. package/src/plugins/manager.ts +213 -0
  488. package/src/plugins/validation.ts +94 -0
  489. package/src/security/README.md +12 -0
  490. package/src/security/auth.ts +72 -0
  491. package/src/security/keychain.ts +226 -0
  492. package/src/utils/README.md +12 -0
  493. package/src/utils/google-auth.ts +159 -0
  494. package/src/utils/retry.ts +41 -0
  495. package/test-all.mjs +1256 -0
  496. package/test.mjs +633 -0
  497. package/tests/README.md +19 -0
  498. package/tests/calculator.test.ts +54 -0
  499. package/tests/docker.test.ts +42 -0
  500. package/tests/load.test.ts +129 -0
  501. package/tests/mcp.test.ts +14 -0
  502. package/tests/shell.test.ts +42 -0
  503. package/tsconfig.json +21 -0
  504. package/vitest.config.ts +14 -0
@@ -0,0 +1,3426 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta name="dashboard-token" content="">
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Conductor</title>
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
9
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%233b82f6'/%3E%3Ctext x='16' y='23' text-anchor='middle' font-family='system-ui,sans-serif' font-size='20' font-weight='700' fill='white'%3EC%3C/text%3E%3C/svg%3E">
10
+ <style>
11
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
12
+ :root{
13
+ --bg: #0a0a0a;
14
+ --surface: #111111;
15
+ --elevated: #161616;
16
+ --border: #262626;
17
+ --border2: #1f1f1f;
18
+ --text: #fafafa;
19
+ --text2: #a1a1aa;
20
+ --muted: #71717a;
21
+ --accent: #3b82f6;
22
+ --accent-h: #2563eb;
23
+ --ok: #22c55e;
24
+ --warn: #f59e0b;
25
+ --err: #ef4444;
26
+ --font: 'Inter', system-ui, sans-serif;
27
+ --mono: 'JetBrains Mono', 'Fira Code', monospace;
28
+ --sidebar-w:220px;
29
+ --r: 6px;
30
+ }
31
+ html,body{height:100%;}
32
+ body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:13px;line-height:1.5;display:flex;overflow:hidden;}
33
+ ::-webkit-scrollbar{width:4px;height:4px;}
34
+ ::-webkit-scrollbar-track{background:var(--surface);}
35
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px;}
36
+ ::-webkit-scrollbar-thumb:hover{background:var(--elevated);}
37
+
38
+ /* SIDEBAR */
39
+ #sidebar{width:var(--sidebar-w);flex-shrink:0;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;height:100vh;overflow-y:auto;}
40
+ .sb-header{padding:16px 14px 14px;border-bottom:1px solid var(--border);}
41
+ .sb-wordmark{font-size:14px;font-weight:600;color:var(--text);letter-spacing:-0.3px;}
42
+ .sb-version{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--elevated);border:1px solid var(--border);border-radius:4px;padding:1px 6px;display:inline-block;margin-top:5px;}
43
+ .sb-nav{padding:6px 0;flex:1;}
44
+ .nav-item{display:flex;align-items:center;gap:9px;padding:7px 14px;cursor:pointer;color:var(--text2);font-size:12px;font-weight:500;user-select:none;border-left:2px solid transparent;transition:color 0.1s,background 0.1s;}
45
+ .nav-item:hover{background:var(--elevated);color:var(--text);}
46
+ .nav-item.active{background:var(--elevated);color:var(--text);border-left-color:var(--accent);}
47
+ .nav-item svg{width:15px;height:15px;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;}
48
+ .sb-footer{padding:10px 14px;border-top:1px solid var(--border);display:flex;align-items:center;gap:7px;}
49
+ .conn-dot{width:7px;height:7px;border-radius:50%;background:var(--muted);flex-shrink:0;display:inline-block;}
50
+ .conn-dot.ok{background:var(--ok);}
51
+ .conn-dot.err{background:var(--err);}
52
+ .conn-status{font-family:var(--mono);font-size:10px;color:var(--muted);}
53
+
54
+ /* MAIN */
55
+ #main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;}
56
+ #topbar{height:40px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 18px;flex-shrink:0;background:var(--surface);}
57
+ .topbar-title{font-size:12px;font-weight:600;letter-spacing:0.04em;text-transform:uppercase;color:var(--text2);}
58
+ .topbar-actions{display:flex;align-items:center;gap:6px;}
59
+ #content{flex:1;overflow-y:auto;padding:18px;}
60
+
61
+ /* BUTTONS */
62
+ .btn{display:inline-flex;align-items:center;gap:5px;padding:5px 12px;border:1px solid var(--border);background:transparent;color:var(--text2);font-size:12px;font-family:var(--font);font-weight:500;cursor:pointer;border-radius:var(--r);line-height:1.4;transition:background 0.1s,color 0.1s,border-color 0.1s;}
63
+ .btn:hover{background:var(--elevated);color:var(--text);border-color:var(--text2);}
64
+ .btn svg{width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0;}
65
+ .btn-primary{background:var(--accent);color:#fff;border-color:var(--accent);}
66
+ .btn-primary:hover{background:var(--accent-h);border-color:var(--accent-h);color:#fff;}
67
+ .btn-sm{padding:3px 9px;font-size:11px;}
68
+ .btn-danger{color:var(--err);border-color:var(--border);}
69
+ .btn-danger:hover{background:var(--err);color:#fff;border-color:var(--err);}
70
+
71
+ /* INPUTS */
72
+ input[type="text"],input[type="password"],input[type="date"],select,textarea{
73
+ background:var(--elevated);border:1px solid var(--border);color:var(--text);font-size:12px;font-family:var(--font);
74
+ padding:5px 9px;outline:none;border-radius:var(--r);
75
+ }
76
+ input[type="text"]:focus,input[type="password"]:focus,select:focus,textarea:focus{border-color:var(--accent);outline:none;}
77
+ input::placeholder,textarea::placeholder{color:var(--muted);}
78
+ select option{background:var(--surface);color:var(--text);}
79
+ input[type="checkbox"]{accent-color:var(--accent);}
80
+
81
+ /* TABLES */
82
+ .data-table{width:100%;border-collapse:collapse;font-size:12px;}
83
+ .data-table th{background:var(--elevated);color:var(--text2);text-align:left;padding:6px 10px;font-size:11px;font-weight:600;letter-spacing:0.04em;text-transform:uppercase;border-bottom:1px solid var(--border);}
84
+ .data-table td{padding:6px 10px;border-bottom:1px solid var(--border2);vertical-align:middle;color:var(--text);}
85
+ .data-table tr:hover td{background:var(--elevated);}
86
+ .data-table td.mono{font-family:var(--mono);}
87
+
88
+ /* SECTIONS */
89
+ .section{border:1px solid var(--border);background:var(--surface);border-radius:var(--r);margin-bottom:14px;overflow:hidden;}
90
+ .section-head{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--elevated);}
91
+ .section-title{font-size:11px;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--text2);}
92
+ .section-body{padding:12px;}
93
+
94
+ /* STAT GRID */
95
+ .stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:0;border:1px solid var(--border);border-radius:var(--r);margin-bottom:14px;overflow:hidden;background:var(--surface);}
96
+ .stat-block{padding:14px 16px;border-right:1px solid var(--border);}
97
+ .stat-block:last-child{border-right:none;}
98
+ .stat-num{font-family:var(--mono);font-size:26px;font-weight:500;line-height:1;color:var(--text);}
99
+ .stat-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;margin-top:5px;}
100
+
101
+ /* PROGRESS BAR */
102
+ .prog-wrap{height:5px;background:var(--elevated);width:100%;margin:5px 0;border-radius:3px;overflow:hidden;}
103
+ .prog-fill{height:100%;background:var(--accent);transition:width 0.4s;}
104
+
105
+ /* TABS */
106
+ .tab-bar{display:flex;border-bottom:1px solid var(--border);background:var(--surface);}
107
+ .tab{padding:8px 16px;font-size:12px;font-weight:500;cursor:pointer;color:var(--muted);user-select:none;border-bottom:2px solid transparent;transition:color 0.1s;}
108
+ .tab:hover{color:var(--text);}
109
+ .tab.active{color:var(--text);border-bottom-color:var(--accent);}
110
+
111
+ /* LOGS */
112
+ .log-stream{font-family:var(--mono);font-size:12px;line-height:1.6;background:var(--bg);border:1px solid var(--border);border-radius:var(--r);padding:10px;height:calc(100vh - 180px);overflow-y:auto;white-space:pre-wrap;word-break:break-all;}
113
+ .log-line-INFO{color:var(--text);}
114
+ .log-line-WARN{color:var(--warn);}
115
+ .log-line-ERROR{color:var(--err);}
116
+ .log-line-DEBUG{color:var(--muted);}
117
+
118
+ /* TERMINAL */
119
+ .terminal-out{font-family:var(--mono);font-size:12px;line-height:1.6;background:var(--bg);border:1px solid var(--border);border-radius:var(--r);padding:12px;min-height:320px;max-height:440px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;margin-bottom:8px;}
120
+ .term-cmd{color:var(--accent);font-weight:600;}
121
+ .term-out{color:var(--text);}
122
+ .term-err{color:var(--err);}
123
+ .term-info{color:var(--muted);}
124
+ .shell-input-row{display:flex;align-items:center;gap:0;border:1px solid var(--border);border-radius:var(--r);overflow:hidden;}
125
+ .shell-prompt{font-family:var(--mono);font-size:12px;color:var(--accent);padding:6px 10px;border-right:1px solid var(--border);background:var(--elevated);flex-shrink:0;}
126
+ .shell-input{flex:1;background:var(--surface);border:none;outline:none;color:var(--text);font-family:var(--mono);font-size:12px;padding:6px 10px;}
127
+
128
+ /* FILE BROWSER */
129
+ .file-cols{display:grid;grid-template-columns:30% 70%;height:calc(100vh - 126px);}
130
+ .file-list-pane{border-right:1px solid var(--border);overflow-y:auto;background:var(--surface);}
131
+ .file-item{display:flex;align-items:center;padding:5px 10px;cursor:pointer;border-bottom:1px solid var(--border2);gap:7px;}
132
+ .file-item:hover{background:var(--elevated);}
133
+ .file-item.selected{background:var(--accent);color:#fff;}
134
+ .file-icon{font-family:var(--mono);font-size:10px;width:14px;flex-shrink:0;color:var(--muted);}
135
+ .file-item.selected .file-icon{color:rgba(255,255,255,0.7);}
136
+ .file-name{flex:1;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);}
137
+ .file-item.selected .file-name{color:#fff;}
138
+ .file-size{font-family:var(--mono);font-size:10px;color:var(--muted);flex-shrink:0;}
139
+ .file-item.selected .file-size{color:rgba(255,255,255,0.6);}
140
+ .file-viewer{overflow:auto;background:var(--bg);}
141
+ .file-content{font-family:var(--mono);font-size:12px;line-height:1.6;padding:12px;white-space:pre;min-height:100%;color:var(--text);}
142
+
143
+ /* NOTE EDITOR */
144
+ .note-cols{display:grid;grid-template-columns:300px 1fr;height:calc(100vh - 40px);}
145
+ .note-list-pane{border-right:1px solid var(--border);overflow-y:auto;background:var(--surface);}
146
+ .note-item{padding:9px 12px;cursor:pointer;border-bottom:1px solid var(--border2);}
147
+ .note-item:hover{background:var(--elevated);}
148
+ .note-item.active{background:var(--elevated);border-left:2px solid var(--accent);}
149
+ .note-item-title{font-size:12px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);}
150
+ .note-item-date{font-family:var(--mono);font-size:10px;color:var(--muted);margin-top:3px;}
151
+ .note-editor-pane{display:flex;flex-direction:column;background:var(--bg);}
152
+ .note-editor-toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--surface);}
153
+ .note-title-input{flex:1;font-size:13px;font-weight:500;border:none;outline:none;padding:0;background:transparent;color:var(--text);}
154
+ .note-body{flex:1;padding:14px;font-size:13px;line-height:1.7;outline:none;overflow-y:auto;color:var(--text);}
155
+
156
+ /* EMPTY */
157
+ .empty{padding:32px;text-align:center;color:var(--muted);font-size:12px;}
158
+
159
+ /* INLINE CONFIRM */
160
+ .confirm-wrap{display:inline-flex;align-items:center;gap:5px;font-size:11px;}
161
+
162
+ /* SPARKLINE */
163
+ .sparkline-wrap{border:1px solid var(--border);background:var(--surface);border-radius:var(--r);margin-bottom:14px;overflow:hidden;}
164
+ .sparkline-head{padding:6px 12px;border-bottom:1px solid var(--border);font-size:11px;font-family:var(--mono);color:var(--muted);display:flex;gap:18px;}
165
+ .sparkline-head span{color:var(--text);}
166
+
167
+ /* NETWORK */
168
+ .iface-pre{font-family:var(--mono);font-size:11px;line-height:1.5;padding:12px;white-space:pre;overflow-x:auto;background:var(--bg);color:var(--text);}
169
+
170
+ /* CRED */
171
+ .cred-masked{font-family:var(--mono);font-size:12px;color:var(--muted);letter-spacing:0.15em;}
172
+
173
+ /* PLUGIN GRID */
174
+ .plugin-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:1px;background:var(--border);}
175
+ .plugin-card{padding:12px 14px;background:var(--surface);}
176
+ .plugin-card:hover{background:var(--elevated);}
177
+ .plugin-name{font-family:var(--mono);font-size:12px;font-weight:500;color:var(--text);margin-bottom:4px;}
178
+ .plugin-status{font-size:11px;text-transform:uppercase;letter-spacing:0.05em;color:var(--muted);margin-bottom:5px;}
179
+ .plugin-status.active{color:var(--ok);}
180
+ .plugin-creds{font-size:11px;color:var(--muted);margin-bottom:8px;font-family:var(--mono);}
181
+
182
+ /* ENV TABLE */
183
+ .env-table{width:100%;border-collapse:collapse;font-size:12px;font-family:var(--mono);}
184
+ .env-table td{padding:5px 10px;border-bottom:1px solid var(--border2);vertical-align:top;}
185
+ .env-table td:first-child{color:var(--text2);font-weight:500;width:220px;white-space:nowrap;}
186
+ .env-table td:last-child{color:var(--muted);word-break:break-all;}
187
+
188
+ /* QUICK ACTIONS */
189
+ .quick-actions{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;}
190
+
191
+ /* OVERVIEW LOG TAIL */
192
+ .log-tail{font-family:var(--mono);font-size:11px;line-height:1.6;padding:8px 10px;background:var(--bg);min-height:80px;}
193
+
194
+ /* SETTINGS */
195
+ .settings-kv{width:100%;border-collapse:collapse;font-size:12px;}
196
+ .settings-kv td{padding:6px 10px;border-bottom:1px solid var(--border2);vertical-align:middle;}
197
+ .settings-kv td:first-child{font-family:var(--mono);color:var(--text2);width:200px;}
198
+ .settings-kv td:last-child{color:var(--text);}
199
+ .settings-kv input[type="text"]{width:100%;}
200
+
201
+ /* BADGE */
202
+ .badge{display:inline-block;font-family:var(--mono);font-size:10px;padding:1px 6px;border-radius:3px;font-weight:500;}
203
+ .badge-ok{background:rgba(34,197,94,0.15);color:var(--ok);}
204
+ .badge-err{background:rgba(239,68,68,0.15);color:var(--err);}
205
+ .badge-warn{background:rgba(245,158,11,0.15);color:var(--warn);}
206
+ .badge-muted{background:var(--elevated);color:var(--muted);}
207
+
208
+ /* TOAST */
209
+ #toast-container{position:fixed;bottom:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:6px;max-width:320px;pointer-events:none;}
210
+ .toast{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:9px 14px;font-size:12px;font-family:var(--font);border-radius:var(--r);opacity:1;transition:opacity 0.25s ease;border-left:3px solid var(--ok);box-shadow:0 4px 12px rgba(0,0,0,0.4);}
211
+ .toast.err{border-left-color:var(--err);}
212
+ .toast.warn{border-left-color:var(--warn);}
213
+ .toast.info{border-left-color:var(--accent);}
214
+
215
+ /* INLINE INPUT */
216
+ .inline-input-row{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--elevated);border-top:1px solid var(--border);}
217
+
218
+ /* SIDEBAR COLLAPSE */
219
+ #sidebar{transition:width 0.2s ease;}
220
+ #sidebar.collapsed{width:40px;overflow:hidden;}
221
+ #sidebar.collapsed .sb-wordmark,#sidebar.collapsed .sb-version,#sidebar.collapsed .conn-status,#sidebar.collapsed .nav-item span:not(.nav-icon){display:none;}
222
+ #sidebar.collapsed .nav-item{padding:7px 0;justify-content:center;}
223
+ #sidebar.collapsed .sb-header{padding:10px 0;display:flex;justify-content:center;}
224
+ #sidebar.collapsed .sb-footer{justify-content:center;}
225
+ #sidebar.collapsed .sb-collapse-btn{transform:rotate(180deg);}
226
+
227
+ /* COMMAND PALETTE */
228
+ #cmd-palette-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10000;align-items:flex-start;justify-content:center;padding-top:80px;}
229
+ #cmd-palette-overlay.open{display:flex;}
230
+ #cmd-palette{background:var(--surface);border:1px solid var(--border);border-radius:8px;width:520px;max-width:90vw;box-shadow:0 16px 48px rgba(0,0,0,0.5);overflow:hidden;}
231
+ #cmd-input{width:100%;background:transparent;border:none;outline:none;color:var(--text);font-family:var(--mono);font-size:14px;padding:14px 16px;border-bottom:1px solid var(--border);}
232
+ #cmd-results{max-height:320px;overflow-y:auto;}
233
+ .cmd-item{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;font-size:13px;color:var(--text2);}
234
+ .cmd-item:hover,.cmd-item.selected{background:var(--elevated);color:var(--text);}
235
+ .cmd-item-icon{font-family:var(--mono);font-size:11px;color:var(--muted);width:20px;flex-shrink:0;}
236
+ .cmd-item-label{flex:1;}
237
+ .cmd-item-hint{font-family:var(--mono);font-size:10px;color:var(--muted);}
238
+
239
+ /* CHAT */
240
+ .chat-wrap{display:flex;flex-direction:column;height:calc(100vh - 40px);}
241
+ .chat-header{padding:8px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--surface);flex-shrink:0;}
242
+ .chat-provider-badge{font-family:var(--mono);font-size:10px;background:var(--elevated);border:1px solid var(--border);border-radius:3px;padding:2px 7px;color:var(--text2);}
243
+ .chat-messages{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:14px;background:var(--bg);}
244
+ .chat-msg{display:flex;flex-direction:column;max-width:78%;gap:4px;}
245
+ .chat-msg.user{align-self:flex-end;align-items:flex-end;}
246
+ .chat-msg.assistant{align-self:flex-start;align-items:flex-start;}
247
+ .chat-msg.system-msg{align-self:center;align-items:center;max-width:90%;}
248
+ .chat-bubble{padding:9px 13px;border-radius:10px;font-size:13px;line-height:1.55;word-break:break-word;white-space:pre-wrap;}
249
+ .chat-msg.user .chat-bubble{background:var(--accent);color:#fff;border-bottom-right-radius:3px;}
250
+ .chat-msg.assistant .chat-bubble{background:var(--surface);border:1px solid var(--border);color:var(--text);border-bottom-left-radius:3px;}
251
+ .chat-msg.system-msg .chat-bubble{background:var(--elevated);border:1px solid var(--border);color:var(--muted);font-size:11px;font-family:var(--mono);border-radius:6px;}
252
+ .chat-tool-chips{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px;}
253
+ .chat-tool-chip{font-family:var(--mono);font-size:10px;padding:2px 7px;border-radius:3px;display:inline-flex;align-items:center;gap:4px;}
254
+ .chat-tool-chip.ok{background:rgba(34,197,94,0.12);color:#4ade80;border:1px solid rgba(34,197,94,0.2);}
255
+ .chat-tool-chip.err{background:rgba(239,68,68,0.12);color:#f87171;border:1px solid rgba(239,68,68,0.2);}
256
+ .chat-typing{align-self:flex-start;background:var(--surface);border:1px solid var(--border);border-radius:10px;border-bottom-left-radius:3px;padding:10px 14px;display:flex;gap:5px;align-items:center;}
257
+ .chat-typing span{width:7px;height:7px;border-radius:50%;background:var(--muted);display:inline-block;animation:chatDot 1.2s infinite;}
258
+ .chat-typing span:nth-child(2){animation-delay:0.2s;}
259
+ .chat-typing span:nth-child(3){animation-delay:0.4s;}
260
+ @keyframes chatDot{0%,80%,100%{transform:scale(0.8);opacity:0.5;}40%{transform:scale(1);opacity:1;}}
261
+ .chat-input-area{border-top:1px solid var(--border);padding:12px 16px;background:var(--surface);flex-shrink:0;display:flex;gap:8px;align-items:flex-end;}
262
+ .chat-input{flex:1;background:var(--elevated);border:1px solid var(--border);color:var(--text);font-size:13px;font-family:var(--font);padding:8px 12px;border-radius:8px;outline:none;resize:none;max-height:120px;line-height:1.5;}
263
+ .chat-input:focus{border-color:var(--accent);}
264
+ .chat-send-btn{background:var(--accent);color:#fff;border:none;border-radius:8px;padding:8px 14px;cursor:pointer;font-size:12px;font-weight:500;font-family:var(--font);flex-shrink:0;transition:background 0.1s;}
265
+ .chat-send-btn:hover{background:var(--accent-h);}
266
+ .chat-send-btn:disabled{opacity:0.5;cursor:not-allowed;}
267
+ .chat-ts{font-size:10px;color:var(--muted);font-family:var(--mono);}
268
+
269
+ /* MARKETPLACE */
270
+ .mkt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1px;background:var(--border);}
271
+ .mkt-card{padding:14px 16px;background:var(--surface);display:flex;flex-direction:column;gap:6px;cursor:default;}
272
+ .mkt-card:hover{background:var(--elevated);}
273
+ .mkt-card-name{font-family:var(--mono);font-size:12px;font-weight:600;color:var(--text);}
274
+ .mkt-card-desc{font-size:12px;color:var(--text2);line-height:1.45;flex:1;}
275
+ .mkt-card-meta{display:flex;align-items:center;justify-content:space-between;margin-top:4px;}
276
+ .mkt-category{font-size:10px;color:var(--muted);font-family:var(--mono);}
277
+ .mkt-filter-bar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px;align-items:center;}
278
+
279
+ /* KEYBOARD SHORTCUT HELP */
280
+ #kb-help-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10001;align-items:center;justify-content:center;}
281
+ #kb-help-overlay.open{display:flex;}
282
+ #kb-help-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px 28px;min-width:340px;max-width:90vw;box-shadow:0 16px 48px rgba(0,0,0,0.5);}
283
+ #kb-help-card h3{font-size:13px;font-weight:600;color:var(--text);margin-bottom:14px;letter-spacing:0.05em;text-transform:uppercase;}
284
+ .kb-row{display:flex;align-items:center;gap:12px;padding:4px 0;font-size:12px;}
285
+ .kb-key{font-family:var(--mono);font-size:11px;background:var(--elevated);border:1px solid var(--border);border-radius:4px;padding:2px 7px;color:var(--text);white-space:nowrap;}
286
+ .kb-desc{color:var(--text2);}
287
+ </style>
288
+ </head>
289
+ <body>
290
+
291
+ <!-- SIDEBAR -->
292
+ <nav id="sidebar">
293
+ <div class="sb-header">
294
+ <div class="sb-wordmark">conductor</div>
295
+ <div class="sb-version" id="sb-version" style="color:var(--muted);">loading</div>
296
+ </div>
297
+ <div class="sb-nav">
298
+ <div class="nav-item active" data-page="overview">
299
+ <svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
300
+ Overview
301
+ </div>
302
+ <div class="nav-item" data-page="chat">
303
+ <svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
304
+ Chat
305
+ </div>
306
+ <div class="nav-item" data-page="tasks">
307
+ <svg viewBox="0 0 24 24"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
308
+ Tasks
309
+ </div>
310
+ <div class="nav-item" data-page="files">
311
+ <svg viewBox="0 0 24 24"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
312
+ Files
313
+ </div>
314
+ <div class="nav-item" data-page="processes">
315
+ <svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
316
+ Processes
317
+ </div>
318
+ <div class="nav-item" data-page="network">
319
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
320
+ Network
321
+ </div>
322
+ <div class="nav-item" data-page="system">
323
+ <svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
324
+ System
325
+ </div>
326
+ <div class="nav-item" data-page="notes">
327
+ <svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
328
+ Notes
329
+ </div>
330
+ <div class="nav-item" data-page="plugins">
331
+ <svg viewBox="0 0 24 24"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
332
+ Plugins
333
+ </div>
334
+ <div class="nav-item" data-page="marketplace">
335
+ <svg viewBox="0 0 24 24"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
336
+ Marketplace
337
+ </div>
338
+ <div class="nav-item" data-page="credentials">
339
+ <svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
340
+ Credentials
341
+ </div>
342
+ <div class="nav-item" data-page="logs">
343
+ <svg viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
344
+ Logs
345
+ </div>
346
+ <div class="nav-item" data-page="settings">
347
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
348
+ Settings
349
+ </div>
350
+ <div class="nav-item" data-page="health">
351
+ <svg viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
352
+ Health
353
+ </div>
354
+ <div class="nav-item" data-page="audit">
355
+ <svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
356
+ Audit Log
357
+ </div>
358
+ <div class="nav-item" data-page="webhooks">
359
+ <svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
360
+ Webhooks
361
+ </div>
362
+ <div class="nav-item" data-page="metrics">
363
+ <svg viewBox="0 0 24 24"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
364
+ Metrics
365
+ </div>
366
+ </div>
367
+ <div class="sb-footer">
368
+ <span class="conn-dot" id="sb-dot"></span>
369
+ <span class="conn-status" id="sb-status">connecting</span>
370
+ <button class="sb-collapse-btn" id="sb-collapse-btn" onclick="toggleSidebar()" style="margin-left:auto;background:none;border:none;cursor:pointer;color:var(--muted);padding:2px 4px;" title="Toggle sidebar [">
371
+ <svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5;"><polyline points="15 18 9 12 15 6"/></svg>
372
+ </button>
373
+ </div>
374
+ </nav>
375
+
376
+ <!-- MAIN -->
377
+ <div id="main">
378
+ <div id="topbar">
379
+ <span class="topbar-title" id="topbar-title">Overview</span>
380
+ <div class="topbar-actions" id="topbar-actions"></div>
381
+ </div>
382
+ <div id="content"></div>
383
+ </div>
384
+
385
+ <div id="toast-container"></div>
386
+
387
+ <div id="cmd-palette-overlay" onclick="if(event.target===this)closeCmdPalette()">
388
+ <div id="cmd-palette">
389
+ <input id="cmd-input" placeholder="Type a page, > for shell, @ for notes..." autocomplete="off" spellcheck="false">
390
+ <div id="cmd-results"></div>
391
+ </div>
392
+ </div>
393
+
394
+ <div id="kb-help-overlay" onclick="if(event.target===this)closeKbHelp()">
395
+ <div id="kb-help-card">
396
+ <h3>Keyboard Shortcuts</h3>
397
+ <div class="kb-row"><span class="kb-key">Cmd/Ctrl+K</span><span class="kb-desc">Command palette</span></div>
398
+ <div class="kb-row"><span class="kb-key">[</span><span class="kb-desc">Toggle sidebar</span></div>
399
+ <div class="kb-row"><span class="kb-key">?</span><span class="kb-desc">This help</span></div>
400
+ <div class="kb-row"><span class="kb-key">n</span><span class="kb-desc">New task (on Tasks page)</span></div>
401
+ <div class="kb-row"><span class="kb-key">Ctrl+L</span><span class="kb-desc">Clear terminal</span></div>
402
+ <div class="kb-row" style="margin-top:8px;"><span class="kb-key">g o</span><span class="kb-desc">Go to Overview</span></div>
403
+ <div class="kb-row"><span class="kb-key">g t</span><span class="kb-desc">Go to Tasks</span></div>
404
+ <div class="kb-row"><span class="kb-key">g f</span><span class="kb-desc">Go to Files</span></div>
405
+ <div class="kb-row"><span class="kb-key">g p</span><span class="kb-desc">Go to Processes</span></div>
406
+ <div class="kb-row"><span class="kb-key">g n</span><span class="kb-desc">Go to Notes</span></div>
407
+ <div class="kb-row"><span class="kb-key">g s</span><span class="kb-desc">Go to System</span></div>
408
+ <div class="kb-row"><span class="kb-key">g l</span><span class="kb-desc">Go to Logs</span></div>
409
+ <div class="kb-row"><span class="kb-key">g c</span><span class="kb-desc">Go to Credentials</span></div>
410
+ <button class="btn btn-sm" onclick="closeKbHelp()" style="margin-top:16px;">Close</button>
411
+ </div>
412
+ </div>
413
+
414
+ <script>
415
+ /* ══════════════════════════════════════════
416
+ TOKEN + API
417
+ ══════════════════════════════════════════ */
418
+ const _dashboardToken = (function(){
419
+ const el = document.querySelector('meta[name="dashboard-token"]');
420
+ return el ? el.getAttribute('content') : '';
421
+ })();
422
+
423
+ async function api(method, path, body) {
424
+ const headers = {'Content-Type':'application/json'};
425
+ if (_dashboardToken) headers['Authorization'] = 'Bearer ' + _dashboardToken;
426
+ const init = {method, headers};
427
+ if (body !== undefined) init.body = JSON.stringify(body);
428
+ const r = await fetch(path, init);
429
+ if (!r.ok) {
430
+ const t = await r.text().catch(() => r.statusText);
431
+ let msg = t;
432
+ try { msg = JSON.parse(t).error || t; } catch(e) {}
433
+ throw new Error(msg || r.statusText);
434
+ }
435
+ if (r.status === 204) return null;
436
+ return r.json();
437
+ }
438
+
439
+ /* ══════════════════════════════════════════
440
+ STATE
441
+ ══════════════════════════════════════════ */
442
+ let _currentPage = 'overview';
443
+ let _status = {};
444
+ let _config = {};
445
+ let _plugins = {all:[],enabled:[],requiredCreds:{}};
446
+ let _credentials = [];
447
+ let _googleStatus = {connected:false};
448
+ let _todoistConfigured = false;
449
+ let _allTasks = [];
450
+ let _taskProjects = [];
451
+ let _taskProjectFilter = 'all';
452
+ let _taskPriorityFilter = 'all';
453
+ let _taskSearch = '';
454
+ let _shellHistory = [];
455
+ let _shellHistoryIdx = -1;
456
+ let _intervals = [];
457
+ let _logPaused = false;
458
+ let _logFilter = 'ALL';
459
+ let _logLines = [];
460
+ let _logAutoScroll = true;
461
+ let _sseSource = null;
462
+ let _netSamples = [];
463
+ let _netInterval = null;
464
+ let _chatMessages = []; // { role, content, toolCalls, ts }
465
+ let _chatPending = false;
466
+ let _chatProvider = '';
467
+ let _chatModel = '';
468
+ let _mktFilter = 'all';
469
+ let _procAutoRefresh = true;
470
+ let _procInterval = null;
471
+ let _selectedNote = null;
472
+ let _notes = [];
473
+ let _noteDebounce = null;
474
+ let _noteDirty = false;
475
+ let _noteSearch = '';
476
+ let _noteMarkdownPreview = false;
477
+ let _logSearch = '';
478
+ let _selectedTaskIds = new Set();
479
+ let _filePath = '';
480
+ let _homeDir = '';
481
+ let _fileHidden = false;
482
+ let _selectedFile = null;
483
+ let _overviewMetricsInterval = null;
484
+ let _overviewLogTail = [];
485
+ let _systemUptime = 0;
486
+
487
+ /* ══════════════════════════════════════════
488
+ HELPERS
489
+ ══════════════════════════════════════════ */
490
+ function esc(s) {
491
+ if (s == null) return '';
492
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
493
+ }
494
+ function fmtTime(ts) {
495
+ if (!ts) return '';
496
+ return new Date(ts).toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
497
+ }
498
+ function fmtUptime(secs) {
499
+ const s = Math.floor(secs);
500
+ const d = Math.floor(s/86400);
501
+ const h = Math.floor((s%86400)/3600);
502
+ const m = Math.floor((s%3600)/60);
503
+ if (d > 0) return d+'d '+h+'h';
504
+ if (h > 0) return h+'h '+m+'m';
505
+ if (m > 0) return m+'m '+(s%60)+'s';
506
+ return s+'s';
507
+ }
508
+ function fmtBytes(b) {
509
+ if (b == null) return '--';
510
+ const gb = b/1073741824;
511
+ if (gb >= 1) return gb.toFixed(1)+' GB';
512
+ return (b/1048576).toFixed(0)+' MB';
513
+ }
514
+ function fmtFileSize(bytes) {
515
+ if (bytes == null) return '';
516
+ if (bytes < 1024) return bytes+'B';
517
+ if (bytes < 1048576) return (bytes/1024).toFixed(1)+'K';
518
+ return (bytes/1048576).toFixed(1)+'M';
519
+ }
520
+ function dueBadgeClass(dateStr) {
521
+ if (!dateStr) return null;
522
+ const today = new Date(); today.setHours(0,0,0,0);
523
+ const due = new Date(dateStr); due.setHours(0,0,0,0);
524
+ if (due < today) return 'overdue';
525
+ if (due.getTime() === today.getTime()) return 'today';
526
+ return 'upcoming';
527
+ }
528
+ function priorityLabel(p) {
529
+ if (p === 4) return 'P1';
530
+ if (p === 3) return 'P2';
531
+ if (p === 2) return 'P3';
532
+ return 'P4';
533
+ }
534
+ function clearAllIntervals() {
535
+ _intervals.forEach(id => clearInterval(id));
536
+ _intervals = [];
537
+ if (_overviewMetricsInterval) { clearInterval(_overviewMetricsInterval); _overviewMetricsInterval = null; }
538
+ if (_netInterval) { clearInterval(_netInterval); _netInterval = null; }
539
+ if (_procInterval) { clearInterval(_procInterval); _procInterval = null; }
540
+ }
541
+ function addInterval(fn, ms) {
542
+ const id = setInterval(fn, ms);
543
+ _intervals.push(id);
544
+ return id;
545
+ }
546
+ function nowHHMMSS() {
547
+ return new Date().toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
548
+ }
549
+
550
+ /* STRIP ANSI */
551
+ function stripAnsi(str) {
552
+ return String(str == null ? '' : str).replace(/\x1B\[[0-9;]*[mGKHF]/g, '');
553
+ }
554
+
555
+ /* PATH DISPLAY — show ~-relative path instead of absolute */
556
+ function pathToDisplay(absPath) {
557
+ if (!absPath) return '~';
558
+ if (_homeDir && absPath === _homeDir) return '~';
559
+ if (_homeDir && absPath.startsWith(_homeDir + '/')) return '~' + absPath.slice(_homeDir.length);
560
+ return absPath;
561
+ }
562
+
563
+ /* TOAST */
564
+ function toast(msg, type = 'ok', ms = 2800) {
565
+ const tc = document.getElementById('toast-container');
566
+ if (!tc) return;
567
+ const el = document.createElement('div');
568
+ el.className = 'toast' + (type !== 'ok' ? ' '+type : '');
569
+ el.textContent = msg;
570
+ tc.appendChild(el);
571
+ setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, ms);
572
+ }
573
+
574
+ /* BUTTON LOADING STATE */
575
+ function btnLoad(id, label) {
576
+ const b = document.getElementById(id);
577
+ if (b) { b._origText = b.textContent; b.textContent = label || '...'; b.disabled = true; }
578
+ }
579
+ function btnReset(id) {
580
+ const b = document.getElementById(id);
581
+ if (b && b._origText) { b.textContent = b._origText; b.disabled = false; }
582
+ }
583
+
584
+ /* ══════════════════════════════════════════
585
+ NAVIGATION
586
+ ══════════════════════════════════════════ */
587
+ function hasUnsavedFileEdit() { return !!document.getElementById('file-edit-area'); }
588
+
589
+ function navigate(page) {
590
+ if (hasUnsavedFileEdit() && page !== _currentPage) {
591
+ if (!confirm('You have unsaved changes in the file editor. Leave anyway?')) return;
592
+ }
593
+ clearAllIntervals();
594
+ _currentPage = page;
595
+ document.querySelectorAll('.nav-item').forEach(el => {
596
+ el.classList.toggle('active', el.dataset.page === page);
597
+ });
598
+ const titles = {
599
+ overview:'Overview', chat:'Chat', tasks:'Tasks', files:'Files', processes:'Processes',
600
+ network:'Network', system:'System', notes:'Notes', plugins:'Plugins',
601
+ marketplace:'Marketplace', credentials:'Credentials', logs:'Logs', settings:'Settings'
602
+ };
603
+ document.getElementById('topbar-title').textContent = titles[page] || page;
604
+ document.title = 'Conductor — ' + (titles[page] || page);
605
+ document.getElementById('topbar-actions').innerHTML = '';
606
+ document.getElementById('content').style.padding = page === 'chat' || page === 'notes' ? '0' : '18px';
607
+ location.hash = '#' + page;
608
+
609
+ switch(page) {
610
+ case 'overview': renderOverview(); break;
611
+ case 'chat': renderChat(); break;
612
+ case 'tasks': renderTasks(); break;
613
+ case 'files': _filePath = ''; renderFiles(); break;
614
+ case 'processes': renderProcesses(); break;
615
+ case 'network': renderNetwork(); break;
616
+ case 'system': renderSystem(); break;
617
+ case 'notes': renderNotes(); break;
618
+ case 'plugins': renderPlugins(); break;
619
+ case 'marketplace': renderMarketplace(); break;
620
+ case 'credentials': renderCredentials(); break;
621
+ case 'logs': renderLogs(); break;
622
+ case 'settings': renderSettings(); break;
623
+ case 'health': renderHealth(); break;
624
+ case 'audit': renderAudit(); break;
625
+ case 'webhooks': renderWebhooks(); break;
626
+ case 'metrics': renderMetrics(); break;
627
+ }
628
+ }
629
+
630
+ window.addEventListener('hashchange', () => {
631
+ const page = location.hash.replace('#','');
632
+ const valid = ['overview','chat','tasks','files','processes','network','system','notes','plugins','marketplace','credentials','logs','settings'];
633
+ if (page && valid.includes(page) && page !== _currentPage) {
634
+ if (hasUnsavedFileEdit()) {
635
+ if (!confirm('You have unsaved changes in the file editor. Leave anyway?')) {
636
+ location.hash = '#' + _currentPage;
637
+ return;
638
+ }
639
+ }
640
+ navigate(page);
641
+ }
642
+ });
643
+
644
+ /* ══════════════════════════════════════════
645
+ INIT
646
+ ══════════════════════════════════════════ */
647
+ async function init() {
648
+ try {
649
+ const [status, cfg, plugins, creds, googleStatus, todoistStatus] = await Promise.all([
650
+ api('GET','/api/status').catch(() => ({})),
651
+ api('GET','/api/config').catch(() => ({})),
652
+ api('GET','/api/plugins').catch(() => ({all:[],enabled:[],requiredCreds:{}})),
653
+ api('GET','/api/credentials').catch(() => []),
654
+ api('GET','/api/auth/google/status').catch(() => ({connected:false})),
655
+ api('GET','/api/todoist/status').catch(() => ({configured:false})),
656
+ ]);
657
+ _status = status || {};
658
+ _config = cfg || {};
659
+ _plugins = Object.assign({all:[],enabled:[],requiredCreds:{}}, plugins);
660
+ _credentials = Array.isArray(creds) ? creds : [];
661
+ _googleStatus = googleStatus || {connected:false};
662
+ _todoistConfigured = !!(todoistStatus && todoistStatus.configured);
663
+
664
+ const dot = document.getElementById('sb-dot');
665
+ dot.className = 'conn-dot ok';
666
+ document.getElementById('sb-status').textContent = 'connected';
667
+ const sbVer = document.getElementById('sb-version');
668
+ if (sbVer) { sbVer.textContent = _status.version ? 'v'+_status.version : 'v?'; sbVer.style.color = ''; }
669
+
670
+ startSSE();
671
+
672
+ // Load shell history from localStorage
673
+ try {
674
+ const saved = localStorage.getItem('conductor_shell_history');
675
+ if (saved) { _shellHistory = JSON.parse(saved).slice(0,100); }
676
+ } catch(e) {}
677
+
678
+ const hash = location.hash.replace('#','');
679
+ const valid = ['overview','chat','tasks','files','processes','network','system','notes','plugins','marketplace','credentials','logs','settings'];
680
+ navigate(valid.includes(hash) ? hash : 'overview');
681
+ } catch(e) {
682
+ document.getElementById('sb-dot').className = 'conn-dot err';
683
+ document.getElementById('sb-status').textContent = 'error';
684
+ document.getElementById('content').innerHTML = `<div class="section"><div class="section-body" style="color:var(--err);font-family:var(--mono);font-size:12px;">Init error: ${esc(e.message)}</div></div>`;
685
+ }
686
+ }
687
+
688
+ /* ══════════════════════════════════════════
689
+ SSE LOG STREAM
690
+ ══════════════════════════════════════════ */
691
+ function startSSE() {
692
+ if (_sseSource) _sseSource.close();
693
+ // Pass token as query param — EventSource cannot set custom headers
694
+ const url = '/api/logs/stream?token=' + encodeURIComponent(_dashboardToken);
695
+ _sseSource = new EventSource(url);
696
+ _sseSource.onmessage = function(e) {
697
+ let line;
698
+ try {
699
+ const d = JSON.parse(e.data);
700
+ const msg = stripAnsi(d.message || d.msg || '');
701
+ // Skip empty-message Winston JSON log entries
702
+ if (!msg) return;
703
+ line = {time: d.timestamp ? fmtTime(d.timestamp) : nowHHMMSS(), level:(d.level||'INFO').toUpperCase(), msg};
704
+ } catch(ex) {
705
+ const msg = stripAnsi(e.data);
706
+ if (!msg.trim()) return;
707
+ line = {time:nowHHMMSS(), level:'INFO', msg};
708
+ }
709
+ _logLines.push(line);
710
+ if (_logLines.length > 2000) _logLines.splice(0, _logLines.length - 2000);
711
+ _overviewLogTail.push(line);
712
+ if (_overviewLogTail.length > 50) _overviewLogTail.shift();
713
+ updateOverviewLogTail();
714
+ if (!_logPaused && _currentPage === 'logs') appendLogLine(line);
715
+ };
716
+ _sseSource.onerror = function() {
717
+ document.getElementById('sb-dot').className = 'conn-dot err';
718
+ document.getElementById('sb-status').textContent = 'stream err';
719
+ };
720
+ }
721
+
722
+ /* ══════════════════════════════════════════
723
+ OVERVIEW
724
+ ══════════════════════════════════════════ */
725
+ function renderOverview() {
726
+ const c = document.getElementById('content');
727
+ const enabledCount = Array.isArray(_plugins.enabled) ? _plugins.enabled.length : 0;
728
+ const allCount = Array.isArray(_plugins.all) ? _plugins.all.length : Object.keys(_plugins.all||{}).length;
729
+
730
+ // Decide what the first two stat cards show: Todoist stats if configured, otherwise notes count + credentials count
731
+ const stat1Label = _todoistConfigured ? 'Tasks Today' : 'Notes';
732
+ const stat2Label = _todoistConfigured ? 'Overdue' : 'Credentials';
733
+
734
+ c.innerHTML = `
735
+ <div class="stat-grid" id="ov-stats">
736
+ <div class="stat-block">
737
+ <div class="stat-num" id="ov-tasks-today"><span style="color:var(--muted);font-size:16px;">…</span></div>
738
+ <div class="stat-label" id="ov-stat1-label">${esc(stat1Label)}</div>
739
+ </div>
740
+ <div class="stat-block">
741
+ <div class="stat-num" id="ov-overdue"><span style="color:var(--muted);font-size:16px;">…</span></div>
742
+ <div class="stat-label" id="ov-stat2-label">${esc(stat2Label)}</div>
743
+ </div>
744
+ <div class="stat-block">
745
+ <div class="stat-num">${esc(enabledCount)}<span style="font-size:14px;color:var(--muted)"> / ${esc(allCount)}</span></div>
746
+ <div class="stat-label">Plugins</div>
747
+ </div>
748
+ <div class="stat-block">
749
+ <div class="stat-num mono" id="ov-uptime" style="font-size:18px;padding-top:4px;"><span style="color:var(--muted);font-size:16px;">…</span></div>
750
+ <div class="stat-label">Uptime</div>
751
+ </div>
752
+ </div>
753
+
754
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px;">
755
+ <div class="section">
756
+ <div class="section-head"><span class="section-title">Memory</span><span class="mono" id="ov-mem-pct" style="font-size:10px;color:var(--muted);"><span style="color:var(--muted);">…</span></span></div>
757
+ <div class="section-body">
758
+ <div class="mono" id="ov-mem-label" style="font-size:11px;color:var(--text2);margin-bottom:6px;"><span style="color:var(--muted);">loading…</span></div>
759
+ <div class="prog-wrap"><div class="prog-fill" id="ov-mem-bar" style="width:0%"></div></div>
760
+ <div class="mono" id="ov-load-label" style="font-size:11px;color:var(--text2);margin-top:8px;"><span style="color:var(--muted);">loading…</span></div>
761
+ <div class="mono" id="ov-cpu-pct" style="font-size:11px;color:var(--text2);margin-top:4px;"></div>
762
+ </div>
763
+ </div>
764
+ <div class="section">
765
+ <div class="section-head"><span class="section-title">System</span></div>
766
+ <div class="section-body">
767
+ <table class="settings-kv" style="font-size:12px;">
768
+ <tr><td>Version</td><td class="mono" style="color:var(--text2);">${esc(_status.version||'--')}</td></tr>
769
+ <tr><td>Platform</td><td class="mono" style="color:var(--text2);">${esc(_status.platform||'--')}</td></tr>
770
+ <tr><td>Node</td><td class="mono" style="color:var(--text2);">${esc(_status.nodeVersion||_status.node_version||'--')}</td></tr>
771
+ <tr><td>AI</td><td><span class="badge badge-ok" style="font-size:10px;">${esc(_config.aiProvider||_status.aiProvider||'claude')}</span></td></tr>
772
+ </table>
773
+ </div>
774
+ </div>
775
+ </div>
776
+
777
+ <div class="quick-actions">
778
+ <button class="btn" onclick="ovScreenshot()">
779
+ <svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="14" rx="2"/><circle cx="12" cy="10" r="3"/></svg>
780
+ Screenshot
781
+ </button>
782
+ <button class="btn" onclick="ovClipboardRead()">
783
+ <svg viewBox="0 0 24 24"><path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg>
784
+ Clipboard
785
+ </button>
786
+ <button class="btn" onclick="navigate('system')">
787
+ <svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
788
+ Terminal
789
+ </button>
790
+ <button class="btn" onclick="ovNotify()">
791
+ <svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
792
+ Notify
793
+ </button>
794
+ </div>
795
+
796
+ <div id="ov-ss-result" style="display:none;margin-bottom:14px;">
797
+ <div class="section">
798
+ <div class="section-head"><span class="section-title">Screenshot</span><button class="btn btn-sm" onclick="document.getElementById('ov-ss-result').style.display='none'">Close</button></div>
799
+ <div class="section-body"><img id="ov-ss-img" style="max-width:100%;border-radius:var(--r);" src="" alt="screenshot"></div>
800
+ </div>
801
+ </div>
802
+
803
+ <div id="ov-clip-result" style="display:none;margin-bottom:14px;">
804
+ <div class="section">
805
+ <div class="section-head"><span class="section-title">Clipboard</span><button class="btn btn-sm" onclick="document.getElementById('ov-clip-result').style.display='none'">Close</button></div>
806
+ <div class="section-body mono" id="ov-clip-text" style="font-size:12px;white-space:pre-wrap;word-break:break-all;color:var(--text2);"></div>
807
+ </div>
808
+ </div>
809
+
810
+ <div id="ov-notify-form" style="display:none;margin-bottom:14px;">
811
+ <div class="section">
812
+ <div class="section-head"><span class="section-title">Send Notification</span></div>
813
+ <div class="section-body" style="display:flex;gap:8px;align-items:center;">
814
+ <input type="text" id="ov-notify-msg" placeholder="Notification message..." style="flex:1;">
815
+ <button class="btn btn-primary btn-sm" onclick="sendNotify()">Send</button>
816
+ <button class="btn btn-sm" onclick="document.getElementById('ov-notify-form').style.display='none'">Cancel</button>
817
+ </div>
818
+ </div>
819
+ </div>
820
+
821
+ <div class="section">
822
+ <div class="section-head">
823
+ <span class="section-title">Live Logs</span>
824
+ <span class="mono" style="font-size:10px;color:var(--ok);">live</span>
825
+ </div>
826
+ <div class="log-tail" id="ov-log-tail"></div>
827
+ </div>
828
+ `;
829
+
830
+ loadOverviewMetrics();
831
+ _overviewMetricsInterval = setInterval(loadOverviewMetrics, 5000);
832
+ _intervals.push(_overviewMetricsInterval);
833
+
834
+ if (_todoistConfigured) {
835
+ loadOverviewTaskStats();
836
+ } else {
837
+ loadOverviewFallbackStats();
838
+ }
839
+
840
+ // Pre-populate live logs with buffered history
841
+ updateOverviewLogTail();
842
+ }
843
+
844
+ async function loadOverviewMetrics() {
845
+ try {
846
+ const m = await api('GET','/api/system/metrics');
847
+ if (!m) return;
848
+ const memUsed = m.memory ? m.memory.used : null;
849
+ const memTotal = m.memory ? m.memory.total : null;
850
+ const pct = (memUsed && memTotal) ? Math.round(memUsed/memTotal*100) : 0;
851
+ if (m.uptime) _systemUptime = m.uptime;
852
+
853
+ const el1 = document.getElementById('ov-mem-label');
854
+ const el2 = document.getElementById('ov-mem-bar');
855
+ const el3 = document.getElementById('ov-load-label');
856
+ const el4 = document.getElementById('ov-mem-pct');
857
+ const elUp = document.getElementById('ov-uptime');
858
+
859
+ if (el1) el1.textContent = fmtBytes(memUsed)+' / '+fmtBytes(memTotal);
860
+ if (el2) el2.style.width = pct+'%';
861
+ if (el4) el4.textContent = pct+'%';
862
+ if (elUp) elUp.textContent = m.uptime ? fmtUptime(m.uptime) : '?';
863
+
864
+ if (el3) {
865
+ const la = m.loadAvg || [];
866
+ const fmt = v => typeof v === 'number' ? v.toFixed(2) : '--';
867
+ el3.textContent = 'Load avg: '+fmt(la[0])+' / '+fmt(la[1])+' / '+fmt(la[2]);
868
+ }
869
+ const cpuEl = document.getElementById('ov-cpu-pct');
870
+ if (cpuEl && m.cpu !== undefined) cpuEl.textContent = 'CPU: '+(typeof m.cpu === 'number' ? m.cpu.toFixed(1)+'%' : '--');
871
+ } catch(e) {
872
+ const elUp = document.getElementById('ov-uptime');
873
+ const el4 = document.getElementById('ov-mem-pct');
874
+ if (elUp) elUp.innerHTML = '<span style="font-size:11px;color:var(--err);">err</span>';
875
+ if (el4) el4.innerHTML = '<span style="color:var(--err);">err</span>';
876
+ }
877
+ }
878
+
879
+ async function loadOverviewTaskStats() {
880
+ try {
881
+ const tasks = await api('GET','/api/todoist/tasks');
882
+ const arr = Array.isArray(tasks) ? tasks : [];
883
+ const today = new Date(); today.setHours(0,0,0,0);
884
+ let todayCount = 0, overdueCount = 0;
885
+ arr.forEach(t => {
886
+ if (t.due && t.due.date) {
887
+ const due = new Date(t.due.date); due.setHours(0,0,0,0);
888
+ if (due.getTime() === today.getTime()) todayCount++;
889
+ if (due < today) overdueCount++;
890
+ }
891
+ });
892
+ const el1 = document.getElementById('ov-tasks-today');
893
+ const el2 = document.getElementById('ov-overdue');
894
+ if (el1) el1.textContent = todayCount;
895
+ if (el2) el2.textContent = overdueCount;
896
+ } catch(e) {
897
+ const el1 = document.getElementById('ov-tasks-today');
898
+ const el2 = document.getElementById('ov-overdue');
899
+ if (el1) el1.innerHTML = '<span style="font-size:12px;color:var(--err);">err</span>';
900
+ if (el2) el2.innerHTML = '<span style="font-size:12px;color:var(--err);">err</span>';
901
+ }
902
+ }
903
+
904
+ async function loadOverviewFallbackStats() {
905
+ // When Todoist is not configured, show notes count and credentials count instead
906
+ try {
907
+ const [notesData, credsData] = await Promise.allSettled([
908
+ api('GET','/api/notes'),
909
+ api('GET','/api/credentials'),
910
+ ]);
911
+ const el1 = document.getElementById('ov-tasks-today');
912
+ const el2 = document.getElementById('ov-overdue');
913
+ if (el1) {
914
+ const notes = notesData.status === 'fulfilled' && notesData.value
915
+ ? (Array.isArray(notesData.value.notes) ? notesData.value.notes.length : 0)
916
+ : 0;
917
+ el1.textContent = notes;
918
+ }
919
+ if (el2) {
920
+ const creds = credsData.status === 'fulfilled' && Array.isArray(credsData.value)
921
+ ? credsData.value.filter(c => c.hasValue).length
922
+ : _credentials.filter(c => c.hasValue).length;
923
+ el2.textContent = creds;
924
+ }
925
+ } catch(e) {
926
+ const el1 = document.getElementById('ov-tasks-today');
927
+ const el2 = document.getElementById('ov-overdue');
928
+ if (el1) el1.textContent = '0';
929
+ if (el2) el2.textContent = _credentials.filter(c => c.hasValue).length;
930
+ }
931
+ }
932
+
933
+ function updateOverviewLogTail() {
934
+ const el = document.getElementById('ov-log-tail');
935
+ if (!el) return;
936
+ el.innerHTML = _overviewLogTail.map(l => {
937
+ const lvlColor = l.level==='ERROR'?'var(--err)':l.level==='WARN'?'var(--warn)':'var(--muted)';
938
+ return `<div style="font-size:11px;"><span style="color:var(--muted)">${esc(l.time)}</span> <span style="color:${lvlColor}">[${esc(l.level)}]</span> <span style="color:var(--text2)">${esc(l.msg)}</span></div>`;
939
+ }).join('');
940
+ }
941
+
942
+ async function ovScreenshot() {
943
+ try {
944
+ const data = await api('GET','/api/system/screenshot');
945
+ if (data && (data.image || data.data)) {
946
+ const src = 'data:'+(data.mimeType||'image/png')+';base64,'+(data.image||data.data);
947
+ const img = document.getElementById('ov-ss-img');
948
+ const wrap = document.getElementById('ov-ss-result');
949
+ if (img) img.src = src;
950
+ if (wrap) wrap.style.display = '';
951
+ toast('Screenshot captured');
952
+ } else { toast('Screenshot unavailable on this system', 'warn'); }
953
+ } catch(e) { toast(e.message, 'err'); }
954
+ }
955
+
956
+ async function ovClipboardRead() {
957
+ try {
958
+ const data = await api('GET','/api/system/clipboard');
959
+ const text = data && (data.text || '');
960
+ const wrap = document.getElementById('ov-clip-result');
961
+ const el = document.getElementById('ov-clip-text');
962
+ if (el) el.textContent = text || '(empty)';
963
+ if (wrap) wrap.style.display = '';
964
+ toast('Clipboard read');
965
+ } catch(e) { toast(e.message, 'err'); }
966
+ }
967
+
968
+ function ovNotify() {
969
+ const form = document.getElementById('ov-notify-form');
970
+ if (form) {
971
+ form.style.display = '';
972
+ const inp = document.getElementById('ov-notify-msg');
973
+ if (inp) inp.focus();
974
+ }
975
+ }
976
+
977
+ async function sendNotify() {
978
+ const inp = document.getElementById('ov-notify-msg');
979
+ if (!inp || !inp.value.trim()) return;
980
+ try {
981
+ await api('POST','/api/system/notify',{title:'Conductor',message:inp.value.trim()});
982
+ toast('Notification sent');
983
+ inp.value = '';
984
+ document.getElementById('ov-notify-form').style.display = 'none';
985
+ } catch(e) { toast(e.message, 'err'); }
986
+ }
987
+
988
+ /* ══════════════════════════════════════════
989
+ TASKS
990
+ ══════════════════════════════════════════ */
991
+ async function renderTasks() {
992
+ const c = document.getElementById('content');
993
+ if (!_todoistConfigured) {
994
+ c.innerHTML = `<div class="section"><div class="section-body"><div class="empty">Todoist not configured. Set your API token in <a href="#" onclick="navigate('credentials');return false;" style="color:var(--accent);">Credentials</a>.</div></div></div>`;
995
+ return;
996
+ }
997
+ c.innerHTML = '<div class="empty">Loading tasks...</div>';
998
+ try {
999
+ const [projects, tasks] = await Promise.all([
1000
+ api('GET','/api/todoist/projects').catch(() => []),
1001
+ api('GET','/api/todoist/tasks')
1002
+ ]);
1003
+ _taskProjects = Array.isArray(projects) ? projects : [];
1004
+ _allTasks = Array.isArray(tasks) ? tasks : [];
1005
+ renderTasksUI();
1006
+ } catch(e) {
1007
+ c.innerHTML = `<div class="section"><div class="section-body" style="color:var(--err);font-family:var(--mono);font-size:12px;">Error: ${esc(e.message)}</div></div>`;
1008
+ }
1009
+ }
1010
+
1011
+ function renderTasksUI() {
1012
+ const c = document.getElementById('content');
1013
+ const projOpts = _taskProjects.map(p =>
1014
+ `<option value="${esc(p.id)}" ${_taskProjectFilter===String(p.id)?'selected':''}>${esc(p.name)}</option>`
1015
+ ).join('');
1016
+
1017
+ c.innerHTML = `
1018
+ <div style="display:flex;gap:8px;margin-bottom:12px;align-items:center;flex-wrap:wrap;">
1019
+ <input type="text" id="new-task-input" placeholder="New task content..." style="flex:1;min-width:200px;">
1020
+ <input type="text" id="new-task-due" placeholder="Due (e.g. tomorrow)" style="width:140px;">
1021
+ <select id="new-task-priority" style="width:72px;" title="Priority">
1022
+ <option value="1">P4</option>
1023
+ <option value="2">P3</option>
1024
+ <option value="3">P2</option>
1025
+ <option value="4">P1</option>
1026
+ </select>
1027
+ <button class="btn btn-primary" onclick="addTask()">Add Task</button>
1028
+ </div>
1029
+ <div style="display:flex;gap:8px;margin-bottom:12px;align-items:center;flex-wrap:wrap;">
1030
+ <select id="task-proj-sel" style="min-width:150px;">
1031
+ <option value="all" ${_taskProjectFilter==='all'?'selected':''}>All Projects</option>
1032
+ ${projOpts}
1033
+ </select>
1034
+ <input type="text" id="task-search" placeholder="Search tasks..." value="${esc(_taskSearch)}" style="flex:1;min-width:120px;">
1035
+ <div style="display:flex;gap:4px;">
1036
+ ${['all','p1','p2','p3'].map(p => `<button class="btn btn-sm ${_taskPriorityFilter===p?'btn-primary':''}" data-pf="${p}" onclick="setPriFilter(this,'${p}')">${p.toUpperCase()}</button>`).join('')}
1037
+ </div>
1038
+ </div>
1039
+ <div id="task-bulk-bar" style="display:none;padding:6px 8px;background:var(--elevated);border:1px solid var(--border);border-radius:var(--r);margin-bottom:8px;gap:8px;align-items:center;">
1040
+ <span id="task-sel-count" class="mono" style="font-size:11px;color:var(--muted);"></span>
1041
+ <button class="btn btn-sm btn-primary" onclick="bulkComplete()">Complete Selected</button>
1042
+ <button class="btn btn-sm btn-danger" onclick="bulkDelete()">Delete Selected</button>
1043
+ <button class="btn btn-sm" onclick="clearTaskSelection()">Clear</button>
1044
+ </div>
1045
+ <div id="task-list-wrap"></div>
1046
+ `;
1047
+ document.getElementById('task-proj-sel').addEventListener('change',function(){_taskProjectFilter=this.value;renderTaskList();});
1048
+ document.getElementById('task-search').addEventListener('input',function(){_taskSearch=this.value;renderTaskList();});
1049
+ document.getElementById('new-task-input').addEventListener('keydown',function(e){if(e.key==='Enter')addTask();});
1050
+ renderTaskList();
1051
+ }
1052
+
1053
+ function setPriFilter(btn, val) {
1054
+ _taskPriorityFilter = val;
1055
+ document.querySelectorAll('[data-pf]').forEach(b => {
1056
+ b.className = 'btn btn-sm' + (b.dataset.pf === val ? ' btn-primary' : '');
1057
+ });
1058
+ renderTaskList();
1059
+ }
1060
+
1061
+ function filteredTasks() {
1062
+ return _allTasks.filter(t => {
1063
+ if (_taskProjectFilter !== 'all' && String(t.project_id) !== _taskProjectFilter) return false;
1064
+ if (_taskSearch && !t.content.toLowerCase().includes(_taskSearch.toLowerCase())) return false;
1065
+ if (_taskPriorityFilter !== 'all') {
1066
+ const pmap = {p1:4,p2:3,p3:2};
1067
+ if (t.priority !== pmap[_taskPriorityFilter]) return false;
1068
+ }
1069
+ return true;
1070
+ });
1071
+ }
1072
+
1073
+ function renderTaskList() {
1074
+ const wrap = document.getElementById('task-list-wrap');
1075
+ if (!wrap) return;
1076
+ const tasks = filteredTasks();
1077
+ if (!tasks.length) {
1078
+ wrap.innerHTML = '<div class="empty">No tasks</div>';
1079
+ return;
1080
+ }
1081
+ wrap.innerHTML = `<table class="data-table">
1082
+ <thead><tr>
1083
+ <th style="width:28px;"></th>
1084
+ <th style="width:44px;">PRI</th>
1085
+ <th>CONTENT</th>
1086
+ <th style="width:100px;">DUE</th>
1087
+ <th style="width:120px;">PROJECT</th>
1088
+ <th style="width:70px;"></th>
1089
+ </tr></thead>
1090
+ <tbody>${tasks.map(t => taskRowHTML(t)).join('')}</tbody>
1091
+ </table>`;
1092
+ }
1093
+
1094
+ function taskRowHTML(t) {
1095
+ const pLabel = priorityLabel(t.priority);
1096
+ const priColor = t.priority===4?'var(--err)':t.priority===3?'var(--warn)':t.priority===2?'var(--accent)':'var(--muted)';
1097
+ const dueDate = t.due ? t.due.date : null;
1098
+ const dc = dueDate ? dueBadgeClass(dueDate) : null;
1099
+ const dueColor = dc==='overdue'?'color:var(--err);font-weight:600;':dc==='today'?'color:var(--warn);':'';
1100
+ const proj = _taskProjects.find(p => String(p.id) === String(t.project_id));
1101
+ const projName = proj ? proj.name : '';
1102
+ return `<tr data-task-id="${esc(t.id)}">
1103
+ <td style="text-align:center;">
1104
+ <div style="display:flex;gap:4px;align-items:center;justify-content:center;">
1105
+ <input type="checkbox" style="width:12px;height:12px;" onchange="toggleTaskSelect('${esc(t.id)}',this.checked)" ${_selectedTaskIds.has(String(t.id))?'checked':''}>
1106
+ <span style="cursor:pointer;font-size:14px;color:var(--border);display:inline-block;width:16px;height:16px;border:1.5px solid var(--border);border-radius:3px;line-height:14px;text-align:center;" id="chk-${esc(t.id)}" onclick="completeTask('${esc(t.id)}',this)"> </span>
1107
+ </div>
1108
+ </td>
1109
+ <td class="mono" style="font-size:11px;color:${priColor};font-weight:600;">${esc(pLabel)}</td>
1110
+ <td style="cursor:pointer;" onclick="expandTask('${esc(t.id)}')">${esc(t.content)}</td>
1111
+ <td class="mono" style="font-size:11px;${dueColor}">${dueDate?esc(relativeDate(dueDate)):''}</td>
1112
+ <td style="font-size:11px;color:var(--muted);">${esc(projName)}</td>
1113
+ <td style="text-align:right;" id="tdel-${esc(t.id)}">
1114
+ <button class="btn btn-sm btn-danger" onclick="promptDeleteTask('${esc(t.id)}')">Del</button>
1115
+ </td>
1116
+ </tr>
1117
+ <tr id="texp-${esc(t.id)}" style="display:none;">
1118
+ <td colspan="6" style="padding:10px 12px;background:var(--elevated);" id="texp-td-${esc(t.id)}">
1119
+ <div style="font-size:12px;margin-bottom:8px;color:var(--text);">${esc(t.content)}</div>
1120
+ ${t.description ? `<div style="font-size:11px;color:var(--muted);margin-bottom:8px;font-family:var(--mono);">${esc(t.description)}</div>` : ''}
1121
+ <div style="display:flex;gap:6px;">
1122
+ <button class="btn btn-sm btn-primary" onclick="completeTask('${esc(t.id)}')">Complete</button>
1123
+ <button class="btn btn-sm" onclick="showEditTask('${esc(t.id)}')">Edit</button>
1124
+ <button class="btn btn-sm btn-danger" onclick="promptDeleteTask('${esc(t.id)}')">Delete</button>
1125
+ <button class="btn btn-sm" onclick="collapseTask('${esc(t.id)}')">Close</button>
1126
+ </div>
1127
+ </td>
1128
+ </tr>`;
1129
+ }
1130
+
1131
+ function expandTask(id) {
1132
+ const row = document.getElementById('texp-'+id);
1133
+ if (row) row.style.display = row.style.display === 'none' ? '' : 'none';
1134
+ }
1135
+ function collapseTask(id) {
1136
+ const row = document.getElementById('texp-'+id);
1137
+ if (row) row.style.display = 'none';
1138
+ }
1139
+
1140
+ function showEditTask(id) {
1141
+ const task = _allTasks.find(t => String(t.id) === String(id));
1142
+ if (!task) return;
1143
+ const td = document.getElementById('texp-td-'+id);
1144
+ if (!td) return;
1145
+ const projOpts = _taskProjects.map(p =>
1146
+ `<option value="${esc(String(p.id))}" ${String(p.id)===String(task.project_id)?'selected':''}>${esc(p.name)}</option>`
1147
+ ).join('');
1148
+ const dueVal = task.due && task.due.date ? task.due.date : '';
1149
+ td.innerHTML = `
1150
+ <div style="display:flex;flex-direction:column;gap:8px;padding:2px 0;">
1151
+ <input type="text" id="tedit-content-${esc(id)}" value="${esc(task.content)}" style="width:100%;font-size:13px;">
1152
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
1153
+ <select id="tedit-pri-${esc(id)}" style="width:72px;" title="Priority">
1154
+ <option value="1" ${task.priority===1?'selected':''}>P4</option>
1155
+ <option value="2" ${task.priority===2?'selected':''}>P3</option>
1156
+ <option value="3" ${task.priority===3?'selected':''}>P2</option>
1157
+ <option value="4" ${task.priority===4?'selected':''}>P1</option>
1158
+ </select>
1159
+ <select id="tedit-proj-${esc(id)}" style="min-width:130px;">${projOpts}</select>
1160
+ <input type="date" id="tedit-due-${esc(id)}" value="${esc(dueVal)}" style="width:140px;" title="Due date">
1161
+ </div>
1162
+ <div style="display:flex;gap:6px;">
1163
+ <button class="btn btn-sm btn-primary" onclick="saveEditTask('${esc(id)}')">Save</button>
1164
+ <button class="btn btn-sm" onclick="cancelEditTask('${esc(id)}')">Cancel</button>
1165
+ </div>
1166
+ </div>`;
1167
+ const inp = document.getElementById('tedit-content-'+id);
1168
+ if (inp) { inp.focus(); inp.select(); }
1169
+ }
1170
+
1171
+ async function saveEditTask(id) {
1172
+ const contentEl = document.getElementById('tedit-content-'+id);
1173
+ const priEl = document.getElementById('tedit-pri-'+id);
1174
+ const projEl = document.getElementById('tedit-proj-'+id);
1175
+ const dueEl = document.getElementById('tedit-due-'+id);
1176
+ if (!contentEl) return;
1177
+ const body = {
1178
+ content: contentEl.value.trim(),
1179
+ priority: parseInt(priEl ? priEl.value : '1', 10),
1180
+ };
1181
+ if (projEl && projEl.value) body.project_id = projEl.value;
1182
+ if (dueEl && dueEl.value) body.due_date = dueEl.value;
1183
+ try {
1184
+ const updated = await api('POST','/api/todoist/tasks/'+id, body);
1185
+ const idx = _allTasks.findIndex(t => String(t.id) === String(id));
1186
+ if (idx !== -1) {
1187
+ if (updated && updated.id) _allTasks[idx] = updated;
1188
+ else {
1189
+ _allTasks[idx].content = body.content;
1190
+ _allTasks[idx].priority = body.priority;
1191
+ if (body.project_id) _allTasks[idx].project_id = body.project_id;
1192
+ if (body.due_date) _allTasks[idx].due = {date: body.due_date};
1193
+ }
1194
+ }
1195
+ toast('Task updated');
1196
+ renderTaskList();
1197
+ } catch(e) { toast(e.message, 'err'); }
1198
+ }
1199
+
1200
+ function cancelEditTask(id) {
1201
+ renderTaskList();
1202
+ const expRow = document.getElementById('texp-'+id);
1203
+ if (expRow) expRow.style.display = '';
1204
+ }
1205
+
1206
+ async function completeTask(id, el) {
1207
+ if (el) { el.textContent = '✓'; el.style.color = 'var(--ok)'; el.style.borderColor = 'var(--ok)'; }
1208
+ try {
1209
+ await api('POST','/api/todoist/tasks/'+id+'/close');
1210
+ _allTasks = _allTasks.filter(t => String(t.id) !== String(id));
1211
+ const tr = document.querySelector(`tr[data-task-id="${id}"]`);
1212
+ const trExp = document.getElementById('texp-'+id);
1213
+ if (tr) tr.remove();
1214
+ if (trExp) trExp.remove();
1215
+ // If no filtered tasks remain, re-render to show empty state (hides table headers)
1216
+ if (!filteredTasks().length) renderTaskList();
1217
+ toast('Task completed');
1218
+ } catch(e) {
1219
+ if (el) { el.textContent = ' '; el.style.color = ''; el.style.borderColor = ''; }
1220
+ toast(e.message, 'err');
1221
+ }
1222
+ }
1223
+
1224
+ function promptDeleteTask(id) {
1225
+ // Collapse detail panel to avoid showing both confirmation and expansion simultaneously
1226
+ collapseTask(id);
1227
+ const wrap = document.getElementById('tdel-'+id);
1228
+ if (!wrap) return;
1229
+ wrap.innerHTML = `<span class="confirm-wrap" style="font-size:11px;">
1230
+ <button class="btn btn-sm btn-danger" onclick="confirmDeleteTask('${esc(id)}')">Confirm</button>
1231
+ <button class="btn btn-sm" onclick="cancelDeleteTask('${esc(id)}')">Cancel</button>
1232
+ </span>`;
1233
+ }
1234
+ function cancelDeleteTask(id) {
1235
+ const wrap = document.getElementById('tdel-'+id);
1236
+ if (wrap) wrap.innerHTML = `<button class="btn btn-sm btn-danger" onclick="promptDeleteTask('${esc(id)}')">Del</button>`;
1237
+ }
1238
+ async function confirmDeleteTask(id) {
1239
+ try {
1240
+ await api('DELETE','/api/todoist/tasks/'+id);
1241
+ _allTasks = _allTasks.filter(t => String(t.id) !== String(id));
1242
+ const tr = document.querySelector(`tr[data-task-id="${id}"]`);
1243
+ const trExp = document.getElementById('texp-'+id);
1244
+ if (tr) tr.remove();
1245
+ if (trExp) trExp.remove();
1246
+ // If no filtered tasks remain, re-render to show empty state (hides table headers)
1247
+ if (!filteredTasks().length) renderTaskList();
1248
+ toast('Task deleted');
1249
+ } catch(e) { toast(e.message, 'err'); }
1250
+ }
1251
+
1252
+ async function addTask() {
1253
+ const inp = document.getElementById('new-task-input');
1254
+ const dueInp = document.getElementById('new-task-due');
1255
+ const priSel = document.getElementById('new-task-priority');
1256
+ if (!inp || !inp.value.trim()) return;
1257
+ const body = {content: inp.value.trim()};
1258
+ if (dueInp && dueInp.value.trim()) body.due_string = dueInp.value.trim();
1259
+ const priority = priSel ? parseInt(priSel.value, 10) : 1;
1260
+ if (priority > 1) body.priority = priority; // only send if not default P4 (=1)
1261
+ try {
1262
+ const task = await api('POST','/api/todoist/tasks',body);
1263
+ inp.value = '';
1264
+ if (dueInp) dueInp.value = '';
1265
+ if (task) { _allTasks.unshift(task); renderTaskList(); toast('Task added'); }
1266
+ } catch(e) { toast(e.message, 'err'); }
1267
+ }
1268
+
1269
+ /* ══════════════════════════════════════════
1270
+ FILES
1271
+ ══════════════════════════════════════════ */
1272
+ function renderFiles() {
1273
+ const c = document.getElementById('content');
1274
+ c.style.padding = '0';
1275
+ c.innerHTML = `
1276
+ <div id="file-bar" style="padding:6px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--surface);flex-shrink:0;">
1277
+ <button onclick="loadFileList('')" style="background:none;border:none;padding:2px 4px;cursor:pointer;display:flex;align-items:center;border-radius:3px;flex-shrink:0;" title="Home directory">
1278
+ <svg viewBox="0 0 24 24" style="width:13px;height:13px;stroke:var(--muted);fill:none;stroke-width:1.5;"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
1279
+ </button>
1280
+ <span class="mono" id="file-breadcrumb" style="font-size:11px;flex:1;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">~</span>
1281
+ <label style="display:flex;align-items:center;gap:5px;font-size:11px;color:var(--muted);cursor:pointer;flex-shrink:0;">
1282
+ <input type="checkbox" id="file-hidden-toggle" ${_fileHidden?'checked':''} onchange="_fileHidden=this.checked;loadFileList(_filePath);">
1283
+ Hidden
1284
+ </label>
1285
+ <button class="btn btn-sm" onclick="showNewFileForm()">
1286
+ <svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2;flex-shrink:0;"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
1287
+ New File
1288
+ </button>
1289
+ </div>
1290
+ <div id="new-file-form" style="display:none;border-bottom:1px solid var(--border);">
1291
+ <div class="inline-input-row">
1292
+ <span style="font-size:11px;color:var(--muted);flex-shrink:0;">Filename:</span>
1293
+ <input type="text" id="new-file-name" placeholder="e.g. notes.txt" style="flex:1;font-family:var(--mono);font-size:12px;" onkeydown="if(event.key==='Enter')confirmNewFile();if(event.key==='Escape')hideNewFileForm();">
1294
+ <button class="btn btn-sm btn-primary" onclick="confirmNewFile()">Create</button>
1295
+ <button class="btn btn-sm" onclick="hideNewFileForm()">Cancel</button>
1296
+ </div>
1297
+ </div>
1298
+ <div class="file-cols" style="height:calc(100vh - 82px);">
1299
+ <div class="file-list-pane" id="file-list-pane"><div class="empty">Loading...</div></div>
1300
+ <div class="file-viewer" id="file-viewer"><div class="empty">Select a file to view</div></div>
1301
+ </div>
1302
+ `;
1303
+ loadFileList(_filePath || '');
1304
+ }
1305
+
1306
+ function showNewFileForm() {
1307
+ const f = document.getElementById('new-file-form');
1308
+ if (f) { f.style.display = ''; document.getElementById('new-file-name').focus(); }
1309
+ }
1310
+ function hideNewFileForm() {
1311
+ const f = document.getElementById('new-file-form');
1312
+ if (f) f.style.display = 'none';
1313
+ }
1314
+ function confirmNewFile() {
1315
+ const inp = document.getElementById('new-file-name');
1316
+ const name = inp ? inp.value.trim() : '';
1317
+ if (!name) { toast('Enter a file name', 'warn'); return; }
1318
+ const fullPath = (_filePath ? (_filePath.endsWith('/')?_filePath:_filePath+'/') : '') + name;
1319
+ api('POST','/api/fs/write',{path:fullPath, content:''})
1320
+ .then(() => { hideNewFileForm(); loadFileList(_filePath); toast('File created'); })
1321
+ .catch(e => toast(e.message, 'err'));
1322
+ }
1323
+
1324
+ async function loadFileList(dirPath) {
1325
+ _filePath = dirPath;
1326
+ const pane = document.getElementById('file-list-pane');
1327
+ if (!pane) return;
1328
+ pane.innerHTML = '<div class="empty">Loading...</div>';
1329
+ try {
1330
+ const qs = dirPath ? '?path='+encodeURIComponent(dirPath) : '';
1331
+ const data = await api('GET','/api/fs/list'+qs);
1332
+ // Use the resolved absolute path returned by the server
1333
+ if (data && data.path) {
1334
+ _filePath = data.path;
1335
+ // Capture home dir on first empty-path load
1336
+ if (!dirPath || dirPath === '' || dirPath === '~') _homeDir = data.path;
1337
+ }
1338
+ const breadcrumb = document.getElementById('file-breadcrumb');
1339
+ if (breadcrumb) {
1340
+ const absPath = _filePath;
1341
+ const displayPath = pathToDisplay(absPath);
1342
+ const absParts = absPath.split('/').filter(Boolean);
1343
+ let html = '';
1344
+ if (displayPath.startsWith('~')) {
1345
+ html += `<span style="cursor:pointer;color:var(--accent);" onclick="loadFileList('')">~</span>`;
1346
+ const homeDepth = _homeDir.split('/').filter(Boolean).length;
1347
+ const relParts = absParts.slice(homeDepth);
1348
+ relParts.forEach((part, i) => {
1349
+ const targetPath = _homeDir + '/' + relParts.slice(0, i+1).join('/');
1350
+ const isLast = i === relParts.length - 1;
1351
+ html += '<span style="color:var(--muted);margin:0 2px;">/</span>';
1352
+ if (isLast) html += `<span style="color:var(--text);">${esc(part)}</span>`;
1353
+ else html += `<span style="cursor:pointer;color:var(--accent);" onclick="loadFileList('${esc(targetPath)}')">${esc(part)}</span>`;
1354
+ });
1355
+ } else {
1356
+ html += '<span style="color:var(--muted);">/</span>';
1357
+ absParts.forEach((part, i) => {
1358
+ const targetPath = '/' + absParts.slice(0, i+1).join('/');
1359
+ const isLast = i === absParts.length - 1;
1360
+ if (i > 0) html += '<span style="color:var(--muted);margin:0 2px;">/</span>';
1361
+ if (isLast) html += `<span style="color:var(--text);">${esc(part)}</span>`;
1362
+ else html += `<span style="cursor:pointer;color:var(--accent);" onclick="loadFileList('${esc(targetPath)}')">${esc(part)}</span>`;
1363
+ });
1364
+ }
1365
+ breadcrumb.innerHTML = html;
1366
+ }
1367
+ let items = (data && Array.isArray(data.entries)) ? data.entries : [];
1368
+ if (!_fileHidden) items = items.filter(f => !(f.name||'').startsWith('.'));
1369
+ items.sort((a,b) => {
1370
+ if (a.type==='dir' && b.type!=='dir') return -1;
1371
+ if (a.type!=='dir' && b.type==='dir') return 1;
1372
+ return (a.name||'').localeCompare(b.name||'');
1373
+ });
1374
+ let html = '';
1375
+ // Show parent link for any directory that has a parent (not root)
1376
+ const curPath = _filePath;
1377
+ if (curPath && curPath !== '/') {
1378
+ const parent = curPath.replace(/\/[^/]+\/?$/, '') || '/';
1379
+ html += `<div class="file-item" onclick="loadFileList('${esc(parent)}')">
1380
+ <span class="file-icon" style="color:var(--accent);">..</span>
1381
+ <span class="file-name" style="color:var(--muted);">Parent directory</span>
1382
+ </div>`;
1383
+ }
1384
+ html += items.map(f => {
1385
+ const name = f.name || '';
1386
+ const isDir = f.type === 'dir';
1387
+ const size = isDir ? '' : fmtFileSize(f.size);
1388
+ const fullPath = curPath.endsWith('/') ? curPath + name : curPath + '/' + name;
1389
+ if (isDir) {
1390
+ return `<div class="file-item" onclick="loadFileList('${esc(fullPath)}')">
1391
+ <span class="file-icon" style="color:var(--accent);">
1392
+ <svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.5;"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
1393
+ </span>
1394
+ <span class="file-name">${esc(name)}</span>
1395
+ </div>`;
1396
+ }
1397
+ return `<div class="file-item" onclick="loadFileContent('${esc(fullPath)}',this)" title="${esc(fullPath)}">
1398
+ <span class="file-icon">
1399
+ <svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.5;"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
1400
+ </span>
1401
+ <span class="file-name">${esc(name)}</span>
1402
+ <span class="file-size">${esc(size)}</span>
1403
+ </div>`;
1404
+ }).join('');
1405
+ pane.innerHTML = html || '<div class="empty">Empty directory</div>';
1406
+ } catch(e) {
1407
+ pane.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
1408
+ }
1409
+ }
1410
+
1411
+ async function loadFileContent(path, el) {
1412
+ _selectedFile = path;
1413
+ document.querySelectorAll('.file-item').forEach(i => i.classList.remove('selected'));
1414
+ if (el) el.classList.add('selected');
1415
+ const viewer = document.getElementById('file-viewer');
1416
+ if (!viewer) return;
1417
+ viewer.innerHTML = '<div class="empty">Loading...</div>';
1418
+ try {
1419
+ const data = await api('GET','/api/fs/read?path='+encodeURIComponent(path));
1420
+ const content = data && (data.content || '');
1421
+ const fileName = path.split('/').pop() || path;
1422
+ viewer.innerHTML = `
1423
+ <div style="padding:8px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--elevated);flex-shrink:0;">
1424
+ <span class="mono" style="font-size:11px;color:var(--text2);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(fileName)}</span>
1425
+ <button class="btn btn-sm btn-primary" onclick="startEditFile('${esc(path)}')">Edit</button>
1426
+ <button class="btn btn-sm btn-danger" id="fdel-btn" onclick="promptDeleteFile('${esc(path)}')">Delete</button>
1427
+ </div>
1428
+ <pre class="file-content" id="file-content-view">${esc(content)}</pre>`;
1429
+ } catch(e) {
1430
+ viewer.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
1431
+ }
1432
+ }
1433
+
1434
+ function startEditFile(path) {
1435
+ const view = document.getElementById('file-content-view');
1436
+ if (!view) return;
1437
+ const content = view.textContent;
1438
+ const viewer = document.getElementById('file-viewer');
1439
+ const fileName = path.split('/').pop() || path;
1440
+ viewer.innerHTML = `
1441
+ <div style="padding:8px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--elevated);flex-shrink:0;">
1442
+ <span class="mono" style="font-size:11px;color:var(--text2);flex:1;">${esc(fileName)}</span>
1443
+ <button class="btn btn-sm btn-primary" onclick="saveEditFile('${esc(path)}')">Save</button>
1444
+ <button class="btn btn-sm" onclick="loadFileContent('${esc(path)}',null)">Cancel</button>
1445
+ </div>
1446
+ <textarea id="file-edit-area" style="flex:1;width:100%;height:calc(100% - 40px);background:var(--bg);border:none;outline:none;color:var(--text);font-family:var(--mono);font-size:12px;padding:12px;resize:none;line-height:1.6;">${esc(content)}</textarea>`;
1447
+ const ta = document.getElementById('file-edit-area');
1448
+ if (ta) ta.focus();
1449
+ }
1450
+
1451
+ async function saveEditFile(path) {
1452
+ const ta = document.getElementById('file-edit-area');
1453
+ if (!ta) return;
1454
+ try {
1455
+ await api('POST','/api/fs/write',{path, content:ta.value});
1456
+ toast('File saved');
1457
+ loadFileContent(path, null);
1458
+ } catch(e) { toast(e.message, 'err'); }
1459
+ }
1460
+
1461
+ function promptDeleteFile(path) {
1462
+ const btn = document.getElementById('fdel-btn');
1463
+ if (!btn) return;
1464
+ const fileName = path.split('/').pop() || path;
1465
+ btn.outerHTML = `<span style="display:flex;align-items:center;gap:5px;font-size:11px;">
1466
+ <span style="color:var(--muted);">Delete ${esc(fileName)}?</span>
1467
+ <button class="btn btn-sm btn-danger" onclick="confirmDeleteFile('${esc(path)}')">Yes</button>
1468
+ <button class="btn btn-sm" onclick="loadFileContent('${esc(path)}',null)">No</button>
1469
+ </span>`;
1470
+ }
1471
+
1472
+ async function confirmDeleteFile(path) {
1473
+ try {
1474
+ await api('POST','/api/fs/delete',{path});
1475
+ toast('File deleted');
1476
+ document.getElementById('file-viewer').innerHTML = '<div class="empty">Select a file to view</div>';
1477
+ loadFileList(_filePath);
1478
+ } catch(e) { toast(e.message, 'err'); }
1479
+ }
1480
+
1481
+ /* ══════════════════════════════════════════
1482
+ PROCESSES
1483
+ ══════════════════════════════════════════ */
1484
+ let _procSearch = '';
1485
+ let _procSort = {col:'cpu',dir:-1};
1486
+ async function renderProcesses() {
1487
+ const c = document.getElementById('content');
1488
+ c.innerHTML = `
1489
+ <div style="display:flex;gap:8px;margin-bottom:14px;align-items:center;flex-wrap:wrap;">
1490
+ <button class="btn" onclick="loadProcesses()">
1491
+ <svg viewBox="0 0 24 24" style="width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:1.5;"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
1492
+ Refresh
1493
+ </button>
1494
+ <button class="btn" id="proc-auto-btn" onclick="toggleProcAuto()">Auto-refresh: ${_procAutoRefresh?'On':'Off'}</button>
1495
+ <input type="text" id="proc-search" placeholder="Filter by command..." value="${esc(_procSearch)}" style="flex:1;min-width:140px;" oninput="_procSearch=this.value;filterProcTable();">
1496
+ <span class="mono" style="font-size:11px;color:var(--muted);" id="proc-status"></span>
1497
+ </div>
1498
+ <div id="proc-table-wrap"><div class="empty">Loading processes...</div></div>
1499
+ `;
1500
+ loadProcesses();
1501
+ if (_procAutoRefresh) {
1502
+ _procInterval = setInterval(loadProcesses, 5000);
1503
+ _intervals.push(_procInterval);
1504
+ }
1505
+ }
1506
+
1507
+ function toggleProcAuto() {
1508
+ _procAutoRefresh = !_procAutoRefresh;
1509
+ const btn = document.getElementById('proc-auto-btn');
1510
+ if (btn) btn.textContent = 'Auto-refresh: '+(_procAutoRefresh?'On':'Off');
1511
+ if (_procAutoRefresh) {
1512
+ _procInterval = setInterval(loadProcesses, 5000);
1513
+ _intervals.push(_procInterval);
1514
+ } else {
1515
+ if (_procInterval) { clearInterval(_procInterval); _procInterval = null; }
1516
+ }
1517
+ }
1518
+
1519
+ async function loadProcesses() {
1520
+ const wrap = document.getElementById('proc-table-wrap');
1521
+ const status = document.getElementById('proc-status');
1522
+ if (!wrap) return;
1523
+ try {
1524
+ const data = await api('GET','/api/system/processes/detail');
1525
+ const procs = data && Array.isArray(data.processes) ? data.processes : [];
1526
+ if (status) status.textContent = procs.length+' processes — '+nowHHMMSS();
1527
+ if (!procs.length) {
1528
+ wrap.innerHTML = '<div class="empty">No processes found</div>';
1529
+ return;
1530
+ }
1531
+ const sortArrow = (col) => _procSort.col===col ? (_procSort.dir>0?'↑':'↓') : '';
1532
+ procs.sort((a,b) => {
1533
+ let av, bv;
1534
+ if (_procSort.col==='cpu') { av=parseFloat(a.cpu||0); bv=parseFloat(b.cpu||0); }
1535
+ else if (_procSort.col==='mem') { av=parseFloat(a.mem||0); bv=parseFloat(b.mem||0); }
1536
+ else if (_procSort.col==='pid') { av=parseInt(a.pid||0); bv=parseInt(b.pid||0); }
1537
+ else if (_procSort.col==='user') { av=(a.user||'').toLowerCase(); bv=(b.user||'').toLowerCase(); }
1538
+ else { av=(a.command||a.cmd||'').toLowerCase(); bv=(b.command||b.cmd||'').toLowerCase(); }
1539
+ return av < bv ? -_procSort.dir : av > bv ? _procSort.dir : 0;
1540
+ });
1541
+ wrap.innerHTML = `<table class="data-table" id="proc-table">
1542
+ <thead><tr>
1543
+ <th style="width:64px;cursor:pointer;" onclick="sortProcs('pid')">PID ${sortArrow('pid')}</th>
1544
+ <th style="width:80px;cursor:pointer;" onclick="sortProcs('user')">USER ${sortArrow('user')}</th>
1545
+ <th style="width:56px;cursor:pointer;" onclick="sortProcs('cpu')">CPU% ${sortArrow('cpu')}</th>
1546
+ <th style="width:56px;cursor:pointer;" onclick="sortProcs('mem')">MEM% ${sortArrow('mem')}</th>
1547
+ <th style="cursor:pointer;" onclick="sortProcs('cmd')">COMMAND ${sortArrow('cmd')}</th>
1548
+ <th style="width:80px;"></th>
1549
+ </tr></thead>
1550
+ <tbody>${procs.map(p => {
1551
+ const cpu = parseFloat(p.cpu || 0);
1552
+ const mem = parseFloat(p.mem || 0);
1553
+ const fullCmd = String(p.command || p.cmd || '');
1554
+ const cmd = fullCmd.substring(0,60);
1555
+ const pid = p.pid || '';
1556
+ const user = p.user || '';
1557
+ const cpuStyle = cpu > 20 ? 'font-weight:600;color:var(--warn);' : '';
1558
+ const rowStyle = cpu > 20 ? 'background:rgba(245,158,11,0.06);' : '';
1559
+ return `<tr style="${rowStyle}">
1560
+ <td class="mono" style="font-size:11px;">${esc(String(pid))}</td>
1561
+ <td style="font-size:11px;color:var(--text2);">${esc(user)}</td>
1562
+ <td class="mono" style="font-size:11px;${cpuStyle}">${cpu.toFixed(1)}</td>
1563
+ <td class="mono" style="font-size:11px;">${mem.toFixed(1)}</td>
1564
+ <td class="mono" style="font-size:11px;color:var(--text2);" title="${esc(fullCmd)}">${esc(cmd)}${fullCmd.length > 60 ? '…' : ''}</td>
1565
+ <td style="text-align:right;" id="pkill-${esc(String(pid))}">
1566
+ <button class="btn btn-sm btn-danger" onclick="promptKill('${esc(String(pid))}')">Kill</button>
1567
+ </td>
1568
+ </tr>`;
1569
+ }).join('')}</tbody>
1570
+ </table>`;
1571
+ // Re-apply any active search filter after re-render
1572
+ filterProcTable();
1573
+ } catch(e) {
1574
+ if (wrap) wrap.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
1575
+ }
1576
+ }
1577
+
1578
+ function promptKill(pid) {
1579
+ const wrap = document.getElementById('pkill-'+pid);
1580
+ if (!wrap) return;
1581
+ wrap.innerHTML = `<span class="confirm-wrap" style="font-size:11px;">
1582
+ PID ${esc(pid)}?
1583
+ <button class="btn btn-sm btn-danger" onclick="confirmKill('${esc(pid)}')">Yes</button>
1584
+ <button class="btn btn-sm" onclick="cancelKill('${esc(pid)}')">No</button>
1585
+ </span>`;
1586
+ }
1587
+ function cancelKill(pid) {
1588
+ const wrap = document.getElementById('pkill-'+pid);
1589
+ if (wrap) wrap.innerHTML = `<button class="btn btn-sm btn-danger" onclick="promptKill('${esc(pid)}')">Kill</button>`;
1590
+ }
1591
+ async function confirmKill(pid) {
1592
+ try {
1593
+ await api('POST','/api/system/processes/kill',{pid:parseInt(pid,10)||0});
1594
+ toast('Process '+pid+' killed');
1595
+ await loadProcesses();
1596
+ } catch(e) { cancelKill(pid); toast(e.message, 'err'); }
1597
+ }
1598
+
1599
+ function filterProcTable() {
1600
+ const q = (_procSearch||'').toLowerCase();
1601
+ document.querySelectorAll('#proc-table tbody tr').forEach(tr => {
1602
+ const cmd = (tr.querySelector('td:nth-child(5)')||{}).textContent||'';
1603
+ const pid = (tr.querySelector('td:nth-child(1)')||{}).textContent||'';
1604
+ tr.style.display = (!q || cmd.toLowerCase().includes(q) || pid.includes(q)) ? '' : 'none';
1605
+ });
1606
+ }
1607
+
1608
+ /* ══════════════════════════════════════════
1609
+ NETWORK
1610
+ ══════════════════════════════════════════ */
1611
+ function renderNetwork() {
1612
+ const c = document.getElementById('content');
1613
+ c.innerHTML = `
1614
+ <div class="sparkline-wrap" style="margin-bottom:14px;">
1615
+ <div class="sparkline-head">
1616
+ LOAD AVERAGE &nbsp;
1617
+ <span style="color:var(--accent);">— 1m</span>
1618
+ <span style="color:var(--text2);">– – 5m</span>
1619
+ <span style="color:var(--muted);">··· 15m</span>
1620
+ </div>
1621
+ <svg id="net-sparkline" width="100%" height="80" style="display:block;background:var(--bg);"></svg>
1622
+ </div>
1623
+ <div class="section" style="margin-bottom:14px;">
1624
+ <div class="section-head"><span class="section-title">Established Connections</span><button class="btn btn-sm" onclick="loadNetworkConnections()">Refresh</button></div>
1625
+ <div id="net-conn-wrap"><div class="empty">Loading...</div></div>
1626
+ </div>
1627
+ <div class="section">
1628
+ <div class="section-head"><span class="section-title">Interfaces</span></div>
1629
+ <div id="net-iface-wrap"><div class="empty">Loading...</div></div>
1630
+ </div>
1631
+ `;
1632
+ loadNetworkConnections();
1633
+ loadNetworkInterfaces();
1634
+ requestAnimationFrame(() => sampleNetworkLoad());
1635
+ _netInterval = setInterval(sampleNetworkLoad, 3000);
1636
+ _intervals.push(_netInterval);
1637
+ }
1638
+
1639
+ async function sampleNetworkLoad() {
1640
+ try {
1641
+ const m = await api('GET','/api/system/metrics');
1642
+ if (!m) return;
1643
+ const la = m.loadAvg || [];
1644
+ _netSamples.push({l1:la[0]||0, l5:la[1]||0, l15:la[2]||0});
1645
+ if (_netSamples.length > 30) _netSamples.shift();
1646
+ drawSparkline();
1647
+ } catch(e) {}
1648
+ }
1649
+
1650
+ function drawSparkline() {
1651
+ const svg = document.getElementById('net-sparkline');
1652
+ if (!svg || !_netSamples.length) return;
1653
+ const w = svg.clientWidth || 600;
1654
+ const h = 80;
1655
+ const leftPad = 34; // space for Y-axis labels
1656
+ const n = _netSamples.length;
1657
+ const allVals = _netSamples.flatMap(s => [s.l1, s.l5, s.l15]);
1658
+ const maxVal = Math.max(...allVals, 1);
1659
+ const chartW = w - leftPad;
1660
+ const xStep = chartW / Math.max(n-1, 1);
1661
+ const topPad = 8, botPad = 6;
1662
+
1663
+ function pts(key) {
1664
+ return _netSamples.map((s,i) => {
1665
+ const x = leftPad + i * xStep;
1666
+ const y = h - botPad - (s[key] / maxVal) * (h - topPad - botPad);
1667
+ return x+','+y;
1668
+ }).join(' ');
1669
+ }
1670
+
1671
+ // Y-axis gridlines and labels at 0, 50%, 100% of max
1672
+ const yLabels = [
1673
+ {val: 0, y: h - botPad},
1674
+ {val: (maxVal / 2).toFixed(1), y: h - botPad - (h - topPad - botPad) / 2},
1675
+ {val: maxVal.toFixed(1), y: topPad},
1676
+ ];
1677
+ const yAxisSvg = yLabels.map(l =>
1678
+ `<text x="${leftPad - 4}" y="${l.y + 3}" text-anchor="end" fill="#71717a" font-size="9" font-family="monospace">${l.val}</text>` +
1679
+ `<line x1="${leftPad}" y1="${l.y}" x2="${w}" y2="${l.y}" stroke="#1f1f1f" stroke-width="1"/>`
1680
+ ).join('');
1681
+
1682
+ svg.innerHTML = yAxisSvg +
1683
+ `<line x1="${leftPad}" y1="${topPad}" x2="${leftPad}" y2="${h - botPad}" stroke="#262626" stroke-width="1"/>` +
1684
+ `<polyline points="${pts('l1')}" stroke="#3b82f6" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>` +
1685
+ `<polyline points="${pts('l5')}" stroke="#a1a1aa" stroke-width="1" fill="none" stroke-dasharray="5,3" stroke-linecap="round"/>` +
1686
+ `<polyline points="${pts('l15')}" stroke="#71717a" stroke-width="1" fill="none" stroke-dasharray="2,4" stroke-linecap="round"/>`;
1687
+ }
1688
+
1689
+ async function loadNetworkConnections() {
1690
+ const wrap = document.getElementById('net-conn-wrap');
1691
+ if (!wrap) return;
1692
+ try {
1693
+ const data = await api('GET','/api/system/network');
1694
+ // API returns raw netstat lines as strings
1695
+ const conns = Array.isArray(data.connections) ? data.connections : [];
1696
+ if (!conns.length) { wrap.innerHTML = '<div class="empty">No established connections</div>'; return; }
1697
+ wrap.innerHTML = `<div style="overflow-x:auto;"><table class="data-table">
1698
+ <thead><tr><th>CONNECTION</th></tr></thead>
1699
+ <tbody>${conns.slice(0,40).map(line =>
1700
+ `<tr><td class="mono" style="font-size:11px;">${esc(String(line))}</td></tr>`
1701
+ ).join('')}</tbody>
1702
+ </table></div>`;
1703
+ } catch(e) {
1704
+ wrap.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
1705
+ }
1706
+ }
1707
+
1708
+ async function loadNetworkInterfaces() {
1709
+ const wrap = document.getElementById('net-iface-wrap');
1710
+ if (!wrap) return;
1711
+ try {
1712
+ const data = await api('GET','/api/system/network');
1713
+ const ifaces = data && data.interfaces;
1714
+ if (typeof ifaces === 'string' && ifaces) {
1715
+ wrap.innerHTML = `<pre class="iface-pre">${esc(ifaces)}</pre>`;
1716
+ } else {
1717
+ wrap.innerHTML = '<div class="empty">No interface data</div>';
1718
+ }
1719
+ } catch(e) {
1720
+ wrap.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
1721
+ }
1722
+ }
1723
+
1724
+ /* ══════════════════════════════════════════
1725
+ SYSTEM
1726
+ ══════════════════════════════════════════ */
1727
+ function renderSystem() {
1728
+ const c = document.getElementById('content');
1729
+ c.style.padding = '0';
1730
+ c.innerHTML = `
1731
+ <div class="tab-bar" style="overflow-x:auto;flex-wrap:nowrap;">
1732
+ <div class="tab active" data-tab="terminal" onclick="switchSysTab('terminal',this)">Terminal</div>
1733
+ <div class="tab" data-tab="screenshot" onclick="switchSysTab('screenshot',this)">Screenshot</div>
1734
+ <div class="tab" data-tab="clipboard" onclick="switchSysTab('clipboard',this)">Clipboard</div>
1735
+ <div class="tab" data-tab="apps" onclick="switchSysTab('apps',this)">Apps</div>
1736
+ <div class="tab" data-tab="env" onclick="switchSysTab('env',this)">Env</div>
1737
+ <div class="tab" data-tab="git" onclick="switchSysTab('git',this)">Git</div>
1738
+ <div class="tab" data-tab="docker" onclick="switchSysTab('docker',this)">Docker</div>
1739
+ <div class="tab" data-tab="cron" onclick="switchSysTab('cron',this)">Cron</div>
1740
+ </div>
1741
+ <div id="sys-tab-body" style="flex:1;overflow:auto;padding:16px;background:var(--bg);min-height:calc(100vh - 80px);"></div>
1742
+ `;
1743
+ switchSysTab('terminal', document.querySelector('[data-tab="terminal"]'));
1744
+ }
1745
+
1746
+ function switchSysTab(tab, el) {
1747
+ document.querySelectorAll('.tab-bar .tab').forEach(t => t.classList.remove('active'));
1748
+ if (el) el.classList.add('active');
1749
+ const body = document.getElementById('sys-tab-body');
1750
+ if (!body) return;
1751
+ switch(tab) {
1752
+ case 'terminal': renderTerminalTab(body); break;
1753
+ case 'screenshot': renderScreenshotTab(body); break;
1754
+ case 'clipboard': renderClipboardTab(body); break;
1755
+ case 'apps': renderAppsTab(body); break;
1756
+ case 'env': renderEnvTab(body); break;
1757
+ case 'git': renderGitTab(body); break;
1758
+ case 'docker': renderDockerTab(body); break;
1759
+ case 'cron': renderCronTab(body); break;
1760
+ }
1761
+ }
1762
+
1763
+ function renderTerminalTab(body) {
1764
+ body.innerHTML = `
1765
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
1766
+ <span class="mono" style="font-size:11px;color:var(--muted);">Shell — ${esc(_status.platform||'unknown')}</span>
1767
+ <button class="btn btn-sm" onclick="clearTerminal()">Clear</button>
1768
+ </div>
1769
+ <div class="terminal-out" id="sys-term-out">
1770
+ <div class="term-info">Ready. Type a command and press Enter.</div>
1771
+ </div>
1772
+ <div class="shell-input-row">
1773
+ <span class="shell-prompt">$</span>
1774
+ <input class="shell-input" id="sys-shell-input" type="text" autocomplete="off" spellcheck="false" placeholder="enter command...">
1775
+ </div>
1776
+ `;
1777
+ const inp = document.getElementById('sys-shell-input');
1778
+ inp.addEventListener('keydown', function(e) {
1779
+ if (e.key === 'Enter') {
1780
+ const cmd = this.value.trim();
1781
+ if (!cmd) return;
1782
+ _shellHistory.unshift(cmd);
1783
+ try { localStorage.setItem('conductor_shell_history', JSON.stringify(_shellHistory.slice(0,100))); } catch(e) {}
1784
+ _shellHistoryIdx = -1;
1785
+ this.value = '';
1786
+ runShell(cmd);
1787
+ } else if (e.key === 'ArrowUp') {
1788
+ e.preventDefault();
1789
+ if (_shellHistoryIdx < _shellHistory.length-1) { _shellHistoryIdx++; this.value = _shellHistory[_shellHistoryIdx]||''; }
1790
+ } else if (e.key === 'ArrowDown') {
1791
+ e.preventDefault();
1792
+ if (_shellHistoryIdx > 0) { _shellHistoryIdx--; this.value = _shellHistory[_shellHistoryIdx]||''; }
1793
+ else { _shellHistoryIdx = -1; this.value = ''; }
1794
+ }
1795
+ });
1796
+ inp.focus();
1797
+ }
1798
+
1799
+ function clearTerminal() {
1800
+ const out = document.getElementById('sys-term-out');
1801
+ if (out) out.innerHTML = '';
1802
+ }
1803
+
1804
+ async function runShell(cmd) {
1805
+ const out = document.getElementById('sys-term-out');
1806
+ if (!out) return;
1807
+ const cmdEl = document.createElement('div');
1808
+ cmdEl.className = 'term-cmd';
1809
+ cmdEl.textContent = '$ '+cmd;
1810
+ out.appendChild(cmdEl);
1811
+ out.scrollTop = out.scrollHeight;
1812
+ try {
1813
+ const result = await api('POST','/api/system/shell',{command:cmd});
1814
+ if (result && result.stdout) {
1815
+ const el = document.createElement('div');
1816
+ el.className = 'term-out';
1817
+ el.textContent = result.stdout;
1818
+ out.appendChild(el);
1819
+ }
1820
+ if (result && result.stderr) {
1821
+ const el = document.createElement('div');
1822
+ el.className = 'term-err';
1823
+ el.textContent = result.stderr;
1824
+ out.appendChild(el);
1825
+ }
1826
+ } catch(e) {
1827
+ const el = document.createElement('div');
1828
+ el.className = 'term-err';
1829
+ el.textContent = 'Error: '+e.message;
1830
+ out.appendChild(el);
1831
+ }
1832
+ out.scrollTop = out.scrollHeight;
1833
+ }
1834
+
1835
+ function renderScreenshotTab(body) {
1836
+ body.innerHTML = `
1837
+ <button class="btn btn-primary" id="ss-take-btn" onclick="takeScreenshot()">Take Screenshot</button>
1838
+ <div id="ss-result" style="margin-top:14px;"></div>
1839
+ `;
1840
+ }
1841
+
1842
+ async function takeScreenshot() {
1843
+ const btn = document.getElementById('ss-take-btn');
1844
+ const res = document.getElementById('ss-result');
1845
+ if (btn) { btn.textContent = 'Capturing...'; btn.disabled = true; }
1846
+ try {
1847
+ const data = await api('GET','/api/system/screenshot');
1848
+ if (data && data.image) {
1849
+ const src = 'data:'+(data.mimeType||'image/png')+';base64,'+data.image;
1850
+ if (res) res.innerHTML = `
1851
+ <div style="display:flex;gap:8px;margin-bottom:8px;">
1852
+ <a href="${esc(src)}" download="screenshot.png" class="btn btn-sm">Download</a>
1853
+ </div>
1854
+ <img src="${esc(src)}" style="max-width:100%;border:1px solid var(--border);border-radius:var(--r);" alt="screenshot">`;
1855
+ toast('Screenshot captured');
1856
+ } else {
1857
+ toast('Screenshot unavailable on this system', 'warn');
1858
+ }
1859
+ } catch(e) {
1860
+ if (res) res.innerHTML = `<div style="color:var(--err);font-size:12px;">${esc(e.message)}</div>`;
1861
+ toast(e.message, 'err');
1862
+ }
1863
+ if (btn) { btn.textContent = 'Take Screenshot'; btn.disabled = false; }
1864
+ }
1865
+
1866
+ function renderClipboardTab(body) {
1867
+ body.innerHTML = `
1868
+ <div style="display:flex;gap:8px;margin-bottom:12px;align-items:center;">
1869
+ <button class="btn" onclick="readClipboard()">Read</button>
1870
+ <button class="btn btn-primary" onclick="writeClipboard()">Write</button>
1871
+ </div>
1872
+ <div style="margin-bottom:12px;">
1873
+ <div style="font-size:11px;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.05em;">Current Clipboard</div>
1874
+ <div id="clip-current" style="border:1px solid var(--border);padding:10px;font-family:var(--mono);font-size:12px;background:var(--elevated);min-height:48px;max-height:140px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;border-radius:var(--r);color:var(--text2);">(click Read)</div>
1875
+ </div>
1876
+ <div>
1877
+ <div style="font-size:11px;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.05em;">Write to Clipboard</div>
1878
+ <textarea id="clip-new-text" rows="4" style="width:100%;" placeholder="New clipboard content..."></textarea>
1879
+ </div>
1880
+ `;
1881
+ readClipboard();
1882
+ }
1883
+
1884
+ async function readClipboard() {
1885
+ const el = document.getElementById('clip-current');
1886
+ if (!el) return;
1887
+ try {
1888
+ const data = await api('GET','/api/system/clipboard');
1889
+ el.textContent = (data && data.text) || '(empty)';
1890
+ toast('Clipboard read');
1891
+ } catch(e) { el.textContent = '(unavailable: '+e.message+')'; toast(e.message,'warn'); }
1892
+ }
1893
+
1894
+ async function writeClipboard() {
1895
+ const inp = document.getElementById('clip-new-text');
1896
+ if (!inp) return;
1897
+ try {
1898
+ await api('POST','/api/system/clipboard',{text:inp.value});
1899
+ toast('Written to clipboard');
1900
+ inp.value = '';
1901
+ readClipboard();
1902
+ } catch(e) { toast(e.message, 'err'); }
1903
+ }
1904
+
1905
+ function renderAppsTab(body) {
1906
+ body.innerHTML = `
1907
+ <button class="btn" style="margin-bottom:12px;" onclick="loadApps()">Refresh</button>
1908
+ <div id="apps-list"><div class="empty">Loading...</div></div>
1909
+ `;
1910
+ loadApps();
1911
+ }
1912
+
1913
+ async function loadApps() {
1914
+ const el = document.getElementById('apps-list');
1915
+ if (!el) return;
1916
+ try {
1917
+ const data = await api('GET','/api/system/windows');
1918
+ // API returns { apps: string[] }
1919
+ const items = data && Array.isArray(data.apps) ? data.apps : [];
1920
+ if (!items.length) { el.innerHTML = '<div class="empty">No running applications found</div>'; return; }
1921
+ el.innerHTML = `<table class="data-table">
1922
+ <thead><tr><th style="width:44px;">#</th><th>Application / Window</th></tr></thead>
1923
+ <tbody>${items.slice(0,80).map((w,i) => {
1924
+ const name = typeof w === 'string' ? w : (w.app||w.name||w.title||JSON.stringify(w));
1925
+ return `<tr><td class="mono" style="font-size:11px;color:var(--muted);">${i+1}</td><td style="font-size:12px;">${esc(name)}</td></tr>`;
1926
+ }).join('')}</tbody>
1927
+ </table>`;
1928
+ } catch(e) { el.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`; }
1929
+ }
1930
+
1931
+ async function renderEnvTab(body) {
1932
+ body.innerHTML = `
1933
+ <div style="margin-bottom:10px;display:flex;gap:8px;align-items:center;">
1934
+ <input type="text" id="env-search" placeholder="Filter variables..." style="flex:1;font-family:var(--mono);font-size:12px;" oninput="filterEnvTable(this.value)">
1935
+ <span style="font-size:11px;color:var(--muted);" title="Only safe/Conductor vars are shown">safe vars only</span>
1936
+ </div>
1937
+ <div id="env-table-wrap"><div class="empty">Loading...</div></div>`;
1938
+ try {
1939
+ const data = await api('GET','/api/system/env');
1940
+ const env = (data && data.env && typeof data.env === 'object') ? data.env : {};
1941
+ const rows = Object.entries(env);
1942
+ const wrap = document.getElementById('env-table-wrap');
1943
+ if (wrap) {
1944
+ if (rows.length) {
1945
+ wrap.innerHTML = `<table class="env-table" id="env-table">
1946
+ <tbody>${rows.map(([k,v]) => `<tr><td>${esc(k)}</td><td>${esc(String(v).substring(0,200))}</td></tr>`).join('')}
1947
+ </tbody></table>`;
1948
+ } else {
1949
+ wrap.innerHTML = '<div class="empty">No environment variables found (only PATH, HOME, USER, SHELL, and CONDUCTOR_* vars are shown)</div>';
1950
+ }
1951
+ }
1952
+ } catch(e) {
1953
+ const wrap = document.getElementById('env-table-wrap');
1954
+ if (wrap) wrap.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
1955
+ }
1956
+ }
1957
+
1958
+ function filterEnvTable(q) {
1959
+ const ql = (q||'').toLowerCase();
1960
+ document.querySelectorAll('#env-table tbody tr').forEach(tr => {
1961
+ const text = tr.textContent.toLowerCase();
1962
+ tr.style.display = (!ql || text.includes(ql)) ? '' : 'none';
1963
+ });
1964
+ }
1965
+
1966
+ /* ── GIT TAB ──────────────────────────────────────────────────────────── */
1967
+ let _gitPath = '';
1968
+
1969
+ function renderGitTab(body) {
1970
+ body.innerHTML = `
1971
+ <div style="display:flex;gap:8px;margin-bottom:14px;align-items:center;">
1972
+ <input type="text" id="git-path-inp" placeholder="Absolute repo path (e.g. /Users/you/project)" value="${esc(_gitPath)}" style="flex:1;font-family:var(--mono);font-size:12px;" onkeydown="if(event.key==='Enter')loadGitStatus()">
1973
+ <button class="btn btn-primary" onclick="loadGitStatus()">Check</button>
1974
+ </div>
1975
+ <div id="git-result"><div class="empty">Enter a repository path and click Check</div></div>`;
1976
+ }
1977
+
1978
+ async function loadGitStatus() {
1979
+ const inp = document.getElementById('git-path-inp');
1980
+ const path = inp ? inp.value.trim() : '';
1981
+ _gitPath = path;
1982
+ const wrap = document.getElementById('git-result');
1983
+ if (!wrap) return;
1984
+ wrap.innerHTML = '<div class="empty">Loading...</div>';
1985
+ try {
1986
+ const qs = path ? '?path='+encodeURIComponent(path) : '';
1987
+ const data = await api('GET','/api/git/status'+qs);
1988
+ if (!data) { wrap.innerHTML = '<div class="empty">No data</div>'; return; }
1989
+ if (data.isRepo === false) {
1990
+ wrap.innerHTML = `<div class="empty" style="color:var(--warn);">Not a git repository: ${esc(path)}</div>`;
1991
+ return;
1992
+ }
1993
+ const sections = [];
1994
+ if (data.branch) sections.push(`<div style="margin-bottom:12px;"><span style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;">Branch</span><div class="mono" style="font-size:13px;color:var(--accent);margin-top:4px;">${esc(data.branch)}</div></div>`);
1995
+ const statusText = data.status && data.status.trim();
1996
+ const statusHtml = statusText
1997
+ ? `<pre style="font-family:var(--mono);font-size:11px;margin-top:4px;white-space:pre-wrap;color:var(--text2);">${esc(statusText)}</pre>`
1998
+ : `<div style="font-size:11px;color:var(--ok);margin-top:4px;">(clean — nothing to commit)</div>`;
1999
+ sections.push(`<div style="margin-bottom:12px;"><span style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;">Status</span>${statusHtml}</div>`);
2000
+ if (data.recentCommits && data.recentCommits.length) {
2001
+ const commits = Array.isArray(data.recentCommits) ? data.recentCommits.join('\n') : String(data.recentCommits);
2002
+ sections.push(`<div><span style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;">Recent Commits</span><pre style="font-family:var(--mono);font-size:11px;margin-top:4px;white-space:pre-wrap;color:var(--text2);">${esc(commits)}</pre></div>`);
2003
+ } else if (data.recentCommits && !data.recentCommits.length) {
2004
+ sections.push(`<div><span style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;">Recent Commits</span><div style="font-size:11px;color:var(--muted);margin-top:4px;">(no commits yet)</div></div>`);
2005
+ }
2006
+ wrap.innerHTML = sections.length ? sections.join('') : `<pre class="iface-pre">${esc(JSON.stringify(data,null,2))}</pre>`;
2007
+ } catch(e) {
2008
+ wrap.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
2009
+ toast(e.message, 'err');
2010
+ }
2011
+ }
2012
+
2013
+ /* ── DOCKER TAB ───────────────────────────────────────────────────────── */
2014
+ function renderDockerTab(body) {
2015
+ body.innerHTML = `
2016
+ <div style="display:flex;gap:8px;margin-bottom:14px;align-items:center;">
2017
+ <button class="btn" onclick="loadDockerContainers()">
2018
+ <svg viewBox="0 0 24 24" style="width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:1.5;"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
2019
+ Refresh
2020
+ </button>
2021
+ </div>
2022
+ <div id="docker-result"><div class="empty">Loading...</div></div>`;
2023
+ loadDockerContainers();
2024
+ }
2025
+
2026
+ async function loadDockerContainers() {
2027
+ const wrap = document.getElementById('docker-result');
2028
+ if (!wrap) return;
2029
+ try {
2030
+ const data = await api('GET','/api/docker/containers');
2031
+ const containers = Array.isArray(data) ? data : (data && Array.isArray(data.containers) ? data.containers : []);
2032
+ if (!containers.length) {
2033
+ wrap.innerHTML = '<div class="empty">No containers found — Docker may not be running</div>';
2034
+ return;
2035
+ }
2036
+ wrap.innerHTML = `<table class="data-table">
2037
+ <thead><tr><th>NAME</th><th>IMAGE</th><th style="width:100px;">STATUS</th><th style="width:170px;"></th></tr></thead>
2038
+ <tbody>${containers.map(con => {
2039
+ const id = esc(con.id||con.Id||con.name||con.Names||'');
2040
+ const name = esc(con.name||con.Names||con.id||'');
2041
+ const image = esc(con.image||con.Image||'');
2042
+ const state = (con.state||con.State||con.status||con.Status||'').toLowerCase();
2043
+ const isRunning = state.includes('running') || state.includes('up');
2044
+ return `<tr>
2045
+ <td class="mono" style="font-size:11px;">${name}</td>
2046
+ <td style="font-size:11px;color:var(--muted);">${image}</td>
2047
+ <td><span class="badge ${isRunning?'badge-ok':'badge-muted'}">${esc(con.status||con.Status||state)}</span></td>
2048
+ <td style="display:flex;gap:4px;">
2049
+ <button class="btn btn-sm" onclick="dockerAction('${id}','start')" ${isRunning?'disabled':''}>Start</button>
2050
+ <button class="btn btn-sm btn-danger" onclick="dockerAction('${id}','stop')" ${!isRunning?'disabled':''}>Stop</button>
2051
+ <button class="btn btn-sm" onclick="dockerAction('${id}','restart')">Restart</button>
2052
+ </td>
2053
+ </tr>`;
2054
+ }).join('')}</tbody>
2055
+ </table>`;
2056
+ } catch(e) {
2057
+ wrap.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
2058
+ toast('Docker: '+e.message, 'warn');
2059
+ }
2060
+ }
2061
+
2062
+ async function dockerAction(id, action) {
2063
+ try {
2064
+ await api('POST','/api/docker/containers/'+encodeURIComponent(id)+'/action',{action});
2065
+ toast(action+' — '+id);
2066
+ setTimeout(() => loadDockerContainers(), 1200);
2067
+ } catch(e) { toast(e.message, 'err'); }
2068
+ }
2069
+
2070
+ /* ── CRON TAB ─────────────────────────────────────────────────────────── */
2071
+ function renderCronTab(body) {
2072
+ body.innerHTML = `
2073
+ <div style="margin-bottom:14px;">
2074
+ <div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;">Current crontab</div>
2075
+ <div id="cron-list"><div class="empty">Loading...</div></div>
2076
+ </div>
2077
+ <div class="section">
2078
+ <div class="section-head"><span class="section-title">Execute Shell Command</span></div>
2079
+ <div class="section-body">
2080
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
2081
+ <input type="text" id="cron-cmd" placeholder="Command to run now (e.g. echo hello)" style="flex:1;min-width:200px;font-family:var(--mono);font-size:12px;" onkeydown="if(event.key==='Enter')runCronCommand()">
2082
+ <button class="btn btn-primary" onclick="runCronCommand()">Run Now</button>
2083
+ </div>
2084
+ <div style="font-size:11px;color:var(--muted);margin-top:6px;">Executes the command immediately via the shell and shows output below.</div>
2085
+ <div id="cron-cmd-output" style="display:none;margin-top:8px;font-family:var(--mono);font-size:11px;white-space:pre-wrap;background:var(--bg);border:1px solid var(--border);border-radius:var(--r);padding:8px;max-height:200px;overflow-y:auto;color:var(--text2);"></div>
2086
+ </div>
2087
+ </div>`;
2088
+ loadCronJobs();
2089
+ }
2090
+
2091
+ async function loadCronJobs() {
2092
+ const wrap = document.getElementById('cron-list');
2093
+ if (!wrap) return;
2094
+ try {
2095
+ const data = await api('GET','/api/cron');
2096
+ const entries = Array.isArray(data.entries) ? data.entries : [];
2097
+ if (!entries.length) {
2098
+ wrap.innerHTML = '<div class="empty">No cron jobs (crontab is empty)</div>';
2099
+ return;
2100
+ }
2101
+ wrap.innerHTML = `<table class="data-table">
2102
+ <thead><tr><th style="width:220px;">SCHEDULE + COMMAND</th></tr></thead>
2103
+ <tbody>${entries.map(e => `<tr><td class="mono" style="font-size:11px;">${esc(e)}</td></tr>`).join('')}
2104
+ </tbody></table>`;
2105
+ } catch(e) {
2106
+ wrap.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
2107
+ }
2108
+ }
2109
+
2110
+ async function runCronCommand() {
2111
+ const cmdEl = document.getElementById('cron-cmd');
2112
+ const cmd = cmdEl ? cmdEl.value.trim() : '';
2113
+ if (!cmd) { toast('Enter a command', 'warn'); return; }
2114
+ const outEl = document.getElementById('cron-cmd-output');
2115
+ if (outEl) { outEl.style.display = ''; outEl.textContent = '$ ' + cmd + '\n(running...)'; }
2116
+ try {
2117
+ const result = await api('POST','/api/system/shell',{command:cmd});
2118
+ const output = [
2119
+ '$ ' + cmd,
2120
+ result.stdout || '',
2121
+ result.stderr ? '[stderr] ' + result.stderr : '',
2122
+ '[exit ' + result.exitCode + ']',
2123
+ ].filter(Boolean).join('\n');
2124
+ if (outEl) outEl.textContent = output;
2125
+ if (result.exitCode === 0) {
2126
+ toast('Command completed (exit 0)');
2127
+ } else {
2128
+ toast('Command exited with code ' + result.exitCode, 'warn');
2129
+ }
2130
+ } catch(e) {
2131
+ if (outEl) outEl.textContent = '$ ' + cmd + '\n[error] ' + e.message;
2132
+ toast(e.message, 'err');
2133
+ }
2134
+ }
2135
+
2136
+ /* ══════════════════════════════════════════
2137
+ NOTES
2138
+ ══════════════════════════════════════════ */
2139
+ async function renderNotes() {
2140
+ const c = document.getElementById('content');
2141
+ c.style.padding = '0';
2142
+ c.innerHTML = `
2143
+ <div class="note-cols">
2144
+ <div class="note-list-pane" id="note-list-pane">
2145
+ <div style="padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--elevated);">
2146
+ <span style="font-size:11px;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--text2);">Notes</span>
2147
+ <button class="btn btn-sm" onclick="newNote()">New</button>
2148
+ </div>
2149
+ <div style="padding:5px 8px;border-bottom:1px solid var(--border);">
2150
+ <input type="text" id="note-search-inp" placeholder="Search notes..." style="width:100%;font-size:11px;" oninput="_noteSearch=this.value;renderNoteList()">
2151
+ </div>
2152
+ <div id="note-list"><div class="empty">Loading...</div></div>
2153
+ </div>
2154
+ <div class="note-editor-pane" id="note-editor-pane">
2155
+ <div class="empty" style="padding:40px;">Select a note or create a new one</div>
2156
+ </div>
2157
+ </div>
2158
+ `;
2159
+ loadNotes();
2160
+ }
2161
+
2162
+ async function loadNotes() {
2163
+ try {
2164
+ const data = await api('GET','/api/notes');
2165
+ _notes = (data && Array.isArray(data.notes)) ? data.notes : (Array.isArray(data) ? data : []);
2166
+ renderNoteList();
2167
+ } catch(e) {
2168
+ const el = document.getElementById('note-list');
2169
+ if (el) el.innerHTML = `<div class="empty" style="color:var(--err);">${esc(e.message)}</div>`;
2170
+ }
2171
+ }
2172
+
2173
+ function renderNoteList() {
2174
+ const el = document.getElementById('note-list');
2175
+ if (!el) return;
2176
+ const visibleNotes = _noteSearch ? _notes.filter(n => (n.title||'').toLowerCase().includes(_noteSearch.toLowerCase()) || (n.content||'').toLowerCase().includes(_noteSearch.toLowerCase())) : _notes;
2177
+ if (!visibleNotes.length) { el.innerHTML = '<div class="empty">'+(_noteSearch?'No matching notes':'No notes yet')+'</div>'; return; }
2178
+ el.innerHTML = visibleNotes.map(n => {
2179
+ const id = n.id || '';
2180
+ const title = n.title || 'Untitled';
2181
+ const date = n.updated || n.created || '';
2182
+ const isActive = _selectedNote && String(id) === String(_selectedNote);
2183
+ return `<div class="note-item ${isActive?'active':''}" id="ni-${esc(String(id))}" onclick="openNote('${esc(String(id))}')">
2184
+ <div class="note-item-title">${esc(title)}</div>
2185
+ <div class="note-item-date">${date ? new Date(date).toLocaleDateString() : ''}</div>
2186
+ </div>`;
2187
+ }).join('');
2188
+ }
2189
+
2190
+ async function openNote(id) {
2191
+ _selectedNote = id;
2192
+ document.querySelectorAll('.note-item').forEach(el => el.classList.remove('active'));
2193
+ const ni = document.getElementById('ni-'+id);
2194
+ if (ni) ni.classList.add('active');
2195
+ const note = _notes.find(n => String(n.id) === String(id));
2196
+ if (!note) return;
2197
+ const pane = document.getElementById('note-editor-pane');
2198
+ if (!pane) return;
2199
+ pane.innerHTML = `
2200
+ <div class="note-editor-toolbar">
2201
+ <input class="note-title-input" id="note-title-inp" type="text" value="${esc(note.title||'')}" placeholder="Note title...">
2202
+ <span id="note-wc" style="font-size:10px;color:var(--muted);font-family:var(--mono);flex-shrink:0;"></span>
2203
+ <button class="btn btn-sm" id="note-preview-btn" onclick="toggleNotePreview()">Preview</button>
2204
+ <button class="btn btn-sm btn-primary" onclick="saveNote('${esc(String(id))}')">Save</button>
2205
+ <span id="ndel-${esc(String(id))}">
2206
+ <button class="btn btn-sm btn-danger" onclick="promptDeleteNote('${esc(String(id))}')">Delete</button>
2207
+ </span>
2208
+ </div>
2209
+ <div class="note-body" id="note-body" contenteditable="true"></div>
2210
+ <div id="note-preview" style="display:none;flex:1;padding:14px;font-size:13px;line-height:1.7;overflow-y:auto;color:var(--text);"></div>
2211
+ `;
2212
+ const noteBodyEl = document.getElementById('note-body');
2213
+ if (noteBodyEl) noteBodyEl.textContent = note.content || '';
2214
+ const body = noteBodyEl;
2215
+ if (noteBodyEl) {
2216
+ noteBodyEl.addEventListener('input', () => {
2217
+ _noteDirty = true;
2218
+ clearTimeout(_noteDebounce);
2219
+ _noteDebounce = setTimeout(() => { if (_noteDirty) { saveNote(id); _noteDirty = false; } }, 500);
2220
+ });
2221
+ noteBodyEl.addEventListener('input', updateNoteWordCount);
2222
+ }
2223
+ updateNoteWordCount();
2224
+ }
2225
+
2226
+ async function saveNote(id) {
2227
+ const titleInp = document.getElementById('note-title-inp');
2228
+ const body = document.getElementById('note-body');
2229
+ const title = titleInp ? titleInp.value : '';
2230
+ const content = body ? body.innerText : '';
2231
+ try {
2232
+ await api('PUT','/api/notes/'+id,{title,content});
2233
+ const note = _notes.find(n => String(n.id) === String(id));
2234
+ if (note) { note.title = title; note.content = content; }
2235
+ renderNoteList();
2236
+ toast('Note saved');
2237
+ } catch(e) { toast(e.message, 'err'); }
2238
+ }
2239
+
2240
+ function promptDeleteNote(id) {
2241
+ const wrap = document.getElementById('ndel-'+id);
2242
+ if (!wrap) return;
2243
+ wrap.innerHTML = `<span style="display:inline-flex;align-items:center;gap:5px;">
2244
+ <span style="font-size:11px;color:var(--muted);">Delete?</span>
2245
+ <button class="btn btn-sm btn-danger" onclick="confirmDeleteNote('${esc(id)}')">Yes</button>
2246
+ <button class="btn btn-sm" onclick="cancelDeleteNote('${esc(id)}')">No</button>
2247
+ </span>`;
2248
+ }
2249
+ function cancelDeleteNote(id) {
2250
+ const wrap = document.getElementById('ndel-'+id);
2251
+ if (wrap) wrap.innerHTML = `<button class="btn btn-sm btn-danger" onclick="promptDeleteNote('${esc(id)}')">Delete</button>`;
2252
+ }
2253
+
2254
+ async function newNote() {
2255
+ try {
2256
+ const data = await api('POST','/api/notes',{title:'New Note',content:''});
2257
+ const note = data && (data.note || data);
2258
+ if (note && note.id) {
2259
+ _notes.unshift(note);
2260
+ renderNoteList();
2261
+ openNote(String(note.id));
2262
+ }
2263
+ } catch(e) { toast(e.message, 'err'); }
2264
+ }
2265
+
2266
+ async function confirmDeleteNote(id) {
2267
+ try {
2268
+ await api('DELETE','/api/notes/'+id);
2269
+ _notes = _notes.filter(n => String(n.id) !== String(id));
2270
+ _selectedNote = null;
2271
+ renderNoteList();
2272
+ toast('Note deleted');
2273
+ const pane = document.getElementById('note-editor-pane');
2274
+ if (pane) pane.innerHTML = '<div class="empty" style="padding:40px;">Select a note or create a new one</div>';
2275
+ } catch(e) { toast(e.message, 'err'); }
2276
+ }
2277
+ async function deleteNote(id) { promptDeleteNote(id); }
2278
+
2279
+ /* ══════════════════════════════════════════
2280
+ PLUGINS
2281
+ ══════════════════════════════════════════ */
2282
+ function renderPlugins() {
2283
+ const c = document.getElementById('content');
2284
+ // _plugins.all is a string[] array of plugin names
2285
+ const allNames = Array.isArray(_plugins.all) ? _plugins.all : Object.keys(_plugins.all || {});
2286
+ const enabled = new Set(_plugins.enabled || []);
2287
+ const reqCreds = _plugins.requiredCreds || {};
2288
+
2289
+ c.innerHTML = `
2290
+ <div class="section" style="margin-bottom:14px;">
2291
+ <div class="section-body" style="display:flex;gap:24px;font-family:var(--mono);font-size:12px;">
2292
+ <span><span style="color:var(--text);font-weight:600;">${enabled.size}</span> <span style="color:var(--muted);">/ ${allNames.length} enabled</span></span>
2293
+ <span><span style="color:var(--text);font-weight:600;">${_credentials.filter(x=>x.hasValue).length}</span> <span style="color:var(--muted);">credentials stored</span></span>
2294
+ </div>
2295
+ </div>
2296
+ <div class="plugin-grid" style="border:1px solid var(--border);border-radius:var(--r);overflow:hidden;">
2297
+ ${allNames.sort().map(name => {
2298
+ const isEnabled = enabled.has(name);
2299
+ const credList = reqCreds[name];
2300
+ const credStr = Array.isArray(credList) ? credList.map(c2 => c2.service+'/'+c2.key).join(', ') : (credList ? String(credList) : null);
2301
+ const hasCred = credList ? !!_credentials.find(c2 => c2.service === (Array.isArray(credList)?credList[0].service:name) && c2.hasValue) : true;
2302
+ return `<div class="plugin-card">
2303
+ <div class="plugin-name">${esc(name)}</div>
2304
+ <div class="plugin-status ${isEnabled?'active':''}">${isEnabled ? 'Active' : 'Inactive'}</div>
2305
+ ${credStr ? '<div class="plugin-creds" style="' + (hasCred ? 'color:var(--ok);' : 'color:var(--warn);') + '">' + esc(credStr) + '&nbsp;' + (hasCred ? '[set]' : '[missing]') + '</div>' : ''}
2306
+ <button class="btn btn-sm ${isEnabled?'btn-danger':''}" id="ptog-${esc(name)}" onclick="togglePlugin('${esc(name)}',${!isEnabled})">${isEnabled?'Disable':'Enable'}</button>
2307
+ </div>`;
2308
+ }).join('')}
2309
+ </div>
2310
+ `;
2311
+ }
2312
+
2313
+ async function togglePlugin(name, enable) {
2314
+ const btn = document.getElementById('ptog-'+name);
2315
+ if (btn) { btn.disabled = true; btn.textContent = '...'; }
2316
+ try {
2317
+ await api('POST','/api/plugins/toggle',{plugin:name,enabled:enable});
2318
+ if (enable) { if (!_plugins.enabled.includes(name)) _plugins.enabled.push(name); }
2319
+ else { _plugins.enabled = _plugins.enabled.filter(n => n !== name); }
2320
+ toast(name+' '+(enable?'enabled':'disabled'));
2321
+ renderPlugins();
2322
+ } catch(e) {
2323
+ if (btn) { btn.disabled = false; btn.textContent = enable?'Enable':'Disable'; }
2324
+ toast(e.message, 'err');
2325
+ }
2326
+ }
2327
+
2328
+ /* ══════════════════════════════════════════
2329
+ CREDENTIALS
2330
+ ══════════════════════════════════════════ */
2331
+ function renderCredentials() {
2332
+ const c = document.getElementById('content');
2333
+ // API returns { connected: boolean }
2334
+ const isGoogleConnected = _googleStatus && _googleStatus.connected;
2335
+
2336
+ c.innerHTML = `
2337
+ <div class="section" style="margin-bottom:14px;">
2338
+ <div class="section-head">
2339
+ <span class="section-title">Google OAuth</span>
2340
+ <div style="display:flex;align-items:center;gap:8px;">
2341
+ <span class="badge ${isGoogleConnected?'badge-ok':'badge-muted'}">${isGoogleConnected?'Connected':'Not connected'}</span>
2342
+ ${isGoogleConnected
2343
+ ? `<button class="btn btn-sm btn-danger" onclick="disconnectGoogle()">Disconnect</button>`
2344
+ : `<button class="btn btn-sm btn-primary" onclick="connectGoogle()">Connect Google</button>`
2345
+ }
2346
+ </div>
2347
+ </div>
2348
+ <div class="section-body" style="font-size:12px;color:var(--muted);">Enables Gmail, Calendar, and Drive plugins.</div>
2349
+ </div>
2350
+
2351
+ <div class="section">
2352
+ <div class="section-head"><span class="section-title">API Keys &amp; Tokens</span></div>
2353
+ <table class="data-table" id="cred-table">
2354
+ <thead><tr>
2355
+ <th style="width:130px;">SERVICE</th>
2356
+ <th style="width:160px;">KEY</th>
2357
+ <th style="width:100px;">STATUS</th>
2358
+ <th>ACTIONS</th>
2359
+ </tr></thead>
2360
+ <tbody>${_credentials.map(cr => credRowHTML(cr)).join('')}</tbody>
2361
+ </table>
2362
+ ${!_credentials.length ? '<div class="empty">No credentials configured</div>' : ''}
2363
+ </div>
2364
+ `;
2365
+ }
2366
+
2367
+ function credRowHTML(cr) {
2368
+ const uid = esc(cr.service+'__'+cr.key);
2369
+ return `
2370
+ <tr id="crow-${uid}">
2371
+ <td class="mono" style="font-size:12px;">${esc(cr.service)}</td>
2372
+ <td class="mono" style="font-size:12px;">${esc(cr.key)}</td>
2373
+ <td>
2374
+ <span class="badge ${cr.hasValue?'badge-ok':'badge-muted'}">${cr.hasValue ? 'Stored' : 'Missing'}</span>
2375
+ </td>
2376
+ <td style="display:flex;gap:6px;align-items:center;" id="cact-${uid}">
2377
+ <button class="btn btn-sm" onclick="showCredEdit('${esc(cr.service)}','${esc(cr.key)}')">Set</button>
2378
+ ${cr.hasValue ? `<button class="btn btn-sm btn-danger" onclick="promptDeleteCred('${esc(cr.service)}','${esc(cr.key)}')">Delete</button>` : ''}
2379
+ </td>
2380
+ </tr>
2381
+ <tr id="cedit-${uid}" style="display:none;">
2382
+ <td colspan="4" style="padding:8px 10px;background:var(--elevated);">
2383
+ <div style="display:flex;gap:8px;align-items:center;">
2384
+ <input type="password" id="cinput-${uid}" placeholder="Paste value here..." style="flex:1;font-family:var(--mono);font-size:12px;">
2385
+ <button class="btn btn-sm btn-primary" onclick="saveCred('${esc(cr.service)}','${esc(cr.key)}')">Save</button>
2386
+ <button class="btn btn-sm" onclick="hideCredEdit('${esc(cr.service)}','${esc(cr.key)}')">Cancel</button>
2387
+ </div>
2388
+ </td>
2389
+ </tr>
2390
+ `;
2391
+ }
2392
+
2393
+ function showCredEdit(service, key) {
2394
+ const uid = service+'__'+key;
2395
+ const row = document.getElementById('cedit-'+uid);
2396
+ if (row) row.style.display = '';
2397
+ const inp = document.getElementById('cinput-'+uid);
2398
+ if (inp) inp.focus();
2399
+ }
2400
+ function hideCredEdit(service, key) {
2401
+ const row = document.getElementById('cedit-'+service+'__'+key);
2402
+ if (row) row.style.display = 'none';
2403
+ }
2404
+
2405
+ async function saveCred(service, key) {
2406
+ const uid = service+'__'+key;
2407
+ const inp = document.getElementById('cinput-'+uid);
2408
+ if (!inp || !inp.value.trim()) return;
2409
+ try {
2410
+ await api('POST','/api/credentials',{service,key,value:inp.value.trim()});
2411
+ const cr = _credentials.find(c2 => c2.service===service && c2.key===key);
2412
+ if (cr) cr.hasValue = true;
2413
+ // Re-check integration statuses that depend on credentials
2414
+ if (service === 'todoist' && key === 'api_token') {
2415
+ const st = await api('GET','/api/todoist/status').catch(() => ({configured:false}));
2416
+ _todoistConfigured = !!(st && st.configured);
2417
+ }
2418
+ if (service === 'google' && (key === 'access_token' || key === 'refresh_token')) {
2419
+ const gs = await api('GET','/api/auth/google/status').catch(() => ({connected:false}));
2420
+ _googleStatus = gs || {connected:false};
2421
+ }
2422
+ toast(service+'/'+key+' saved');
2423
+ renderCredentials();
2424
+ } catch(e) { toast(e.message, 'err'); }
2425
+ }
2426
+
2427
+ function promptDeleteCred(service, key) {
2428
+ const uid = service+'__'+key;
2429
+ const cell = document.getElementById('cact-'+uid);
2430
+ if (!cell) return;
2431
+ cell.innerHTML = `<span style="font-size:11px;color:var(--muted);">Delete?</span>
2432
+ <button class="btn btn-sm btn-danger" onclick="doDeleteCred('${esc(service)}','${esc(key)}')">Yes</button>
2433
+ <button class="btn btn-sm" onclick="renderCredentials()">No</button>`;
2434
+ }
2435
+
2436
+ async function doDeleteCred(service, key) {
2437
+ try {
2438
+ await api('DELETE','/api/credentials/'+encodeURIComponent(service)+'/'+encodeURIComponent(key));
2439
+ const cr = _credentials.find(c2 => c2.service===service && c2.key===key);
2440
+ if (cr) cr.hasValue = false;
2441
+ // Update integration statuses that depend on this credential
2442
+ if (service === 'todoist' && key === 'api_token') {
2443
+ _todoistConfigured = false;
2444
+ }
2445
+ if (service === 'google' && (key === 'access_token' || key === 'refresh_token')) {
2446
+ const gs = await api('GET','/api/auth/google/status').catch(() => ({connected:false}));
2447
+ _googleStatus = gs || {connected:false};
2448
+ }
2449
+ toast(service+'/'+key+' deleted');
2450
+ renderCredentials();
2451
+ } catch(e) { toast(e.message, 'err'); }
2452
+ }
2453
+
2454
+ async function connectGoogle() {
2455
+ try {
2456
+ const data = await api('GET','/api/auth/google/url');
2457
+ if (data && data.url) { window.open(data.url,'_blank'); toast('Google OAuth opened in new tab', 'info'); }
2458
+ else toast('Could not get Google auth URL', 'warn');
2459
+ } catch(e) { toast(e.message, 'err'); }
2460
+ }
2461
+
2462
+ async function disconnectGoogle() {
2463
+ try {
2464
+ await api('DELETE','/api/auth/google');
2465
+ _googleStatus = {connected:false};
2466
+ toast('Google disconnected');
2467
+ renderCredentials();
2468
+ } catch(e) { toast(e.message, 'err'); }
2469
+ }
2470
+
2471
+ /* ══════════════════════════════════════════
2472
+ LOGS
2473
+ ══════════════════════════════════════════ */
2474
+ function renderLogs() {
2475
+ const c = document.getElementById('content');
2476
+ c.innerHTML = `
2477
+ <div style="display:flex;gap:8px;margin-bottom:10px;align-items:center;flex-wrap:wrap;">
2478
+ <button class="btn ${_logPaused?'btn-primary':''}" id="log-pause-btn" onclick="toggleLogPause()">${_logPaused?'Resume':'Pause'}</button>
2479
+ <button class="btn" onclick="clearLogs()">Clear</button>
2480
+ <div style="display:flex;gap:4px;">
2481
+ ${['ALL','INFO','WARN','ERROR'].map(f => {
2482
+ const cnt = f === 'ALL' ? _logLines.length : _logLines.filter(l => l.level === f).length;
2483
+ return `<button class="btn btn-sm ${_logFilter===f?'btn-primary':''}" onclick="setLogFilter('${f}')">${f}${cnt?' ('+cnt+')':''}</button>`;
2484
+ }).join('')}
2485
+ </div>
2486
+ <button class="btn btn-sm ${_logAutoScroll?'btn-primary':''}" onclick="toggleLogAutoScroll()">Auto-scroll</button>
2487
+ <input type="text" id="log-search" placeholder="Search logs..." style="width:160px;font-family:var(--mono);font-size:11px;" oninput="setLogSearch(this.value)" value="${esc(_logSearch||'')}">
2488
+ <button class="btn btn-sm" onclick="downloadLogs()">Download</button>
2489
+ <span class="mono" style="font-size:10px;color:var(--muted);">${_logLines.length} lines</span>
2490
+ </div>
2491
+ <div class="log-stream" id="log-stream"></div>
2492
+ `;
2493
+ const stream = document.getElementById('log-stream');
2494
+ const filtered = _logLines.filter(l => (_logFilter === 'ALL' || l.level === _logFilter) && (!_logSearch || (l.msg||'').toLowerCase().includes(_logSearch.toLowerCase())));
2495
+ filtered.forEach(l => stream.appendChild(buildLogLineEl(l, _logSearch)));
2496
+ if (_logAutoScroll) stream.scrollTop = stream.scrollHeight;
2497
+ }
2498
+
2499
+ function buildLogLineEl(l, searchQ) {
2500
+ const div = document.createElement('div');
2501
+ div.className = 'log-line-'+(l.level || 'INFO');
2502
+ const text = (l.time||nowHHMMSS()) + ' [' + (l.level||'INFO') + '] ' + (l.msg||'');
2503
+ if (searchQ && (l.msg||'').toLowerCase().includes(searchQ.toLowerCase())) {
2504
+ const escaped = esc(text);
2505
+ const re = new RegExp('('+searchQ.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+')','gi');
2506
+ div.innerHTML = escaped.replace(re, '<mark style="background:#854f0b;color:#fac775">$1</mark>');
2507
+ } else {
2508
+ div.textContent = text;
2509
+ }
2510
+ return div;
2511
+ }
2512
+
2513
+ function appendLogLine(l) {
2514
+ if (_logFilter !== 'ALL' && l.level !== _logFilter) return;
2515
+ if (_logSearch && !(l.msg||'').toLowerCase().includes(_logSearch.toLowerCase())) return;
2516
+ const stream = document.getElementById('log-stream');
2517
+ if (!stream) return;
2518
+ stream.appendChild(buildLogLineEl(l, _logSearch));
2519
+ if (_logAutoScroll) stream.scrollTop = stream.scrollHeight;
2520
+ }
2521
+
2522
+ function toggleLogPause() {
2523
+ _logPaused = !_logPaused;
2524
+ const btn = document.getElementById('log-pause-btn');
2525
+ if (btn) { btn.textContent = _logPaused ? 'Resume' : 'Pause'; btn.className = _logPaused ? 'btn btn-primary' : 'btn'; }
2526
+ }
2527
+ function clearLogs() {
2528
+ _logLines = [];
2529
+ const stream = document.getElementById('log-stream');
2530
+ if (stream) stream.innerHTML = '';
2531
+ }
2532
+ function setLogFilter(f) { _logFilter = f; renderLogs(); }
2533
+ function toggleLogAutoScroll() { _logAutoScroll = !_logAutoScroll; renderLogs(); }
2534
+
2535
+ /* ══════════════════════════════════════════
2536
+ SETTINGS
2537
+ ══════════════════════════════════════════ */
2538
+ async function renderSettings() {
2539
+ const c = document.getElementById('content');
2540
+ c.innerHTML = '<div class="empty">Loading...</div>';
2541
+ let cfgRows = [];
2542
+ let freshCfg = {};
2543
+ try {
2544
+ const data = await api('GET','/api/config');
2545
+ if (data && typeof data === 'object') {
2546
+ cfgRows = Object.entries(data);
2547
+ freshCfg = data;
2548
+ // Sync global config cache with fresh data
2549
+ Object.assign(_config, data);
2550
+ }
2551
+ } catch(e) {}
2552
+
2553
+ const aiProvider = freshCfg.aiProvider || freshCfg.ai_provider || _config.aiProvider || _config.ai_provider || 'claude';
2554
+ const aiModel = freshCfg.aiModel || freshCfg.ai_model || freshCfg.model || _config.aiModel || _config.ai_model || _config.model || '';
2555
+ // Initialize the selected provider tracker to the stored value so Save always has a value
2556
+ _selectedProvider = aiProvider;
2557
+ const version = _status.version || '--';
2558
+ const nodeVer = _status.nodeVersion || '--';
2559
+ const sysCmd = !!(freshCfg.systemCommands || freshCfg.system_commands);
2560
+ const deskCtl = !!(freshCfg.desktopControl || freshCfg.desktop_control);
2561
+ const fsAccess = !!(freshCfg.filesystemAccess || freshCfg.filesystem_access);
2562
+
2563
+ c.innerHTML = `
2564
+ <div class="section" style="margin-bottom:14px;">
2565
+ <div class="section-head"><span class="section-title">Version Info</span></div>
2566
+ <div class="section-body">
2567
+ <table class="settings-kv">
2568
+ <tr><td>Conductor</td><td class="mono">v${esc(version)}</td></tr>
2569
+ <tr><td>Node.js</td><td class="mono">${esc(nodeVer)}</td></tr>
2570
+ <tr><td>Platform</td><td class="mono">${esc(_status.platform||'--')}</td></tr>
2571
+ <tr><td>Config dir</td><td class="mono" style="font-size:11px;">${esc(_status.configDir||'--')}</td></tr>
2572
+ </table>
2573
+ </div>
2574
+ </div>
2575
+
2576
+ <div class="section" style="margin-bottom:14px;">
2577
+ <div class="section-head"><span class="section-title">Dashboard Token</span></div>
2578
+ <div class="section-body">
2579
+ <div style="display:flex;align-items:center;gap:8px;">
2580
+ <span class="cred-masked" id="token-display">••••••••••••••••••••</span>
2581
+ <button class="btn btn-sm" onclick="toggleTokenDisplay()">Show</button>
2582
+ </div>
2583
+ <div class="mono" id="token-real" style="display:none;font-size:11px;color:var(--muted);margin-top:6px;word-break:break-all;">${esc(_dashboardToken)}</div>
2584
+ </div>
2585
+ </div>
2586
+
2587
+ <div class="section" style="margin-bottom:14px;">
2588
+ <div class="section-head"><span class="section-title">AI Provider</span></div>
2589
+ <div class="section-body">
2590
+ <div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap;">
2591
+ ${['claude','openai','gemini','ollama','openrouter'].map(p =>
2592
+ `<button class="btn btn-sm ${aiProvider===p?'btn-primary':''}" id="prov-${esc(p)}" onclick="selectProvider('${esc(p)}')">${esc(p)}</button>`
2593
+ ).join('')}
2594
+ </div>
2595
+ <div style="display:flex;gap:8px;align-items:center;">
2596
+ <span style="font-size:12px;color:var(--muted);flex-shrink:0;">Model:</span>
2597
+ <input type="text" id="ai-model-inp" value="${esc(aiModel)}" placeholder="e.g. claude-sonnet-4-6" style="flex:1;font-family:var(--mono);font-size:12px;">
2598
+ <button class="btn btn-sm btn-primary" onclick="saveAiSettings()">Save</button>
2599
+ </div>
2600
+ </div>
2601
+ </div>
2602
+
2603
+ <div class="section" style="margin-bottom:14px;">
2604
+ <div class="section-head"><span class="section-title">Security</span></div>
2605
+ <div class="section-body">
2606
+ <table class="settings-kv">
2607
+ <tr>
2608
+ <td>Shell execution</td>
2609
+ <td><label style="display:flex;align-items:center;gap:7px;cursor:pointer;">
2610
+ <input type="checkbox" id="tog-syscmd" ${sysCmd?'checked':''} onchange="saveSecuritySetting('systemCommands',this.checked)">
2611
+ <span style="font-size:12px;color:var(--muted);">Allow shell commands</span>
2612
+ </label></td>
2613
+ </tr>
2614
+ <tr>
2615
+ <td>Desktop control</td>
2616
+ <td><label style="display:flex;align-items:center;gap:7px;cursor:pointer;">
2617
+ <input type="checkbox" id="tog-desktop" ${deskCtl?'checked':''} onchange="saveSecuritySetting('desktopControl',this.checked)">
2618
+ <span style="font-size:12px;color:var(--muted);">Mouse &amp; keyboard automation</span>
2619
+ </label></td>
2620
+ </tr>
2621
+ <tr>
2622
+ <td>Filesystem access</td>
2623
+ <td><label style="display:flex;align-items:center;gap:7px;cursor:pointer;">
2624
+ <input type="checkbox" id="tog-fs" ${fsAccess?'checked':''} onchange="saveSecuritySetting('filesystemAccess',this.checked)">
2625
+ <span style="font-size:12px;color:var(--muted);">Read &amp; write files</span>
2626
+ </label></td>
2627
+ </tr>
2628
+ </table>
2629
+ </div>
2630
+ </div>
2631
+
2632
+ <div class="section">
2633
+ <div class="section-head"><span class="section-title">Config</span></div>
2634
+ <div id="settings-cfg-wrap"></div>
2635
+ </div>
2636
+ `;
2637
+ // Populate the config table separately to avoid nested template-literal issues
2638
+ const cfgWrap = document.getElementById('settings-cfg-wrap');
2639
+ if (cfgWrap) {
2640
+ const safe = cfgRows.filter(function(pair) { return !pair[0].match(/secret|token|password/i); });
2641
+ if (!safe.length) {
2642
+ cfgWrap.innerHTML = '<div class="empty">No config keys set yet. Use the fields above to configure Conductor.</div>';
2643
+ } else {
2644
+ let rows = '';
2645
+ safe.forEach(function(pair) {
2646
+ const val = pair[1];
2647
+ let rendered;
2648
+ if (val !== null && typeof val === 'object') {
2649
+ rendered = '<pre style="font-family:var(--mono);font-size:10px;color:var(--muted);white-space:pre-wrap;margin:0;max-height:80px;overflow-y:auto;word-break:break-all;">' + esc(JSON.stringify(val, null, 2)) + '</pre>';
2650
+ } else {
2651
+ rendered = '<span class="mono" style="font-size:11px;color:var(--muted);">' + esc(String(val).substring(0, 120)) + '</span>';
2652
+ }
2653
+ rows += '<tr><td class="mono" style="font-size:11px;">' + esc(pair[0]) + '</td><td>' + rendered + '</td></tr>';
2654
+ });
2655
+ cfgWrap.innerHTML = '<table class="data-table"><thead><tr><th>KEY</th><th>VALUE</th></tr></thead><tbody>' + rows + '</tbody></table>';
2656
+ }
2657
+ }
2658
+ }
2659
+
2660
+ let _selectedProvider = null;
2661
+ function selectProvider(p) {
2662
+ _selectedProvider = p;
2663
+ _config.aiProvider = p; // keep global cache in sync so re-renders preserve the selection
2664
+ ['claude','openai','gemini','ollama','openrouter'].forEach(name => {
2665
+ const btn = document.getElementById('prov-'+name);
2666
+ if (btn) btn.className = 'btn btn-sm' + (name===p?' btn-primary':'');
2667
+ });
2668
+ }
2669
+
2670
+ function toggleTokenDisplay() {
2671
+ const disp = document.getElementById('token-display');
2672
+ const real = document.getElementById('token-real');
2673
+ if (!disp || !real) return;
2674
+ const hidden = real.style.display === 'none';
2675
+ real.style.display = hidden ? '' : 'none';
2676
+ disp.style.display = hidden ? 'none' : '';
2677
+ }
2678
+
2679
+ async function saveAiSettings() {
2680
+ const provider = _selectedProvider || _config.aiProvider || 'claude';
2681
+ const modelInp = document.getElementById('ai-model-inp');
2682
+ const model = modelInp ? modelInp.value.trim() : '';
2683
+ const saveBtn = document.querySelector('[onclick="saveAiSettings()"]');
2684
+ if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; }
2685
+ try {
2686
+ await api('POST','/api/config',{key:'aiProvider',value:provider});
2687
+ if (model) await api('POST','/api/config',{key:'aiModel',value:model});
2688
+ _config.aiProvider = provider;
2689
+ if (model) _config.aiModel = model;
2690
+ toast('AI settings saved');
2691
+ } catch(e) {
2692
+ toast(e.message, 'err');
2693
+ } finally {
2694
+ if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Save'; }
2695
+ }
2696
+ }
2697
+
2698
+ async function saveSecuritySetting(key, value) {
2699
+ try {
2700
+ await api('POST','/api/config',{key,value});
2701
+ const freshCfg = await api('GET','/api/config').catch(() => ({}));
2702
+ if (freshCfg && typeof freshCfg === 'object') Object.assign(_config, freshCfg);
2703
+ toast(key+': '+(value?'enabled':'disabled'));
2704
+ } catch(e) {
2705
+ toast(e.message, 'err');
2706
+ const idMap = {systemCommands:'tog-syscmd',desktopControl:'tog-desktop',filesystemAccess:'tog-fs'};
2707
+ const el = document.getElementById(idMap[key]);
2708
+ if (el) el.checked = !value;
2709
+ }
2710
+ }
2711
+
2712
+ /* ══════════════════════════════════════════
2713
+ SIDEBAR TOGGLE
2714
+ ══════════════════════════════════════════ */
2715
+ function toggleSidebar() {
2716
+ document.getElementById('sidebar').classList.toggle('collapsed');
2717
+ }
2718
+
2719
+ /* ══════════════════════════════════════════
2720
+ COMMAND PALETTE
2721
+ ══════════════════════════════════════════ */
2722
+ const CMD_PAGES = [
2723
+ {label:'Overview',page:'overview',icon:'⊞'},
2724
+ {label:'Tasks',page:'tasks',icon:'✓'},
2725
+ {label:'Files',page:'files',icon:'📄'},
2726
+ {label:'Processes',page:'processes',icon:'▣'},
2727
+ {label:'Network',page:'network',icon:'◉'},
2728
+ {label:'System',page:'system',icon:'$_'},
2729
+ {label:'Notes',page:'notes',icon:'📝'},
2730
+ {label:'Plugins',page:'plugins',icon:'⬡'},
2731
+ {label:'Credentials',page:'credentials',icon:'🔑'},
2732
+ {label:'Logs',page:'logs',icon:'≡'},
2733
+ {label:'Settings',page:'settings',icon:'⚙'},
2734
+ ];
2735
+ let _cmdIdx = 0;
2736
+
2737
+ function openCmdPalette() {
2738
+ const overlay = document.getElementById('cmd-palette-overlay');
2739
+ const inp = document.getElementById('cmd-input');
2740
+ overlay.classList.add('open');
2741
+ inp.value = '';
2742
+ inp.focus();
2743
+ renderCmdResults('');
2744
+ }
2745
+ function closeCmdPalette() {
2746
+ document.getElementById('cmd-palette-overlay').classList.remove('open');
2747
+ }
2748
+ function renderCmdResults(q) {
2749
+ const el = document.getElementById('cmd-results');
2750
+ const results = buildCmdResults(q);
2751
+ _cmdIdx = 0;
2752
+ el.innerHTML = results.map((r,i) =>
2753
+ `<div class="cmd-item ${i===0?'selected':''}" data-idx="${i}" onmouseenter="setCmdIdx(${i})" onclick="execCmd(${i},'${esc(q)}')">
2754
+ <span class="cmd-item-icon">${esc(r.icon||'')}</span>
2755
+ <span class="cmd-item-label">${esc(r.label)}</span>
2756
+ ${r.hint ? `<span class="cmd-item-hint">${esc(r.hint)}</span>` : ''}
2757
+ </div>`
2758
+ ).join('') || '<div class="cmd-item" style="color:var(--muted);">No results</div>';
2759
+ if (results.length) el.querySelector('[data-idx="0"]').classList.add('selected');
2760
+ }
2761
+ function buildCmdResults(q) {
2762
+ const qt = q.trim();
2763
+ if (qt.startsWith('>')) {
2764
+ const cmd = qt.slice(1).trim();
2765
+ return cmd ? [{icon:'$',label:'Run: '+cmd,hint:'shell',_type:'shell',_val:cmd}] : [{icon:'$',label:'Type a shell command after >',hint:''}];
2766
+ }
2767
+ if (qt.startsWith('@')) {
2768
+ const s = qt.slice(1).toLowerCase().trim();
2769
+ return _notes.filter(n => !s || (n.title||'').toLowerCase().includes(s) || (n.content||'').toLowerCase().includes(s))
2770
+ .slice(0,8).map(n => ({icon:'📝',label:n.title||'Untitled',hint:'note',_type:'note',_id:n.id}));
2771
+ }
2772
+ return CMD_PAGES.filter(p => !qt || p.label.toLowerCase().includes(qt.toLowerCase()))
2773
+ .map(p => ({...p,hint:'page',_type:'page'}));
2774
+ }
2775
+ function setCmdIdx(i) {
2776
+ _cmdIdx = i;
2777
+ document.querySelectorAll('.cmd-item').forEach((el,j) => el.classList.toggle('selected',j===i));
2778
+ }
2779
+ function execCmd(idx, q) {
2780
+ const results = buildCmdResults(document.getElementById('cmd-input').value);
2781
+ const r = results[idx !== undefined ? idx : _cmdIdx];
2782
+ if (!r) return;
2783
+ closeCmdPalette();
2784
+ if (r._type === 'page') navigate(r.page);
2785
+ else if (r._type === 'shell') { navigate('system'); setTimeout(() => { const inp = document.getElementById('sys-shell-input'); if (inp) { inp.value = r._val; runShell(r._val); } }, 300); }
2786
+ else if (r._type === 'note') { navigate('notes'); setTimeout(() => openNote(String(r._id)), 400); }
2787
+ }
2788
+ document.getElementById('cmd-input').addEventListener('input', function() { renderCmdResults(this.value); });
2789
+ document.getElementById('cmd-input').addEventListener('keydown', function(e) {
2790
+ const items = document.querySelectorAll('.cmd-item[data-idx]');
2791
+ if (e.key === 'ArrowDown') { e.preventDefault(); setCmdIdx(Math.min(_cmdIdx+1,items.length-1)); }
2792
+ else if (e.key === 'ArrowUp') { e.preventDefault(); setCmdIdx(Math.max(_cmdIdx-1,0)); }
2793
+ else if (e.key === 'Enter') { e.preventDefault(); execCmd(_cmdIdx, this.value); }
2794
+ else if (e.key === 'Escape') closeCmdPalette();
2795
+ });
2796
+
2797
+ /* ══════════════════════════════════════════
2798
+ KEYBOARD SHORTCUTS
2799
+ ══════════════════════════════════════════ */
2800
+ function closeKbHelp() { document.getElementById('kb-help-overlay').classList.remove('open'); }
2801
+ let _gPrefix = false;
2802
+ document.addEventListener('keydown', function(e) {
2803
+ const tag = (e.target.tagName || '').toLowerCase();
2804
+ const inInput = tag === 'input' || tag === 'textarea' || e.target.isContentEditable;
2805
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
2806
+ e.preventDefault();
2807
+ const overlay = document.getElementById('cmd-palette-overlay');
2808
+ overlay.classList.contains('open') ? closeCmdPalette() : openCmdPalette();
2809
+ return;
2810
+ }
2811
+ if (e.ctrlKey && e.key === 'l') {
2812
+ const term = document.getElementById('sys-term-out');
2813
+ if (term) { e.preventDefault(); clearTerminal(); return; }
2814
+ }
2815
+ if (inInput) return;
2816
+ if (e.key === '[') { e.preventDefault(); toggleSidebar(); return; }
2817
+ if (e.key === '?') {
2818
+ e.preventDefault();
2819
+ const overlay = document.getElementById('kb-help-overlay');
2820
+ overlay.classList.contains('open') ? closeKbHelp() : overlay.classList.add('open');
2821
+ return;
2822
+ }
2823
+ if (e.key === 'Escape') {
2824
+ document.getElementById('cmd-palette-overlay').classList.remove('open');
2825
+ document.getElementById('kb-help-overlay').classList.remove('open');
2826
+ return;
2827
+ }
2828
+ if (e.key === 'n' && _currentPage === 'tasks') {
2829
+ e.preventDefault();
2830
+ const inp = document.getElementById('new-task-input');
2831
+ if (inp) inp.focus();
2832
+ return;
2833
+ }
2834
+ if (e.key === 'g' && !_gPrefix) { _gPrefix = true; setTimeout(() => { _gPrefix = false; }, 1000); return; }
2835
+ if (_gPrefix) {
2836
+ _gPrefix = false;
2837
+ const gmap = {o:'overview',t:'tasks',f:'files',p:'processes',n:'notes',s:'system',l:'logs',c:'credentials'};
2838
+ if (gmap[e.key]) { e.preventDefault(); navigate(gmap[e.key]); }
2839
+ }
2840
+ });
2841
+
2842
+ /* ══════════════════════════════════════════
2843
+ TASKS: BULK ACTIONS
2844
+ ══════════════════════════════════════════ */
2845
+ function toggleTaskSelect(id, checked) {
2846
+ if (checked) _selectedTaskIds.add(String(id));
2847
+ else _selectedTaskIds.delete(String(id));
2848
+ updateTaskBulkBar();
2849
+ }
2850
+ function updateTaskBulkBar() {
2851
+ const bar = document.getElementById('task-bulk-bar');
2852
+ const cnt = document.getElementById('task-sel-count');
2853
+ if (!bar) return;
2854
+ const n = _selectedTaskIds.size;
2855
+ bar.style.display = n > 0 ? 'flex' : 'none';
2856
+ if (cnt) cnt.textContent = n + ' selected';
2857
+ }
2858
+ function clearTaskSelection() {
2859
+ _selectedTaskIds.clear();
2860
+ renderTaskList();
2861
+ }
2862
+ async function bulkComplete() {
2863
+ const ids = [..._selectedTaskIds];
2864
+ for (const id of ids) {
2865
+ try { await api('POST','/api/todoist/tasks/'+id+'/close'); _allTasks = _allTasks.filter(t=>String(t.id)!==id); } catch(e) {}
2866
+ }
2867
+ _selectedTaskIds.clear();
2868
+ renderTaskList();
2869
+ toast(ids.length+' tasks completed');
2870
+ }
2871
+ async function bulkDelete() {
2872
+ if (!confirm('Delete '+_selectedTaskIds.size+' tasks?')) return;
2873
+ const ids = [..._selectedTaskIds];
2874
+ for (const id of ids) {
2875
+ try { await api('DELETE','/api/todoist/tasks/'+id); _allTasks = _allTasks.filter(t=>String(t.id)!==id); } catch(e) {}
2876
+ }
2877
+ _selectedTaskIds.clear();
2878
+ renderTaskList();
2879
+ toast(ids.length+' tasks deleted');
2880
+ }
2881
+
2882
+ /* ══════════════════════════════════════════
2883
+ TASKS: RELATIVE DATES
2884
+ ══════════════════════════════════════════ */
2885
+ function relativeDate(dateStr) {
2886
+ if (!dateStr) return '';
2887
+ const today = new Date(); today.setHours(0,0,0,0);
2888
+ const d = new Date(dateStr); d.setHours(0,0,0,0);
2889
+ const diff = Math.round((d - today) / 86400000);
2890
+ if (diff === 0) return 'Today';
2891
+ if (diff === 1) return 'Tomorrow';
2892
+ if (diff === -1) return 'Yesterday';
2893
+ if (diff > 0 && diff < 7) return diff + ' days';
2894
+ if (diff >= 7 && diff < 14) return '1 week';
2895
+ if (diff >= 14 && diff < 30) return Math.round(diff/7) + ' weeks';
2896
+ if (diff < 0 && diff > -7) return Math.abs(diff) + 'd ago';
2897
+ if (diff < -6) return Math.abs(Math.round(diff/7)) + 'w ago';
2898
+ return dateStr;
2899
+ }
2900
+
2901
+ /* ══════════════════════════════════════════
2902
+ PROCESSES: SORT
2903
+ ══════════════════════════════════════════ */
2904
+ function sortProcs(col) {
2905
+ if (_procSort.col === col) _procSort.dir *= -1;
2906
+ else { _procSort.col = col; _procSort.dir = col==='cpu'||col==='mem'||col==='pid' ? -1 : 1; }
2907
+ loadProcesses();
2908
+ }
2909
+
2910
+ /* ══════════════════════════════════════════
2911
+ LOGS: SEARCH + DOWNLOAD
2912
+ ══════════════════════════════════════════ */
2913
+ function setLogSearch(val) { _logSearch = val; renderLogs(); }
2914
+ function downloadLogs() {
2915
+ const filtered = _logLines.filter(l => _logFilter === 'ALL' || l.level === _logFilter)
2916
+ .filter(l => !_logSearch || (l.msg||'').toLowerCase().includes(_logSearch.toLowerCase()));
2917
+ const text = filtered.map(l => l.time+' ['+l.level+'] '+(l.msg||'')).join('\n');
2918
+ const a = document.createElement('a');
2919
+ a.href = 'data:text/plain;charset=utf-8,'+encodeURIComponent(text);
2920
+ a.download = 'conductor-logs.txt';
2921
+ a.click();
2922
+ }
2923
+
2924
+ /* ══════════════════════════════════════════
2925
+ NOTES: WORD COUNT + MARKDOWN PREVIEW
2926
+ ══════════════════════════════════════════ */
2927
+ function updateNoteWordCount() {
2928
+ const body = document.getElementById('note-body');
2929
+ const wc = document.getElementById('note-wc');
2930
+ if (!body || !wc) return;
2931
+ const words = (body.innerText||'').trim().split(/\s+/).filter(Boolean).length;
2932
+ wc.textContent = words + ' words';
2933
+ }
2934
+ function toggleNotePreview() {
2935
+ _noteMarkdownPreview = !_noteMarkdownPreview;
2936
+ const body = document.getElementById('note-body');
2937
+ const preview = document.getElementById('note-preview');
2938
+ const btn = document.getElementById('note-preview-btn');
2939
+ if (!body || !preview) return;
2940
+ if (_noteMarkdownPreview) {
2941
+ const text = body.innerText || '';
2942
+ preview.innerHTML = renderMarkdown(text);
2943
+ body.style.display = 'none';
2944
+ preview.style.display = '';
2945
+ if (btn) btn.textContent = 'Edit';
2946
+ } else {
2947
+ body.style.display = '';
2948
+ preview.style.display = 'none';
2949
+ if (btn) btn.textContent = 'Preview';
2950
+ }
2951
+ }
2952
+ function renderMarkdown(text) {
2953
+ let html = esc(text);
2954
+ html = html.replace(/^### (.+)$/gm, '<h3 style="font-size:13px;font-weight:600;margin:10px 0 4px;color:var(--text);">$1</h3>');
2955
+ html = html.replace(/^## (.+)$/gm, '<h2 style="font-size:15px;font-weight:600;margin:12px 0 5px;color:var(--text);">$1</h2>');
2956
+ html = html.replace(/^# (.+)$/gm, '<h1 style="font-size:18px;font-weight:600;margin:14px 0 6px;color:var(--text);">$1</h1>');
2957
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
2958
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
2959
+ html = html.replace(/`(.+?)`/g, '<code style="font-family:var(--mono);font-size:11px;background:var(--elevated);padding:1px 4px;border-radius:3px;">$1</code>');
2960
+ html = html.replace(/^- (.+)$/gm, '<div style="margin:2px 0;">• $1</div>');
2961
+ html = html.replace(/\n/g, '<br>');
2962
+ return html;
2963
+ }
2964
+
2965
+ /* ══════════════════════════════════════════
2966
+ CHAT
2967
+ ══════════════════════════════════════════ */
2968
+ function renderChat() {
2969
+ const c = document.getElementById('content');
2970
+ c.style.padding = '0';
2971
+ c.innerHTML = `
2972
+ <div class="chat-wrap" id="chat-wrap">
2973
+ <div class="chat-header">
2974
+ <span id="chat-provider-badge" class="chat-provider-badge">${esc(_chatProvider || 'loading...')}</span>
2975
+ <span style="flex:1;font-size:11px;color:var(--muted);" id="chat-model-label">${esc(_chatModel)}</span>
2976
+ <button class="btn btn-sm btn-danger" onclick="clearChatHistory()" title="Clear conversation">Clear</button>
2977
+ </div>
2978
+ <div class="chat-messages" id="chat-messages"></div>
2979
+ <div class="chat-input-area">
2980
+ <textarea class="chat-input" id="chat-input" rows="1" placeholder="Message Conductor... (Enter to send, Shift+Enter for newline)" onkeydown="chatInputKeyDown(event)" oninput="chatInputAutosize(this)"></textarea>
2981
+ <button class="chat-send-btn" id="chat-send-btn" onclick="sendChatMessage()">Send</button>
2982
+ </div>
2983
+ </div>`;
2984
+
2985
+ // Load provider info
2986
+ api('GET', '/api/config').then(cfg => {
2987
+ _chatProvider = cfg?.ai?.provider ?? 'unknown';
2988
+ _chatModel = cfg?.ai?.model ?? '';
2989
+ const badge = document.getElementById('chat-provider-badge');
2990
+ const label = document.getElementById('chat-model-label');
2991
+ if (badge) badge.textContent = _chatProvider;
2992
+ if (label) label.textContent = _chatModel;
2993
+ }).catch(() => {});
2994
+
2995
+ renderChatMessages();
2996
+ setTimeout(() => { const inp = document.getElementById('chat-input'); if (inp) inp.focus(); }, 50);
2997
+ }
2998
+
2999
+ function renderChatMessages() {
3000
+ const box = document.getElementById('chat-messages');
3001
+ if (!box) return;
3002
+
3003
+ if (_chatMessages.length === 0) {
3004
+ box.innerHTML = `<div class="chat-msg system-msg"><div class="chat-bubble">Conductor is ready. Ask me anything — I have access to ${(_plugins.enabled||[]).length} active plugins and 150+ tools.</div></div>`;
3005
+ return;
3006
+ }
3007
+
3008
+ box.innerHTML = _chatMessages.map(msg => {
3009
+ if (msg.role === 'system') {
3010
+ return `<div class="chat-msg system-msg"><div class="chat-bubble">${esc(msg.content)}</div></div>`;
3011
+ }
3012
+ const isUser = msg.role === 'user';
3013
+ const toolChips = (!isUser && msg.toolCalls && msg.toolCalls.length > 0)
3014
+ ? `<div class="chat-tool-chips">${msg.toolCalls.map(tc =>
3015
+ `<span class="chat-tool-chip ${tc.success ? 'ok' : 'err'}">⚙ ${esc(tc.tool)}</span>`
3016
+ ).join('')}</div>`
3017
+ : '';
3018
+ const ts = msg.ts ? `<span class="chat-ts">${new Date(msg.ts).toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit'})}</span>` : '';
3019
+ return `<div class="chat-msg ${isUser ? 'user' : 'assistant'}">
3020
+ ${toolChips}
3021
+ <div class="chat-bubble">${esc(msg.content)}</div>
3022
+ ${ts}
3023
+ </div>`;
3024
+ }).join('');
3025
+
3026
+ if (_chatPending) {
3027
+ box.innerHTML += `<div class="chat-typing" id="chat-typing"><span></span><span></span><span></span></div>`;
3028
+ }
3029
+
3030
+ box.scrollTop = box.scrollHeight;
3031
+ }
3032
+
3033
+ function chatInputAutosize(el) {
3034
+ el.style.height = 'auto';
3035
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
3036
+ }
3037
+
3038
+ function chatInputKeyDown(e) {
3039
+ if (e.key === 'Enter' && !e.shiftKey) {
3040
+ e.preventDefault();
3041
+ sendChatMessage();
3042
+ }
3043
+ }
3044
+
3045
+ async function sendChatMessage() {
3046
+ const input = document.getElementById('chat-input');
3047
+ if (!input || _chatPending) return;
3048
+ const text = input.value.trim();
3049
+ if (!text) return;
3050
+
3051
+ input.value = '';
3052
+ input.style.height = 'auto';
3053
+
3054
+ _chatMessages.push({ role: 'user', content: text, ts: Date.now() });
3055
+ _chatPending = true;
3056
+ renderChatMessages();
3057
+
3058
+ const sendBtn = document.getElementById('chat-send-btn');
3059
+ if (sendBtn) sendBtn.disabled = true;
3060
+
3061
+ try {
3062
+ const result = await api('POST', '/api/chat', { message: text, userId: 'dashboard-user' });
3063
+ _chatProvider = result.provider || _chatProvider;
3064
+ _chatModel = result.model || _chatModel;
3065
+ _chatMessages.push({
3066
+ role: 'assistant',
3067
+ content: result.response || '(no response)',
3068
+ toolCalls: result.toolCalls || [],
3069
+ ts: Date.now(),
3070
+ });
3071
+ const badge = document.getElementById('chat-provider-badge');
3072
+ const label = document.getElementById('chat-model-label');
3073
+ if (badge) badge.textContent = _chatProvider;
3074
+ if (label) label.textContent = _chatModel;
3075
+ } catch(e) {
3076
+ _chatMessages.push({ role: 'system', content: 'Error: ' + e.message, ts: Date.now() });
3077
+ } finally {
3078
+ _chatPending = false;
3079
+ if (sendBtn) sendBtn.disabled = false;
3080
+ renderChatMessages();
3081
+ setTimeout(() => { const inp = document.getElementById('chat-input'); if (inp) inp.focus(); }, 50);
3082
+ }
3083
+ }
3084
+
3085
+ async function clearChatHistory() {
3086
+ if (!confirm('Clear conversation history?')) return;
3087
+ try {
3088
+ await api('DELETE', '/api/chat/history?userId=dashboard-user');
3089
+ _chatMessages = [];
3090
+ renderChatMessages();
3091
+ toast('Conversation cleared');
3092
+ } catch(e) {
3093
+ toast('Failed to clear: ' + e.message, 'err');
3094
+ }
3095
+ }
3096
+
3097
+ /* ══════════════════════════════════════════
3098
+ MARKETPLACE
3099
+ ══════════════════════════════════════════ */
3100
+ const MKT_CATEGORIES = ['All','Utilities','Google','Developer','System','Security','Productivity','Communication','Automation','Social','Entertainment','Smart Home','AI'];
3101
+
3102
+ function renderMarketplace() {
3103
+ const c = document.getElementById('content');
3104
+ c.innerHTML = `
3105
+ <div class="section">
3106
+ <div class="section-head">
3107
+ <span class="section-title">Plugin Marketplace</span>
3108
+ <span style="font-size:11px;color:var(--muted);" id="mkt-count">Loading…</span>
3109
+ </div>
3110
+ <div class="section-body" style="padding:10px 12px 6px;">
3111
+ <div class="mkt-filter-bar" id="mkt-filter-bar">
3112
+ ${MKT_CATEGORIES.map(cat =>
3113
+ `<button class="btn btn-sm${cat.toLowerCase() === _mktFilter ? ' btn-primary' : ''}" onclick="setMktFilter('${cat.toLowerCase()}')">${esc(cat)}</button>`
3114
+ ).join('')}
3115
+ </div>
3116
+ </div>
3117
+ </div>
3118
+ <div id="mkt-grid-wrap"></div>`;
3119
+
3120
+ loadMarketplace();
3121
+ }
3122
+
3123
+ function setMktFilter(cat) {
3124
+ _mktFilter = cat;
3125
+ renderMarketplace();
3126
+ }
3127
+
3128
+ async function loadMarketplace() {
3129
+ const wrap = document.getElementById('mkt-grid-wrap');
3130
+ const count = document.getElementById('mkt-count');
3131
+ if (!wrap) return;
3132
+
3133
+ try {
3134
+ const data = await api('GET', '/api/marketplace');
3135
+ const plugins = data.plugins || [];
3136
+
3137
+ const filtered = _mktFilter === 'all'
3138
+ ? plugins
3139
+ : plugins.filter(p => (p.category || '').toLowerCase() === _mktFilter);
3140
+
3141
+ if (count) count.textContent = `${filtered.length} plugin${filtered.length !== 1 ? 's' : ''}`;
3142
+
3143
+ if (filtered.length === 0) {
3144
+ wrap.innerHTML = `<div class="empty">No plugins in this category</div>`;
3145
+ return;
3146
+ }
3147
+
3148
+ wrap.innerHTML = `<div class="mkt-grid">${filtered.map(p => {
3149
+ const isEnabled = p.enabled;
3150
+ const missingCreds = !p.enabled && p.requiresAuth;
3151
+ const statusBadge = isEnabled
3152
+ ? `<span class="badge badge-ok">enabled</span>`
3153
+ : missingCreds
3154
+ ? `<span class="badge badge-warn">${esc(p.authLabel || 'needs auth')}</span>`
3155
+ : `<span class="badge badge-muted">disabled</span>`;
3156
+ const toggleLabel = isEnabled ? 'Disable' : 'Enable';
3157
+ const toggleClass = isEnabled ? 'btn btn-sm btn-danger' : 'btn btn-sm btn-primary';
3158
+ return `<div class="mkt-card">
3159
+ <div class="mkt-card-name">${esc(p.name)}</div>
3160
+ <div class="mkt-card-desc">${esc(p.desc)}</div>
3161
+ <div class="mkt-card-meta">
3162
+ <span class="mkt-category">${esc(p.category)}</span>
3163
+ ${statusBadge}
3164
+ </div>
3165
+ <div style="margin-top:8px;display:flex;gap:6px;align-items:center;">
3166
+ <button class="${toggleClass}" onclick="mktToggle('${esc(p.name)}',${!isEnabled})">${toggleLabel}</button>
3167
+ ${p.requiresAuth && !isEnabled ? `<span style="font-size:10px;color:var(--muted);">Requires: ${esc(p.authLabel||'auth')}</span>` : ''}
3168
+ </div>
3169
+ </div>`;
3170
+ }).join('')}</div>`;
3171
+ } catch(e) {
3172
+ wrap.innerHTML = `<div class="empty" style="color:var(--err);">Failed to load marketplace: ${esc(e.message)}</div>`;
3173
+ }
3174
+ }
3175
+
3176
+ async function mktToggle(pluginName, enable) {
3177
+ try {
3178
+ await api('POST', '/api/plugins/toggle', { plugin: pluginName, enabled: enable });
3179
+ _plugins.enabled = enable
3180
+ ? [...(_plugins.enabled||[]).filter(p => p !== pluginName), pluginName]
3181
+ : (_plugins.enabled||[]).filter(p => p !== pluginName);
3182
+ toast(pluginName + (enable ? ' enabled' : ' disabled'));
3183
+ loadMarketplace();
3184
+ } catch(e) {
3185
+ toast(e.message, 'err');
3186
+ }
3187
+ }
3188
+
3189
+ /* ══════════════════════════════════════════
3190
+ HEALTH PAGE
3191
+ ══════════════════════════════════════════ */
3192
+
3193
+ async function renderHealth() {
3194
+ const content = document.getElementById('content');
3195
+ content.innerHTML = `<div class="empty">Loading health status…</div>`;
3196
+ try {
3197
+ const report = await api('GET', '/api/health');
3198
+ const statusIcon = report.status === 'ok' ? '✅' : report.status === 'degraded' ? '⚠️' : '❌';
3199
+ const statusColor = report.status === 'ok' ? 'var(--ok)' : report.status === 'degraded' ? 'var(--warn)' : 'var(--err)';
3200
+ content.innerHTML = `
3201
+ <div class="stat-grid">
3202
+ <div class="stat-block"><div class="stat-num" style="color:${statusColor}">${statusIcon} ${report.status.toUpperCase()}</div><div class="stat-label">Overall Status</div></div>
3203
+ <div class="stat-block"><div class="stat-num">${report.version}</div><div class="stat-label">Version</div></div>
3204
+ <div class="stat-block"><div class="stat-num">${formatUptime(report.uptime)}</div><div class="stat-label">Uptime</div></div>
3205
+ <div class="stat-block"><div class="stat-num">${report.components?.length ?? 0}</div><div class="stat-label">Components</div></div>
3206
+ </div>
3207
+ ${report.metrics ? `
3208
+ <div class="stat-grid">
3209
+ <div class="stat-block"><div class="stat-num">${report.metrics.totalToolCalls}</div><div class="stat-label">Tool Calls</div></div>
3210
+ <div class="stat-block"><div class="stat-num" style="color:${report.metrics.failedToolCalls > 0 ? 'var(--err)' : 'var(--ok)'}">${report.metrics.failedToolCalls}</div><div class="stat-label">Failures</div></div>
3211
+ <div class="stat-block"><div class="stat-num">${report.metrics.avgLatencyMs}ms</div><div class="stat-label">Avg Latency</div></div>
3212
+ <div class="stat-block"><div class="stat-num">${report.metrics.openCircuits}</div><div class="stat-label">Open Circuits</div></div>
3213
+ </div>` : ''}
3214
+ <div class="section">
3215
+ <div class="section-head"><span class="section-title">Components</span></div>
3216
+ <div class="section-body">
3217
+ <table class="data-table">
3218
+ <thead><tr><th>Component</th><th>Status</th><th>Details</th></tr></thead>
3219
+ <tbody>${(report.components ?? []).map(c => {
3220
+ const badge = c.status === 'ok' ? '<span class="badge badge-ok">OK</span>' : c.status === 'degraded' ? '<span class="badge badge-warn">DEGRADED</span>' : '<span class="badge badge-err">DOWN</span>';
3221
+ return `<tr><td class="mono">${esc(c.name)}</td><td>${badge}</td><td>${c.message ? esc(c.message) : '—'}</td></tr>`;
3222
+ }).join('')}</tbody>
3223
+ </table>
3224
+ </div>
3225
+ </div>
3226
+ `;
3227
+ } catch(e) {
3228
+ content.innerHTML = `<div class="empty" style="color:var(--err);">Failed to load health: ${esc(e.message)}</div>`;
3229
+ }
3230
+ }
3231
+
3232
+ /* ══════════════════════════════════════════
3233
+ AUDIT LOG PAGE
3234
+ ══════════════════════════════════════════ */
3235
+
3236
+ async function renderAudit() {
3237
+ const content = document.getElementById('content');
3238
+ content.innerHTML = `
3239
+ <div style="display:flex;gap:8px;margin-bottom:14px;align-items:center;">
3240
+ <input type="text" id="audit-filter-action" placeholder="Filter by action…" style="flex:1;">
3241
+ <input type="text" id="audit-filter-resource" placeholder="Filter by resource…" style="flex:1;">
3242
+ <select id="audit-filter-result"><option value="">All Results</option><option value="success">Success</option><option value="failure">Failure</option><option value="denied">Denied</option></select>
3243
+ <button class="btn btn-primary" onclick="loadAuditLogs()">Search</button>
3244
+ </div>
3245
+ <div id="audit-log-body"><div class="empty">Loading audit log…</div></div>
3246
+ `;
3247
+ loadAuditLogs();
3248
+ }
3249
+
3250
+ async function loadAuditLogs() {
3251
+ const wrap = document.getElementById('audit-log-body');
3252
+ if (!wrap) return;
3253
+ wrap.innerHTML = `<div class="empty">Loading…</div>`;
3254
+ try {
3255
+ const action = document.getElementById('audit-filter-action')?.value || '';
3256
+ const resource = document.getElementById('audit-filter-resource')?.value || '';
3257
+ const result = document.getElementById('audit-filter-result')?.value || '';
3258
+ const params = new URLSearchParams();
3259
+ if (action) params.set('action', action);
3260
+ if (resource) params.set('resource', resource);
3261
+ if (result) params.set('result', result);
3262
+ params.set('limit', '100');
3263
+ const entries = await api('GET', `/api/audit?${params}`);
3264
+ if (!entries || entries.length === 0) {
3265
+ wrap.innerHTML = `<div class="empty">No audit entries found.</div>`;
3266
+ return;
3267
+ }
3268
+ wrap.innerHTML = `
3269
+ <div class="section">
3270
+ <div class="section-head"><span class="section-title">${entries.length} entries</span></div>
3271
+ <div class="section-body" style="padding:0;">
3272
+ <table class="data-table">
3273
+ <thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Resource</th><th>Result</th></tr></thead>
3274
+ <tbody>${entries.map(e => {
3275
+ const badge = e.result === 'success' ? '<span class="badge badge-ok">OK</span>' : e.result === 'failure' ? '<span class="badge badge-err">FAIL</span>' : e.result === 'denied' ? '<span class="badge badge-warn">DENIED</span>' : '<span class="badge badge-muted">—</span>';
3276
+ return `<tr><td class="mono" style="font-size:11px;">${new Date(e.timestamp).toLocaleTimeString()}</td><td class="mono">${esc(e.actor)}</td><td class="mono">${esc(e.action)}</td><td class="mono">${esc(e.resource)}</td><td>${badge}</td></tr>`;
3277
+ }).join('')}</tbody>
3278
+ </table>
3279
+ </div>
3280
+ </div>
3281
+ `;
3282
+ } catch(e) {
3283
+ wrap.innerHTML = `<div class="empty" style="color:var(--err);">Failed to load audit log: ${esc(e.message)}</div>`;
3284
+ }
3285
+ }
3286
+
3287
+ /* ══════════════════════════════════════════
3288
+ WEBHOOKS PAGE
3289
+ ══════════════════════════════════════════ */
3290
+
3291
+ async function renderWebhooks() {
3292
+ const content = document.getElementById('content');
3293
+ content.innerHTML = `
3294
+ <div style="display:flex;gap:8px;margin-bottom:14px;">
3295
+ <input type="text" id="webhook-url" placeholder="https://your-server.com/webhook" style="flex:1;">
3296
+ <input type="text" id="webhook-events" placeholder="Events (comma-separated, * for all)" style="flex:1;">
3297
+ <button class="btn btn-primary" onclick="createWebhook()">Create</button>
3298
+ </div>
3299
+ <div id="webhooks-body"><div class="empty">Loading webhooks…</div></div>
3300
+ `;
3301
+ loadWebhooks();
3302
+ }
3303
+
3304
+ async function loadWebhooks() {
3305
+ const wrap = document.getElementById('webhooks-body');
3306
+ if (!wrap) return;
3307
+ try {
3308
+ const subs = await api('GET', '/api/webhooks');
3309
+ if (!subs || subs.length === 0) {
3310
+ wrap.innerHTML = `<div class="empty">No webhooks configured.</div>`;
3311
+ return;
3312
+ }
3313
+ wrap.innerHTML = `
3314
+ <div class="section">
3315
+ <div class="section-head"><span class="section-title">${subs.length} subscriptions</span></div>
3316
+ <div class="section-body" style="padding:0;">
3317
+ <table class="data-table">
3318
+ <thead><tr><th>URL</th><th>Events</th><th>Status</th><th>Failures</th><th>Last Success</th><th></th></tr></thead>
3319
+ <tbody>${subs.map(s => {
3320
+ const statusBadge = s.active ? '<span class="badge badge-ok">Active</span>' : '<span class="badge badge-err">Disabled</span>';
3321
+ return `<tr>
3322
+ <td class="mono" style="font-size:11px;max-width:300px;overflow:hidden;text-overflow:ellipsis;">${esc(s.url)}</td>
3323
+ <td class="mono" style="font-size:11px;">${(s.events ?? []).join(', ')}</td>
3324
+ <td>${statusBadge}</td>
3325
+ <td>${s.consecutiveFailures ?? 0}</td>
3326
+ <td class="mono" style="font-size:11px;">${s.lastSuccessAt ? new Date(s.lastSuccessAt).toLocaleString() : '—'}</td>
3327
+ <td><button class="btn btn-sm btn-danger" onclick="deleteWebhook('${s.id}')">Delete</button></td>
3328
+ </tr>`;
3329
+ }).join('')}</tbody>
3330
+ </table>
3331
+ </div>
3332
+ </div>
3333
+ `;
3334
+ } catch(e) {
3335
+ wrap.innerHTML = `<div class="empty" style="color:var(--err);">Failed to load webhooks: ${esc(e.message)}</div>`;
3336
+ }
3337
+ }
3338
+
3339
+ async function createWebhook() {
3340
+ const url = document.getElementById('webhook-url')?.value?.trim();
3341
+ const eventsStr = document.getElementById('webhook-events')?.value?.trim() || '*';
3342
+ if (!url) { toast('URL is required', 'err'); return; }
3343
+ const events = eventsStr.split(',').map(e => e.trim()).filter(Boolean);
3344
+ try {
3345
+ await api('POST', '/api/webhooks', { url, events });
3346
+ toast('Webhook created');
3347
+ loadWebhooks();
3348
+ } catch(e) { toast(e.message, 'err'); }
3349
+ }
3350
+
3351
+ async function deleteWebhook(id) {
3352
+ if (!confirm('Delete this webhook?')) return;
3353
+ try {
3354
+ await api('DELETE', `/api/webhooks/${id}`);
3355
+ toast('Webhook deleted');
3356
+ loadWebhooks();
3357
+ } catch(e) { toast(e.message, 'err'); }
3358
+ }
3359
+
3360
+ /* ══════════════════════════════════════════
3361
+ METRICS PAGE
3362
+ ══════════════════════════════════════════ */
3363
+
3364
+ async function renderMetrics() {
3365
+ const content = document.getElementById('content');
3366
+ content.innerHTML = `<div class="empty">Loading metrics…</div>`;
3367
+ try {
3368
+ const metrics = await api('GET', '/api/metrics');
3369
+ const entries = Object.entries(metrics).sort((a, b) => (b[1].calls ?? 0) - (a[1].calls ?? 0));
3370
+ if (entries.length === 0) {
3371
+ content.innerHTML = `<div class="empty">No metrics yet. Call some tools first!</div>`;
3372
+ return;
3373
+ }
3374
+ const totalCalls = entries.reduce((sum, [, m]) => sum + (m.calls ?? 0), 0);
3375
+ const totalErrors = entries.reduce((sum, [, m]) => sum + (m.errors ?? 0), 0);
3376
+ content.innerHTML = `
3377
+ <div class="stat-grid">
3378
+ <div class="stat-block"><div class="stat-num">${totalCalls}</div><div class="stat-label">Total Calls</div></div>
3379
+ <div class="stat-block"><div class="stat-num" style="color:${totalErrors > 0 ? 'var(--err)' : 'var(--ok)'}">${totalErrors}</div><div class="stat-label">Errors</div></div>
3380
+ <div class="stat-block"><div class="stat-num">${entries.length}</div><div class="stat-label">Tools Used</div></div>
3381
+ <div class="stat-block"><div class="stat-num">${totalCalls > 0 ? ((1 - totalErrors / totalCalls) * 100).toFixed(1) : 100}%</div><div class="stat-label">Success Rate</div></div>
3382
+ </div>
3383
+ <div class="section">
3384
+ <div class="section-head"><span class="section-title">Tool Metrics</span></div>
3385
+ <div class="section-body" style="padding:0;">
3386
+ <table class="data-table">
3387
+ <thead><tr><th>Tool</th><th>Calls</th><th>Errors</th><th>Avg Latency</th><th>Last Call</th><th>Success Rate</th></tr></thead>
3388
+ <tbody>${entries.map(([name, m]) => {
3389
+ const rate = m.calls > 0 ? ((1 - m.errors / m.calls) * 100).toFixed(1) : '100.0';
3390
+ const rateColor = parseFloat(rate) >= 95 ? 'var(--ok)' : parseFloat(rate) >= 80 ? 'var(--warn)' : 'var(--err)';
3391
+ return `<tr>
3392
+ <td class="mono">${esc(name)}</td>
3393
+ <td>${m.calls ?? 0}</td>
3394
+ <td style="color:${m.errors > 0 ? 'var(--err)' : 'inherit'}">${m.errors ?? 0}</td>
3395
+ <td class="mono">${Math.round(m.avgLatencyMs ?? 0)}ms</td>
3396
+ <td class="mono" style="font-size:11px;">${m.lastCallAt ? new Date(m.lastCallAt).toLocaleString() : '—'}</td>
3397
+ <td><span style="color:${rateColor};font-weight:600;">${rate}%</span></td>
3398
+ </tr>`;
3399
+ }).join('')}</tbody>
3400
+ </table>
3401
+ </div>
3402
+ </div>
3403
+ `;
3404
+ } catch(e) {
3405
+ content.innerHTML = `<div class="empty" style="color:var(--err);">Failed to load metrics: ${esc(e.message)}</div>`;
3406
+ }
3407
+ }
3408
+
3409
+ function formatUptime(seconds) {
3410
+ if (seconds < 60) return seconds + 's';
3411
+ if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
3412
+ if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
3413
+ return Math.floor(seconds / 86400) + 'd ' + Math.floor((seconds % 86400) / 3600) + 'h';
3414
+ }
3415
+
3416
+ /* ══════════════════════════════════════════
3417
+ NAV + BOOT
3418
+ ══════════════════════════════════════════ */
3419
+ document.querySelectorAll('.nav-item').forEach(el => {
3420
+ el.addEventListener('click', () => navigate(el.dataset.page));
3421
+ });
3422
+
3423
+ init();
3424
+ </script>
3425
+ </body>
3426
+ </html>