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.
- package/CHANGELOG.md +50 -0
- package/README.md +285 -0
- package/adapters/antigravity.js +15 -0
- package/adapters/claude-code.js +17 -0
- package/adapters/cursor.js +16 -0
- package/commands/bootstrap.md +49 -0
- package/commands/build.md +48 -0
- package/commands/content.md +48 -0
- package/commands/continuity.md +60 -0
- package/commands/debug.md +51 -0
- package/commands/demo.md +96 -0
- package/commands/deploy.md +51 -0
- package/commands/plan.md +42 -0
- package/commands/review.md +55 -0
- package/commands/track.md +46 -0
- package/commands/ux.md +46 -0
- package/dist/agent-dispatch.js +161 -0
- package/dist/chains/builtin.js +85 -0
- package/dist/continuity.js +385 -0
- package/dist/dashboard.js +926 -0
- package/dist/data.js +122 -0
- package/dist/index.js +2434 -0
- package/dist/judge.js +252 -0
- package/dist/parallel-dispatch.js +359 -0
- package/dist/parallel-quality.js +172 -0
- package/dist/skill-chain.js +258 -0
- package/install.sh +513 -0
- package/package.json +79 -0
- package/skills/.content-factory-state.json +132 -0
- package/skills/.git 2/logs/refs/heads/main +1 -0
- package/skills/.git 2/logs/refs/remotes/origin/main +1 -0
- package/skills/.git 2/objects/02/fb0956734b5f8ba3f918b7defd04a89cfe0076 +0 -0
- package/skills/.git 2/objects/08/1e129d75dc6feac6c02037272e6bd1a04e3324 +0 -0
- package/skills/.git 2/objects/0c/5393416f3c5e01c9a655a802bff0dd52f76f0a +0 -0
- package/skills/.git 2/objects/10/0b9be46978a946a77188f68be725098a122001 +0 -0
- package/skills/.git 2/objects/10/cf041167fc9843610eb3d90259ef3396315fdc +0 -0
- package/skills/.git 2/objects/12/5e19538dd6e1338ffe74f6c4c165b00435bf48 +0 -0
- package/skills/.git 2/objects/16/a9b9d0088d5c1347628b45a2620b479d8ad57c +0 -0
- package/skills/.git 2/objects/17/8c2a9ef93c33ae4eec9d58e82321f9229843a1 +0 -0
- package/skills/.git 2/objects/25/397ae41d09104d763bdcac2695209d85cdea89 +0 -0
- package/skills/.git 2/objects/2f/a836b7947f2d458e1f639788bf4bb0983a3305 +0 -0
- package/skills/.git 2/objects/3a/baaaf0a1c0909c0828335791557125fba911e0 +0 -0
- package/skills/.git 2/objects/42/2924221b81f5ce3c4e4daac9a64a24f9b01f9a +0 -0
- package/skills/.git 2/objects/42/ec0ce707447dc11446a34c9995fb8533801731 +0 -0
- package/skills/.git 2/objects/46/e43ce92866d56ce74b1d750db307cfe6154a15 +0 -0
- package/skills/.git 2/objects/48/5e41b633c63f55b8277bcc59f44f67681f671a +0 -0
- package/skills/.git 2/objects/49/49c596a3a89fa240642acd95dd3258e261eb09 +0 -0
- package/skills/.git 2/objects/50/9d42d8412ef8eaf7f7e138476bac2e4d10ce60 +0 -0
- package/skills/.git 2/objects/55/0c8c389d981b463ef849aeb792d8be3ccb6ec8 +0 -0
- package/skills/.git 2/objects/5d/82d3b18410cdda3ace3677436f0cb599dbe2d2 +0 -0
- package/skills/.git 2/objects/60/0617c58e871a38b33bf29e282d132bb3c381ad +0 -0
- package/skills/.git 2/objects/6a/8369a99c687b7245c92ffaf0e0f0dab9014504 +0 -0
- package/skills/.git 2/objects/79/bea435d40ab531c1aaf6be0432c6a5b7aaed21 +0 -0
- package/skills/.git 2/objects/7e/5ebd79251c2f14e4aceb86c74b6b6daae6b500 +0 -0
- package/skills/.git 2/objects/81/98a822a60178d6d5023ddb3e222cddf048742e +0 -0
- package/skills/.git 2/objects/86/0a0e1943dfe53411d2e499a1f16f46a96ef758 +0 -0
- package/skills/.git 2/objects/86/971fb55fdc081fdbae52376f0f13e57a4e9b04 +0 -0
- package/skills/.git 2/objects/88/b89dd609a0a03f8d4fe8bfde20d5b8fc1d326d +0 -0
- package/skills/.git 2/objects/90/8737edb6b7809e32cc01590b4e08ba42a9d40d +0 -0
- package/skills/.git 2/objects/93/d5a8a9a7d4fb7f11491cb596a6880528725118 +0 -0
- package/skills/.git 2/objects/98/46a2ab81d0c3b3eb00ef88fc56989aa7e9f316 +0 -0
- package/skills/.git 2/objects/9b/d8dd1e49cf274eaf9c555f3ab39dce7af5715e +0 -0
- package/skills/.git 2/objects/a1/13329fb0cec96ae78b222d33a24c3b5bc7fa1f +0 -0
- package/skills/.git 2/objects/a9/e6effe626e8a3aea3a8fc3364b492191c6e7d0 +0 -0
- package/skills/.git 2/objects/ad/6de7e48d9782cca9353d1ff0aa1aab7fe1df85 +0 -0
- package/skills/.git 2/objects/af/54ae316f771ff692e299ffcd8bf2f06b413b59 +0 -0
- package/skills/.git 2/objects/b0/4cb8b0b00dad633e731c1472161419e738d674 +0 -0
- package/skills/.git 2/objects/b3/094abb0b9ed46419b269e4a4e36a459690e3b0 +0 -0
- package/skills/.git 2/objects/b9/435c5d4baac2cfc5c83009ddd27b46b60db5f1 +0 -0
- package/skills/.git 2/objects/ba/5da17dbaec5ec2dcfdfd126aead518d1171d5c +0 -0
- package/skills/.git 2/objects/c0/bf58703aa258ba5dd63083bebaec8f223d844c +0 -0
- package/skills/.git 2/objects/c4/701a34edf1fc1bad58ccc57bd03f9426acb59a +0 -0
- package/skills/.git 2/objects/c7/5ccce9a4e5cc74d9b3174550cf6d993ca43638 +0 -0
- package/skills/.git 2/objects/c7/710d59b5a35b0f1f0a0399386643a0bd94c929 +0 -0
- package/skills/.git 2/objects/d1/fe58237112e953e5fec52da22cf38e08be3df9 +5 -0
- package/skills/.git 2/objects/d2/2bbe9fd2f74c95bc5583e803f5e435f1e2cd86 +0 -0
- package/skills/.git 2/objects/d7/e72852ea2bff74581dbf247d400120086229f4 +0 -0
- package/skills/.git 2/objects/d8/d4c3b5553e4fd72807e1d4b49ef07d9ef3ac35 +0 -0
- package/skills/.git 2/objects/dc/75050c2876f6a02ae2a53a3c886f395b622977 +0 -0
- package/skills/.git 2/objects/ee/e8546f95acec500187c08a28a8b9ee02db0dec +0 -0
- package/skills/.git 2/objects/ef/263c059208b416c2146434f10cb2b9fabcba16 +0 -0
- package/skills/.git 2/objects/f3/ae597e84d9a59b88acd21c99bde2eaf686d785 +0 -0
- package/skills/.git 2/objects/f3/f6f5673c821d3d8e76fa267a9e882e7a5387ea +0 -0
- package/skills/.git 2/objects/f9/6e6d0ad02624dd11d5848594d056caef7a5e8b +0 -0
- package/skills/.git 2/objects/ff/278988fc1edf0db3abcf18de795f4cc0b4f3e1 +0 -0
- package/skills/.git 2/refs/heads/main +1 -0
- package/skills/.git 2/refs/remotes/origin/main +1 -0
- package/skills/.pytest_cache 2/v/cache/nodeids +76 -0
- package/skills/.pytest_cache 2/v/cache/stepwise +1 -0
- package/skills/_shared/helpers.md +123 -0
- package/skills/_shared/outputs-convention.md +24 -0
- package/skills/cm-ads-tracker/SKILL.md +109 -0
- package/skills/cm-ads-tracker/evals/evals.json +55 -0
- package/skills/cm-ads-tracker/references/gtm-architecture.md +321 -0
- package/skills/cm-ads-tracker/references/industry-events.md +294 -0
- package/skills/cm-ads-tracker/references/platforms-api.md +238 -0
- package/skills/cm-ads-tracker/templates/capi-payload.md +79 -0
- package/skills/cm-ads-tracker/templates/datalayer-push.js +104 -0
- package/skills/cm-ads-tracker/templates/gtm-variables.js +56 -0
- package/skills/cm-brainstorm-idea/SKILL.md +423 -0
- package/skills/cm-code-review/SKILL.md +151 -0
- package/skills/cm-content-factory/SKILL.md +416 -0
- package/skills/cm-continuity/SKILL.md +399 -0
- package/skills/cm-dashboard/SKILL.md +533 -0
- package/skills/cm-dashboard/ui/app.js +1270 -0
- package/skills/cm-dashboard/ui/index.html +206 -0
- package/skills/cm-dashboard/ui/style.css +440 -0
- package/skills/cm-debugging/SKILL.md +412 -0
- package/skills/cm-deep-search/SKILL.md +242 -0
- package/skills/cm-design-system/SKILL.md +97 -0
- package/skills/cm-design-system/resources/halo-modern.md +40 -0
- package/skills/cm-design-system/resources/lunaris-advanced.md +40 -0
- package/skills/cm-design-system/resources/nitro-enterprise.md +39 -0
- package/skills/cm-design-system/resources/shadcn-default.md +37 -0
- package/skills/cm-dockit/README.md +100 -0
- package/skills/cm-dockit/SKILL.md +302 -0
- package/skills/cm-dockit/index.html +443 -0
- package/skills/cm-dockit/package-lock.json +1850 -0
- package/skills/cm-dockit/package.json +14 -0
- package/skills/cm-dockit/prompts/analysis.md +34 -0
- package/skills/cm-dockit/prompts/api-reference.md +24 -0
- package/skills/cm-dockit/prompts/architecture.md +21 -0
- package/skills/cm-dockit/prompts/data-flow.md +20 -0
- package/skills/cm-dockit/prompts/database.md +21 -0
- package/skills/cm-dockit/prompts/deployment.md +22 -0
- package/skills/cm-dockit/prompts/flows.md +21 -0
- package/skills/cm-dockit/prompts/jtbd.md +20 -0
- package/skills/cm-dockit/prompts/personas.md +24 -0
- package/skills/cm-dockit/prompts/sop-modules.md +40 -0
- package/skills/cm-dockit/scripts/doc-gen.sh +121 -0
- package/skills/cm-dockit/scripts/dockit-dashboard.sh +142 -0
- package/skills/cm-dockit/scripts/dockit-runner.sh +607 -0
- package/skills/cm-dockit/scripts/dockit-task.sh +166 -0
- package/skills/cm-dockit/skills/analyze-codebase.md +174 -0
- package/skills/cm-dockit/skills/api-reference.md +237 -0
- package/skills/cm-dockit/skills/changelog-guide.md +195 -0
- package/skills/cm-dockit/skills/content-guidelines.md +190 -0
- package/skills/cm-dockit/skills/sop-guide.md +184 -0
- package/skills/cm-dockit/skills/tech-docs.md +287 -0
- package/skills/cm-dockit/templates/markdown/structure.md +60 -0
- package/skills/cm-dockit/templates/vitepress-premium/.vitepress/config.mts +110 -0
- package/skills/cm-dockit/templates/vitepress-premium/.vitepress/theme/custom.css +189 -0
- package/skills/cm-dockit/templates/vitepress-premium/.vitepress/theme/index.ts +4 -0
- package/skills/cm-dockit/templates/vitepress-premium/package.json +19 -0
- package/skills/cm-dockit/templates/vitepress-premium/tests/frontend.test.ts +45 -0
- package/skills/cm-dockit/tests/runner.test.ts +66 -0
- package/skills/cm-dockit/workflows/export-markdown.md +82 -0
- package/skills/cm-dockit/workflows/generate-docs.md +68 -0
- package/skills/cm-dockit/workflows/setup-vitepress.md +181 -0
- package/skills/cm-example/SKILL.md +26 -0
- package/skills/cm-execution/SKILL.md +268 -0
- package/skills/cm-git-worktrees/SKILL.md +164 -0
- package/skills/cm-how-it-work/SKILL.md +189 -0
- package/skills/cm-identity-guard/SKILL.md +412 -0
- package/skills/cm-jtbd/SKILL.md +98 -0
- package/skills/cm-planning/SKILL.md +130 -0
- package/skills/cm-project-bootstrap/SKILL.md +161 -0
- package/skills/cm-project-bootstrap/templates/AGENTS.md +42 -0
- package/skills/cm-project-bootstrap/templates/frontend-safety.test.js +51 -0
- package/skills/cm-project-bootstrap/templates/i18n-sync.test.js +38 -0
- package/skills/cm-project-bootstrap/templates/pr-template.md +12 -0
- package/skills/cm-project-bootstrap/templates/project-identity.json +29 -0
- package/skills/cm-project-bootstrap/templates/vitest.config.js +10 -0
- package/skills/cm-quality-gate/SKILL.md +218 -0
- package/skills/cm-readit/SKILL.md +289 -0
- package/skills/cm-readit/audio-player.md +206 -0
- package/skills/cm-readit/examples/blog-reader.js +352 -0
- package/skills/cm-readit/examples/voice-cro.js +390 -0
- package/skills/cm-readit/tts-engine.md +262 -0
- package/skills/cm-readit/ui-patterns.md +362 -0
- package/skills/cm-readit/voice-cro.md +223 -0
- package/skills/cm-safe-deploy/SKILL.md +120 -0
- package/skills/cm-safe-deploy/templates/deploy.sh +89 -0
- package/skills/cm-safe-i18n/SKILL.md +473 -0
- package/skills/cm-secret-shield/SKILL.md +580 -0
- package/skills/cm-skill-chain/SKILL.md +78 -0
- package/skills/cm-skill-index/SKILL.md +318 -0
- package/skills/cm-skill-mastery/SKILL.md +169 -0
- package/skills/cm-start/SKILL.md +65 -0
- package/skills/cm-status/SKILL.md +12 -0
- package/skills/cm-tdd/SKILL.md +370 -0
- package/skills/cm-terminal/SKILL.md +177 -0
- package/skills/cm-test-gate/SKILL.md +242 -0
- package/skills/cm-ui-preview/SKILL.md +291 -0
- package/skills/cm-ux-master/DESIGN_STANDARD_TEMPLATE.md +54 -0
- package/skills/cm-ux-master/SKILL.md +114 -0
- package/skills/cro-methodology/SKILL.md +98 -0
- package/skills/cro-methodology/references/COPYWRITING.md +178 -0
- package/skills/cro-methodology/references/OBJECTIONS.md +135 -0
- package/skills/cro-methodology/references/PERSUASION.md +158 -0
- package/skills/cro-methodology/references/RESEARCH.md +220 -0
- package/skills/cro-methodology/references/funnel-analysis.md +365 -0
- 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
|
+
})();
|