@monoes/monomindcli 1.14.7 → 1.15.1

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 (329) hide show
  1. package/.claude/agents/reengineer-squad/boss.md +113 -0
  2. package/.claude/agents/reengineer-squad/critic-architect.md +132 -0
  3. package/.claude/agents/reengineer-squad/git-manager.md +145 -0
  4. package/.claude/agents/reengineer-squad/idea-generator.md +95 -0
  5. package/.claude/agents/reengineer-squad/implementer.md +112 -0
  6. package/.claude/agents/reengineer-squad/integration-planner.md +112 -0
  7. package/.claude/agents/reengineer-squad/source-analyst.md +103 -0
  8. package/.claude/agents/reengineer-squad/target-analyst.md +118 -0
  9. package/.claude/agents/reengineer-squad/tester.md +105 -0
  10. package/.claude/commands/mastermind/master.md +35 -14
  11. package/.claude/helpers/handlers/capture-handler.cjs +155 -18
  12. package/.claude/helpers/monolean-activate.cjs +20 -0
  13. package/.claude/helpers/monolean-config.cjs +76 -0
  14. package/.claude/helpers/monolean-instructions.cjs +109 -0
  15. package/.claude/helpers/monolean-propagate.cjs +9 -0
  16. package/.claude/helpers/monolean-tracker.cjs +18 -0
  17. package/.claude/helpers/skill-registry.json +2 -2
  18. package/.claude/skills/agent-browser-testing/SKILL.md +301 -18
  19. package/.claude/skills/mastermind/runorg.md +69 -23
  20. package/.claude/skills/monodesign/SKILL.md +32 -1
  21. package/.claude/skills/monodesign/adapt.md +53 -0
  22. package/.claude/skills/monodesign/agents/monodesign-asset-producer.md +100 -0
  23. package/.claude/skills/monodesign/animate.md +65 -0
  24. package/.claude/skills/monodesign/audit.md +89 -0
  25. package/.claude/skills/monodesign/bolder.md +50 -0
  26. package/.claude/skills/monodesign/clarify.md +64 -0
  27. package/.claude/skills/monodesign/colorize.md +68 -0
  28. package/.claude/skills/monodesign/craft.md +51 -0
  29. package/.claude/skills/monodesign/critique.md +66 -0
  30. package/.claude/skills/monodesign/delight.md +47 -0
  31. package/.claude/skills/monodesign/distill.md +56 -0
  32. package/.claude/skills/monodesign/document.md +80 -0
  33. package/.claude/skills/monodesign/extract.md +74 -0
  34. package/.claude/skills/monodesign/harden.md +65 -0
  35. package/.claude/skills/monodesign/live.md +59 -0
  36. package/.claude/skills/monodesign/onboard.md +50 -0
  37. package/.claude/skills/monodesign/optimize.md +64 -0
  38. package/.claude/skills/monodesign/overdrive.md +56 -0
  39. package/.claude/skills/monodesign/polish.md +68 -0
  40. package/.claude/skills/monodesign/quieter.md +57 -0
  41. package/.claude/skills/monodesign/reference/antipatterns-catalog.md +248 -76
  42. package/.claude/skills/monodesign/reference/codex.md +107 -0
  43. package/.claude/skills/monodesign/reference/craft.md +3 -0
  44. package/.claude/skills/monodesign/reference/hooks.md +99 -0
  45. package/.claude/skills/monodesign/reference/image-prompts.md +12 -0
  46. package/.claude/skills/monodesign/shape.md +71 -0
  47. package/.claude/skills/monodesign/teach.md +69 -0
  48. package/.claude/skills/monodesign/typeset.md +59 -0
  49. package/.claude/skills/monolean/SKILL.md +118 -0
  50. package/.claude/skills/monolean-audit/SKILL.md +41 -0
  51. package/.claude/skills/monolean-debt/SKILL.md +46 -0
  52. package/.claude/skills/monolean-help/SKILL.md +60 -0
  53. package/.claude/skills/monolean-review/SKILL.md +57 -0
  54. package/bin/cli.js +3 -1
  55. package/dist/dashboard/server.js +137 -0
  56. package/dist/src/__tests__/browse-adapters.test.d.ts +2 -0
  57. package/dist/src/__tests__/browse-adapters.test.d.ts.map +1 -0
  58. package/dist/src/__tests__/browse-adapters.test.js +51 -0
  59. package/dist/src/__tests__/browse-adapters.test.js.map +1 -0
  60. package/dist/src/__tests__/browse-analyzer.test.d.ts +2 -0
  61. package/dist/src/__tests__/browse-analyzer.test.d.ts.map +1 -0
  62. package/dist/src/__tests__/browse-analyzer.test.js +68 -0
  63. package/dist/src/__tests__/browse-analyzer.test.js.map +1 -0
  64. package/dist/src/__tests__/browse-builtin-handlers.test.d.ts +2 -0
  65. package/dist/src/__tests__/browse-builtin-handlers.test.d.ts.map +1 -0
  66. package/dist/src/__tests__/browse-builtin-handlers.test.js +139 -0
  67. package/dist/src/__tests__/browse-builtin-handlers.test.js.map +1 -0
  68. package/dist/src/__tests__/browse-cdp.test.d.ts +2 -0
  69. package/dist/src/__tests__/browse-cdp.test.d.ts.map +1 -0
  70. package/dist/src/__tests__/browse-cdp.test.js +169 -0
  71. package/dist/src/__tests__/browse-cdp.test.js.map +1 -0
  72. package/dist/src/__tests__/browse-dashboard.test.d.ts +2 -0
  73. package/dist/src/__tests__/browse-dashboard.test.d.ts.map +1 -0
  74. package/dist/src/__tests__/browse-dashboard.test.js +179 -0
  75. package/dist/src/__tests__/browse-dashboard.test.js.map +1 -0
  76. package/dist/src/__tests__/browse-engine.test.d.ts +2 -0
  77. package/dist/src/__tests__/browse-engine.test.d.ts.map +1 -0
  78. package/dist/src/__tests__/browse-engine.test.js +122 -0
  79. package/dist/src/__tests__/browse-engine.test.js.map +1 -0
  80. package/dist/src/__tests__/browse-expression.test.d.ts +2 -0
  81. package/dist/src/__tests__/browse-expression.test.d.ts.map +1 -0
  82. package/dist/src/__tests__/browse-expression.test.js +54 -0
  83. package/dist/src/__tests__/browse-expression.test.js.map +1 -0
  84. package/dist/src/__tests__/browse-store.test.d.ts +2 -0
  85. package/dist/src/__tests__/browse-store.test.d.ts.map +1 -0
  86. package/dist/src/__tests__/browse-store.test.js +99 -0
  87. package/dist/src/__tests__/browse-store.test.js.map +1 -0
  88. package/dist/src/__tests__/browse-workflow-types.test.d.ts +2 -0
  89. package/dist/src/__tests__/browse-workflow-types.test.d.ts.map +1 -0
  90. package/dist/src/__tests__/browse-workflow-types.test.js +33 -0
  91. package/dist/src/__tests__/browse-workflow-types.test.js.map +1 -0
  92. package/dist/src/browser/action-builder/analyzer.d.ts +11 -0
  93. package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -0
  94. package/dist/src/browser/action-builder/analyzer.js +71 -0
  95. package/dist/src/browser/action-builder/analyzer.js.map +1 -0
  96. package/dist/src/browser/action-builder/types.d.ts +47 -0
  97. package/dist/src/browser/action-builder/types.d.ts.map +1 -0
  98. package/dist/src/browser/action-builder/types.js +2 -0
  99. package/dist/src/browser/action-builder/types.js.map +1 -0
  100. package/dist/src/browser/adapters/gemini.d.ts +3 -0
  101. package/dist/src/browser/adapters/gemini.d.ts.map +1 -0
  102. package/dist/src/browser/adapters/gemini.js +16 -0
  103. package/dist/src/browser/adapters/gemini.js.map +1 -0
  104. package/dist/src/browser/adapters/google.d.ts +3 -0
  105. package/dist/src/browser/adapters/google.d.ts.map +1 -0
  106. package/dist/src/browser/adapters/google.js +17 -0
  107. package/dist/src/browser/adapters/google.js.map +1 -0
  108. package/dist/src/browser/adapters/index.d.ts +19 -0
  109. package/dist/src/browser/adapters/index.d.ts.map +1 -0
  110. package/dist/src/browser/adapters/index.js +23 -0
  111. package/dist/src/browser/adapters/index.js.map +1 -0
  112. package/dist/src/browser/adapters/instagram.d.ts +3 -0
  113. package/dist/src/browser/adapters/instagram.d.ts.map +1 -0
  114. package/dist/src/browser/adapters/instagram.js +17 -0
  115. package/dist/src/browser/adapters/instagram.js.map +1 -0
  116. package/dist/src/browser/adapters/linkedin.d.ts +3 -0
  117. package/dist/src/browser/adapters/linkedin.d.ts.map +1 -0
  118. package/dist/src/browser/adapters/linkedin.js +19 -0
  119. package/dist/src/browser/adapters/linkedin.js.map +1 -0
  120. package/dist/src/browser/adapters/microsoft.d.ts +3 -0
  121. package/dist/src/browser/adapters/microsoft.d.ts.map +1 -0
  122. package/dist/src/browser/adapters/microsoft.js +16 -0
  123. package/dist/src/browser/adapters/microsoft.js.map +1 -0
  124. package/dist/src/browser/adapters/x.d.ts +3 -0
  125. package/dist/src/browser/adapters/x.d.ts.map +1 -0
  126. package/dist/src/browser/adapters/x.js +19 -0
  127. package/dist/src/browser/adapters/x.js.map +1 -0
  128. package/dist/src/browser/dashboard/api-types.d.ts +50 -0
  129. package/dist/src/browser/dashboard/api-types.d.ts.map +1 -0
  130. package/dist/src/browser/dashboard/api-types.js +14 -0
  131. package/dist/src/browser/dashboard/api-types.js.map +1 -0
  132. package/dist/src/browser/dashboard/server.d.ts +9 -0
  133. package/dist/src/browser/dashboard/server.d.ts.map +1 -0
  134. package/dist/src/browser/dashboard/server.js +62 -0
  135. package/dist/src/browser/dashboard/server.js.map +1 -0
  136. package/dist/src/browser/dashboard/ui.html +1811 -0
  137. package/dist/src/browser/workflow/builtin-handlers.d.ts +3 -0
  138. package/dist/src/browser/workflow/builtin-handlers.d.ts.map +1 -0
  139. package/dist/src/browser/workflow/builtin-handlers.js +343 -0
  140. package/dist/src/browser/workflow/builtin-handlers.js.map +1 -0
  141. package/dist/src/browser/workflow/engine.d.ts +15 -0
  142. package/dist/src/browser/workflow/engine.d.ts.map +1 -0
  143. package/dist/src/browser/workflow/engine.js +127 -0
  144. package/dist/src/browser/workflow/engine.js.map +1 -0
  145. package/dist/src/browser/workflow/expression.d.ts +4 -0
  146. package/dist/src/browser/workflow/expression.d.ts.map +1 -0
  147. package/dist/src/browser/workflow/expression.js +64 -0
  148. package/dist/src/browser/workflow/expression.js.map +1 -0
  149. package/dist/src/browser/workflow/store.d.ts +24 -0
  150. package/dist/src/browser/workflow/store.d.ts.map +1 -0
  151. package/dist/src/browser/workflow/store.js +145 -0
  152. package/dist/src/browser/workflow/store.js.map +1 -0
  153. package/dist/src/browser/workflow/types.d.ts +48 -0
  154. package/dist/src/browser/workflow/types.d.ts.map +1 -0
  155. package/dist/src/browser/workflow/types.js +2 -0
  156. package/dist/src/browser/workflow/types.js.map +1 -0
  157. package/dist/src/commands/browse-action.d.ts +4 -0
  158. package/dist/src/commands/browse-action.d.ts.map +1 -0
  159. package/dist/src/commands/browse-action.js +151 -0
  160. package/dist/src/commands/browse-action.js.map +1 -0
  161. package/dist/src/commands/browse-platform.d.ts +4 -0
  162. package/dist/src/commands/browse-platform.d.ts.map +1 -0
  163. package/dist/src/commands/browse-platform.js +117 -0
  164. package/dist/src/commands/browse-platform.js.map +1 -0
  165. package/dist/src/commands/browse-workflow.d.ts +4 -0
  166. package/dist/src/commands/browse-workflow.d.ts.map +1 -0
  167. package/dist/src/commands/browse-workflow.js +153 -0
  168. package/dist/src/commands/browse-workflow.js.map +1 -0
  169. package/dist/src/commands/browse.d.ts +10 -6
  170. package/dist/src/commands/browse.d.ts.map +1 -1
  171. package/dist/src/commands/browse.js +11 -2154
  172. package/dist/src/commands/browse.js.map +1 -1
  173. package/dist/src/commands/design-detect.d.ts +21 -0
  174. package/dist/src/commands/design-detect.d.ts.map +1 -0
  175. package/dist/src/commands/design-detect.js +127 -0
  176. package/dist/src/commands/design-detect.js.map +1 -0
  177. package/dist/src/commands/design-palette.d.ts +22 -0
  178. package/dist/src/commands/design-palette.d.ts.map +1 -0
  179. package/dist/src/commands/design-palette.js +539 -0
  180. package/dist/src/commands/design-palette.js.map +1 -0
  181. package/dist/src/commands/hooks-core-commands.d.ts +10 -0
  182. package/dist/src/commands/hooks-core-commands.d.ts.map +1 -0
  183. package/dist/src/commands/hooks-core-commands.js +377 -0
  184. package/dist/src/commands/hooks-core-commands.js.map +1 -0
  185. package/dist/src/commands/hooks-coverage-commands.d.ts +12 -0
  186. package/dist/src/commands/hooks-coverage-commands.d.ts.map +1 -0
  187. package/dist/src/commands/hooks-coverage-commands.js +1217 -0
  188. package/dist/src/commands/hooks-coverage-commands.js.map +1 -0
  189. package/dist/src/commands/hooks-coverage-utils.d.ts +42 -0
  190. package/dist/src/commands/hooks-coverage-utils.d.ts.map +1 -0
  191. package/dist/src/commands/hooks-coverage-utils.js +220 -0
  192. package/dist/src/commands/hooks-coverage-utils.js.map +1 -0
  193. package/dist/src/commands/hooks-extended-commands.d.ts +14 -0
  194. package/dist/src/commands/hooks-extended-commands.d.ts.map +1 -0
  195. package/dist/src/commands/hooks-extended-commands.js +579 -0
  196. package/dist/src/commands/hooks-extended-commands.js.map +1 -0
  197. package/dist/src/commands/hooks-formatting.d.ts +13 -0
  198. package/dist/src/commands/hooks-formatting.d.ts.map +1 -0
  199. package/dist/src/commands/hooks-formatting.js +42 -0
  200. package/dist/src/commands/hooks-formatting.js.map +1 -0
  201. package/dist/src/commands/hooks-routing-commands.d.ts +15 -0
  202. package/dist/src/commands/hooks-routing-commands.d.ts.map +1 -0
  203. package/dist/src/commands/hooks-routing-commands.js +723 -0
  204. package/dist/src/commands/hooks-routing-commands.js.map +1 -0
  205. package/dist/src/commands/hooks-workers.d.ts +9 -0
  206. package/dist/src/commands/hooks-workers.d.ts.map +1 -0
  207. package/dist/src/commands/hooks-workers.js +782 -0
  208. package/dist/src/commands/hooks-workers.js.map +1 -0
  209. package/dist/src/commands/hooks.d.ts +8 -0
  210. package/dist/src/commands/hooks.d.ts.map +1 -1
  211. package/dist/src/commands/hooks.js +179 -4103
  212. package/dist/src/commands/hooks.js.map +1 -1
  213. package/dist/src/commands/index.d.ts +1 -0
  214. package/dist/src/commands/index.d.ts.map +1 -1
  215. package/dist/src/commands/index.js +6 -0
  216. package/dist/src/commands/index.js.map +1 -1
  217. package/dist/src/commands/org.d.ts.map +1 -1
  218. package/dist/src/commands/org.js +14 -15
  219. package/dist/src/commands/org.js.map +1 -1
  220. package/dist/src/commands/tokens.d.ts.map +1 -1
  221. package/dist/src/commands/tokens.js +77 -1
  222. package/dist/src/commands/tokens.js.map +1 -1
  223. package/dist/src/init/executor.d.ts.map +1 -1
  224. package/dist/src/init/executor.js +18 -8
  225. package/dist/src/init/executor.js.map +1 -1
  226. package/dist/src/init/settings-generator.d.ts.map +1 -1
  227. package/dist/src/init/settings-generator.js +39 -5
  228. package/dist/src/init/settings-generator.js.map +1 -1
  229. package/dist/src/init/statusline-generator.d.ts.map +1 -1
  230. package/dist/src/init/statusline-generator.js +25 -5
  231. package/dist/src/init/statusline-generator.js.map +1 -1
  232. package/dist/src/mcp-tools/browser-tools.d.ts +3 -5
  233. package/dist/src/mcp-tools/browser-tools.d.ts.map +1 -1
  234. package/dist/src/mcp-tools/browser-tools.js +619 -326
  235. package/dist/src/mcp-tools/browser-tools.js.map +1 -1
  236. package/dist/src/mcp-tools/hooks-embedding.d.ts +161 -0
  237. package/dist/src/mcp-tools/hooks-embedding.d.ts.map +1 -0
  238. package/dist/src/mcp-tools/hooks-embedding.js +506 -0
  239. package/dist/src/mcp-tools/hooks-embedding.js.map +1 -0
  240. package/dist/src/mcp-tools/hooks-intelligence.d.ts +26 -0
  241. package/dist/src/mcp-tools/hooks-intelligence.d.ts.map +1 -0
  242. package/dist/src/mcp-tools/hooks-intelligence.js +1328 -0
  243. package/dist/src/mcp-tools/hooks-intelligence.js.map +1 -0
  244. package/dist/src/mcp-tools/hooks-routing.d.ts +27 -0
  245. package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -0
  246. package/dist/src/mcp-tools/hooks-routing.js +1591 -0
  247. package/dist/src/mcp-tools/hooks-routing.js.map +1 -0
  248. package/dist/src/mcp-tools/hooks-tools.d.ts +3 -38
  249. package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
  250. package/dist/src/mcp-tools/hooks-tools.js +5 -3393
  251. package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
  252. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  253. package/dist/src/mcp-tools/monograph-tools.js +24 -14
  254. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  255. package/dist/src/mcp-tools/workflow-tools.d.ts.map +1 -1
  256. package/dist/src/mcp-tools/workflow-tools.js +54 -1
  257. package/dist/src/mcp-tools/workflow-tools.js.map +1 -1
  258. package/dist/src/memory/embedding-operations.d.ts +58 -0
  259. package/dist/src/memory/embedding-operations.d.ts.map +1 -0
  260. package/dist/src/memory/embedding-operations.js +299 -0
  261. package/dist/src/memory/embedding-operations.js.map +1 -0
  262. package/dist/src/memory/ewc-consolidation.d.ts.map +1 -1
  263. package/dist/src/memory/ewc-consolidation.js +37 -3
  264. package/dist/src/memory/ewc-consolidation.js.map +1 -1
  265. package/dist/src/memory/hnsw-operations.d.ts +130 -0
  266. package/dist/src/memory/hnsw-operations.d.ts.map +1 -0
  267. package/dist/src/memory/hnsw-operations.js +400 -0
  268. package/dist/src/memory/hnsw-operations.js.map +1 -0
  269. package/dist/src/memory/intelligence.d.ts.map +1 -1
  270. package/dist/src/memory/intelligence.js +42 -23
  271. package/dist/src/memory/intelligence.js.map +1 -1
  272. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  273. package/dist/src/memory/memory-bridge.js +52 -8
  274. package/dist/src/memory/memory-bridge.js.map +1 -1
  275. package/dist/src/memory/memory-crud.d.ts +67 -0
  276. package/dist/src/memory/memory-crud.d.ts.map +1 -0
  277. package/dist/src/memory/memory-crud.js +415 -0
  278. package/dist/src/memory/memory-crud.js.map +1 -0
  279. package/dist/src/memory/memory-initializer.d.ts +9 -322
  280. package/dist/src/memory/memory-initializer.d.ts.map +1 -1
  281. package/dist/src/memory/memory-initializer.js +17 -1794
  282. package/dist/src/memory/memory-initializer.js.map +1 -1
  283. package/dist/src/memory/memory-migrations.d.ts +30 -0
  284. package/dist/src/memory/memory-migrations.d.ts.map +1 -0
  285. package/dist/src/memory/memory-migrations.js +134 -0
  286. package/dist/src/memory/memory-migrations.js.map +1 -0
  287. package/dist/src/memory/memory-read.d.ts +78 -0
  288. package/dist/src/memory/memory-read.d.ts.map +1 -0
  289. package/dist/src/memory/memory-read.js +331 -0
  290. package/dist/src/memory/memory-read.js.map +1 -0
  291. package/dist/src/memory/memory-schema.d.ts +13 -0
  292. package/dist/src/memory/memory-schema.d.ts.map +1 -0
  293. package/dist/src/memory/memory-schema.js +167 -0
  294. package/dist/src/memory/memory-schema.js.map +1 -0
  295. package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
  296. package/dist/src/memory/sona-optimizer.js +37 -4
  297. package/dist/src/memory/sona-optimizer.js.map +1 -1
  298. package/dist/src/monovector/route-outcomes.d.ts.map +1 -1
  299. package/dist/src/monovector/route-outcomes.js +16 -6
  300. package/dist/src/monovector/route-outcomes.js.map +1 -1
  301. package/dist/src/pricing/model-pricing.d.ts +41 -0
  302. package/dist/src/pricing/model-pricing.d.ts.map +1 -0
  303. package/dist/src/pricing/model-pricing.js +61 -0
  304. package/dist/src/pricing/model-pricing.js.map +1 -0
  305. package/dist/src/ui/.monomind/capture/active-run.json +1 -0
  306. package/dist/src/ui/.monomind/orgs/system-trial-qa/runs/real-events-1782290897.convs.jsonl +3 -0
  307. package/dist/src/ui/.monomind/orgs/system-trial-qa/runs/real-events-1782290897.jsonl +11 -0
  308. package/dist/src/ui/.monomind/orgs/system-trial-qa/runs/rigid-qa-restart-1782288201.jsonl +540 -0
  309. package/dist/src/ui/.monomind/orgs/system-trial-qa-threads.jsonl +3 -0
  310. package/dist/src/ui/.monomind/orgs/test-event-fix/runs/rigid-qa-restart-1782288201.jsonl +2 -0
  311. package/dist/src/ui/MODULARIZATION_PLAN.md +79 -0
  312. package/dist/src/ui/collector.mjs +23 -13
  313. package/dist/src/ui/dashboard.html +1653 -14
  314. package/dist/src/ui/data/known-projects.json +1 -0
  315. package/dist/src/ui/data/mastermind-events.jsonl +553 -0
  316. package/dist/src/ui/data/sessions/_index.json +1 -0
  317. package/dist/src/ui/data/sessions/final-sess-001.jsonl +542 -0
  318. package/dist/src/ui/data/unknown-events.jsonl +1 -0
  319. package/dist/src/ui/orgs.html +154 -10
  320. package/dist/src/ui/server.mjs +1162 -168
  321. package/dist/src/ui/sse-manager.mjs +119 -0
  322. package/dist/src/update/checker.js +1 -1
  323. package/dist/src/update/checker.js.map +1 -1
  324. package/dist/tsconfig.tsbuildinfo +1 -1
  325. package/dist/workflow/builtin-handlers.js +321 -0
  326. package/dist/workflow/engine.js +253 -0
  327. package/dist/workflow/expression.js +98 -0
  328. package/dist/workflow/types.js +2 -0
  329. package/package.json +8 -6
