codymaster 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +285 -0
  3. package/adapters/antigravity.js +15 -0
  4. package/adapters/claude-code.js +17 -0
  5. package/adapters/cursor.js +16 -0
  6. package/commands/bootstrap.md +49 -0
  7. package/commands/build.md +48 -0
  8. package/commands/content.md +48 -0
  9. package/commands/continuity.md +60 -0
  10. package/commands/debug.md +51 -0
  11. package/commands/demo.md +96 -0
  12. package/commands/deploy.md +51 -0
  13. package/commands/plan.md +42 -0
  14. package/commands/review.md +55 -0
  15. package/commands/track.md +46 -0
  16. package/commands/ux.md +46 -0
  17. package/dist/agent-dispatch.js +161 -0
  18. package/dist/chains/builtin.js +85 -0
  19. package/dist/continuity.js +385 -0
  20. package/dist/dashboard.js +926 -0
  21. package/dist/data.js +122 -0
  22. package/dist/index.js +2434 -0
  23. package/dist/judge.js +252 -0
  24. package/dist/parallel-dispatch.js +359 -0
  25. package/dist/parallel-quality.js +172 -0
  26. package/dist/skill-chain.js +258 -0
  27. package/install.sh +513 -0
  28. package/package.json +79 -0
  29. package/skills/.content-factory-state.json +132 -0
  30. package/skills/.git 2/logs/refs/heads/main +1 -0
  31. package/skills/.git 2/logs/refs/remotes/origin/main +1 -0
  32. package/skills/.git 2/objects/02/fb0956734b5f8ba3f918b7defd04a89cfe0076 +0 -0
  33. package/skills/.git 2/objects/08/1e129d75dc6feac6c02037272e6bd1a04e3324 +0 -0
  34. package/skills/.git 2/objects/0c/5393416f3c5e01c9a655a802bff0dd52f76f0a +0 -0
  35. package/skills/.git 2/objects/10/0b9be46978a946a77188f68be725098a122001 +0 -0
  36. package/skills/.git 2/objects/10/cf041167fc9843610eb3d90259ef3396315fdc +0 -0
  37. package/skills/.git 2/objects/12/5e19538dd6e1338ffe74f6c4c165b00435bf48 +0 -0
  38. package/skills/.git 2/objects/16/a9b9d0088d5c1347628b45a2620b479d8ad57c +0 -0
  39. package/skills/.git 2/objects/17/8c2a9ef93c33ae4eec9d58e82321f9229843a1 +0 -0
  40. package/skills/.git 2/objects/25/397ae41d09104d763bdcac2695209d85cdea89 +0 -0
  41. package/skills/.git 2/objects/2f/a836b7947f2d458e1f639788bf4bb0983a3305 +0 -0
  42. package/skills/.git 2/objects/3a/baaaf0a1c0909c0828335791557125fba911e0 +0 -0
  43. package/skills/.git 2/objects/42/2924221b81f5ce3c4e4daac9a64a24f9b01f9a +0 -0
  44. package/skills/.git 2/objects/42/ec0ce707447dc11446a34c9995fb8533801731 +0 -0
  45. package/skills/.git 2/objects/46/e43ce92866d56ce74b1d750db307cfe6154a15 +0 -0
  46. package/skills/.git 2/objects/48/5e41b633c63f55b8277bcc59f44f67681f671a +0 -0
  47. package/skills/.git 2/objects/49/49c596a3a89fa240642acd95dd3258e261eb09 +0 -0
  48. package/skills/.git 2/objects/50/9d42d8412ef8eaf7f7e138476bac2e4d10ce60 +0 -0
  49. package/skills/.git 2/objects/55/0c8c389d981b463ef849aeb792d8be3ccb6ec8 +0 -0
  50. package/skills/.git 2/objects/5d/82d3b18410cdda3ace3677436f0cb599dbe2d2 +0 -0
  51. package/skills/.git 2/objects/60/0617c58e871a38b33bf29e282d132bb3c381ad +0 -0
  52. package/skills/.git 2/objects/6a/8369a99c687b7245c92ffaf0e0f0dab9014504 +0 -0
  53. package/skills/.git 2/objects/79/bea435d40ab531c1aaf6be0432c6a5b7aaed21 +0 -0
  54. package/skills/.git 2/objects/7e/5ebd79251c2f14e4aceb86c74b6b6daae6b500 +0 -0
  55. package/skills/.git 2/objects/81/98a822a60178d6d5023ddb3e222cddf048742e +0 -0
  56. package/skills/.git 2/objects/86/0a0e1943dfe53411d2e499a1f16f46a96ef758 +0 -0
  57. package/skills/.git 2/objects/86/971fb55fdc081fdbae52376f0f13e57a4e9b04 +0 -0
  58. package/skills/.git 2/objects/88/b89dd609a0a03f8d4fe8bfde20d5b8fc1d326d +0 -0
  59. package/skills/.git 2/objects/90/8737edb6b7809e32cc01590b4e08ba42a9d40d +0 -0
  60. package/skills/.git 2/objects/93/d5a8a9a7d4fb7f11491cb596a6880528725118 +0 -0
  61. package/skills/.git 2/objects/98/46a2ab81d0c3b3eb00ef88fc56989aa7e9f316 +0 -0
  62. package/skills/.git 2/objects/9b/d8dd1e49cf274eaf9c555f3ab39dce7af5715e +0 -0
  63. package/skills/.git 2/objects/a1/13329fb0cec96ae78b222d33a24c3b5bc7fa1f +0 -0
  64. package/skills/.git 2/objects/a9/e6effe626e8a3aea3a8fc3364b492191c6e7d0 +0 -0
  65. package/skills/.git 2/objects/ad/6de7e48d9782cca9353d1ff0aa1aab7fe1df85 +0 -0
  66. package/skills/.git 2/objects/af/54ae316f771ff692e299ffcd8bf2f06b413b59 +0 -0
  67. package/skills/.git 2/objects/b0/4cb8b0b00dad633e731c1472161419e738d674 +0 -0
  68. package/skills/.git 2/objects/b3/094abb0b9ed46419b269e4a4e36a459690e3b0 +0 -0
  69. package/skills/.git 2/objects/b9/435c5d4baac2cfc5c83009ddd27b46b60db5f1 +0 -0
  70. package/skills/.git 2/objects/ba/5da17dbaec5ec2dcfdfd126aead518d1171d5c +0 -0
  71. package/skills/.git 2/objects/c0/bf58703aa258ba5dd63083bebaec8f223d844c +0 -0
  72. package/skills/.git 2/objects/c4/701a34edf1fc1bad58ccc57bd03f9426acb59a +0 -0
  73. package/skills/.git 2/objects/c7/5ccce9a4e5cc74d9b3174550cf6d993ca43638 +0 -0
  74. package/skills/.git 2/objects/c7/710d59b5a35b0f1f0a0399386643a0bd94c929 +0 -0
  75. package/skills/.git 2/objects/d1/fe58237112e953e5fec52da22cf38e08be3df9 +5 -0
  76. package/skills/.git 2/objects/d2/2bbe9fd2f74c95bc5583e803f5e435f1e2cd86 +0 -0
  77. package/skills/.git 2/objects/d7/e72852ea2bff74581dbf247d400120086229f4 +0 -0
  78. package/skills/.git 2/objects/d8/d4c3b5553e4fd72807e1d4b49ef07d9ef3ac35 +0 -0
  79. package/skills/.git 2/objects/dc/75050c2876f6a02ae2a53a3c886f395b622977 +0 -0
  80. package/skills/.git 2/objects/ee/e8546f95acec500187c08a28a8b9ee02db0dec +0 -0
  81. package/skills/.git 2/objects/ef/263c059208b416c2146434f10cb2b9fabcba16 +0 -0
  82. package/skills/.git 2/objects/f3/ae597e84d9a59b88acd21c99bde2eaf686d785 +0 -0
  83. package/skills/.git 2/objects/f3/f6f5673c821d3d8e76fa267a9e882e7a5387ea +0 -0
  84. package/skills/.git 2/objects/f9/6e6d0ad02624dd11d5848594d056caef7a5e8b +0 -0
  85. package/skills/.git 2/objects/ff/278988fc1edf0db3abcf18de795f4cc0b4f3e1 +0 -0
  86. package/skills/.git 2/refs/heads/main +1 -0
  87. package/skills/.git 2/refs/remotes/origin/main +1 -0
  88. package/skills/.pytest_cache 2/v/cache/nodeids +76 -0
  89. package/skills/.pytest_cache 2/v/cache/stepwise +1 -0
  90. package/skills/_shared/helpers.md +123 -0
  91. package/skills/_shared/outputs-convention.md +24 -0
  92. package/skills/cm-ads-tracker/SKILL.md +109 -0
  93. package/skills/cm-ads-tracker/evals/evals.json +55 -0
  94. package/skills/cm-ads-tracker/references/gtm-architecture.md +321 -0
  95. package/skills/cm-ads-tracker/references/industry-events.md +294 -0
  96. package/skills/cm-ads-tracker/references/platforms-api.md +238 -0
  97. package/skills/cm-ads-tracker/templates/capi-payload.md +79 -0
  98. package/skills/cm-ads-tracker/templates/datalayer-push.js +104 -0
  99. package/skills/cm-ads-tracker/templates/gtm-variables.js +56 -0
  100. package/skills/cm-brainstorm-idea/SKILL.md +423 -0
  101. package/skills/cm-code-review/SKILL.md +151 -0
  102. package/skills/cm-content-factory/SKILL.md +416 -0
  103. package/skills/cm-continuity/SKILL.md +399 -0
  104. package/skills/cm-dashboard/SKILL.md +533 -0
  105. package/skills/cm-dashboard/ui/app.js +1270 -0
  106. package/skills/cm-dashboard/ui/index.html +206 -0
  107. package/skills/cm-dashboard/ui/style.css +440 -0
  108. package/skills/cm-debugging/SKILL.md +412 -0
  109. package/skills/cm-deep-search/SKILL.md +242 -0
  110. package/skills/cm-design-system/SKILL.md +97 -0
  111. package/skills/cm-design-system/resources/halo-modern.md +40 -0
  112. package/skills/cm-design-system/resources/lunaris-advanced.md +40 -0
  113. package/skills/cm-design-system/resources/nitro-enterprise.md +39 -0
  114. package/skills/cm-design-system/resources/shadcn-default.md +37 -0
  115. package/skills/cm-dockit/README.md +100 -0
  116. package/skills/cm-dockit/SKILL.md +302 -0
  117. package/skills/cm-dockit/index.html +443 -0
  118. package/skills/cm-dockit/package-lock.json +1850 -0
  119. package/skills/cm-dockit/package.json +14 -0
  120. package/skills/cm-dockit/prompts/analysis.md +34 -0
  121. package/skills/cm-dockit/prompts/api-reference.md +24 -0
  122. package/skills/cm-dockit/prompts/architecture.md +21 -0
  123. package/skills/cm-dockit/prompts/data-flow.md +20 -0
  124. package/skills/cm-dockit/prompts/database.md +21 -0
  125. package/skills/cm-dockit/prompts/deployment.md +22 -0
  126. package/skills/cm-dockit/prompts/flows.md +21 -0
  127. package/skills/cm-dockit/prompts/jtbd.md +20 -0
  128. package/skills/cm-dockit/prompts/personas.md +24 -0
  129. package/skills/cm-dockit/prompts/sop-modules.md +40 -0
  130. package/skills/cm-dockit/scripts/doc-gen.sh +121 -0
  131. package/skills/cm-dockit/scripts/dockit-dashboard.sh +142 -0
  132. package/skills/cm-dockit/scripts/dockit-runner.sh +607 -0
  133. package/skills/cm-dockit/scripts/dockit-task.sh +166 -0
  134. package/skills/cm-dockit/skills/analyze-codebase.md +174 -0
  135. package/skills/cm-dockit/skills/api-reference.md +237 -0
  136. package/skills/cm-dockit/skills/changelog-guide.md +195 -0
  137. package/skills/cm-dockit/skills/content-guidelines.md +190 -0
  138. package/skills/cm-dockit/skills/sop-guide.md +184 -0
  139. package/skills/cm-dockit/skills/tech-docs.md +287 -0
  140. package/skills/cm-dockit/templates/markdown/structure.md +60 -0
  141. package/skills/cm-dockit/templates/vitepress-premium/.vitepress/config.mts +110 -0
  142. package/skills/cm-dockit/templates/vitepress-premium/.vitepress/theme/custom.css +189 -0
  143. package/skills/cm-dockit/templates/vitepress-premium/.vitepress/theme/index.ts +4 -0
  144. package/skills/cm-dockit/templates/vitepress-premium/package.json +19 -0
  145. package/skills/cm-dockit/templates/vitepress-premium/tests/frontend.test.ts +45 -0
  146. package/skills/cm-dockit/tests/runner.test.ts +66 -0
  147. package/skills/cm-dockit/workflows/export-markdown.md +82 -0
  148. package/skills/cm-dockit/workflows/generate-docs.md +68 -0
  149. package/skills/cm-dockit/workflows/setup-vitepress.md +181 -0
  150. package/skills/cm-example/SKILL.md +26 -0
  151. package/skills/cm-execution/SKILL.md +268 -0
  152. package/skills/cm-git-worktrees/SKILL.md +164 -0
  153. package/skills/cm-how-it-work/SKILL.md +189 -0
  154. package/skills/cm-identity-guard/SKILL.md +412 -0
  155. package/skills/cm-jtbd/SKILL.md +98 -0
  156. package/skills/cm-planning/SKILL.md +130 -0
  157. package/skills/cm-project-bootstrap/SKILL.md +161 -0
  158. package/skills/cm-project-bootstrap/templates/AGENTS.md +42 -0
  159. package/skills/cm-project-bootstrap/templates/frontend-safety.test.js +51 -0
  160. package/skills/cm-project-bootstrap/templates/i18n-sync.test.js +38 -0
  161. package/skills/cm-project-bootstrap/templates/pr-template.md +12 -0
  162. package/skills/cm-project-bootstrap/templates/project-identity.json +29 -0
  163. package/skills/cm-project-bootstrap/templates/vitest.config.js +10 -0
  164. package/skills/cm-quality-gate/SKILL.md +218 -0
  165. package/skills/cm-readit/SKILL.md +289 -0
  166. package/skills/cm-readit/audio-player.md +206 -0
  167. package/skills/cm-readit/examples/blog-reader.js +352 -0
  168. package/skills/cm-readit/examples/voice-cro.js +390 -0
  169. package/skills/cm-readit/tts-engine.md +262 -0
  170. package/skills/cm-readit/ui-patterns.md +362 -0
  171. package/skills/cm-readit/voice-cro.md +223 -0
  172. package/skills/cm-safe-deploy/SKILL.md +120 -0
  173. package/skills/cm-safe-deploy/templates/deploy.sh +89 -0
  174. package/skills/cm-safe-i18n/SKILL.md +473 -0
  175. package/skills/cm-secret-shield/SKILL.md +580 -0
  176. package/skills/cm-skill-chain/SKILL.md +78 -0
  177. package/skills/cm-skill-index/SKILL.md +318 -0
  178. package/skills/cm-skill-mastery/SKILL.md +169 -0
  179. package/skills/cm-start/SKILL.md +65 -0
  180. package/skills/cm-status/SKILL.md +12 -0
  181. package/skills/cm-tdd/SKILL.md +370 -0
  182. package/skills/cm-terminal/SKILL.md +177 -0
  183. package/skills/cm-test-gate/SKILL.md +242 -0
  184. package/skills/cm-ui-preview/SKILL.md +291 -0
  185. package/skills/cm-ux-master/DESIGN_STANDARD_TEMPLATE.md +54 -0
  186. package/skills/cm-ux-master/SKILL.md +114 -0
  187. package/skills/cro-methodology/SKILL.md +98 -0
  188. package/skills/cro-methodology/references/COPYWRITING.md +178 -0
  189. package/skills/cro-methodology/references/OBJECTIONS.md +135 -0
  190. package/skills/cro-methodology/references/PERSUASION.md +158 -0
  191. package/skills/cro-methodology/references/RESEARCH.md +220 -0
  192. package/skills/cro-methodology/references/funnel-analysis.md +365 -0
  193. package/skills/cro-methodology/references/testing-methodology.md +330 -0