@@ -0,0 +1,1811 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>monomind dashboard</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { background: #0b0b14; color: #ccc; font-family: system-ui,-apple-system,sans-serif; font-size: 13px; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
10
+
11
+ /* ── top bar ─────────────────────────────────────────────── */
12
+ #topbar {
13
+ background: #12122080;
14
+ border-bottom: 1px solid #1e1e30;
15
+ padding: 0 16px;
16
+ height: 42px;
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 12px;
20
+ flex-shrink: 0;
21
+ backdrop-filter: blur(8px);
22
+ }
23
+ #topbar .logo { font-size: 14px; font-weight: 700; color: #a78bfa; letter-spacing: .5px; margin-right: 6px; }
24
+ #topbar .logo span { color: #555; font-weight: 400; }
25
+ .pill { border-radius: 10px; padding: 1px 8px; font-size: 10px; border: 1px solid; }
26
+ .pill-run { background: #7c3aed22; border-color: #7c3aed55; color: #a78bfa; }
27
+ .pill-done { background: #22c55e15; border-color: #22c55e44; color: #22c55e; }
28
+ .pill-fail { background: #ef444415; border-color: #ef444444; color: #ef4444; }
29
+ #conn { margin-left: auto; font-size: 10px; color: #555; display: flex; align-items: center; gap: 4px; }
30
+ #conn-dot { width: 6px; height: 6px; border-radius: 50%; background: #555; }
31
+
32
+ /* ── layout ──────────────────────────────────────────────── */
33
+ #layout { display: flex; flex: 1; overflow: hidden; }
34
+
35
+ /* ── sidebar ─────────────────────────────────────────────── */
36
+ #sidebar {
37
+ width: 180px;
38
+ background: #0f0f1e;
39
+ border-right: 1px solid #1a1a2a;
40
+ display: flex;
41
+ flex-direction: column;
42
+ flex-shrink: 0;
43
+ padding: 12px 0;
44
+ }
45
+ .nav-section-label {
46
+ font-size: 9px;
47
+ font-weight: 700;
48
+ text-transform: uppercase;
49
+ letter-spacing: 1px;
50
+ color: #3a3a5a;
51
+ padding: 12px 14px 4px;
52
+ }
53
+ .nav-item {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 8px;
57
+ padding: 6px 14px;
58
+ cursor: pointer;
59
+ color: #666;
60
+ font-size: 12px;
61
+ border-left: 2px solid transparent;
62
+ transition: all .12s;
63
+ user-select: none;
64
+ }
65
+ .nav-item:hover { color: #aaa; background: #ffffff08; }
66
+ .nav-item.active { color: #c4b5fd; border-left-color: #7c3aed; background: #7c3aed12; }
67
+ .nav-item .nav-icon { font-size: 14px; width: 16px; text-align: center; }
68
+ .nav-badge { margin-left: auto; background: #7c3aed33; border-radius: 8px; padding: 0 5px; font-size: 9px; color: #a78bfa; min-width: 16px; text-align: center; }
69
+ .nav-badge.green { background: #22c55e22; color: #22c55e; }
70
+
71
+ /* ── main panel ──────────────────────────────────────────── */
72
+ #main { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
73
+ .panel { display: none; flex: 1; flex-direction: column; overflow: hidden; }
74
+ .panel.active { display: flex; }
75
+ .panel-header {
76
+ padding: 14px 18px 10px;
77
+ border-bottom: 1px solid #1a1a2a;
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 10px;
81
+ flex-shrink: 0;
82
+ }
83
+ .panel-title { font-size: 14px; font-weight: 600; color: #ddd; }
84
+ .panel-subtitle { color: #555; font-size: 11px; margin-left: auto; }
85
+
86
+ /* ── playbook run cards ──────────────────────────────────── */
87
+ #pb-cards { padding: 12px; display: grid; gap: 10px; overflow-y: auto; }
88
+ .card {
89
+ background: #131320;
90
+ border: 1px solid #7c3aed30;
91
+ border-radius: 8px;
92
+ padding: 12px 14px;
93
+ transition: border-color .2s;
94
+ }
95
+ .card:hover { border-color: #7c3aed66; }
96
+ .card.done { background: #0e0e1c; border-color: #1e1e30; opacity: .7; }
97
+ .card.done:hover { opacity: .9; border-color: #2a2a40; }
98
+ .card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
99
+ .card-title { color: #e2e2f0; font-weight: 600; font-size: 13px; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
100
+ .status-badge { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; border-radius: 4px; padding: 2px 6px; border: 1px solid; flex-shrink: 0; }
101
+ .s-running { background: #7c3aed18; border-color: #7c3aed55; color: #a78bfa; }
102
+ .s-completed { background: #22c55e15; border-color: #22c55e44; color: #22c55e; }
103
+ .s-failed { background: #ef444415; border-color: #ef444444; color: #ef4444; }
104
+ .s-stopped { background: #f59e0b15; border-color: #f59e0b44; color: #f59e0b; }
105
+ .card-meta { display: flex; gap: 14px; font-size: 10px; color: #444; margin-bottom: 6px; align-items: center; }
106
+ .card-meta .mitem { display: flex; align-items: center; gap: 3px; }
107
+ .stop-btn { margin-left: auto; background: #ef444415; border: 1px solid #ef444433; color: #ef4444; border-radius: 4px; padding: 2px 8px; font-size: 10px; cursor: pointer; transition: background .15s; }
108
+ .stop-btn:hover { background: #ef444430; }
109
+ .progress-track { height: 2px; background: #1a1a2a; border-radius: 2px; overflow: hidden; margin-bottom: 6px; }
110
+ .progress-fill { height: 100%; background: linear-gradient(90deg, #7c3aed, #a78bfa); border-radius: 2px; transition: width .4s; }
111
+ .log-area { background: #09090f; border-radius: 4px; padding: 5px 8px; font-family: monospace; font-size: 10px; color: #555; line-height: 1.6; max-height: 72px; overflow: hidden; }
112
+ .log-ts { color: #2a2a40; }
113
+ .log-ok { color: #22c55e88; }
114
+ .log-err { color: #ef444488; }
115
+ .log-info { color: #3b82f688; }
116
+
117
+ /* ── empty state ─────────────────────────────────────────── */
118
+ .empty {
119
+ flex: 1;
120
+ display: flex;
121
+ flex-direction: column;
122
+ align-items: center;
123
+ justify-content: center;
124
+ color: #333;
125
+ gap: 8px;
126
+ padding: 40px;
127
+ }
128
+ .empty-icon { font-size: 32px; }
129
+ .empty-title { color: #555; font-size: 14px; font-weight: 600; }
130
+ .empty-sub { font-size: 11px; text-align: center; line-height: 1.6; }
131
+
132
+ /* ── placeholder panels ─────────────────────────────────── */
133
+ #agents-panel .placeholder,
134
+ #memory-panel .placeholder,
135
+ #sessions-panel .placeholder {
136
+ flex: 1;
137
+ display: flex;
138
+ flex-direction: column;
139
+ align-items: center;
140
+ justify-content: center;
141
+ color: #2a2a3a;
142
+ gap: 8px;
143
+ font-size: 12px;
144
+ }
145
+ #agents-panel .placeholder .big { font-size: 28px; }
146
+
147
+ /* ── playbook builder ────────────────────────────────────── */
148
+ #builder-panel { display: none; flex-direction: column; overflow: hidden; }
149
+ #builder-panel.active { display: flex; }
150
+
151
+ /* Builder toolbar */
152
+ #builder-toolbar {
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 8px;
156
+ padding: 8px 12px;
157
+ border-bottom: 1px solid #1a1a2a;
158
+ background: #0f0f1e;
159
+ flex-shrink: 0;
160
+ }
161
+ #pb-name-input {
162
+ background: #0b0b14;
163
+ border: 1px solid #1e1e30;
164
+ border-radius: 5px;
165
+ padding: 4px 10px;
166
+ color: #ddd;
167
+ font-size: 12px;
168
+ width: 200px;
169
+ outline: none;
170
+ }
171
+ #pb-name-input:focus { border-color: #7c3aed66; }
172
+ .tb-btn {
173
+ background: #131320;
174
+ border: 1px solid #1e1e30;
175
+ border-radius: 5px;
176
+ padding: 4px 10px;
177
+ color: #999;
178
+ font-size: 11px;
179
+ cursor: pointer;
180
+ transition: all .12s;
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 5px;
184
+ white-space: nowrap;
185
+ }
186
+ .tb-btn:hover { border-color: #7c3aed55; color: #c4b5fd; background: #7c3aed12; }
187
+ .tb-btn.run-btn { border-color: #22c55e44; color: #22c55e; background: #22c55e10; }
188
+ .tb-btn.run-btn:hover { background: #22c55e22; }
189
+ .tb-btn.danger { border-color: #ef444433; color: #ef4444; background: #ef444410; }
190
+ .tb-btn.danger:hover { background: #ef444425; }
191
+ .tb-sep { width: 1px; height: 20px; background: #1e1e30; }
192
+
193
+ /* Builder body = palette + canvas + properties */
194
+ #builder-body { display: flex; flex: 1; overflow: hidden; }
195
+
196
+ /* Node palette */
197
+ #node-palette {
198
+ width: 200px;
199
+ background: #0c0c1a;
200
+ border-right: 1px solid #1a1a2a;
201
+ display: flex;
202
+ flex-direction: column;
203
+ flex-shrink: 0;
204
+ overflow-y: auto;
205
+ }
206
+ #palette-search {
207
+ margin: 8px;
208
+ background: #0b0b14;
209
+ border: 1px solid #1e1e30;
210
+ border-radius: 5px;
211
+ padding: 5px 8px;
212
+ color: #ccc;
213
+ font-size: 11px;
214
+ outline: none;
215
+ flex-shrink: 0;
216
+ }
217
+ #palette-search:focus { border-color: #7c3aed55; }
218
+ .palette-cat-label {
219
+ font-size: 9px;
220
+ font-weight: 700;
221
+ text-transform: uppercase;
222
+ letter-spacing: 1px;
223
+ color: #3a3a5a;
224
+ padding: 8px 10px 3px;
225
+ flex-shrink: 0;
226
+ }
227
+ .palette-node {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 7px;
231
+ padding: 5px 10px;
232
+ cursor: grab;
233
+ font-size: 11px;
234
+ color: #777;
235
+ border-left: 2px solid transparent;
236
+ transition: all .1s;
237
+ user-select: none;
238
+ flex-shrink: 0;
239
+ }
240
+ .palette-node:hover { color: #bbb; background: #ffffff08; }
241
+ .palette-node:active { cursor: grabbing; }
242
+ .palette-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
243
+
244
+ /* Canvas area */
245
+ #canvas-wrap {
246
+ flex: 1;
247
+ position: relative;
248
+ overflow: hidden;
249
+ background: #0b0b14;
250
+ background-image:
251
+ radial-gradient(circle, #1e1e30 1px, transparent 1px);
252
+ background-size: 24px 24px;
253
+ }
254
+ #canvas-svg {
255
+ position: absolute;
256
+ top: 0; left: 0;
257
+ width: 100%; height: 100%;
258
+ pointer-events: none;
259
+ }
260
+ #canvas-nodes {
261
+ position: absolute;
262
+ top: 0; left: 0;
263
+ width: 0; height: 0;
264
+ transform-origin: 0 0;
265
+ }
266
+
267
+ /* Canvas node */
268
+ .cn {
269
+ position: absolute;
270
+ width: 200px;
271
+ background: linear-gradient(160deg,#0d0d1e 0%,#090914 100%);
272
+ border: 1.5px solid rgba(124,58,237,0.15);
273
+ border-radius: 9px;
274
+ user-select: none;
275
+ cursor: default;
276
+ box-shadow: 0 6px 20px rgba(0,0,0,.5);
277
+ transition: border-color .14s, box-shadow .14s;
278
+ }
279
+ .cn.selected { box-shadow: 0 0 0 1.5px #7c3aed55, 0 12px 32px rgba(0,0,0,.7); }
280
+ .cn.running { animation: nodePulse 1s ease-in-out infinite; }
281
+ .cn-header {
282
+ height: 40px;
283
+ border-radius: 8px 8px 0 0;
284
+ display: flex;
285
+ align-items: center;
286
+ padding: 0 8px 0 10px;
287
+ cursor: grab;
288
+ gap: 6px;
289
+ border-bottom: 1px solid rgba(124,58,237,0.1);
290
+ }
291
+ .cn-header:active { cursor: grabbing; }
292
+ .cn-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
293
+ .cn-label { flex: 1; font-size: 11px; font-weight: 600; color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: monospace; }
294
+ .cn-del { background: none; border: none; cursor: pointer; color: rgba(148,163,184,.3); font-size: 14px; padding: 1px 3px; line-height: 1; transition: color .1s; }
295
+ .cn-del:hover { color: #ef4444; }
296
+ .cn-ports { padding: 6px 0; }
297
+ .cn-row { height: 26px; display: flex; align-items: center; justify-content: space-between; }
298
+ .cn-port-area { display: flex; align-items: center; gap: 5px; }
299
+ .cn-port {
300
+ width: 11px; height: 11px;
301
+ border-radius: 50%;
302
+ background: #1e1e30;
303
+ border: 1.5px solid rgba(124,58,237,0.4);
304
+ cursor: crosshair;
305
+ flex-shrink: 0;
306
+ transition: all .1s;
307
+ }
308
+ .cn-port:hover, .cn-port.highlight { background: #7c3aed; border-color: #7c3aed; }
309
+ .cn-port.in { margin-left: -6px; }
310
+ .cn-port.out { margin-right: -6px; }
311
+ .cn-port-label { font-size: 9px; color: #3a3a5a; font-family: monospace; }
312
+ .cn-status {
313
+ position: absolute; top: -9px; right: -5px;
314
+ font-size: 8px; font-weight: 700;
315
+ border-radius: 8px; padding: 1px 5px;
316
+ pointer-events: none;
317
+ white-space: nowrap;
318
+ }
319
+ .cn-status.running { background: #7c3aed; color: #fff; box-shadow: 0 0 8px #7c3aed66; }
320
+ .cn-status.ok { background: #10b981; color: #fff; box-shadow: 0 0 8px #10b98166; }
321
+ .cn-status.error { background: #ef4444; color: #fff; box-shadow: 0 0 8px #ef444466; }
322
+ .cn-status.skipped { background: #6b7280; color: #fff; }
323
+
324
+ /* Properties panel */
325
+ #props-panel {
326
+ width: 240px;
327
+ background: #0c0c1a;
328
+ border-left: 1px solid #1a1a2a;
329
+ display: flex;
330
+ flex-direction: column;
331
+ flex-shrink: 0;
332
+ overflow-y: auto;
333
+ }
334
+ #props-header {
335
+ padding: 10px 12px 8px;
336
+ border-bottom: 1px solid #1a1a2a;
337
+ font-size: 10px;
338
+ font-weight: 700;
339
+ text-transform: uppercase;
340
+ letter-spacing: 1px;
341
+ color: #3a3a5a;
342
+ flex-shrink: 0;
343
+ }
344
+ #props-body { padding: 10px 12px; flex: 1; }
345
+ .prop-row { margin-bottom: 10px; }
346
+ .prop-label { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #444; margin-bottom: 4px; }
347
+ .prop-input, .prop-select, .prop-textarea {
348
+ width: 100%;
349
+ background: #0b0b14;
350
+ border: 1px solid #1e1e30;
351
+ border-radius: 4px;
352
+ padding: 5px 8px;
353
+ color: #ccc;
354
+ font-size: 11px;
355
+ outline: none;
356
+ font-family: monospace;
357
+ }
358
+ .prop-input:focus, .prop-select:focus, .prop-textarea:focus { border-color: #7c3aed55; }
359
+ .prop-select { appearance: none; cursor: pointer; }
360
+ .prop-textarea { resize: vertical; min-height: 60px; }
361
+
362
+ /* Playbook list inside builder */
363
+ #pb-list-overlay {
364
+ display: none;
365
+ position: absolute;
366
+ inset: 0;
367
+ background: rgba(10,10,20,0.85);
368
+ z-index: 100;
369
+ align-items: center;
370
+ justify-content: center;
371
+ }
372
+ #pb-list-overlay.show { display: flex; }
373
+ #pb-list-box {
374
+ width: 480px;
375
+ max-height: 70vh;
376
+ background: #0d0d1a;
377
+ border: 1px solid #7c3aed33;
378
+ border-radius: 10px;
379
+ display: flex;
380
+ flex-direction: column;
381
+ overflow: hidden;
382
+ box-shadow: 0 24px 60px rgba(0,0,0,.8);
383
+ }
384
+ .pblo-header {
385
+ padding: 12px 14px;
386
+ border-bottom: 1px solid #1a1a2a;
387
+ display: flex;
388
+ align-items: center;
389
+ gap: 8px;
390
+ flex-shrink: 0;
391
+ }
392
+ .pblo-title { font-size: 12px; font-weight: 700; color: #ddd; flex: 1; font-family: monospace; text-transform: uppercase; letter-spacing: 1px; }
393
+ .pblo-close { background: none; border: none; cursor: pointer; color: #555; font-size: 16px; }
394
+ .pblo-close:hover { color: #aaa; }
395
+ .pblo-body { flex: 1; overflow-y: auto; }
396
+ .pblo-item {
397
+ display: flex;
398
+ align-items: center;
399
+ gap: 10px;
400
+ padding: 10px 14px;
401
+ border-bottom: 1px solid #1a1a2a10;
402
+ cursor: pointer;
403
+ transition: background .1s;
404
+ }
405
+ .pblo-item:hover { background: #7c3aed10; }
406
+ .pblo-item-name { flex: 1; font-size: 12px; color: #ccc; }
407
+ .pblo-item-meta { font-size: 10px; color: #444; }
408
+ .pblo-del { background: none; border: none; cursor: pointer; color: #444; font-size: 12px; padding: 2px 5px; }
409
+ .pblo-del:hover { color: #ef4444; }
410
+ .pblo-empty { padding: 30px; text-align: center; color: #333; font-size: 12px; }
411
+
412
+ @keyframes nodePulse {
413
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(124,58,237,0.4), 0 6px 20px rgba(0,0,0,.5); }
414
+ 50% { box-shadow: 0 0 0 4px rgba(124,58,237,0.1), 0 6px 20px rgba(0,0,0,.5); }
415
+ }
416
+ </style>
417
+ </head>
418
+ <body>
419
+
420
+ <!-- ── top bar ─────────────────────────────────────── -->
421
+ <div id="topbar">
422
+ <span class="logo">monomind <span>dashboard</span></span>
423
+ <span class="pill pill-run" id="p-run">● 0</span>
424
+ <span class="pill pill-done" id="p-done">✓ 0</span>
425
+ <span class="pill pill-fail" id="p-fail">✗ 0</span>
426
+ <span id="conn">
427
+ <span id="conn-dot"></span>
428
+ <span id="conn-label">connecting…</span>
429
+ </span>
430
+ </div>
431
+
432
+ <!-- ── layout ──────────────────────────────────────── -->
433
+ <div id="layout">
434
+
435
+ <!-- sidebar -->
436
+ <nav id="sidebar">
437
+ <div class="nav-section-label">Project</div>
438
+ <div class="nav-item active" data-panel="playbooks">
439
+ <span class="nav-icon">⚡</span> Runs
440
+ <span class="nav-badge" id="nb-run">0</span>
441
+ </div>
442
+ <div class="nav-item" data-panel="builder">
443
+ <span class="nav-icon">🔧</span> Builder
444
+ </div>
445
+
446
+ <div class="nav-section-label" style="margin-top:8px">System</div>
447
+ <div class="nav-item" data-panel="agents">
448
+ <span class="nav-icon">🤖</span> Agents
449
+ </div>
450
+ <div class="nav-item" data-panel="memory">
451
+ <span class="nav-icon">🧠</span> Memory
452
+ </div>
453
+ <div class="nav-item" data-panel="sessions">
454
+ <span class="nav-icon">🔗</span> Sessions
455
+ </div>
456
+ </nav>
457
+
458
+ <!-- main -->
459
+ <div id="main">
460
+
461
+ <!-- Playbook Runs panel -->
462
+ <div class="panel active" id="pb-panel">
463
+ <div class="panel-header">
464
+ <span class="panel-title">Playbook Runs</span>
465
+ <span class="panel-subtitle" id="pb-subtitle">0 runs</span>
466
+ </div>
467
+ <div id="pb-cards" style="overflow-y:auto;flex:1;padding:12px;display:grid;gap:10px;align-content:start"></div>
468
+ <div class="empty" id="pb-empty">
469
+ <span class="empty-icon">⚡</span>
470
+ <span class="empty-title">No playbook runs yet</span>
471
+ <span class="empty-sub">Use the Builder tab to create and run playbooks,<br>or run via CLI: <code style="color:#7c3aed">npx monomind browse playbook run &lt;name&gt;</code></span>
472
+ </div>
473
+ </div>
474
+
475
+ <!-- Builder panel -->
476
+ <div class="panel" id="builder-panel">
477
+ <!-- Toolbar -->
478
+ <div id="builder-toolbar">
479
+ <input id="pb-name-input" type="text" placeholder="Untitled Playbook" value="Untitled Playbook" />
480
+ <div class="tb-sep"></div>
481
+ <button class="tb-btn" id="btn-new" title="New playbook">+ New</button>
482
+ <button class="tb-btn" id="btn-open" title="Open saved playbook">📂 Open</button>
483
+ <button class="tb-btn" id="btn-save" title="Save playbook">💾 Save</button>
484
+ <div class="tb-sep"></div>
485
+ <button class="tb-btn run-btn" id="btn-run" title="Run playbook">▶ Run</button>
486
+ <div class="tb-sep"></div>
487
+ <button class="tb-btn" id="btn-zoom-in" title="Zoom in">+</button>
488
+ <button class="tb-btn" id="btn-zoom-out" title="Zoom out">-</button>
489
+ <button class="tb-btn" id="btn-fit" title="Fit canvas">⊡</button>
490
+ </div>
491
+
492
+ <!-- Builder body -->
493
+ <div id="builder-body">
494
+ <!-- Node palette -->
495
+ <div id="node-palette">
496
+ <input id="palette-search" type="text" placeholder="Search nodes…" />
497
+ <div id="palette-list"></div>
498
+ </div>
499
+
500
+ <!-- Canvas -->
501
+ <div id="canvas-wrap">
502
+ <svg id="canvas-svg" xmlns="http://www.w3.org/2000/svg">
503
+ <defs>
504
+ <marker id="arrowhead" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
505
+ <polygon points="0 0, 7 3.5, 0 7" fill="#7c3aed88" />
506
+ </marker>
507
+ </defs>
508
+ <g id="edge-layer"></g>
509
+ <path id="drag-edge" d="" stroke="#a78bfa" stroke-width="1.5" fill="none" stroke-dasharray="6,3" style="display:none"/>
510
+ </svg>
511
+ <div id="canvas-nodes"></div>
512
+
513
+ <!-- Playbook list overlay -->
514
+ <div id="pb-list-overlay">
515
+ <div id="pb-list-box">
516
+ <div class="pblo-header">
517
+ <span class="pblo-title">Saved Playbooks</span>
518
+ <button class="pblo-close" id="pblo-close-btn">✕</button>
519
+ </div>
520
+ <div class="pblo-body" id="pblo-body"></div>
521
+ </div>
522
+ </div>
523
+ </div>
524
+
525
+ <!-- Properties panel -->
526
+ <div id="props-panel">
527
+ <div id="props-header">Properties</div>
528
+ <div id="props-body">
529
+ <div class="empty" style="padding:20px;color:#333;font-size:11px;text-align:center">
530
+ Select a node to edit its properties
531
+ </div>
532
+ </div>
533
+ </div>
534
+ </div>
535
+ </div>
536
+
537
+ <!-- Agents panel -->
538
+ <div class="panel" id="agents-panel">
539
+ <div class="panel-header"><span class="panel-title">Agents</span></div>
540
+ <div class="placeholder">
541
+ <span class="big">🤖</span>
542
+ <span>Agent monitoring coming soon</span>
543
+ </div>
544
+ </div>
545
+
546
+ <!-- Memory panel -->
547
+ <div class="panel" id="memory-panel">
548
+ <div class="panel-header"><span class="panel-title">Memory</span></div>
549
+ <div class="placeholder">
550
+ <span class="big">🧠</span>
551
+ <span>Memory browser coming soon</span>
552
+ </div>
553
+ </div>
554
+
555
+ <!-- Sessions panel -->
556
+ <div class="panel" id="sessions-panel">
557
+ <div class="panel-header"><span class="panel-title">Sessions</span></div>
558
+ <div class="placeholder">
559
+ <span class="big">🔗</span>
560
+ <span>Session viewer coming soon</span>
561
+ </div>
562
+ </div>
563
+
564
+ </div>
565
+ </div>
566
+
567
+ <script>
568
+ // ═══════════════════════════════════════════════════════════════════
569
+ // NAV
570
+ // ═══════════════════════════════════════════════════════════════════
571
+ function navigateToPanel(target) {
572
+ document.querySelectorAll('.nav-item').forEach(function(n) { n.classList.remove('active'); });
573
+ var navItem = document.querySelector('[data-panel="' + target + '"]');
574
+ if (navItem) navItem.classList.add('active');
575
+ document.querySelectorAll('.panel').forEach(function(p) { p.classList.remove('active'); });
576
+ var panelId = target === 'playbooks' ? 'pb-panel'
577
+ : target === 'builder' ? 'builder-panel'
578
+ : target + '-panel';
579
+ var panel = document.getElementById(panelId);
580
+ if (panel) panel.classList.add('active');
581
+ if (target === 'builder') renderEdges();
582
+ location.hash = target;
583
+ }
584
+
585
+ document.querySelectorAll('.nav-item').forEach(function(item) {
586
+ item.addEventListener('click', function() {
587
+ var target = item.getAttribute('data-panel');
588
+ navigateToPanel(target);
589
+ });
590
+ });
591
+
592
+ // Hash-based routing on load
593
+ (function() {
594
+ var hash = location.hash.replace('#', '');
595
+ var validPanels = ['playbooks', 'builder', 'agents', 'memory', 'sessions'];
596
+ if (hash && validPanels.indexOf(hash) !== -1) {
597
+ // defer until after renderEdges is defined
598
+ window.addEventListener('load', function() { navigateToPanel(hash); });
599
+ }
600
+ })();
601
+
602
+ // ═══════════════════════════════════════════════════════════════════
603
+ // PLAYBOOK RUN STATE (existing panel)
604
+ // ═══════════════════════════════════════════════════════════════════
605
+ var runs = {};
606
+ var runLogs = {};
607
+
608
+ function ts(ms) {
609
+ return new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
610
+ }
611
+ function esc(s) {
612
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
613
+ }
614
+
615
+ function updateHeader() {
616
+ var running = 0, done = 0, fail = 0;
617
+ for (var id in runs) {
618
+ var s = runs[id].status;
619
+ if (s === 'running') running++;
620
+ else if (s === 'completed') done++;
621
+ else if (s === 'failed') fail++;
622
+ }
623
+ document.getElementById('p-run').textContent = '● ' + running;
624
+ document.getElementById('p-done').textContent = '✓ ' + done;
625
+ document.getElementById('p-fail').textContent = '✗ ' + fail;
626
+ document.getElementById('nb-run').textContent = String(running || Object.keys(runs).length);
627
+ document.getElementById('nb-run').className = 'nav-badge' + (running ? '' : ' green');
628
+ var total = Object.keys(runs).length;
629
+ document.getElementById('pb-subtitle').textContent = total + (total === 1 ? ' run' : ' runs');
630
+ }
631
+
632
+ function progressPct(r) {
633
+ if (!r.itemsTotal || r.itemsTotal <= 0) return r.status === 'completed' ? 100 : 0;
634
+ return Math.min(100, Math.round((r.itemsProcessed / r.itemsTotal) * 100));
635
+ }
636
+ function statusLabel(r) {
637
+ if (r.status === 'running') return ['s-running', '● running'];
638
+ if (r.status === 'completed') return ['s-completed', '✓ done'];
639
+ if (r.status === 'failed') return ['s-failed', '✗ failed'];
640
+ return ['s-stopped', '⏹ stopped'];
641
+ }
642
+ function elapsed(r) {
643
+ var ms = (r.endedAt || r.completedAt || Date.now()) - r.startedAt;
644
+ if (ms < 1000) return ms + 'ms';
645
+ if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
646
+ return Math.floor(ms / 60000) + 'm ' + Math.floor((ms % 60000) / 1000) + 's';
647
+ }
648
+ function renderCards() {
649
+ var cards = document.getElementById('pb-cards');
650
+ var empty = document.getElementById('pb-empty');
651
+ var sorted = Object.values(runs).sort(function(a, b) { return b.startedAt - a.startedAt; });
652
+ if (!sorted.length) { cards.innerHTML = ''; empty.style.display = 'flex'; return; }
653
+ empty.style.display = 'none';
654
+ cards.innerHTML = sorted.map(function(r) {
655
+ var sl = statusLabel(r);
656
+ var isRunning = r.status === 'running';
657
+ var pct = progressPct(r);
658
+ var logLines = (runLogs[r.id] || []).slice(-4);
659
+ var logHtml = logLines.length
660
+ ? logLines.map(function(l) {
661
+ var cls = l.type === 'ok' ? 'log-ok' : l.type === 'err' ? 'log-err' : 'log-info';
662
+ return '<div><span class="log-ts">' + esc(l.ts) + '</span> <span class="' + cls + '">' + esc(l.text) + '</span></div>';
663
+ }).join('')
664
+ : '<div style="color:#2a2a3a">no events yet</div>';
665
+ return '<div class="card' + (!isRunning ? ' done' : '') + '">' +
666
+ '<div class="card-header">' +
667
+ '<span class="card-title">' + esc(r.playbookName) + '</span>' +
668
+ '<span class="status-badge ' + sl[0] + '">' + sl[1] + '</span>' +
669
+ '</div>' +
670
+ '<div class="card-meta">' +
671
+ '<span class="mitem">⏱ ' + esc(elapsed(r)) + '</span>' +
672
+ '<span class="mitem">📦 ' + esc(r.itemsProcessed) + '/' + esc(r.itemsTotal || '?') + '</span>' +
673
+ '<span class="mitem" style="color:#3a3a5a">id: ' + esc(r.id.slice(0,8)) + '</span>' +
674
+ (isRunning ? '<button class="stop-btn" onclick="stopRun(\'' + esc(r.id) + '\')">■ stop</button>' : '') +
675
+ '</div>' +
676
+ '<div class="progress-track"><div class="progress-fill" style="width:' + pct + '%"></div></div>' +
677
+ '<div class="log-area">' + logHtml + '</div>' +
678
+ '</div>';
679
+ }).join('');
680
+ }
681
+ function stopRun(id) { fetch('/stop/' + id, { method: 'POST' }).catch(function() {}); }
682
+
683
+ // ═══════════════════════════════════════════════════════════════════
684
+ // NODE PALETTE DEFINITIONS
685
+ // ═══════════════════════════════════════════════════════════════════
686
+ var NODE_TYPES = [
687
+ { cat: 'Triggers', type: 'trigger.manual', label: 'Manual Trigger', color: '#7c3aed',
688
+ inputs: [], outputs: [{ label: 'items' }],
689
+ defaults: { items: [] } },
690
+ { cat: 'Triggers', type: 'trigger.schedule', label: 'Schedule', color: '#7c3aed',
691
+ inputs: [], outputs: [{ label: 'items' }],
692
+ defaults: { cron: '0 * * * *' } },
693
+
694
+ { cat: 'Control', type: 'core.if', label: 'If / Branch', color: '#0891b2',
695
+ inputs: [{ label: 'in' }], outputs: [{ label: 'true' }, { label: 'false' }],
696
+ defaults: { expression: '$item.data.value > 0' } },
697
+ { cat: 'Control', type: 'core.filter', label: 'Filter', color: '#0891b2',
698
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
699
+ defaults: { expression: '$item.data.value > 0' } },
700
+
701
+ { cat: 'Data', type: 'core.set', label: 'Set Fields', color: '#d97706',
702
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
703
+ defaults: {} },
704
+ { cat: 'Data', type: 'action.log', label: 'Log', color: '#d97706',
705
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
706
+ defaults: { label: 'debug' } },
707
+
708
+ { cat: 'HTTP', type: 'action.http', label: 'HTTP Request', color: '#d97706',
709
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
710
+ defaults: { url: 'https://example.com/api', method: 'GET' } },
711
+ { cat: 'HTTP', type: 'http.request', label: 'HTTP Advanced', color: '#d97706',
712
+ inputs: [{ label: 'in' }], outputs: [{ label: 'main' }, { label: 'error' }],
713
+ defaults: { url: 'https://example.com/api', method: 'GET', response_format: 'json' } },
714
+
715
+ { cat: 'Services', type: 'service.google_drive', label: 'Google Drive', color: '#0f766e',
716
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
717
+ defaults: { operation: 'list_files', access_token: '' } },
718
+ { cat: 'Services', type: 'service.gmail', label: 'Gmail', color: '#0f766e',
719
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
720
+ defaults: { operation: 'list_messages', access_token: '' } },
721
+ { cat: 'Services', type: 'service.github', label: 'GitHub', color: '#0f766e',
722
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
723
+ defaults: { operation: 'list_repos', token: '' } },
724
+ { cat: 'Services', type: 'service.google_sheets', label: 'Google Sheets', color: '#0f766e',
725
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
726
+ defaults: { operation: 'read_rows', access_token: '', spreadsheet_id: '' } },
727
+
728
+ { cat: 'Browser', type: 'action.gemini_image', label: 'Gemini Image', color: '#9333ea',
729
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
730
+ defaults: { prompt: 'A beautiful landscape' } },
731
+ { cat: 'Browser', type: 'action.save_file', label: 'Save File', color: '#9333ea',
732
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
733
+ defaults: { path: './output.txt' } },
734
+
735
+ { cat: 'Services', type: 'service.notion', label: 'Notion', color: '#0f766e',
736
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
737
+ defaults: { operation: 'query_database', token: '' } },
738
+ { cat: 'Services', type: 'service.linear', label: 'Linear', color: '#0f766e',
739
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
740
+ defaults: { operation: 'list_issues', token: '' } },
741
+ { cat: 'Services', type: 'service.airtable', label: 'Airtable', color: '#0f766e',
742
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
743
+ defaults: { operation: 'list_records', token: '', base_id: '', table: '' } },
744
+ { cat: 'Services', type: 'service.stripe', label: 'Stripe', color: '#0f766e',
745
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
746
+ defaults: { operation: 'list_customers', api_key: '' } },
747
+
748
+ { cat: 'Data', type: 'core.aggregate', label: 'Aggregate', color: '#d97706',
749
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
750
+ defaults: { group_by: '', operations: [] } },
751
+ { cat: 'Data', type: 'core.sort', label: 'Sort', color: '#d97706',
752
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
753
+ defaults: { field: '', order: 'asc' } },
754
+ { cat: 'Data', type: 'core.limit', label: 'Limit', color: '#d97706',
755
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
756
+ defaults: { count: 10 } },
757
+ { cat: 'Data', type: 'core.merge', label: 'Merge', color: '#d97706',
758
+ inputs: [{ label: 'in1' }, { label: 'in2' }], outputs: [{ label: 'out' }],
759
+ defaults: { mode: 'append' } },
760
+ { cat: 'Data', type: 'core.remove_duplicates', label: 'Remove Duplicates', color: '#d97706',
761
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
762
+ defaults: { field: '' } },
763
+ { cat: 'Data', type: 'core.switch', label: 'Switch', color: '#0891b2',
764
+ inputs: [{ label: 'in' }], outputs: [{ label: 'case0' }, { label: 'default' }],
765
+ defaults: { field: '', cases: [] } },
766
+ { cat: 'Data', type: 'core.wait', label: 'Wait', color: '#d97706',
767
+ inputs: [{ label: 'in' }], outputs: [{ label: 'out' }],
768
+ defaults: { seconds: 5 } },
769
+ { cat: 'Data', type: 'core.split_in_batches', label: 'Split in Batches', color: '#d97706',
770
+ inputs: [{ label: 'in' }], outputs: [{ label: 'batch' }],
771
+ defaults: { batch_size: 10 } },
772
+ ];
773
+
774
+ var CAT_ORDER = ['Triggers','Control','Data','HTTP','Services','Browser'];
775
+
776
+ // Fields shown for each node type in the properties panel
777
+ var NODE_FIELDS = {
778
+ 'trigger.manual': [{ key:'items', label:'Initial Items (JSON array)', type:'textarea' }],
779
+ 'trigger.schedule': [{ key:'cron', label:'Cron expression', type:'text' }],
780
+ 'core.if': [{ key:'expression', label:'Expression', type:'text' }],
781
+ 'core.filter': [{ key:'expression', label:'Expression', type:'text' }],
782
+ 'core.set': [{ key:'__note', label:'Config (JSON)', type:'textarea', jsonKey:'_config_json' }],
783
+ 'action.log': [{ key:'label', label:'Label', type:'text' }],
784
+ 'action.http': [
785
+ { key:'url', label:'URL', type:'text' },
786
+ { key:'method', label:'Method', type:'select', options:['GET','POST','PUT','PATCH','DELETE'] },
787
+ { key:'responseField', label:'Response field', type:'text' },
788
+ ],
789
+ 'http.request': [
790
+ { key:'url', label:'URL', type:'text' },
791
+ { key:'method', label:'Method', type:'select', options:['GET','POST','PUT','PATCH','DELETE'] },
792
+ { key:'auth_type', label:'Auth type', type:'select', options:['none','bearer','basic','api_key'] },
793
+ { key:'auth_token', label:'Bearer token', type:'text' },
794
+ { key:'response_format', label:'Response format', type:'select', options:['json','text','binary'] },
795
+ { key:'timeout_seconds', label:'Timeout (s)', type:'text' },
796
+ ],
797
+ 'service.google_drive': [
798
+ { key:'access_token', label:'Access token', type:'text' },
799
+ { key:'operation', label:'Operation', type:'select', options:['list_files','get_file','upload_file','download_file','create_folder','delete_file','share_file'] },
800
+ { key:'file_id', label:'File ID', type:'text' },
801
+ { key:'file_name', label:'File name', type:'text' },
802
+ { key:'query', label:'Search query', type:'text' },
803
+ { key:'page_size', label:'Page size', type:'text' },
804
+ ],
805
+ 'service.gmail': [
806
+ { key:'access_token', label:'Access token', type:'text' },
807
+ { key:'operation', label:'Operation', type:'select', options:['list_messages','get_message','send_message','list_labels','trash_message'] },
808
+ { key:'message_id', label:'Message ID', type:'text' },
809
+ { key:'to', label:'To', type:'text' },
810
+ { key:'subject', label:'Subject', type:'text' },
811
+ { key:'body', label:'Body', type:'textarea' },
812
+ { key:'query', label:'Search query', type:'text' },
813
+ ],
814
+ 'service.github': [
815
+ { key:'token', label:'GitHub token', type:'text' },
816
+ { key:'operation', label:'Operation', type:'select', options:['list_repos','list_issues','get_issue','create_issue','update_issue','list_prs','create_pr','list_releases','create_release'] },
817
+ { key:'owner', label:'Owner', type:'text' },
818
+ { key:'repo', label:'Repo', type:'text' },
819
+ { key:'number', label:'Issue/PR number', type:'text' },
820
+ { key:'title', label:'Title', type:'text' },
821
+ { key:'state', label:'State', type:'select', options:['','open','closed','all'] },
822
+ ],
823
+ 'service.google_sheets': [
824
+ { key:'access_token', label:'Access token', type:'text' },
825
+ { key:'spreadsheet_id', label:'Spreadsheet ID', type:'text' },
826
+ { key:'operation', label:'Operation', type:'select', options:['read_rows','append_rows','update_rows','clear_range','create_spreadsheet'] },
827
+ { key:'sheet_name', label:'Sheet name', type:'text' },
828
+ { key:'range', label:'Range (e.g. A1:Z100)', type:'text' },
829
+ { key:'title', label:'Title (new sheet)', type:'text' },
830
+ ],
831
+ 'action.gemini_image': [
832
+ { key:'prompt', label:'Prompt', type:'textarea' },
833
+ { key:'outputPath', label:'Output path', type:'text' },
834
+ { key:'cdpPort', label:'CDP port', type:'text' },
835
+ ],
836
+ 'action.save_file': [
837
+ { key:'path', label:'File path', type:'text' },
838
+ { key:'field', label:'Data field', type:'text' },
839
+ { key:'encoding', label:'Encoding', type:'text' },
840
+ ],
841
+ 'service.notion': [
842
+ { key:'token', label:'Integration token', type:'text' },
843
+ { key:'operation', label:'Operation', type:'select', options:['get_page','create_page','update_page','query_database','get_database','create_database','append_blocks'] },
844
+ { key:'page_id', label:'Page ID', type:'text' },
845
+ { key:'database_id', label:'Database ID', type:'text' },
846
+ { key:'parent_id', label:'Parent ID', type:'text' },
847
+ { key:'properties', label:'Properties (JSON)', type:'textarea' },
848
+ ],
849
+ 'service.linear': [
850
+ { key:'token', label:'API token', type:'text' },
851
+ { key:'operation', label:'Operation', type:'select', options:['list_issues','get_issue','create_issue','update_issue','list_teams','list_projects'] },
852
+ { key:'team_id', label:'Team ID', type:'text' },
853
+ { key:'issue_id', label:'Issue ID', type:'text' },
854
+ { key:'title', label:'Title', type:'text' },
855
+ { key:'description', label:'Description', type:'textarea' },
856
+ { key:'priority', label:'Priority (0-4)', type:'text' },
857
+ { key:'state_id', label:'State ID', type:'text' },
858
+ ],
859
+ 'service.airtable': [
860
+ { key:'token', label:'Personal access token', type:'text' },
861
+ { key:'base_id', label:'Base ID', type:'text' },
862
+ { key:'table', label:'Table name', type:'text' },
863
+ { key:'operation', label:'Operation', type:'select', options:['list_records','get_record','create_record','update_record','delete_record'] },
864
+ { key:'record_id', label:'Record ID', type:'text' },
865
+ { key:'fields', label:'Fields (JSON)', type:'textarea' },
866
+ { key:'filter_formula', label:'Filter formula', type:'text' },
867
+ { key:'view', label:'View name', type:'text' },
868
+ ],
869
+ 'service.stripe': [
870
+ { key:'api_key', label:'Secret key', type:'text' },
871
+ { key:'operation', label:'Operation', type:'select', options:['list_customers','create_customer','get_customer','list_charges','create_charge','list_subscriptions','create_subscription','cancel_subscription','list_products','create_payment_intent'] },
872
+ { key:'customer_id', label:'Customer ID', type:'text' },
873
+ { key:'email', label:'Email', type:'text' },
874
+ { key:'name', label:'Name', type:'text' },
875
+ { key:'amount', label:'Amount (cents)', type:'text' },
876
+ { key:'currency', label:'Currency', type:'text' },
877
+ { key:'price_id', label:'Price ID', type:'text' },
878
+ { key:'subscription_id', label:'Subscription ID', type:'text' },
879
+ ],
880
+ 'core.aggregate': [
881
+ { key:'group_by', label:'Group by field', type:'text' },
882
+ { key:'operations', label:'Operations (JSON array)', type:'textarea' },
883
+ ],
884
+ 'core.sort': [
885
+ { key:'field', label:'Field to sort by', type:'text' },
886
+ { key:'order', label:'Order', type:'select', options:['asc','desc'] },
887
+ ],
888
+ 'core.limit': [
889
+ { key:'count', label:'Max items', type:'text' },
890
+ ],
891
+ 'core.merge': [
892
+ { key:'mode', label:'Mode', type:'select', options:['append','merge_by_field'] },
893
+ { key:'field', label:'Merge key field (for merge_by_field)', type:'text' },
894
+ ],
895
+ 'core.remove_duplicates': [
896
+ { key:'field', label:'Deduplicate by field (empty = all fields)', type:'text' },
897
+ ],
898
+ 'core.switch': [
899
+ { key:'field', label:'Field to switch on', type:'text' },
900
+ { key:'cases', label:'Case values (JSON array)', type:'textarea' },
901
+ ],
902
+ 'core.wait': [
903
+ { key:'seconds', label:'Seconds to wait', type:'text' },
904
+ ],
905
+ 'core.split_in_batches': [
906
+ { key:'batch_size', label:'Batch size', type:'text' },
907
+ ],
908
+ };
909
+
910
+ // ═══════════════════════════════════════════════════════════════════
911
+ // CANVAS STATE
912
+ // ═══════════════════════════════════════════════════════════════════
913
+ var canvas = {
914
+ nodes: [], // { id, type, name, config, x, y, _def, runStatus, runItems, runDuration }
915
+ edges: [], // { id, from, to, handle, fromPort, toPort }
916
+ zoom: 1,
917
+ panX: 0,
918
+ panY: 0,
919
+ selectedId: null,
920
+ playbookId: null,
921
+ };
922
+ var _nodeSeq = 1;
923
+
924
+ function uid() { return 'n' + (_nodeSeq++) + '_' + Date.now().toString(36); }
925
+
926
+ // Find node type def
927
+ function getNodeDef(type) {
928
+ return NODE_TYPES.find(function(t) { return t.type === type; }) || null;
929
+ }
930
+
931
+ // ═══════════════════════════════════════════════════════════════════
932
+ // PALETTE
933
+ // ═══════════════════════════════════════════════════════════════════
934
+ function buildPalette(filter) {
935
+ filter = (filter || '').toLowerCase();
936
+ var list = document.getElementById('palette-list');
937
+ var html = '';
938
+ CAT_ORDER.forEach(function(cat) {
939
+ var nodes = NODE_TYPES.filter(function(n) {
940
+ return n.cat === cat && (!filter || n.label.toLowerCase().includes(filter) || n.type.toLowerCase().includes(filter));
941
+ });
942
+ if (!nodes.length) return;
943
+ html += '<div class="palette-cat-label">' + cat + '</div>';
944
+ nodes.forEach(function(n) {
945
+ html += '<div class="palette-node" draggable="true" data-type="' + n.type + '" data-label="' + esc(n.label) + '">' +
946
+ '<div class="palette-dot" style="background:' + n.color + '"></div>' +
947
+ '<span>' + esc(n.label) + '</span></div>';
948
+ });
949
+ });
950
+ list.innerHTML = html;
951
+
952
+ list.querySelectorAll('.palette-node').forEach(function(el) {
953
+ el.addEventListener('dragstart', function(e) {
954
+ e.dataTransfer.setData('text/plain', el.getAttribute('data-type'));
955
+ e.dataTransfer.effectAllowed = 'copy';
956
+ });
957
+ });
958
+ }
959
+
960
+ document.getElementById('palette-search').addEventListener('input', function(e) {
961
+ buildPalette(e.target.value);
962
+ });
963
+ buildPalette('');
964
+
965
+ // ═══════════════════════════════════════════════════════════════════
966
+ // CANVAS TRANSFORM
967
+ // ═══════════════════════════════════════════════════════════════════
968
+ function applyTransform() {
969
+ document.getElementById('canvas-nodes').style.transform =
970
+ 'translate(' + canvas.panX + 'px,' + canvas.panY + 'px) scale(' + canvas.zoom + ')';
971
+ renderEdges();
972
+ }
973
+
974
+ // Canvas pan + zoom
975
+ var _panning = false, _panStart = null;
976
+
977
+ document.getElementById('canvas-wrap').addEventListener('wheel', function(e) {
978
+ e.preventDefault();
979
+ var rect = this.getBoundingClientRect();
980
+ var mx = e.clientX - rect.left;
981
+ var my = e.clientY - rect.top;
982
+ var delta = e.deltaY < 0 ? 1.1 : 0.9;
983
+ var newZoom = Math.max(0.2, Math.min(3, canvas.zoom * delta));
984
+ // Zoom toward mouse position
985
+ canvas.panX = mx - (mx - canvas.panX) * (newZoom / canvas.zoom);
986
+ canvas.panY = my - (my - canvas.panY) * (newZoom / canvas.zoom);
987
+ canvas.zoom = newZoom;
988
+ applyTransform();
989
+ }, { passive: false });
990
+
991
+ document.getElementById('canvas-wrap').addEventListener('mousedown', function(e) {
992
+ if (e.button === 1 || (e.button === 0 && e.target === this)) {
993
+ _panning = true;
994
+ _panStart = { x: e.clientX - canvas.panX, y: e.clientY - canvas.panY };
995
+ e.preventDefault();
996
+ }
997
+ });
998
+ document.addEventListener('mousemove', function(e) {
999
+ if (_panning) {
1000
+ canvas.panX = e.clientX - _panStart.x;
1001
+ canvas.panY = e.clientY - _panStart.y;
1002
+ applyTransform();
1003
+ }
1004
+ if (_dragEdge.active) updateDragEdge(e);
1005
+ if (_draggingNode) updateNodeDrag(e);
1006
+ });
1007
+ document.addEventListener('mouseup', function(e) {
1008
+ _panning = false;
1009
+ if (_draggingNode) endNodeDrag(e);
1010
+ if (_dragEdge.active) endDragEdge(e);
1011
+ });
1012
+
1013
+ // Deselect on canvas click
1014
+ document.getElementById('canvas-wrap').addEventListener('mousedown', function(e) {
1015
+ if (e.target === this || e.target === document.getElementById('canvas-svg')) {
1016
+ canvas.selectedId = null;
1017
+ renderPropsPanel();
1018
+ renderNodes();
1019
+ }
1020
+ });
1021
+
1022
+ // Drop from palette
1023
+ document.getElementById('canvas-wrap').addEventListener('dragover', function(e) { e.preventDefault(); });
1024
+ document.getElementById('canvas-wrap').addEventListener('drop', function(e) {
1025
+ e.preventDefault();
1026
+ var type = e.dataTransfer.getData('text/plain');
1027
+ if (!type) return;
1028
+ var def = getNodeDef(type);
1029
+ if (!def) return;
1030
+ var rect = this.getBoundingClientRect();
1031
+ var wx = (e.clientX - rect.left - canvas.panX) / canvas.zoom;
1032
+ var wy = (e.clientY - rect.top - canvas.panY) / canvas.zoom;
1033
+ addNode(type, wx - 100, wy - 20);
1034
+ });
1035
+
1036
+ // Zoom buttons
1037
+ document.getElementById('btn-zoom-in').addEventListener('click', function() {
1038
+ canvas.zoom = Math.min(3, canvas.zoom * 1.2);
1039
+ applyTransform();
1040
+ });
1041
+ document.getElementById('btn-zoom-out').addEventListener('click', function() {
1042
+ canvas.zoom = Math.max(0.2, canvas.zoom / 1.2);
1043
+ applyTransform();
1044
+ });
1045
+ document.getElementById('btn-fit').addEventListener('click', function() {
1046
+ fitCanvas();
1047
+ });
1048
+
1049
+ function fitCanvas() {
1050
+ if (!canvas.nodes.length) { canvas.zoom = 1; canvas.panX = 40; canvas.panY = 40; applyTransform(); return; }
1051
+ var wrap = document.getElementById('canvas-wrap');
1052
+ var W = wrap.clientWidth, H = wrap.clientHeight;
1053
+ var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1054
+ canvas.nodes.forEach(function(n) {
1055
+ minX = Math.min(minX, n.x); minY = Math.min(minY, n.y);
1056
+ maxX = Math.max(maxX, n.x + 200); maxY = Math.max(maxY, n.y + nodeH(n));
1057
+ });
1058
+ var pw = maxX - minX + 80, ph = maxY - minY + 80;
1059
+ var zoom = Math.min(1, Math.min(W / pw, H / ph));
1060
+ canvas.zoom = zoom;
1061
+ canvas.panX = (W - pw * zoom) / 2 - minX * zoom + 40;
1062
+ canvas.panY = (H - ph * zoom) / 2 - minY * zoom + 40;
1063
+ applyTransform();
1064
+ }
1065
+
1066
+ function nodeH(n) {
1067
+ var def = n._def || getNodeDef(n.type) || { inputs: [{}], outputs: [{}] };
1068
+ var rows = Math.max(def.inputs.length, def.outputs.length, 1);
1069
+ return 40 + 6 + rows * 26 + 6;
1070
+ }
1071
+
1072
+ // ═══════════════════════════════════════════════════════════════════
1073
+ // ADD / REMOVE NODE
1074
+ // ═══════════════════════════════════════════════════════════════════
1075
+ function addNode(type, x, y) {
1076
+ var def = getNodeDef(type);
1077
+ if (!def) return;
1078
+ var node = {
1079
+ id: uid(),
1080
+ type: type,
1081
+ name: def.label,
1082
+ config: Object.assign({}, def.defaults),
1083
+ x: x || 100,
1084
+ y: y || 100,
1085
+ _def: def,
1086
+ runStatus: null,
1087
+ runItems: 0,
1088
+ runDuration: null,
1089
+ };
1090
+ canvas.nodes.push(node);
1091
+ canvas.selectedId = node.id;
1092
+ renderNodes();
1093
+ renderEdges();
1094
+ renderPropsPanel();
1095
+ return node;
1096
+ }
1097
+
1098
+ function removeNode(id) {
1099
+ canvas.nodes = canvas.nodes.filter(function(n) { return n.id !== id; });
1100
+ canvas.edges = canvas.edges.filter(function(e) { return e.from !== id && e.to !== id; });
1101
+ if (canvas.selectedId === id) { canvas.selectedId = null; renderPropsPanel(); }
1102
+ renderNodes();
1103
+ renderEdges();
1104
+ }
1105
+
1106
+ // ═══════════════════════════════════════════════════════════════════
1107
+ // NODE DRAG
1108
+ // ═══════════════════════════════════════════════════════════════════
1109
+ var _draggingNode = null;
1110
+ var _dragNodeStart = null;
1111
+
1112
+ function startNodeDrag(e, nodeId) {
1113
+ e.stopPropagation();
1114
+ _draggingNode = nodeId;
1115
+ var n = canvas.nodes.find(function(n) { return n.id === nodeId; });
1116
+ _dragNodeStart = {
1117
+ mx: e.clientX, my: e.clientY,
1118
+ nx: n.x, ny: n.y,
1119
+ };
1120
+ canvas.selectedId = nodeId;
1121
+ renderPropsPanel();
1122
+ renderNodes();
1123
+ }
1124
+ function updateNodeDrag(e) {
1125
+ if (!_draggingNode) return;
1126
+ var dx = (e.clientX - _dragNodeStart.mx) / canvas.zoom;
1127
+ var dy = (e.clientY - _dragNodeStart.my) / canvas.zoom;
1128
+ var n = canvas.nodes.find(function(n) { return n.id === _draggingNode; });
1129
+ if (n) { n.x = _dragNodeStart.nx + dx; n.y = _dragNodeStart.ny + dy; }
1130
+ renderNodePosition(_draggingNode);
1131
+ renderEdges();
1132
+ }
1133
+ function endNodeDrag(e) {
1134
+ _draggingNode = null;
1135
+ _dragNodeStart = null;
1136
+ }
1137
+ function renderNodePosition(id) {
1138
+ var el = document.querySelector('[data-node-id="' + id + '"]');
1139
+ var n = canvas.nodes.find(function(n) { return n.id === id; });
1140
+ if (el && n) { el.style.left = n.x + 'px'; el.style.top = n.y + 'px'; }
1141
+ }
1142
+
1143
+ // ═══════════════════════════════════════════════════════════════════
1144
+ // EDGE DRAG
1145
+ // ═══════════════════════════════════════════════════════════════════
1146
+ var _dragEdge = { active: false, fromNodeId: null, fromPortIdx: null, x: 0, y: 0 };
1147
+
1148
+ function startDragEdge(e, nodeId, portIdx) {
1149
+ e.stopPropagation();
1150
+ e.preventDefault();
1151
+ _dragEdge.active = true;
1152
+ _dragEdge.fromNodeId = nodeId;
1153
+ _dragEdge.fromPortIdx = portIdx;
1154
+ var pos = getOutPortPos(nodeId, portIdx);
1155
+ _dragEdge.x = pos.x; _dragEdge.y = pos.y;
1156
+ var path = document.getElementById('drag-edge');
1157
+ path.style.display = '';
1158
+ }
1159
+ function updateDragEdge(e) {
1160
+ var wrap = document.getElementById('canvas-wrap');
1161
+ var rect = wrap.getBoundingClientRect();
1162
+ var tx = (e.clientX - rect.left - canvas.panX) / canvas.zoom;
1163
+ var ty = (e.clientY - rect.top - canvas.panY) / canvas.zoom;
1164
+ var sx = _dragEdge.x, sy = _dragEdge.y;
1165
+ var dx = Math.max(60, Math.abs(tx - sx) * 0.5);
1166
+ document.getElementById('drag-edge').setAttribute('d',
1167
+ 'M' + sx + ',' + sy + ' C' + (sx+dx) + ',' + sy + ' ' + (tx-dx) + ',' + ty + ' ' + tx + ',' + ty);
1168
+ }
1169
+ function endDragEdge(e) {
1170
+ document.getElementById('drag-edge').style.display = 'none';
1171
+ _dragEdge.active = false;
1172
+ }
1173
+ function dropOnInputPort(e, toNodeId, toPortIdx) {
1174
+ if (!_dragEdge.active) return;
1175
+ e.stopPropagation();
1176
+ if (_dragEdge.fromNodeId === toNodeId) { endDragEdge(e); return; }
1177
+ var fromNode = canvas.nodes.find(function(n) { return n.id === _dragEdge.fromNodeId; });
1178
+ var def = fromNode && (fromNode._def || getNodeDef(fromNode.type));
1179
+ var handle = def && def.outputs[_dragEdge.fromPortIdx] ? def.outputs[_dragEdge.fromPortIdx].label : undefined;
1180
+ // Remove existing edge from same out port if any
1181
+ canvas.edges = canvas.edges.filter(function(ed) {
1182
+ return !(ed.from === _dragEdge.fromNodeId && ed.fromPort === _dragEdge.fromPortIdx && ed.to === toNodeId && ed.toPort === toPortIdx);
1183
+ });
1184
+ canvas.edges.push({
1185
+ id: uid(),
1186
+ from: _dragEdge.fromNodeId,
1187
+ to: toNodeId,
1188
+ handle: handle,
1189
+ fromPort: _dragEdge.fromPortIdx,
1190
+ toPort: toPortIdx,
1191
+ });
1192
+ endDragEdge(e);
1193
+ renderEdges();
1194
+ }
1195
+
1196
+ function getOutPortPos(nodeId, portIdx) {
1197
+ var n = canvas.nodes.find(function(n) { return n.id === nodeId; });
1198
+ if (!n) return {x:0,y:0};
1199
+ return { x: n.x + 200, y: n.y + 40 + 6 + portIdx * 26 + 13 };
1200
+ }
1201
+ function getInPortPos(nodeId, portIdx) {
1202
+ var n = canvas.nodes.find(function(n) { return n.id === nodeId; });
1203
+ if (!n) return {x:0,y:0};
1204
+ return { x: n.x, y: n.y + 40 + 6 + portIdx * 26 + 13 };
1205
+ }
1206
+
1207
+ // ═══════════════════════════════════════════════════════════════════
1208
+ // RENDER NODES
1209
+ // ═══════════════════════════════════════════════════════════════════
1210
+ function renderNodes() {
1211
+ var container = document.getElementById('canvas-nodes');
1212
+ var existingIds = new Set();
1213
+ canvas.nodes.forEach(function(n) { existingIds.add(n.id); });
1214
+ // Remove nodes that no longer exist
1215
+ container.querySelectorAll('[data-node-id]').forEach(function(el) {
1216
+ if (!existingIds.has(el.getAttribute('data-node-id'))) el.remove();
1217
+ });
1218
+ // Add or update each node
1219
+ canvas.nodes.forEach(function(n) {
1220
+ var existing = container.querySelector('[data-node-id="' + n.id + '"]');
1221
+ var def = n._def || getNodeDef(n.type) || { inputs:[{label:'in'}], outputs:[{label:'out'}], color:'#7c3aed' };
1222
+ var color = def.color || '#7c3aed';
1223
+ var rows = Math.max(def.inputs.length, def.outputs.length, 1);
1224
+ var h = 40 + 6 + rows * 26 + 6;
1225
+ var isSelected = canvas.selectedId === n.id;
1226
+
1227
+ if (!existing) {
1228
+ var el = document.createElement('div');
1229
+ el.className = 'cn' + (isSelected ? ' selected' : '') + (n.runStatus === 'running' ? ' running' : '');
1230
+ el.setAttribute('data-node-id', n.id);
1231
+ el.style.left = n.x + 'px';
1232
+ el.style.top = n.y + 'px';
1233
+ el.style.height = h + 'px';
1234
+ el.style.borderColor = isSelected ? color : (n.runStatus === 'error' ? '#ef444444' : n.runStatus === 'ok' ? '#10b98133' : 'rgba(124,58,237,0.15)');
1235
+
1236
+ // Status badge
1237
+ var statusHtml = '';
1238
+ if (n.runStatus) {
1239
+ var statusText = n.runStatus === 'ok' ? '✓ ' + n.runItems + ' item' + (n.runItems !== 1 ? 's' : '') + (n.runDuration ? ' · ' + n.runDuration + 'ms' : '')
1240
+ : n.runStatus === 'running' ? '⏳ running'
1241
+ : n.runStatus === 'error' ? '✕ error'
1242
+ : '— skipped';
1243
+ statusHtml = '<div class="cn-status ' + n.runStatus + '">' + esc(statusText) + '</div>';
1244
+ }
1245
+
1246
+ // Header
1247
+ var headerHtml = '<div class="cn-header" style="background:linear-gradient(110deg,' + color + '1a 0%,' + color + '0a 100%);border-bottom:1px solid ' + color + '22">' +
1248
+ '<div class="cn-dot" style="background:' + color + '"></div>' +
1249
+ '<span class="cn-label">' + esc(n.name || def.label) + '</span>' +
1250
+ '<button class="cn-del" data-del="' + n.id + '" title="Delete">✕</button>' +
1251
+ '</div>';
1252
+
1253
+ // Ports
1254
+ var portsHtml = '<div class="cn-ports">';
1255
+ for (var i = 0; i < rows; i++) {
1256
+ var inPort = def.inputs[i];
1257
+ var outPort = def.outputs[i];
1258
+ portsHtml += '<div class="cn-row">';
1259
+ if (inPort) {
1260
+ portsHtml += '<div class="cn-port-area">' +
1261
+ '<div class="cn-port in" data-in-port="' + i + '" data-node="' + n.id + '" title="Input: ' + esc(inPort.label) + '"></div>' +
1262
+ '<span class="cn-port-label">' + esc(inPort.label) + '</span>' +
1263
+ '</div>';
1264
+ } else {
1265
+ portsHtml += '<div></div>';
1266
+ }
1267
+ if (outPort) {
1268
+ portsHtml += '<div class="cn-port-area" style="justify-content:flex-end">' +
1269
+ '<span class="cn-port-label">' + esc(outPort.label) + '</span>' +
1270
+ '<div class="cn-port out" data-out-port="' + i + '" data-node="' + n.id + '" title="Output: ' + esc(outPort.label) + '"></div>' +
1271
+ '</div>';
1272
+ } else {
1273
+ portsHtml += '<div></div>';
1274
+ }
1275
+ portsHtml += '</div>';
1276
+ }
1277
+ portsHtml += '</div>';
1278
+
1279
+ el.innerHTML = statusHtml + headerHtml + portsHtml;
1280
+
1281
+ // Events
1282
+ el.querySelector('.cn-header').addEventListener('mousedown', function(e) {
1283
+ if (e.target.closest('.cn-del')) return;
1284
+ startNodeDrag(e, n.id);
1285
+ });
1286
+ el.addEventListener('mousedown', function(e) {
1287
+ if (e.target.closest('.cn-del') || e.target.closest('.cn-port')) return;
1288
+ e.stopPropagation();
1289
+ canvas.selectedId = n.id;
1290
+ renderPropsPanel();
1291
+ renderNodes();
1292
+ });
1293
+ var delBtn = el.querySelector('.cn-del');
1294
+ if (delBtn) delBtn.addEventListener('mousedown', function(e) {
1295
+ e.stopPropagation();
1296
+ removeNode(n.id);
1297
+ });
1298
+ el.querySelectorAll('.cn-port.out').forEach(function(port) {
1299
+ port.addEventListener('mousedown', function(e) {
1300
+ e.stopPropagation();
1301
+ var portIdx = parseInt(port.getAttribute('data-out-port'), 10);
1302
+ startDragEdge(e, n.id, portIdx);
1303
+ });
1304
+ });
1305
+ el.querySelectorAll('.cn-port.in').forEach(function(port) {
1306
+ port.addEventListener('mouseup', function(e) {
1307
+ var portIdx = parseInt(port.getAttribute('data-in-port'), 10);
1308
+ dropOnInputPort(e, n.id, portIdx);
1309
+ });
1310
+ port.addEventListener('mouseenter', function() { port.classList.add('highlight'); });
1311
+ port.addEventListener('mouseleave', function() { port.classList.remove('highlight'); });
1312
+ });
1313
+
1314
+ container.appendChild(el);
1315
+ } else {
1316
+ // Update position, selection, status
1317
+ existing.style.left = n.x + 'px';
1318
+ existing.style.top = n.y + 'px';
1319
+ existing.className = 'cn' + (isSelected ? ' selected' : '') + (n.runStatus === 'running' ? ' running' : '');
1320
+ existing.style.borderColor = isSelected ? color : (n.runStatus === 'error' ? '#ef444444' : n.runStatus === 'ok' ? '#10b98133' : 'rgba(124,58,237,0.15)');
1321
+ // Update status badge
1322
+ var badge = existing.querySelector('.cn-status');
1323
+ if (n.runStatus) {
1324
+ var statusText2 = n.runStatus === 'ok' ? '✓ ' + n.runItems + ' item' + (n.runItems !== 1 ? 's' : '') + (n.runDuration ? ' · ' + n.runDuration + 'ms' : '')
1325
+ : n.runStatus === 'running' ? '⏳ running'
1326
+ : n.runStatus === 'error' ? '✕ error'
1327
+ : '— skipped';
1328
+ if (!badge) {
1329
+ var newBadge = document.createElement('div');
1330
+ newBadge.className = 'cn-status ' + n.runStatus;
1331
+ newBadge.textContent = statusText2;
1332
+ existing.insertBefore(newBadge, existing.firstChild);
1333
+ } else {
1334
+ badge.className = 'cn-status ' + n.runStatus;
1335
+ badge.textContent = statusText2;
1336
+ }
1337
+ } else if (badge) {
1338
+ badge.remove();
1339
+ }
1340
+ }
1341
+ });
1342
+ }
1343
+
1344
+ // ═══════════════════════════════════════════════════════════════════
1345
+ // RENDER EDGES (SVG)
1346
+ // ═══════════════════════════════════════════════════════════════════
1347
+ function renderEdges() {
1348
+ var layer = document.getElementById('edge-layer');
1349
+ layer.innerHTML = '';
1350
+ canvas.edges.forEach(function(edge) {
1351
+ var fromNode = canvas.nodes.find(function(n) { return n.id === edge.from; });
1352
+ var toNode = canvas.nodes.find(function(n) { return n.id === edge.to; });
1353
+ if (!fromNode || !toNode) return;
1354
+ var sx = (fromNode.x + 200) * canvas.zoom + canvas.panX;
1355
+ var sy = (fromNode.y + 40 + 6 + edge.fromPort * 26 + 13) * canvas.zoom + canvas.panY;
1356
+ var tx = toNode.x * canvas.zoom + canvas.panX;
1357
+ var ty = (toNode.y + 40 + 6 + edge.toPort * 26 + 13) * canvas.zoom + canvas.panY;
1358
+ var dx = Math.max(60, Math.abs(tx - sx) * 0.5) * canvas.zoom;
1359
+ var d = 'M' + sx + ',' + sy + ' C' + (sx+dx) + ',' + sy + ' ' + (tx-dx) + ',' + ty + ' ' + tx + ',' + ty;
1360
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1361
+ path.setAttribute('d', d);
1362
+ path.setAttribute('stroke', '#7c3aed88');
1363
+ path.setAttribute('stroke-width', '1.5');
1364
+ path.setAttribute('fill', 'none');
1365
+ path.setAttribute('marker-end', 'url(#arrowhead)');
1366
+ // Add label if handle differs from default
1367
+ layer.appendChild(path);
1368
+
1369
+ // Remove edge on click
1370
+ (function(edgeId, midX, midY) {
1371
+ path.style.cursor = 'pointer';
1372
+ path.style.strokeWidth = '4';
1373
+ path.style.stroke = 'transparent';
1374
+ var hit = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1375
+ hit.setAttribute('d', d);
1376
+ hit.setAttribute('stroke', 'transparent');
1377
+ hit.setAttribute('stroke-width', '8');
1378
+ hit.setAttribute('fill', 'none');
1379
+ hit.style.cursor = 'pointer';
1380
+ hit.addEventListener('click', function() {
1381
+ canvas.edges = canvas.edges.filter(function(e) { return e.id !== edgeId; });
1382
+ renderEdges();
1383
+ });
1384
+ layer.appendChild(hit);
1385
+ })(edge.id, (sx + tx) / 2, (sy + ty) / 2);
1386
+ });
1387
+ }
1388
+
1389
+ // ═══════════════════════════════════════════════════════════════════
1390
+ // PROPERTIES PANEL
1391
+ // ═══════════════════════════════════════════════════════════════════
1392
+ function renderPropsPanel() {
1393
+ var body = document.getElementById('props-body');
1394
+ var node = canvas.nodes.find(function(n) { return n.id === canvas.selectedId; });
1395
+ if (!node) {
1396
+ body.innerHTML = '<div class="empty" style="padding:20px;color:#333;font-size:11px;text-align:center">Select a node to edit its properties</div>';
1397
+ return;
1398
+ }
1399
+ var fields = NODE_FIELDS[node.type] || [];
1400
+ var html = '<div class="prop-row"><div class="prop-label">Node name</div>' +
1401
+ '<input class="prop-input" id="prop-node-name" value="' + esc(node.name || '') + '" /></div>';
1402
+ html += '<div class="prop-row"><div class="prop-label">On error</div>' +
1403
+ '<select class="prop-select" id="prop-on-error">' +
1404
+ ['stop','skip'].map(function(v) {
1405
+ return '<option value="' + v + '"' + (node.onError === v ? ' selected' : '') + '>' + v + '</option>';
1406
+ }).join('') + '</select></div>';
1407
+
1408
+ fields.forEach(function(f) {
1409
+ var val = node.config[f.key] !== undefined ? String(node.config[f.key]) : '';
1410
+ html += '<div class="prop-row"><div class="prop-label">' + esc(f.label) + '</div>';
1411
+ if (f.type === 'select') {
1412
+ html += '<select class="prop-select" data-key="' + f.key + '">' +
1413
+ (f.options || []).map(function(o) {
1414
+ return '<option value="' + esc(o) + '"' + (val === o ? ' selected' : '') + '>' + esc(o) + '</option>';
1415
+ }).join('') + '</select>';
1416
+ } else if (f.type === 'textarea') {
1417
+ html += '<textarea class="prop-textarea" data-key="' + f.key + '">' + esc(val) + '</textarea>';
1418
+ } else {
1419
+ html += '<input class="prop-input" data-key="' + f.key + '" value="' + esc(val) + '" />';
1420
+ }
1421
+ html += '</div>';
1422
+ });
1423
+
1424
+ body.innerHTML = html;
1425
+
1426
+ body.querySelector('#prop-node-name').addEventListener('input', function(e) {
1427
+ node.name = e.target.value;
1428
+ var el = document.querySelector('[data-node-id="' + node.id + '"] .cn-label');
1429
+ if (el) el.textContent = node.name;
1430
+ });
1431
+ body.querySelector('#prop-on-error').addEventListener('change', function(e) {
1432
+ node.onError = e.target.value;
1433
+ });
1434
+ body.querySelectorAll('[data-key]').forEach(function(el) {
1435
+ el.addEventListener('input', function() {
1436
+ node.config[el.getAttribute('data-key')] = el.value;
1437
+ });
1438
+ el.addEventListener('change', function() {
1439
+ node.config[el.getAttribute('data-key')] = el.value;
1440
+ });
1441
+ });
1442
+ }
1443
+
1444
+ // ═══════════════════════════════════════════════════════════════════
1445
+ // PLAYBOOK SERIALIZATION
1446
+ // ═══════════════════════════════════════════════════════════════════
1447
+ function toPlaybookDef() {
1448
+ var id = canvas.playbookId || ('pb_' + Date.now().toString(36));
1449
+ canvas.playbookId = id;
1450
+ var name = document.getElementById('pb-name-input').value.trim() || 'Untitled Playbook';
1451
+ return {
1452
+ id: id,
1453
+ name: name,
1454
+ nodes: canvas.nodes.map(function(n) {
1455
+ return { id: n.id, type: n.type, name: n.name, config: n.config, onError: n.onError || 'stop' };
1456
+ }),
1457
+ connections: canvas.edges.map(function(e) {
1458
+ var res = { from: e.from, to: e.to };
1459
+ if (e.handle && e.handle !== 'main' && e.handle !== 'out' && e.handle !== 'items') res.handle = e.handle;
1460
+ return res;
1461
+ }),
1462
+ };
1463
+ }
1464
+
1465
+ function loadPlaybookDef(pb) {
1466
+ canvas.nodes = [];
1467
+ canvas.edges = [];
1468
+ canvas.playbookId = pb.id;
1469
+ canvas.selectedId = null;
1470
+ document.getElementById('pb-name-input').value = pb.name || 'Untitled';
1471
+ // Clear existing nodes from DOM
1472
+ document.getElementById('canvas-nodes').innerHTML = '';
1473
+
1474
+ // Lay out nodes in a simple column if no positions stored
1475
+ var x = 100, y = 60;
1476
+ (pb.nodes || []).forEach(function(n, i) {
1477
+ var def = getNodeDef(n.type) || { inputs:[{label:'in'}], outputs:[{label:'out'}], color:'#7c3aed', label: n.type };
1478
+ var node = {
1479
+ id: n.id,
1480
+ type: n.type,
1481
+ name: n.name || def.label,
1482
+ config: n.config || {},
1483
+ onError: n.onError,
1484
+ x: n._x || (x + (i % 3) * 260),
1485
+ y: n._y || (y + Math.floor(i / 3) * 140),
1486
+ _def: def,
1487
+ runStatus: null,
1488
+ runItems: 0,
1489
+ runDuration: null,
1490
+ };
1491
+ canvas.nodes.push(node);
1492
+ });
1493
+
1494
+ // Build edges from connections
1495
+ (pb.connections || []).forEach(function(c) {
1496
+ var fromNode = canvas.nodes.find(function(n) { return n.id === c.from; });
1497
+ var toNode = canvas.nodes.find(function(n) { return n.id === c.to; });
1498
+ if (!fromNode || !toNode) return;
1499
+ var fromDef = fromNode._def || { outputs: [{label:'main'}] };
1500
+ var handle = c.handle || (fromDef.outputs[0] ? fromDef.outputs[0].label : 'main');
1501
+ var fromPortIdx = fromDef.outputs.findIndex(function(o) { return o.label === handle; });
1502
+ if (fromPortIdx < 0) fromPortIdx = 0;
1503
+ canvas.edges.push({
1504
+ id: uid(),
1505
+ from: c.from,
1506
+ to: c.to,
1507
+ handle: c.handle,
1508
+ fromPort: fromPortIdx,
1509
+ toPort: 0,
1510
+ });
1511
+ });
1512
+
1513
+ renderNodes();
1514
+ renderEdges();
1515
+ renderPropsPanel();
1516
+ setTimeout(fitCanvas, 50);
1517
+ }
1518
+
1519
+ // ═══════════════════════════════════════════════════════════════════
1520
+ // TOOLBAR ACTIONS
1521
+ // ═══════════════════════════════════════════════════════════════════
1522
+ document.getElementById('btn-new').addEventListener('click', function() {
1523
+ canvas.nodes = [];
1524
+ canvas.edges = [];
1525
+ canvas.playbookId = null;
1526
+ canvas.selectedId = null;
1527
+ document.getElementById('canvas-nodes').innerHTML = '';
1528
+ document.getElementById('pb-name-input').value = 'Untitled Playbook';
1529
+ renderNodes();
1530
+ renderEdges();
1531
+ renderPropsPanel();
1532
+ });
1533
+
1534
+ document.getElementById('btn-save').addEventListener('click', function() {
1535
+ var pb = toPlaybookDef();
1536
+ fetch('/api/playbooks', {
1537
+ method: 'POST',
1538
+ headers: { 'Content-Type': 'application/json' },
1539
+ body: JSON.stringify(pb),
1540
+ }).then(function(r) { return r.json(); }).then(function(saved) {
1541
+ canvas.playbookId = saved.id;
1542
+ showToast('Playbook saved: ' + saved.name);
1543
+ }).catch(function(e) {
1544
+ showToast('Save failed: ' + e, true);
1545
+ });
1546
+ });
1547
+
1548
+ document.getElementById('btn-open').addEventListener('click', function() {
1549
+ openPlaybookList();
1550
+ });
1551
+
1552
+ document.getElementById('btn-run').addEventListener('click', function() {
1553
+ if (!canvas.playbookId) {
1554
+ // Auto-save first
1555
+ var pb = toPlaybookDef();
1556
+ fetch('/api/playbooks', {
1557
+ method: 'POST',
1558
+ headers: { 'Content-Type': 'application/json' },
1559
+ body: JSON.stringify(pb),
1560
+ }).then(function() {
1561
+ runCurrentPlaybook(pb.id);
1562
+ }).catch(function(e) { showToast('Error: ' + e, true); });
1563
+ } else {
1564
+ // Save then run
1565
+ var pb2 = toPlaybookDef();
1566
+ fetch('/api/playbooks', {
1567
+ method: 'POST',
1568
+ headers: { 'Content-Type': 'application/json' },
1569
+ body: JSON.stringify(pb2),
1570
+ }).then(function() {
1571
+ runCurrentPlaybook(pb2.id);
1572
+ }).catch(function(e) { showToast('Error: ' + e, true); });
1573
+ }
1574
+ });
1575
+
1576
+ function runCurrentPlaybook(playbookId) {
1577
+ // Reset node statuses
1578
+ canvas.nodes.forEach(function(n) { n.runStatus = null; n.runItems = 0; n.runDuration = null; });
1579
+ renderNodes();
1580
+ fetch('/api/playbooks/' + playbookId + '/run', { method: 'POST' })
1581
+ .then(function(r) { return r.json(); })
1582
+ .then(function(res) {
1583
+ if (res.ok) {
1584
+ showToast('Playbook started — watch the Runs tab');
1585
+ // Switch to runs panel
1586
+ document.querySelectorAll('.nav-item').forEach(function(n) { n.classList.remove('active'); });
1587
+ document.querySelector('[data-panel="playbooks"]').classList.add('active');
1588
+ document.querySelectorAll('.panel').forEach(function(p) { p.classList.remove('active'); });
1589
+ document.getElementById('pb-panel').classList.add('active');
1590
+ }
1591
+ })
1592
+ .catch(function(e) { showToast('Run failed: ' + e, true); });
1593
+ }
1594
+
1595
+ // ═══════════════════════════════════════════════════════════════════
1596
+ // PLAYBOOK LIST OVERLAY
1597
+ // ═══════════════════════════════════════════════════════════════════
1598
+ function openPlaybookList() {
1599
+ var overlay = document.getElementById('pb-list-overlay');
1600
+ overlay.classList.add('show');
1601
+ fetch('/api/playbooks')
1602
+ .then(function(r) { return r.json(); })
1603
+ .then(function(list) {
1604
+ var body = document.getElementById('pblo-body');
1605
+ if (!list.length) {
1606
+ body.innerHTML = '<div class="pblo-empty">No saved playbooks yet. Build one and click Save!</div>';
1607
+ return;
1608
+ }
1609
+ body.innerHTML = list.map(function(pb) {
1610
+ return '<div class="pblo-item" data-pb-id="' + esc(pb.id) + '">' +
1611
+ '<span class="pblo-item-name">' + esc(pb.name) + '</span>' +
1612
+ '<span class="pblo-item-meta">' + (pb.nodes ? pb.nodes.length : 0) + ' nodes</span>' +
1613
+ '<button class="pblo-del" data-del-id="' + esc(pb.id) + '" title="Delete">✕</button>' +
1614
+ '</div>';
1615
+ }).join('');
1616
+ body.querySelectorAll('.pblo-item').forEach(function(el) {
1617
+ el.addEventListener('click', function(e) {
1618
+ if (e.target.closest('.pblo-del')) return;
1619
+ var pbId = el.getAttribute('data-pb-id');
1620
+ fetch('/api/playbooks/' + pbId).then(function(r) { return r.json(); }).then(function(pb) {
1621
+ loadPlaybookDef(pb);
1622
+ overlay.classList.remove('show');
1623
+ });
1624
+ });
1625
+ });
1626
+ body.querySelectorAll('.pblo-del').forEach(function(btn) {
1627
+ btn.addEventListener('click', function(e) {
1628
+ e.stopPropagation();
1629
+ var delId = btn.getAttribute('data-del-id');
1630
+ if (!confirm('Delete this playbook?')) return;
1631
+ fetch('/api/playbooks/' + delId, { method: 'DELETE' }).then(function() {
1632
+ if (canvas.playbookId === delId) {
1633
+ canvas.playbookId = null;
1634
+ canvas.nodes = [];
1635
+ canvas.edges = [];
1636
+ canvas.selectedId = null;
1637
+ document.getElementById('canvas-nodes').innerHTML = '';
1638
+ document.getElementById('pb-name-input').value = 'Untitled Playbook';
1639
+ renderNodes();
1640
+ renderEdges();
1641
+ renderPropsPanel();
1642
+ }
1643
+ openPlaybookList(); // refresh
1644
+ });
1645
+ });
1646
+ });
1647
+ })
1648
+ .catch(function() {
1649
+ document.getElementById('pblo-body').innerHTML = '<div class="pblo-empty">Failed to load playbooks.</div>';
1650
+ });
1651
+ }
1652
+
1653
+ document.getElementById('pblo-close-btn').addEventListener('click', function() {
1654
+ document.getElementById('pb-list-overlay').classList.remove('show');
1655
+ });
1656
+
1657
+ // ═══════════════════════════════════════════════════════════════════
1658
+ // TOAST
1659
+ // ═══════════════════════════════════════════════════════════════════
1660
+ function showToast(msg, isError) {
1661
+ var t = document.createElement('div');
1662
+ t.textContent = msg;
1663
+ t.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9999;padding:8px 14px;border-radius:6px;font-size:11px;font-family:monospace;max-width:320px;pointer-events:none;transition:opacity .3s;' +
1664
+ (isError ? 'background:#ef444420;border:1px solid #ef444466;color:#ef4444;' : 'background:#7c3aed20;border:1px solid #7c3aed66;color:#c4b5fd;');
1665
+ document.body.appendChild(t);
1666
+ setTimeout(function() { t.style.opacity = '0'; setTimeout(function() { t.remove(); }, 300); }, 2500);
1667
+ }
1668
+
1669
+ // ═══════════════════════════════════════════════════════════════════
1670
+ // LIVE EVENTS → update node statuses in builder
1671
+ // ═══════════════════════════════════════════════════════════════════
1672
+ function handleBuilderEvent(evt) {
1673
+ if (!evt.nodeId) return;
1674
+ var node = canvas.nodes.find(function(n) { return n.id === evt.nodeId; });
1675
+ if (!node) return;
1676
+ if (evt.eventType === 'step_started') {
1677
+ node.runStatus = 'running';
1678
+ } else if (evt.eventType === 'step_completed') {
1679
+ node.runStatus = 'ok';
1680
+ node.runItems = evt.itemTotal || 0;
1681
+ node.runDuration = evt.durationMs || null;
1682
+ } else if (evt.eventType === 'step_failed') {
1683
+ node.runStatus = 'error';
1684
+ }
1685
+ // Only re-render if builder is active
1686
+ var builderPanel = document.getElementById('builder-panel');
1687
+ if (builderPanel.classList.contains('active')) {
1688
+ renderNodes();
1689
+ }
1690
+ }
1691
+
1692
+ // ═══════════════════════════════════════════════════════════════════
1693
+ // EVENT HANDLER (runs + builder)
1694
+ // ═══════════════════════════════════════════════════════════════════
1695
+ function handleEvent(evt) {
1696
+ // Route to builder status
1697
+ handleBuilderEvent(evt);
1698
+
1699
+ var id = evt.runId;
1700
+ if (!id) return;
1701
+ if (!runs[id]) {
1702
+ runs[id] = { id: id, playbookId: evt.playbookId, playbookName: evt.playbookName || id,
1703
+ status: 'running', startedAt: evt.timestamp || Date.now(),
1704
+ itemsProcessed: 0, itemsTotal: 0 };
1705
+ }
1706
+ if (!runLogs[id]) runLogs[id] = [];
1707
+ var r = runs[id];
1708
+ var t = ts(evt.timestamp || Date.now());
1709
+
1710
+ if (evt.eventType === 'run_started') {
1711
+ r.status = 'running';
1712
+ r.startedAt = evt.timestamp || Date.now();
1713
+ } else if (evt.eventType === 'step_started') {
1714
+ runLogs[id].push({ ts: t, type: 'info', text: (evt.nodeName || '?') + ' — started' });
1715
+ } else if (evt.eventType === 'step_completed') {
1716
+ r.itemsProcessed = (r.itemsProcessed || 0) + (evt.itemTotal || 1);
1717
+ runLogs[id].push({ ts: t, type: 'ok', text: (evt.nodeName || '?') + ' ✓ ' + (evt.durationMs ? evt.durationMs + 'ms' : '') });
1718
+ } else if (evt.eventType === 'step_failed') {
1719
+ runLogs[id].push({ ts: t, type: 'err', text: (evt.nodeName || '?') + ' ✗ ' + (evt.error || '') });
1720
+ } else if (evt.eventType === 'run_completed') {
1721
+ r.status = 'completed';
1722
+ r.endedAt = evt.timestamp || Date.now();
1723
+ // Clear running status on nodes
1724
+ canvas.nodes.forEach(function(n) {
1725
+ if (n.runStatus === 'running') { n.runStatus = null; }
1726
+ });
1727
+ } else if (evt.eventType === 'run_failed') {
1728
+ r.status = 'failed';
1729
+ r.endedAt = evt.timestamp || Date.now();
1730
+ } else if (evt.eventType === 'run_stopped') {
1731
+ r.status = evt.error ? 'failed' : 'stopped';
1732
+ r.endedAt = evt.timestamp || Date.now();
1733
+ }
1734
+
1735
+ if (runLogs[id].length > 80) runLogs[id] = runLogs[id].slice(-80);
1736
+ updateHeader();
1737
+ renderCards();
1738
+ }
1739
+
1740
+ // ═══════════════════════════════════════════════════════════════════
1741
+ // CONNECTION
1742
+ // ═══════════════════════════════════════════════════════════════════
1743
+ var connDot = document.getElementById('conn-dot');
1744
+ var connLabel = document.getElementById('conn-label');
1745
+
1746
+ function setConnStatus(state) {
1747
+ if (state === 'live') {
1748
+ connDot.style.background = '#22c55e';
1749
+ connLabel.style.color = '#22c55e';
1750
+ connLabel.textContent = 'live';
1751
+ } else if (state === 'error') {
1752
+ connDot.style.background = '#f59e0b';
1753
+ connLabel.style.color = '#f59e0b';
1754
+ connLabel.textContent = 'reconnecting…';
1755
+ } else {
1756
+ connDot.style.background = '#555';
1757
+ connLabel.style.color = '#555';
1758
+ connLabel.textContent = 'connecting…';
1759
+ }
1760
+ }
1761
+
1762
+ var _sseRetryDelay = 1000;
1763
+ function connectSSE() {
1764
+ setConnStatus('connecting');
1765
+ var es = new EventSource('/events');
1766
+ es.onopen = function() { setConnStatus('live'); _sseRetryDelay = 1000; };
1767
+ es.onmessage = function(e) { handleEvent(JSON.parse(e.data)); };
1768
+ es.onerror = function() {
1769
+ setConnStatus('error');
1770
+ es.close();
1771
+ setTimeout(connectSSE, Math.min(_sseRetryDelay *= 2, 30000));
1772
+ };
1773
+ }
1774
+
1775
+ function connect() {
1776
+ setConnStatus('connecting');
1777
+ var sock = new WebSocket('ws://' + location.host + '/ws');
1778
+ sock.onopen = function() { setConnStatus('live'); };
1779
+ sock.onmessage = function(e) {
1780
+ var msg = JSON.parse(e.data);
1781
+ if (msg.type === 'history') {
1782
+ msg.runs.forEach(function(r) {
1783
+ runs[r.id] = r;
1784
+ if (!runLogs[r.id]) runLogs[r.id] = [];
1785
+ });
1786
+ updateHeader();
1787
+ renderCards();
1788
+ } else {
1789
+ handleEvent(msg);
1790
+ }
1791
+ };
1792
+ var wsFailedOnce = false;
1793
+ sock.onclose = function() {
1794
+ setConnStatus('error');
1795
+ if (!wsFailedOnce) {
1796
+ wsFailedOnce = true;
1797
+ fetch('/events', { method: 'HEAD' }).then(function(r) {
1798
+ if (r.ok) { connectSSE(); } else { setTimeout(connect, 2500); }
1799
+ }).catch(function() { setTimeout(connect, 2500); });
1800
+ } else {
1801
+ setTimeout(connect, 2500);
1802
+ }
1803
+ };
1804
+ }
1805
+
1806
+ // ── init ───────────────────────────────────────────────────
1807
+ connect();
1808
+ setInterval(function() { if (Object.values(runs).some(function(r){return r.status==='running';})) renderCards(); }, 5000);
1809
+ </script>
1810
+ </body>
1811
+ </html>