@@ -0,0 +1,1270 @@
1
+ /* CodyMaster Mission Control v4 โ€” Multi-Project, History, Deploy, Changelog, Auto-Sync */
2
+
3
+ (function () {
4
+ 'use strict';
5
+
6
+ // โ”€โ”€ Theme Management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
7
+ const THEME_KEY = 'cm-theme';
8
+ const darkMQ = window.matchMedia('(prefers-color-scheme: dark)');
9
+
10
+ function getEffectiveTheme() {
11
+ const saved = localStorage.getItem(THEME_KEY);
12
+ if (saved === 'light' || saved === 'dark') return saved;
13
+ return 'dark'; // New premium UI is dark by default
14
+ }
15
+
16
+ function applyTheme(theme) {
17
+ document.documentElement.setAttribute('data-theme', theme);
18
+ const sunIcon = document.getElementById('theme-icon-sun');
19
+ const moonIcon = document.getElementById('theme-icon-moon');
20
+ if (sunIcon && moonIcon) {
21
+ sunIcon.style.display = theme === 'dark' ? 'block' : 'none';
22
+ moonIcon.style.display = theme === 'dark' ? 'none' : 'block';
23
+ }
24
+ }
25
+
26
+ // Apply immediately to avoid flash
27
+ applyTheme(getEffectiveTheme());
28
+
29
+ // Listen for OS preference changes (only when no manual override)
30
+ darkMQ.addEventListener('change', () => {
31
+ if (!localStorage.getItem(THEME_KEY)) {
32
+ applyTheme(getEffectiveTheme());
33
+ }
34
+ });
35
+ const API = '/api';
36
+ const AGENT_COLORS = {
37
+ 'antigravity': '#3fb950', 'claude-code': '#bc8cff', 'cursor': '#58a6ff',
38
+ 'codex': '#ec4899', 'windsurf': '#f97316', 'cline': '#a1887f',
39
+ 'copilot': '#8b949e', 'cli': '#d29922', 'gemini-cli': '#d29922', 'manual': '#e6edf3',
40
+ };
41
+ const AGENT_LABELS = {
42
+ 'antigravity': 'Antigravity', 'claude-code': 'Claude Code', 'cursor': 'Cursor',
43
+ 'codex': 'Codex', 'windsurf': 'Windsurf / Cline', 'cline': 'Windsurf / Cline',
44
+ 'copilot': 'GitHub Copilot', 'cli': 'CLI', 'gemini-cli': 'CLI', 'manual': 'Manual',
45
+ };
46
+ const ACTIVITY_ICONS = {
47
+ 'task_created': 'โœจ', 'task_moved': 'โ†”๏ธ', 'task_done': 'โœ…', 'task_deleted': '๐Ÿ—‘๏ธ', 'task_updated': 'โœ๏ธ',
48
+ 'task_dispatched': '๐Ÿš€', 'task_transitioned': '๐Ÿ”€',
49
+ 'project_created': '๐Ÿ“ฆ', 'project_deleted': '๐Ÿ—‘๏ธ',
50
+ 'deploy_staging': '๐ŸŸก', 'deploy_production': '๐Ÿš€', 'deploy_failed': 'โŒ', 'rollback': 'โช',
51
+ 'git_push': '๐Ÿ“ค', 'changelog_added': '๐Ÿ“',
52
+ 'learning_deleted': '๐Ÿงน', 'decision_deleted': '๐Ÿงน',
53
+ };
54
+
55
+ // โ”€โ”€ Valid Transition Map โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
56
+ const VALID_TRANSITIONS = {
57
+ 'backlog': ['in-progress'],
58
+ 'in-progress': ['review', 'done', 'backlog'],
59
+ 'review': ['done', 'in-progress'],
60
+ 'done': ['backlog'],
61
+ };
62
+
63
+ const TRANSITION_LABELS = {
64
+ 'in-progress': { label: 'โ–ถ Start', icon: 'โ–ถ' },
65
+ 'review': { label: 'โ†’ Review', icon: '๐Ÿ”' },
66
+ 'done': { label: 'โœ“ Done', icon: 'โœ…' },
67
+ 'backlog': { label: 'โ† Backlog', icon: '๐Ÿ“‹' },
68
+ };
69
+
70
+ // โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
71
+ let projects = [], tasks = [], activities = [], deployments = [], changelog = [];
72
+ let selectedProjectId = null;
73
+ let draggedTaskId = null;
74
+ let currentTab = 'kanban';
75
+
76
+ // โ”€โ”€ Auto-Refresh State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
77
+ const AUTO_REFRESH_KEY = 'cm-auto-refresh';
78
+ const AUTO_REFRESH_INTERVAL = 15000; // 15 seconds
79
+ let autoRefreshEnabled = localStorage.getItem(AUTO_REFRESH_KEY) !== 'false'; // default ON
80
+ let autoRefreshTimer = null;
81
+ let lastSyncTime = Date.now();
82
+ let isModalOpen = false;
83
+ let isDragging = false;
84
+ let syncTickTimer = null;
85
+
86
+ // โ”€โ”€ DOM Refs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
87
+ const columns = {
88
+ backlog: document.getElementById('list-backlog'),
89
+ 'in-progress': document.getElementById('list-in-progress'),
90
+ review: document.getElementById('list-review'),
91
+ done: document.getElementById('list-done'),
92
+ };
93
+
94
+ const sidebar = document.getElementById('sidebar');
95
+ const projectListEl = document.getElementById('project-list');
96
+ const agentListEl = document.getElementById('agent-list');
97
+ const headerProjectName = document.getElementById('header-project-name');
98
+ const taskStats = document.getElementById('task-stats');
99
+
100
+ const modalOverlay = document.getElementById('modal-overlay');
101
+ const modalTitle = document.getElementById('modal-title');
102
+ const taskForm = document.getElementById('task-form');
103
+ const formId = document.getElementById('form-id');
104
+ const formTitle = document.getElementById('form-title');
105
+ const formDescription = document.getElementById('form-description');
106
+ const formPriority = document.getElementById('form-priority');
107
+ const formColumn = document.getElementById('form-column');
108
+ const formAgent = document.getElementById('form-agent');
109
+ const formSkill = document.getElementById('form-skill');
110
+ const btnSubmit = document.getElementById('btn-submit');
111
+
112
+
113
+ const deployModalOverlay = document.getElementById('deploy-modal-overlay');
114
+ const deployForm = document.getElementById('deploy-form');
115
+ const changelogModalOverlay = document.getElementById('changelog-modal-overlay');
116
+ const changelogForm = document.getElementById('changelog-form');
117
+
118
+ const deleteOverlay = document.getElementById('delete-overlay');
119
+ const deleteTaskName = document.getElementById('delete-task-name');
120
+ const deleteConfirm = document.getElementById('delete-confirm');
121
+
122
+ const dispatchOverlay = document.getElementById('dispatch-overlay');
123
+ const dispatchClose = document.getElementById('dispatch-close');
124
+ const dispatchPrompt = document.getElementById('dispatch-prompt');
125
+ const dispatchCommand = document.getElementById('dispatch-command');
126
+ const copyPromptBtn = document.getElementById('copy-prompt');
127
+ const copyCommandBtn = document.getElementById('copy-command');
128
+ const dispatchDoneBtn = document.getElementById('dispatch-done-btn');
129
+
130
+ const toastContainer = document.getElementById('toast-container');
131
+ const refreshBtn = document.getElementById('btn-refresh');
132
+ const autoRefreshBtn = document.getElementById('btn-auto-refresh');
133
+ const syncDot = document.getElementById('sync-dot');
134
+ const syncLabel = document.getElementById('sync-label');
135
+ const syncStatus = document.getElementById('sync-status');
136
+
137
+ // โ”€โ”€ API Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
138
+ async function fetchJSON(url, opts) {
139
+ const res = await fetch(url, opts);
140
+ if (res.status === 204) return null;
141
+ if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || 'Request failed'); }
142
+ return res.json();
143
+ }
144
+
145
+ // โ”€โ”€ Data Loading โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
146
+ async function loadAll() {
147
+ const pq = selectedProjectId ? 'projectId=' + selectedProjectId : '';
148
+ const aq = [pq, 'limit=100'].filter(Boolean).join('&');
149
+ const qs = pq ? '?' + pq : '';
150
+ const [p, t, a, d, c] = await Promise.all([
151
+ fetchJSON(`${API}/projects`),
152
+ fetchJSON(`${API}/tasks${qs}`),
153
+ fetchJSON(`${API}/activities?${aq}`),
154
+ fetchJSON(`${API}/deployments${qs}`),
155
+ fetchJSON(`${API}/changelog${qs}`),
156
+ ]);
157
+ projects = p || []; tasks = t || [];
158
+ activities = a || []; deployments = d || []; changelog = c || [];
159
+ }
160
+
161
+ async function refreshData(silent = false) {
162
+ refreshBtn.classList.add('refreshing');
163
+ updateSyncStatus('syncing');
164
+ try {
165
+ await loadAll();
166
+ renderSidebar();
167
+ renderCurrentTab();
168
+ lastSyncTime = Date.now();
169
+ updateSyncStatus('synced');
170
+ if (!silent) showToast('info', 'Refreshed');
171
+ } catch (err) {
172
+ updateSyncStatus('error');
173
+ if (!silent) showToast('error', 'Refresh failed: ' + err.message);
174
+ }
175
+ setTimeout(() => refreshBtn.classList.remove('refreshing'), 600);
176
+ }
177
+
178
+ // โ”€โ”€ Sync Status Indicator โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
179
+ function updateSyncStatus(state) {
180
+ if (!syncDot || !syncLabel) return;
181
+ syncDot.className = 'sync-dot ' + state;
182
+ if (state === 'syncing') {
183
+ syncLabel.textContent = 'Syncingโ€ฆ';
184
+ } else if (state === 'synced') {
185
+ syncLabel.textContent = 'Synced';
186
+ } else if (state === 'error') {
187
+ syncLabel.textContent = 'Error';
188
+ }
189
+ }
190
+
191
+ function updateSyncTimeTick() {
192
+ if (!syncLabel || !syncDot) return;
193
+ if (syncDot.classList.contains('syncing')) return;
194
+ const elapsed = Math.floor((Date.now() - lastSyncTime) / 1000);
195
+ if (elapsed < 5) {
196
+ syncLabel.textContent = 'Synced';
197
+ } else if (elapsed < 60) {
198
+ syncLabel.textContent = `${elapsed}s ago`;
199
+ } else {
200
+ syncLabel.textContent = `${Math.floor(elapsed / 60)}m ago`;
201
+ }
202
+ }
203
+
204
+ // โ”€โ”€ Auto-Refresh Engine โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
205
+ function startAutoRefresh() {
206
+ stopAutoRefresh();
207
+ if (!autoRefreshEnabled) return;
208
+ autoRefreshTimer = setInterval(() => {
209
+ // Skip if modal open or dragging
210
+ if (isModalOpen || isDragging) return;
211
+ refreshData(true); // silent refresh
212
+ }, AUTO_REFRESH_INTERVAL);
213
+ // Update time tick every 5s
214
+ syncTickTimer = setInterval(updateSyncTimeTick, 5000);
215
+ updateAutoRefreshUI();
216
+ }
217
+
218
+ function stopAutoRefresh() {
219
+ if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = null; }
220
+ if (syncTickTimer) { clearInterval(syncTickTimer); syncTickTimer = null; }
221
+ }
222
+
223
+ function toggleAutoRefresh() {
224
+ autoRefreshEnabled = !autoRefreshEnabled;
225
+ localStorage.setItem(AUTO_REFRESH_KEY, autoRefreshEnabled ? 'true' : 'false');
226
+ if (autoRefreshEnabled) {
227
+ startAutoRefresh();
228
+ showToast('info', 'โšก Auto-refresh ON (every 15s)');
229
+ } else {
230
+ stopAutoRefresh();
231
+ showToast('info', 'โธ Auto-refresh OFF');
232
+ }
233
+ updateAutoRefreshUI();
234
+ }
235
+
236
+ function updateAutoRefreshUI() {
237
+ if (!autoRefreshBtn) return;
238
+ autoRefreshBtn.classList.toggle('active', autoRefreshEnabled);
239
+ autoRefreshBtn.title = autoRefreshEnabled ? 'Auto-refresh ON (click to disable)' : 'Auto-refresh OFF (click to enable)';
240
+ if (syncStatus) {
241
+ syncStatus.style.opacity = autoRefreshEnabled ? '1' : '0.4';
242
+ syncStatus.title = autoRefreshEnabled ? 'Auto-refresh active' : 'Auto-refresh paused';
243
+ }
244
+ }
245
+
246
+ // Track modal state for smart pause
247
+ function setModalOpen(open) {
248
+ isModalOpen = open;
249
+ }
250
+
251
+ // โ”€โ”€ Tab Navigation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
252
+ function switchTab(tabName) {
253
+ currentTab = tabName;
254
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tabName));
255
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + tabName));
256
+ renderCurrentTab();
257
+ }
258
+
259
+ function renderCurrentTab() {
260
+ switch (currentTab) {
261
+ case 'kanban': renderBoard(); break;
262
+ case 'history': renderHistory(); break;
263
+ case 'deploys': renderDeploys(); break;
264
+ case 'changelog': renderChangelog(); break;
265
+ case 'brain': renderBrain(); break;
266
+ }
267
+ }
268
+
269
+ document.querySelectorAll('.tab-btn').forEach(btn => {
270
+ btn.addEventListener('click', () => switchTab(btn.dataset.tab));
271
+ });
272
+
273
+ // โ”€โ”€ Sidebar Rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
274
+ function renderSidebar() {
275
+ let html = `<div class="project-item project-item-all ${!selectedProjectId ? 'active' : ''}" data-project-id="">
276
+ <span class="project-icon">๐Ÿ“</span><span class="project-name">All Projects</span>
277
+ <span class="project-task-count">${tasks.length || countAllTasks()}</span></div>`;
278
+ projects.forEach(p => {
279
+ html += `<div class="project-item ${selectedProjectId === p.id ? 'active' : ''}" data-project-id="${p.id}">
280
+ <span class="project-icon">๐Ÿ“ฆ</span><span class="project-name" title="${esc(p.path)}">${esc(p.name)}</span>
281
+ <span class="project-task-count">${p.taskCount || 0}</span>
282
+ <button class="project-delete-btn" data-delete-project="${p.id}" title="Delete project">โœ•</button></div>`;
283
+ });
284
+ projectListEl.innerHTML = html;
285
+
286
+ projectListEl.querySelectorAll('.project-item').forEach(el => {
287
+ el.addEventListener('click', async e => {
288
+ if (e.target.closest('.project-delete-btn')) return;
289
+ selectedProjectId = el.dataset.projectId || null;
290
+ await refreshData();
291
+ });
292
+ });
293
+
294
+ projectListEl.querySelectorAll('.project-delete-btn').forEach(btn => {
295
+ btn.addEventListener('click', async e => {
296
+ e.stopPropagation();
297
+ const pid = btn.dataset.deleteProject;
298
+ const proj = projects.find(p => p.id === pid);
299
+ if (!proj || !confirm(`Delete "${proj.name}" and all its tasks?`)) return;
300
+ try {
301
+ await fetchJSON(`${API}/projects/${pid}`, { method: 'DELETE' });
302
+ if (selectedProjectId === pid) selectedProjectId = null;
303
+ await refreshData();
304
+ showToast('success', 'Project deleted');
305
+ } catch (err) { showToast('error', err.message); }
306
+ });
307
+ });
308
+
309
+ // Agents
310
+ const allAgents = {};
311
+ tasks.forEach(t => { if (t.agent) allAgents[t.agent] = (allAgents[t.agent] || 0) + 1; });
312
+ if (Object.keys(allAgents).length === 0) {
313
+ agentListEl.innerHTML = '<div class="agent-empty">No active agents</div>';
314
+ } else {
315
+ agentListEl.innerHTML = Object.entries(allAgents).sort((a, b) => b[1] - a[1]).map(([agent, count]) => {
316
+ const color = AGENT_COLORS[agent] || '#8b949e';
317
+ return `<div class="agent-badge"><span class="agent-dot" style="background:${color}"></span><span>${esc(AGENT_LABELS[agent] || agent)}</span><span class="agent-task-count">${count}</span></div>`;
318
+ }).join('');
319
+ }
320
+
321
+ headerProjectName.textContent = selectedProjectId ? (projects.find(p => p.id === selectedProjectId)?.name || 'Unknown') : 'All Projects';
322
+ }
323
+
324
+ function countAllTasks() { return projects.reduce((s, p) => s + (p.taskCount || 0), 0); }
325
+
326
+ // โ”€โ”€ Board Rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
327
+ function renderBoard() {
328
+ const colNames = ['backlog', 'in-progress', 'review', 'done'];
329
+ const emptyIcons = { backlog: '๐Ÿ“‹', 'in-progress': 'โšก', review: '๐Ÿ”', done: 'โœ…' };
330
+ const emptyTexts = { backlog: 'No tasks in backlog', 'in-progress': 'Nothing in progress', review: 'No tasks to review', done: 'No completed tasks' };
331
+
332
+ colNames.forEach(col => {
333
+ const list = columns[col];
334
+ const colTasks = tasks.filter(t => t.column === col).sort((a, b) => a.order - b.order);
335
+ list.innerHTML = '';
336
+ if (colTasks.length === 0) {
337
+ list.innerHTML = `<div class="empty-state"><span class="empty-state-icon">${emptyIcons[col]}</span><span class="empty-state-text">${emptyTexts[col]}</span></div>`;
338
+ } else {
339
+ colTasks.forEach(task => list.appendChild(createCard(task)));
340
+ }
341
+ const ce = document.querySelector(`[data-count="${col}"]`);
342
+ if (ce) ce.textContent = colTasks.length;
343
+ });
344
+ renderStats();
345
+ renderStuckBanner();
346
+ }
347
+
348
+ function createCard(task) {
349
+ const card = document.createElement('div');
350
+ card.className = 'task-card'; card.dataset.taskId = task.id; card.draggable = true;
351
+ const ac = AGENT_COLORS[task.agent] || '#8b949e';
352
+ const al = AGENT_LABELS[task.agent] || task.agent;
353
+ const priLabels = { low: 'Low', medium: 'Medium', high: 'High', urgent: 'Urgent' };
354
+
355
+ // Stuck indicator
356
+ const elapsed = Date.now() - new Date(task.updatedAt).getTime();
357
+ const isStuck = task.column === 'in-progress' && elapsed > 30 * 60 * 1000;
358
+ if (isStuck) card.classList.add('stuck');
359
+
360
+ // Dispatch status badge
361
+ let dispatchBadge = '';
362
+ if (task.dispatchStatus === 'dispatched') {
363
+ dispatchBadge = '<span class="dispatch-badge dispatched" title="Dispatched to agent">๐Ÿš€ Dispatched</span>';
364
+ } else if (task.dispatchStatus === 'failed') {
365
+ dispatchBadge = `<span class="dispatch-badge failed" title="${esc(task.dispatchError || 'Dispatch failed')}">โŒ Failed</span>`;
366
+ }
367
+
368
+ // Dispatch button (only if agent is assigned and not manual)
369
+ let dispatchBtn = '';
370
+ if (task.agent && task.agent !== 'manual') {
371
+ const isRedispatch = task.dispatchStatus === 'dispatched';
372
+ const dispatchTitle = isRedispatch ? 'Re-dispatch to agent' : 'Dispatch to agent';
373
+ dispatchBtn = `<button class="card-action-btn dispatch" title="${dispatchTitle}" data-id="${task.id}"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 1v10M4 8l4 4 4-4M2 14h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></button>`;
374
+ }
375
+
376
+ let meta = '';
377
+ if (task.agent || task.skill || dispatchBadge) {
378
+ meta = '<div class="card-meta">';
379
+ if (task.agent) meta += `<span class="card-agent-badge"><span class="card-agent-dot" style="background:${ac}"></span>${esc(al)}</span>`;
380
+ if (task.skill) meta += `<span class="card-skill-badge">${esc(task.skill)}</span>`;
381
+ if (dispatchBadge) meta += dispatchBadge;
382
+ meta += '</div>';
383
+ }
384
+
385
+ // Quick transition buttons
386
+ const transitions = VALID_TRANSITIONS[task.column] || [];
387
+ let transitionBtns = '';
388
+ if (transitions.length > 0) {
389
+ transitionBtns = '<div class="card-transitions">';
390
+ transitions.forEach(target => {
391
+ const info = TRANSITION_LABELS[target] || { label: target, icon: 'โ†’' };
392
+ const cls = target === 'done' ? 'transition-done' : target === 'backlog' ? 'transition-back' : 'transition-forward';
393
+ transitionBtns += `<button class="transition-btn ${cls}" data-task-id="${task.id}" data-target="${target}" title="Move to ${target}">${info.icon} ${info.label}</button>`;
394
+ });
395
+ transitionBtns += '</div>';
396
+ }
397
+
398
+ card.innerHTML = `<div class="card-top"><span class="card-title">${esc(task.title)}</span>
399
+ <div class="card-actions">
400
+ ${dispatchBtn}
401
+ <button class="card-action-btn edit" title="Edit" data-id="${task.id}"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
402
+ <button class="card-action-btn delete" title="Delete" data-id="${task.id}"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M5.34 4V2.67a1.34 1.34 0 011.34-1.34h2.66a1.34 1.34 0 011.34 1.34V4m2 0v9.33a1.34 1.34 0 01-1.34 1.34H4.67a1.34 1.34 0 01-1.34-1.34V4h9.34z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
403
+ </div></div>
404
+ ${task.description ? `<p class="card-description">${esc(task.description)}</p>` : ''}
405
+ ${meta}
406
+ <div class="card-footer"><span class="priority-badge priority-${task.priority}">${priLabels[task.priority] || task.priority}</span><span class="card-time">${formatTimeAgo(task.updatedAt)}</span></div>
407
+ ${transitionBtns}`;
408
+ card.addEventListener('dragstart', handleDragStart);
409
+ card.addEventListener('dragend', handleDragEnd);
410
+ card.querySelector('.edit').addEventListener('click', e => { e.stopPropagation(); openEditModal(task); });
411
+ card.querySelector('.delete').addEventListener('click', e => { e.stopPropagation(); openDeleteModal(task); });
412
+ const dispatchEl = card.querySelector('.dispatch');
413
+ if (dispatchEl) {
414
+ dispatchEl.addEventListener('click', e => { e.stopPropagation(); handleDispatch(task); });
415
+ }
416
+ // Bind transition buttons
417
+ card.querySelectorAll('.transition-btn').forEach(btn => {
418
+ btn.addEventListener('click', e => { e.stopPropagation(); handleTransition(btn.dataset.taskId, btn.dataset.target); });
419
+ });
420
+ return card;
421
+ }
422
+
423
+ function renderStats() {
424
+ const total = tasks.length, done = tasks.filter(t => t.column === 'done').length, ip = tasks.filter(t => t.column === 'in-progress').length;
425
+ taskStats.innerHTML = `<span class="stat"><span class="stat-dot" style="background:var(--col-in-progress)"></span>${ip} active</span>
426
+ <span class="stat"><span class="stat-dot" style="background:var(--col-done)"></span>${done}/${total} done</span>`;
427
+ }
428
+
429
+ // โ”€โ”€ History Rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
430
+ function renderHistory() {
431
+ const container = document.getElementById('timeline');
432
+ const countEl = document.getElementById('activity-count');
433
+ countEl.textContent = `${activities.length} events`;
434
+
435
+ if (activities.length === 0) {
436
+ container.innerHTML = '<div class="timeline-empty">No activity yet. Create tasks, deploy, or add changelog entries to see history.</div>';
437
+ return;
438
+ }
439
+
440
+ container.innerHTML = activities.map(a => {
441
+ const icon = ACTIVITY_ICONS[a.type] || '๐Ÿ“Œ';
442
+ const ac = AGENT_COLORS[a.agent] || '#8b949e';
443
+ const al = AGENT_LABELS[a.agent] || a.agent;
444
+ const agentHtml = a.agent ? `<span class="timeline-agent"><span class="timeline-agent-dot" style="background:${ac}"></span>${esc(al)}</span>` : '';
445
+ const proj = projects.find(p => p.id === a.projectId);
446
+ const projName = proj ? proj.name : '';
447
+
448
+ return `<div class="timeline-item type-${a.type}">
449
+ <span class="timeline-icon">${icon}</span>
450
+ <div class="timeline-content">
451
+ <div class="timeline-message">${esc(a.message)}</div>
452
+ <div class="timeline-meta">
453
+ <span>${formatTimeAgo(a.createdAt)}</span>
454
+ ${agentHtml}
455
+ ${projName ? `<span style="color:var(--text-muted)">๐Ÿ“ฆ ${esc(projName)}</span>` : ''}
456
+ </div>
457
+ </div></div>`;
458
+ }).join('');
459
+ }
460
+
461
+ // โ”€โ”€ Deploys Rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
462
+ function renderDeploys() {
463
+ const container = document.getElementById('deploy-list');
464
+ if (deployments.length === 0) {
465
+ container.innerHTML = '<div class="deploy-empty">No deployments yet. Deploy from CLI with: <code>codymaster deploy staging</code></div>';
466
+ return;
467
+ }
468
+
469
+ container.innerHTML = deployments.map(d => {
470
+ const proj = projects.find(p => p.id === d.projectId);
471
+ const canRollback = d.status === 'success' && !d.rollbackOf;
472
+ return `<div class="deploy-card ${d.rollbackOf ? 'is-rollback' : ''}">
473
+ <span class="deploy-status-dot ${d.status}"></span>
474
+ <div class="deploy-info">
475
+ <div class="deploy-message">${esc(d.message)}</div>
476
+ <div class="deploy-detail">
477
+ <span class="deploy-env-badge ${d.env}">${d.env}</span>
478
+ <span class="deploy-status-badge ${d.status}">${d.status.replace('_', ' ')}</span>
479
+ ${d.commit ? `<span>๐Ÿ”— ${esc(d.commit.substring(0, 7))}</span>` : ''}
480
+ ${d.branch ? `<span>๐ŸŒฟ ${esc(d.branch)}</span>` : ''}
481
+ ${proj ? `<span>๐Ÿ“ฆ ${esc(proj.name)}</span>` : ''}
482
+ <span>${formatTimeAgo(d.startedAt)}</span>
483
+ </div>
484
+ </div>
485
+ <div class="deploy-actions">
486
+ ${canRollback ? `<button class="btn-rollback" data-rollback="${d.id}">โช Rollback</button>` : ''}
487
+ </div></div>`;
488
+ }).join('');
489
+
490
+ container.querySelectorAll('.btn-rollback').forEach(btn => {
491
+ btn.addEventListener('click', async () => {
492
+ const depId = btn.dataset.rollback;
493
+ if (!confirm('Rollback this deployment?')) return;
494
+ try {
495
+ await fetchJSON(`${API}/deployments/${depId}/rollback`, {
496
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),
497
+ });
498
+ await loadAll();
499
+ renderDeploys();
500
+ renderSidebar();
501
+ showToast('success', 'Deployment rolled back');
502
+ } catch (err) { showToast('error', err.message); }
503
+ });
504
+ });
505
+ }
506
+
507
+ // โ”€โ”€ Changelog Rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
508
+ function renderChangelog() {
509
+ const container = document.getElementById('changelog-list');
510
+ if (changelog.length === 0) {
511
+ container.innerHTML = '<div class="changelog-empty">No changelog entries yet. Add one with the button above or CLI: <code>codymaster changelog add</code></div>';
512
+ return;
513
+ }
514
+
515
+ container.innerHTML = changelog.map(c => {
516
+ const changesHtml = c.changes.length > 0 ? `<ul class="changelog-changes">${c.changes.map(ch => `<li>${esc(ch)}</li>`).join('')}</ul>` : '';
517
+ return `<div class="changelog-entry">
518
+ <div class="changelog-version">
519
+ <span class="changelog-version-badge">${esc(c.version)}</span>
520
+ <span class="changelog-title">${esc(c.title)}</span>
521
+ <span class="changelog-date">${formatTimeAgo(c.createdAt)}</span>
522
+ </div>
523
+ ${changesHtml}
524
+ </div>`;
525
+ }).join('');
526
+ }
527
+
528
+ // โ”€โ”€ Drag & Drop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
529
+ function handleDragStart(e) {
530
+ isDragging = true;
531
+ draggedTaskId = e.currentTarget.dataset.taskId;
532
+ // Store the source column for validation
533
+ const sourceTask = tasks.find(t => t.id === draggedTaskId);
534
+ e.currentTarget.dataset.sourceColumn = sourceTask ? sourceTask.column : '';
535
+ e.currentTarget.classList.add('dragging');
536
+ e.dataTransfer.effectAllowed = 'move';
537
+ e.dataTransfer.setData('text/plain', draggedTaskId);
538
+ e.dataTransfer.setData('source-column', sourceTask ? sourceTask.column : '');
539
+ requestAnimationFrame(() => { e.currentTarget.style.opacity = '0.4'; });
540
+ }
541
+ function handleDragEnd(e) {
542
+ isDragging = false;
543
+ e.currentTarget.classList.remove('dragging'); e.currentTarget.style.opacity = '';
544
+ draggedTaskId = null;
545
+ document.querySelectorAll('.column').forEach(c => c.classList.remove('drag-over', 'drag-blocked'));
546
+ document.querySelectorAll('.drop-placeholder').forEach(el => el.remove());
547
+ }
548
+
549
+ // Allow all valid transitions
550
+ function isDropAllowed(sourceColumn, targetColumn) {
551
+ if (sourceColumn === targetColumn) return true; // Reorder within same column
552
+ const allowed = VALID_TRANSITIONS[sourceColumn] || [];
553
+ return allowed.includes(targetColumn);
554
+ }
555
+
556
+ Object.keys(columns).forEach(colName => {
557
+ const list = columns[colName];
558
+ const column = list.closest('.column');
559
+
560
+ list.addEventListener('dragover', e => {
561
+ e.preventDefault();
562
+ // Check if drop is allowed
563
+ const sourceTask = tasks.find(t => t.id === draggedTaskId);
564
+ const sourceCol = sourceTask ? sourceTask.column : '';
565
+ const allowed = isDropAllowed(sourceCol, colName);
566
+
567
+ if (allowed) {
568
+ e.dataTransfer.dropEffect = 'move';
569
+ column.classList.add('drag-over');
570
+ column.classList.remove('drag-blocked');
571
+ if (!list.querySelector('.drop-placeholder')) { const ph = document.createElement('div'); ph.className = 'drop-placeholder'; list.appendChild(ph); }
572
+ const after = getDragAfterElement(list, e.clientY);
573
+ const ph = list.querySelector('.drop-placeholder');
574
+ if (after) list.insertBefore(ph, after); else list.appendChild(ph);
575
+ } else {
576
+ e.dataTransfer.dropEffect = 'none';
577
+ column.classList.add('drag-blocked');
578
+ column.classList.remove('drag-over');
579
+ }
580
+ });
581
+
582
+ list.addEventListener('dragleave', e => {
583
+ if (!column.contains(e.relatedTarget)) {
584
+ column.classList.remove('drag-over', 'drag-blocked');
585
+ const ph = list.querySelector('.drop-placeholder'); if (ph) ph.remove();
586
+ }
587
+ });
588
+
589
+ list.addEventListener('drop', async e => {
590
+ e.preventDefault(); column.classList.remove('drag-over', 'drag-blocked');
591
+ const ph = list.querySelector('.drop-placeholder');
592
+ // Save taskId locally โ€” handleDragEnd will null draggedTaskId before async completes
593
+ const taskId = draggedTaskId;
594
+ if (!taskId) return;
595
+
596
+ // Validate drop
597
+ const sourceTask = tasks.find(t => t.id === taskId);
598
+ const sourceCol = sourceTask ? sourceTask.column : '';
599
+ if (!isDropAllowed(sourceCol, colName)) {
600
+ if (ph) ph.remove();
601
+ const allowed = (VALID_TRANSITIONS[sourceCol] || []).join(', ');
602
+ showToast('error', `Cannot move from ${sourceCol} โ†’ ${colName}. Allowed: ${allowed}`);
603
+ return;
604
+ }
605
+
606
+ let newOrder = 0;
607
+ if (ph) {
608
+ newOrder = [...list.children].slice(0, [...list.children].indexOf(ph)).filter(el => el.classList.contains('task-card') && el.dataset.taskId !== taskId).length;
609
+ ph.remove();
610
+ }
611
+
612
+ const isMovingToInProgress = sourceCol === 'backlog' && colName === 'in-progress';
613
+ const isCrossColumn = sourceCol !== colName;
614
+
615
+ try {
616
+ if (isCrossColumn) {
617
+ // Use transition API for cross-column moves (validated)
618
+ await fetchJSON(`${API}/tasks/${taskId}/transition`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ column: colName }) });
619
+ } else {
620
+ // Use move API for reorder within same column
621
+ await fetchJSON(`${API}/tasks/${taskId}/move`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ column: colName, order: newOrder }) });
622
+ }
623
+ await loadAll(); renderBoard(); renderSidebar();
624
+
625
+ if (isMovingToInProgress) {
626
+ // Auto-dispatch if task has an agent assigned
627
+ const movedTask = tasks.find(t => t.id === taskId);
628
+ if (movedTask && movedTask.agent && movedTask.agent !== 'manual') {
629
+ showToast('info', 'โšก Starting dispatch...');
630
+ try {
631
+ const forceParam = movedTask.dispatchStatus === 'dispatched' ? '?force=true' : '';
632
+ const res = await fetchJSON(`${API}/tasks/${taskId}/dispatch${forceParam}`, {
633
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),
634
+ });
635
+ await loadAll(); renderBoard(); renderSidebar();
636
+ openDispatchModal(res);
637
+ } catch (dispatchErr) {
638
+ showToast('error', 'Dispatch failed: ' + dispatchErr.message);
639
+ }
640
+ } else if (movedTask && !movedTask.agent) {
641
+ showToast('success', 'Task moved to In Progress (no agent assigned โ€” dispatch skipped)');
642
+ } else {
643
+ showToast('success', 'Task moved to In Progress');
644
+ }
645
+ } else if (isCrossColumn) {
646
+ showToast('success', `Task moved: ${sourceCol} โ†’ ${colName}`);
647
+ } else {
648
+ showToast('success', 'Task reordered');
649
+ }
650
+ } catch (err) { showToast('error', err.message); }
651
+ });
652
+ });
653
+
654
+ function getDragAfterElement(list, y) {
655
+ const cards = [...list.querySelectorAll('.task-card:not(.dragging)')];
656
+ let closest = null, closestOffset = Number.NEGATIVE_INFINITY;
657
+ cards.forEach(card => { const box = card.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closestOffset) { closestOffset = offset; closest = card; } });
658
+ return closest;
659
+ }
660
+
661
+ // โ”€โ”€ Task Modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
662
+ function openAddModal() {
663
+ modalTitle.textContent = 'New Task'; btnSubmit.textContent = 'Create Task';
664
+ formId.value = ''; taskForm.reset();
665
+ formPriority.value = 'medium'; formColumn.value = 'backlog'; formAgent.value = ''; formSkill.value = '';
666
+ modalOverlay.classList.add('active');
667
+ setModalOpen(true);
668
+ setTimeout(() => formTitle.focus(), 200);
669
+ }
670
+ function openEditModal(task) {
671
+ modalTitle.textContent = 'Edit Task'; btnSubmit.textContent = 'Save Changes';
672
+ formId.value = task.id; formTitle.value = task.title; formDescription.value = task.description;
673
+ formPriority.value = task.priority; formColumn.value = task.column;
674
+ formAgent.value = task.agent || ''; formSkill.value = task.skill || '';
675
+ modalOverlay.classList.add('active');
676
+ setTimeout(() => formTitle.focus(), 200);
677
+ }
678
+ function closeModal() { modalOverlay.classList.remove('active'); setModalOpen(false); }
679
+
680
+ // โ”€โ”€ Project Modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
681
+
682
+
683
+ // โ”€โ”€ Deploy Modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
684
+ function openDeployModal() { deployForm.reset(); document.getElementById('deploy-branch').value = 'main'; deployModalOverlay.classList.add('active'); setModalOpen(true); }
685
+ function closeDeployModal() { deployModalOverlay.classList.remove('active'); setModalOpen(false); }
686
+
687
+ // โ”€โ”€ Changelog Modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
688
+ function openChangelogModal() { changelogForm.reset(); changelogModalOverlay.classList.add('active'); setModalOpen(true); }
689
+ function closeChangelogModal() { changelogModalOverlay.classList.remove('active'); setModalOpen(false); }
690
+
691
+ // โ”€โ”€ Delete Modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
692
+ let deleteTaskId = null;
693
+ function openDeleteModal(task) { deleteTaskId = task.id; deleteTaskName.textContent = task.title; deleteOverlay.classList.add('active'); }
694
+ function closeDeleteModal() { deleteOverlay.classList.remove('active'); deleteTaskId = null; }
695
+
696
+ // โ”€โ”€ Event Handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
697
+ function setupEventListeners() {
698
+ const btnAddTask = document.getElementById('btn-add-task');
699
+ if (btnAddTask) btnAddTask.addEventListener('click', openAddModal);
700
+
701
+ const sidebarRefreshBtn = document.getElementById('btn-sidebar-refresh');
702
+ if (sidebarRefreshBtn) sidebarRefreshBtn.addEventListener('click', () => refreshData(false));
703
+
704
+ const newDeployBtn = document.getElementById('btn-new-deploy');
705
+ if (newDeployBtn) newDeployBtn.addEventListener('click', openDeployModal);
706
+
707
+ const newChangelogBtn = document.getElementById('btn-new-changelog');
708
+ if (newChangelogBtn) newChangelogBtn.addEventListener('click', openChangelogModal);
709
+
710
+ if (refreshBtn) refreshBtn.addEventListener('click', () => refreshData(false));
711
+ if (autoRefreshBtn) autoRefreshBtn.addEventListener('click', toggleAutoRefresh);
712
+
713
+ // Sidebar toggles
714
+ const sbToggleBtn = document.getElementById('sidebar-toggle');
715
+ if (sbToggleBtn) {
716
+ sbToggleBtn.addEventListener('click', () => {
717
+ const sb = document.getElementById('sidebar');
718
+ if (sb) sb.classList.toggle('collapsed');
719
+ });
720
+ }
721
+
722
+ const sbCloseBtn = document.getElementById('sidebar-close');
723
+ if (sbCloseBtn) {
724
+ sbCloseBtn.addEventListener('click', () => {
725
+ const sb = document.getElementById('sidebar');
726
+ if (sb) sb.classList.add('collapsed');
727
+ });
728
+ }
729
+
730
+ // Auto collapse on small screens
731
+ if (window.innerWidth <= 900) {
732
+ document.getElementById('sidebar')?.classList.add('collapsed');
733
+ }
734
+
735
+ // Theme toggle
736
+ const themeBtn = document.getElementById('theme-toggle');
737
+ if (themeBtn) {
738
+ themeBtn.addEventListener('click', () => {
739
+ const current = getEffectiveTheme();
740
+ const next = current === 'dark' ? 'light' : 'dark';
741
+ localStorage.setItem(THEME_KEY, next);
742
+ applyTheme(next);
743
+ });
744
+ }
745
+
746
+ // Close modals
747
+ const modalClose = document.getElementById('modal-close');
748
+ if (modalClose) modalClose.addEventListener('click', closeModal);
749
+
750
+ const cancelBtn = document.getElementById('btn-cancel');
751
+ if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
752
+
753
+ if (modalOverlay) {
754
+ modalOverlay.addEventListener('click', e => { if (e.target === modalOverlay) closeModal(); });
755
+ }
756
+
757
+ const deployClose = document.getElementById('deploy-modal-close');
758
+ if (deployClose) deployClose.addEventListener('click', closeDeployModal);
759
+
760
+ const deployCancel = document.getElementById('deploy-cancel');
761
+ if (deployCancel) deployCancel.addEventListener('click', closeDeployModal);
762
+
763
+ if (deployModalOverlay) {
764
+ deployModalOverlay.addEventListener('click', e => { if (e.target === deployModalOverlay) closeDeployModal(); });
765
+ }
766
+
767
+ const changelogClose = document.getElementById('cl-modal-close') || document.getElementById('changelog-modal-close');
768
+ if (changelogClose) changelogClose.addEventListener('click', closeChangelogModal);
769
+
770
+ const changelogCancel = document.getElementById('cl-cancel') || document.getElementById('changelog-cancel');
771
+ if (changelogCancel) changelogCancel.addEventListener('click', closeChangelogModal);
772
+
773
+ if (changelogModalOverlay) {
774
+ changelogModalOverlay.addEventListener('click', e => { if (e.target === changelogModalOverlay) closeChangelogModal(); });
775
+ }
776
+
777
+ const deleteClose = document.getElementById('delete-close') || document.getElementById('delete-modal-close');
778
+ if (deleteClose) deleteClose.addEventListener('click', closeDeleteModal);
779
+
780
+ const deleteCancel = document.getElementById('delete-cancel');
781
+ if (deleteCancel) deleteCancel.addEventListener('click', closeDeleteModal);
782
+
783
+ if (deleteOverlay) {
784
+ deleteOverlay.addEventListener('click', e => { if (e.target === deleteOverlay) closeDeleteModal(); });
785
+ }
786
+
787
+ if (deleteConfirm) {
788
+ deleteConfirm.addEventListener('click', async () => {
789
+ if (!deleteTaskId) return;
790
+ try {
791
+ await fetchJSON(`${API}/tasks/${deleteTaskId}`, { method: 'DELETE' });
792
+ tasks = tasks.filter(t => t.id !== deleteTaskId);
793
+ renderBoard(); renderSidebar(); closeDeleteModal();
794
+ showToast('success', 'Task deleted');
795
+ } catch (err) { showToast('error', err.message); }
796
+ });
797
+ }
798
+
799
+ // Dispatch
800
+ if (dispatchClose) dispatchClose.addEventListener('click', closeDispatchModal);
801
+ if (dispatchOverlay) {
802
+ dispatchOverlay.addEventListener('click', e => { if (e.target === dispatchOverlay) closeDispatchModal(); });
803
+ }
804
+ if (copyPromptBtn) {
805
+ copyPromptBtn.addEventListener('click', () => {
806
+ navigator.clipboard.writeText(dispatchPrompt.textContent);
807
+ showToast('success', 'Prompt copied');
808
+ });
809
+ }
810
+ if (copyCommandBtn) {
811
+ copyCommandBtn.addEventListener('click', () => {
812
+ navigator.clipboard.writeText(dispatchCommand.textContent);
813
+ showToast('success', 'Command copied');
814
+ });
815
+ }
816
+ if (dispatchDoneBtn) dispatchDoneBtn.addEventListener('click', closeDispatchModal);
817
+
818
+ // Form submission
819
+ const taskFormEl = document.getElementById('task-form');
820
+ if (taskFormEl) {
821
+ taskFormEl.addEventListener('submit', async e => {
822
+ e.preventDefault();
823
+ const title = formTitle.value.trim(); if (!title) return;
824
+ const data = {
825
+ title, description: formDescription.value.trim(),
826
+ priority: formPriority.value, column: formColumn.value,
827
+ agent: formAgent.value, skill: formSkill.value,
828
+ projectId: selectedProjectId || undefined,
829
+ };
830
+ try {
831
+ if (formId.value) {
832
+ await fetchJSON(`${API}/tasks/${formId.value}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
833
+ const et = tasks.find(t => t.id === formId.value);
834
+ if (et && et.column !== data.column) {
835
+ await fetchJSON(`${API}/tasks/${formId.value}/move`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ column: data.column, order: 0 }) });
836
+ }
837
+ showToast('success', 'Task updated');
838
+ } else {
839
+ await fetchJSON(`${API}/tasks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
840
+ showToast('success', 'Task created');
841
+ }
842
+ await loadAll(); renderBoard(); renderSidebar(); closeModal();
843
+ } catch (err) { showToast('error', err.message); }
844
+ });
845
+ }
846
+
847
+ const deployFormEl = document.getElementById('deploy-form');
848
+ if (deployFormEl) {
849
+ deployFormEl.addEventListener('submit', async e => {
850
+ e.preventDefault();
851
+ const pid = selectedProjectId || (projects.length > 0 ? projects[0].id : '');
852
+ try {
853
+ await fetchJSON(`${API}/deployments`, {
854
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
855
+ body: JSON.stringify({
856
+ projectId: pid, env: document.getElementById('deploy-env').value,
857
+ message: document.getElementById('deploy-message').value.trim() || `Deploy to ${document.getElementById('deploy-env').value}`,
858
+ commit: document.getElementById('deploy-commit').value.trim(),
859
+ branch: document.getElementById('deploy-branch').value.trim() || 'main',
860
+ }),
861
+ });
862
+ await loadAll(); renderDeploys(); renderSidebar(); closeDeployModal();
863
+ showToast('success', 'Deployment recorded');
864
+ } catch (err) { showToast('error', err.message); }
865
+ });
866
+ }
867
+
868
+ const changelogFormEl = document.getElementById('changelog-form');
869
+ if (changelogFormEl) {
870
+ changelogFormEl.addEventListener('submit', async e => {
871
+ e.preventDefault();
872
+ const version = document.getElementById('cl-version')?.value.trim() || document.getElementById('changelog-version')?.value.trim();
873
+ const title = document.getElementById('cl-title')?.value.trim() || 'Release';
874
+ if (!version) return;
875
+ const changes = document.getElementById('cl-changes')?.value.split('\n').map(l => l.trim()).filter(Boolean) || [];
876
+ const pid = selectedProjectId || (projects.length > 0 ? projects[0].id : '');
877
+ try {
878
+ await fetchJSON(`${API}/changelog`, {
879
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
880
+ body: JSON.stringify({ projectId: pid, version, title, changes }),
881
+ });
882
+ await loadAll(); renderChangelog(); closeChangelogModal();
883
+ showToast('success', 'Changelog entry added');
884
+ } catch (err) { showToast('error', err.message); }
885
+ });
886
+ }
887
+
888
+ // Keyboard shortcuts
889
+ document.addEventListener('keydown', e => {
890
+ if (e.key === 'Escape') { closeModal(); closeDeployModal(); closeChangelogModal(); closeDeleteModal(); }
891
+ if ((e.ctrlKey || e.metaKey) && e.key === 'n') { e.preventDefault(); openAddModal(); }
892
+ if ((e.ctrlKey || e.metaKey) && e.key === 'r' && !e.shiftKey) { e.preventDefault(); refreshData(); }
893
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 't' || e.key === 'T')) {
894
+ e.preventDefault();
895
+ const current = getEffectiveTheme();
896
+ const next = current === 'dark' ? 'light' : 'dark';
897
+ localStorage.setItem(THEME_KEY, next);
898
+ applyTheme(next);
899
+ }
900
+ });
901
+
902
+ // Header Dropdown Toggle
903
+ const btnMoreMenu = document.getElementById('btn-more-menu');
904
+ const headerActions = document.getElementById('header-actions');
905
+ if (btnMoreMenu && headerActions) {
906
+ btnMoreMenu.addEventListener('click', (e) => {
907
+ e.stopPropagation();
908
+ headerActions.classList.toggle('active');
909
+ });
910
+ document.addEventListener('click', (e) => {
911
+ if (!headerActions.contains(e.target) && !btnMoreMenu.contains(e.target)) {
912
+ headerActions.classList.remove('active');
913
+ }
914
+ });
915
+ }
916
+ }
917
+
918
+ // โ”€โ”€ Toast System โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
919
+ function showToast(type, message) {
920
+ const icons = { success: 'โœ…', error: 'โŒ', info: 'โ„น๏ธ' };
921
+ const toast = document.createElement('div');
922
+ toast.className = `toast toast-${type}`;
923
+ toast.innerHTML = `<span class="toast-icon">${icons[type] || '๐Ÿ“Œ'}</span><span>${esc(message)}</span>`;
924
+ toastContainer.appendChild(toast);
925
+ setTimeout(() => { toast.classList.add('toast-out'); toast.addEventListener('animationend', () => toast.remove()); }, 2800);
926
+ }
927
+
928
+ // โ”€โ”€ Utilities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
929
+ function esc(str) { const d = document.createElement('div'); d.textContent = str; return d.innerHTML; }
930
+ function formatTimeAgo(dateStr) {
931
+ const ms = Date.now() - new Date(dateStr).getTime();
932
+ const m = Math.floor(ms / 60000), h = Math.floor(ms / 3600000), d = Math.floor(ms / 86400000);
933
+ if (m < 1) return 'just now'; if (m < 60) return `${m}m ago`; if (h < 24) return `${h}h ago`;
934
+ if (d < 7) return `${d}d ago`;
935
+ return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
936
+ }
937
+
938
+ // โ”€โ”€ Quick Transition Handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
939
+ async function handleTransition(taskId, targetColumn) {
940
+ try {
941
+ await fetchJSON(`${API}/tasks/${taskId}/transition`, {
942
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
943
+ body: JSON.stringify({ column: targetColumn }),
944
+ });
945
+ await loadAll(); renderBoard(); renderSidebar();
946
+
947
+ // Auto-dispatch when moving to in-progress
948
+ if (targetColumn === 'in-progress') {
949
+ const movedTask = tasks.find(t => t.id === taskId);
950
+ if (movedTask && movedTask.agent && movedTask.agent !== 'manual') {
951
+ showToast('info', 'โšก Starting dispatch...');
952
+ try {
953
+ const forceParam = movedTask.dispatchStatus === 'dispatched' ? '?force=true' : '';
954
+ const res = await fetchJSON(`${API}/tasks/${taskId}/dispatch${forceParam}`, {
955
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),
956
+ });
957
+ await loadAll(); renderBoard(); renderSidebar();
958
+ openDispatchModal(res);
959
+ } catch (dispatchErr) {
960
+ showToast('error', 'Dispatch failed: ' + dispatchErr.message);
961
+ }
962
+ return;
963
+ }
964
+ }
965
+ showToast('success', `Task moved to ${targetColumn}`);
966
+ } catch (err) { showToast('error', err.message); }
967
+ }
968
+
969
+ // โ”€โ”€ Stuck Tasks Banner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
970
+ async function renderStuckBanner() {
971
+ let bannerEl = document.getElementById('stuck-banner');
972
+ try {
973
+ const pq = selectedProjectId ? `?projectId=${selectedProjectId}` : '';
974
+ const stuckTasks = await fetchJSON(`${API}/tasks/stuck${pq}`);
975
+ if (!stuckTasks || stuckTasks.length === 0) {
976
+ if (bannerEl) bannerEl.remove();
977
+ return;
978
+ }
979
+
980
+ if (!bannerEl) {
981
+ bannerEl = document.createElement('div');
982
+ bannerEl.id = 'stuck-banner';
983
+ const kanbanPanel = document.getElementById('panel-kanban');
984
+ if (kanbanPanel) kanbanPanel.insertBefore(bannerEl, kanbanPanel.querySelector('.board'));
985
+ }
986
+
987
+ const taskNames = stuckTasks.slice(0, 3).map(t => `"${esc(t.title)}"`).join(', ');
988
+ const extra = stuckTasks.length > 3 ? ` +${stuckTasks.length - 3} more` : '';
989
+ bannerEl.innerHTML = `<div class="stuck-banner-content">
990
+ <span class="stuck-banner-icon">โš ๏ธ</span>
991
+ <span class="stuck-banner-text"><strong>${stuckTasks.length} task${stuckTasks.length > 1 ? 's' : ''} stuck</strong> in progress: ${taskNames}${extra}</span>
992
+ <div class="stuck-banner-actions">
993
+ <button class="stuck-btn review" data-action="review">โ†’ Move to Review</button>
994
+ <button class="stuck-btn done" data-action="done">โœ“ Mark Done</button>
995
+ <button class="stuck-btn backlog" data-action="backlog">โ† Back to Backlog</button>
996
+ </div>
997
+ </div>`;
998
+
999
+ bannerEl.querySelectorAll('.stuck-btn').forEach(btn => {
1000
+ btn.addEventListener('click', async () => {
1001
+ const targetCol = btn.dataset.action;
1002
+ const ids = stuckTasks.map(t => t.id);
1003
+ try {
1004
+ await fetchJSON(`${API}/tasks/bulk-transition`, {
1005
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1006
+ body: JSON.stringify({ taskIds: ids, column: targetCol, reason: 'Bulk action from stuck banner' }),
1007
+ });
1008
+ await loadAll(); renderBoard(); renderSidebar();
1009
+ showToast('success', `${ids.length} tasks moved to ${targetCol}`);
1010
+ } catch (err) { showToast('error', err.message); }
1011
+ });
1012
+ });
1013
+ } catch { /* silently fail */ }
1014
+ }
1015
+
1016
+ // โ”€โ”€ Dispatch Handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1017
+ async function handleDispatch(task) {
1018
+ const isRedispatch = task.dispatchStatus === 'dispatched';
1019
+ if (isRedispatch) {
1020
+ if (!confirm(`Task "${task.title}" was already dispatched. Re-dispatch?`)) return;
1021
+ }
1022
+ const forceParam = isRedispatch ? '?force=true' : '';
1023
+ try {
1024
+ const result = await fetchJSON(`${API}/tasks/${task.id}/dispatch${forceParam}`, {
1025
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),
1026
+ });
1027
+ await loadAll(); renderBoard(); renderSidebar();
1028
+ openDispatchModal(result);
1029
+ } catch (err) {
1030
+ showToast('error', 'Dispatch failed: ' + err.message);
1031
+ }
1032
+ }
1033
+
1034
+ // โ”€โ”€ Dispatch Modal Logic โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1035
+ function openDispatchModal(result) {
1036
+ if (!result || !result.prompt) return;
1037
+ dispatchPrompt.textContent = result.prompt;
1038
+ dispatchCommand.textContent = result.cliCommand;
1039
+ dispatchOverlay.classList.add('active');
1040
+ }
1041
+
1042
+ function closeDispatchModal() {
1043
+ dispatchOverlay.classList.remove('active');
1044
+ }
1045
+
1046
+ // Event listeners for Dispatch Modal
1047
+ if (dispatchClose) dispatchClose.addEventListener('click', closeDispatchModal);
1048
+ if (dispatchDoneBtn) dispatchDoneBtn.addEventListener('click', closeDispatchModal);
1049
+
1050
+ if (copyPromptBtn) copyPromptBtn.addEventListener('click', () => {
1051
+ navigator.clipboard.writeText(dispatchPrompt.textContent)
1052
+ .then(() => showToast('success', 'Prompt copied to clipboard!'))
1053
+ .catch(err => showToast('error', 'Failed to copy: ' + err));
1054
+ });
1055
+
1056
+ if (copyCommandBtn) copyCommandBtn.addEventListener('click', () => {
1057
+ navigator.clipboard.writeText(dispatchCommand.textContent)
1058
+ .then(() => showToast('success', 'CLI command copied!'))
1059
+ .catch(err => showToast('error', 'Failed to copy: ' + err));
1060
+ });
1061
+
1062
+ // โ”€โ”€ Brain Tab Rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1063
+ let brainData = { continuity: null, learnings: [], decisions: [] };
1064
+ let brainSearchQuery = '';
1065
+
1066
+ async function loadBrainData() {
1067
+ if (!selectedProjectId) {
1068
+ // Load from first project with .cm/
1069
+ for (const p of projects) {
1070
+ try {
1071
+ const status = await fetchJSON(`${API}/continuity/${p.id}`);
1072
+ if (status && status.initialized) {
1073
+ const [learnings, decisions] = await Promise.all([
1074
+ fetchJSON(`${API}/learnings/${p.id}`),
1075
+ fetchJSON(`${API}/decisions/${p.id}`),
1076
+ ]);
1077
+ brainData = { continuity: status, learnings: learnings || [], decisions: decisions || [], projectId: p.id, projectName: p.name };
1078
+ return;
1079
+ }
1080
+ } catch { /* skip */ }
1081
+ }
1082
+ brainData = { continuity: null, learnings: [], decisions: [], projectId: null, projectName: null };
1083
+ return;
1084
+ }
1085
+ try {
1086
+ const [status, learnings, decisions] = await Promise.all([
1087
+ fetchJSON(`${API}/continuity/${selectedProjectId}`),
1088
+ fetchJSON(`${API}/learnings/${selectedProjectId}`),
1089
+ fetchJSON(`${API}/decisions/${selectedProjectId}`),
1090
+ ]);
1091
+ const proj = projects.find(p => p.id === selectedProjectId);
1092
+ brainData = { continuity: status, learnings: learnings || [], decisions: decisions || [], projectId: selectedProjectId, projectName: proj ? proj.name : 'Unknown' };
1093
+ } catch {
1094
+ brainData = { continuity: null, learnings: [], decisions: [], projectId: selectedProjectId, projectName: null };
1095
+ }
1096
+ }
1097
+
1098
+ async function renderBrain() {
1099
+ await loadBrainData();
1100
+ const { continuity, learnings, decisions, projectId, projectName } = brainData;
1101
+ const statsEl = document.getElementById('brain-stats');
1102
+ const contEl = document.getElementById('brain-continuity-content');
1103
+ const learnEl = document.getElementById('brain-learnings-list');
1104
+ const decEl = document.getElementById('brain-decisions-list');
1105
+ const searchEl = document.getElementById('brain-search');
1106
+
1107
+ if (!continuity || !continuity.initialized) {
1108
+ statsEl.innerHTML = '';
1109
+ contEl.innerHTML = `<div class="brain-empty"><div class="brain-empty-icon">๐Ÿง </div><div>Working memory not initialized for this project.</div>${projectId ? `<button class="brain-init-btn" data-init-project="${projectId}">โšก Initialize Memory</button>` : '<div style="margin-top:8px;font-size:12px">Select a project from the sidebar first.</div>'}</div>`;
1110
+ learnEl.innerHTML = '';
1111
+ decEl.innerHTML = '';
1112
+ // Wire init button
1113
+ const initBtn = contEl.querySelector('.brain-init-btn');
1114
+ if (initBtn) {
1115
+ initBtn.addEventListener('click', async () => {
1116
+ try {
1117
+ await fetch(`${API}/continuity/${projectId}/init`, { method: 'POST' });
1118
+ showToast('success', 'โœ… Memory initialized!');
1119
+ renderBrain();
1120
+ } catch { showToast('error', 'Failed to initialize memory'); }
1121
+ });
1122
+ }
1123
+ return;
1124
+ }
1125
+
1126
+ // Stats cards
1127
+ const phase = continuity.phase || 'idle';
1128
+ const phaseClass = 'phase-' + phase;
1129
+ statsEl.innerHTML = `
1130
+ <div class="brain-stat-card stat-learnings">
1131
+ <div class="brain-stat-label">Learnings</div>
1132
+ <div class="brain-stat-value">${learnings.length}</div>
1133
+ <div class="brain-stat-detail">Mistakes captured</div>
1134
+ </div>
1135
+ <div class="brain-stat-card stat-decisions">
1136
+ <div class="brain-stat-label">Decisions</div>
1137
+ <div class="brain-stat-value">${decisions.length}</div>
1138
+ <div class="brain-stat-detail">Architecture choices</div>
1139
+ </div>
1140
+ <div class="brain-stat-card stat-phase">
1141
+ <div class="brain-stat-label">Phase</div>
1142
+ <div class="brain-stat-value ${phaseClass}" style="font-size:20px">${phase.charAt(0).toUpperCase() + phase.slice(1)}</div>
1143
+ <div class="brain-stat-detail">${projectName || 'No project'}</div>
1144
+ </div>
1145
+ <div class="brain-stat-card stat-updated">
1146
+ <div class="brain-stat-label">Last Updated</div>
1147
+ <div class="brain-stat-value" style="font-size:16px">${continuity.lastUpdated ? formatTimeAgo(continuity.lastUpdated) : 'Never'}</div>
1148
+ <div class="brain-stat-detail">Iteration ${continuity.iteration || 0}</div>
1149
+ </div>`;
1150
+
1151
+ // Continuity status
1152
+ contEl.innerHTML = `<div class="brain-continuity-grid">
1153
+ <div class="brain-continuity-item"><div class="brain-continuity-label">Project</div><div class="brain-continuity-value">${esc(continuity.project || 'โ€”')}</div></div>
1154
+ <div class="brain-continuity-item"><div class="brain-continuity-label">Active Goal</div><div class="brain-continuity-value">${esc(continuity.activeGoal || 'No active goal')}</div></div>
1155
+ <div class="brain-continuity-item"><div class="brain-continuity-label">Current Task</div><div class="brain-continuity-value">${esc(continuity.currentTask || 'No active task')}</div></div>
1156
+ <div class="brain-continuity-item"><div class="brain-continuity-label">Blockers</div><div class="brain-continuity-value">${continuity.blockerCount > 0 ? `๐Ÿšง ${continuity.blockerCount} blocker(s)` : 'โœ… No blockers'}</div></div>
1157
+ <div class="brain-continuity-item"><div class="brain-continuity-label">Completed</div><div class="brain-continuity-value">${continuity.completedCount || 0} items</div></div>
1158
+ <div class="brain-continuity-item"><div class="brain-continuity-label">Iteration</div><div class="brain-continuity-value">#${continuity.iteration || 0}</div></div>
1159
+ </div>`;
1160
+
1161
+ // Search filter
1162
+ const query = brainSearchQuery.toLowerCase();
1163
+ const filteredLearnings = query
1164
+ ? learnings.filter(l => (l.whatFailed || '').toLowerCase().includes(query) || (l.whyFailed || '').toLowerCase().includes(query) || (l.howToPrevent || '').toLowerCase().includes(query))
1165
+ : learnings;
1166
+
1167
+ // Learnings
1168
+ if (filteredLearnings.length === 0) {
1169
+ learnEl.innerHTML = `<div class="brain-empty"><div class="brain-empty-icon">๐ŸŽ‰</div>${query ? 'No learnings match your search.' : 'No learnings captured yet. Great start!'}</div>`;
1170
+ } else {
1171
+ learnEl.innerHTML = filteredLearnings.slice().reverse().map(l => `
1172
+ <div class="brain-learning-card" data-learning-id="${l.id}">
1173
+ <div class="brain-learning-header">
1174
+ <div class="brain-learning-what">${esc(l.whatFailed)}</div>
1175
+ <button class="brain-delete-btn" data-delete-learning="${l.id}" title="Delete">๐Ÿ—‘๏ธ</button>
1176
+ </div>
1177
+ <div class="brain-learning-body">
1178
+ <div class="brain-learning-why">${esc(l.whyFailed || '')}</div>
1179
+ <div class="brain-learning-fix">${esc(l.howToPrevent || '')}</div>
1180
+ <div class="brain-learning-meta">
1181
+ <span>${l.agent || 'unknown agent'}</span>
1182
+ <span>${l.timestamp ? formatTimeAgo(l.timestamp) : ''}</span>
1183
+ ${l.module ? `<span>๐Ÿ“ฆ ${esc(l.module)}</span>` : ''}
1184
+ </div>
1185
+ </div>
1186
+ </div>`).join('');
1187
+ }
1188
+
1189
+ // Decisions
1190
+ if (decisions.length === 0) {
1191
+ decEl.innerHTML = '<div class="brain-empty"><div class="brain-empty-icon">๐Ÿ“‹</div>No decisions recorded yet.</div>';
1192
+ } else {
1193
+ decEl.innerHTML = decisions.slice().reverse().map(d => `
1194
+ <div class="brain-decision-card" data-decision-id="${d.id}">
1195
+ <div class="brain-decision-header">
1196
+ <div class="brain-decision-what">${esc(d.decision)}</div>
1197
+ <button class="brain-delete-btn" data-delete-decision="${d.id}" title="Delete">๐Ÿ—‘๏ธ</button>
1198
+ </div>
1199
+ <div class="brain-decision-rationale">${esc(d.rationale || '')}</div>
1200
+ <div class="brain-decision-meta">
1201
+ <span>${d.agent || 'unknown'}</span>
1202
+ <span>${d.timestamp ? formatTimeAgo(d.timestamp) : ''}</span>
1203
+ </div>
1204
+ </div>`).join('');
1205
+ }
1206
+
1207
+ // Wire learning card expand/collapse
1208
+ learnEl.querySelectorAll('.brain-learning-card').forEach(card => {
1209
+ card.addEventListener('click', e => {
1210
+ if (e.target.closest('.brain-delete-btn')) return;
1211
+ card.classList.toggle('expanded');
1212
+ });
1213
+ });
1214
+
1215
+ // Wire delete learning buttons
1216
+ learnEl.querySelectorAll('[data-delete-learning]').forEach(btn => {
1217
+ btn.addEventListener('click', async e => {
1218
+ e.stopPropagation();
1219
+ const lid = btn.dataset.deleteLearning;
1220
+ if (!confirm('Delete this learning?')) return;
1221
+ try {
1222
+ await fetch(`${API}/learnings/${brainData.projectId}/${lid}`, { method: 'DELETE' });
1223
+ showToast('success', '๐Ÿงน Learning deleted');
1224
+ renderBrain();
1225
+ } catch { showToast('error', 'Failed to delete learning'); }
1226
+ });
1227
+ });
1228
+
1229
+ // Wire delete decision buttons
1230
+ decEl.querySelectorAll('[data-delete-decision]').forEach(btn => {
1231
+ btn.addEventListener('click', async e => {
1232
+ e.stopPropagation();
1233
+ const did = btn.dataset.deleteDecision;
1234
+ if (!confirm('Delete this decision?')) return;
1235
+ try {
1236
+ await fetch(`${API}/decisions/${brainData.projectId}/${did}`, { method: 'DELETE' });
1237
+ showToast('success', '๐Ÿงน Decision deleted');
1238
+ renderBrain();
1239
+ } catch { showToast('error', 'Failed to delete decision'); }
1240
+ });
1241
+ });
1242
+
1243
+ // Wire search
1244
+ if (searchEl && !searchEl._brainWired) {
1245
+ searchEl._brainWired = true;
1246
+ searchEl.addEventListener('input', e => {
1247
+ brainSearchQuery = e.target.value;
1248
+ renderBrain();
1249
+ });
1250
+ }
1251
+ }
1252
+
1253
+ // โ”€โ”€ Init โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1254
+ async function init() {
1255
+ try {
1256
+ await loadAll();
1257
+ renderSidebar();
1258
+ renderCurrentTab();
1259
+ setupEventListeners(); // Initialize listeners after first render
1260
+ lastSyncTime = Date.now();
1261
+ updateSyncStatus('synced');
1262
+ updateAutoRefreshUI();
1263
+ startAutoRefresh();
1264
+ } catch (err) {
1265
+ showToast('error', 'Failed to load: ' + err.message);
1266
+ updateSyncStatus('error');
1267
+ }
1268
+ }
1269
+ init();
1270
+ })();