ai-control-center 1.15.2

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +584 -0
  3. package/bin/aicc.js +772 -0
  4. package/lib/actions/approve.js +71 -0
  5. package/lib/actions/assign-project.js +132 -0
  6. package/lib/actions/browser-test.js +64 -0
  7. package/lib/actions/cleanup.js +174 -0
  8. package/lib/actions/debug.js +298 -0
  9. package/lib/actions/deploy.js +1229 -0
  10. package/lib/actions/fix-bug.js +134 -0
  11. package/lib/actions/new-feature.js +255 -0
  12. package/lib/actions/reject.js +307 -0
  13. package/lib/actions/review.js +706 -0
  14. package/lib/actions/status.js +47 -0
  15. package/lib/agents/browser-qa-agent.js +611 -0
  16. package/lib/agents/payment-agent.js +116 -0
  17. package/lib/agents/suggestion-agent.js +88 -0
  18. package/lib/cli.js +303 -0
  19. package/lib/config.js +243 -0
  20. package/lib/hub/hub-server.js +440 -0
  21. package/lib/hub/project-poller.js +75 -0
  22. package/lib/hub/skill-registry.js +89 -0
  23. package/lib/hub/state-aggregator.js +204 -0
  24. package/lib/index.js +471 -0
  25. package/lib/init/doctor.js +523 -0
  26. package/lib/init/presets.js +222 -0
  27. package/lib/init/skill-fetcher.js +77 -0
  28. package/lib/init/wizard.js +973 -0
  29. package/lib/integrations/codex-runner.js +128 -0
  30. package/lib/integrations/github-actions.js +248 -0
  31. package/lib/integrations/github-reporter.js +229 -0
  32. package/lib/integrations/screenshot-store.js +102 -0
  33. package/lib/openclaw/bridge.js +650 -0
  34. package/lib/openclaw/generate-skill.js +235 -0
  35. package/lib/openclaw/openclaw.json +64 -0
  36. package/lib/orchestrator/autonomous-loop.js +429 -0
  37. package/lib/orchestrator/thread-triggers.js +63 -0
  38. package/lib/roleplay/agent-messenger.js +75 -0
  39. package/lib/roleplay/discussion-threads.js +303 -0
  40. package/lib/roleplay/health-monitor.js +121 -0
  41. package/lib/roleplay/pm-agent.js +513 -0
  42. package/lib/roleplay/roleplay-config.js +25 -0
  43. package/lib/roleplay/room.js +164 -0
  44. package/lib/shared/action-runner.js +2330 -0
  45. package/lib/shared/event-bus.js +185 -0
  46. package/lib/slack/bot.js +378 -0
  47. package/lib/telegram/bot.js +416 -0
  48. package/lib/telegram/commands.js +1267 -0
  49. package/lib/telegram/keyboards.js +113 -0
  50. package/lib/telegram/notifications.js +247 -0
  51. package/lib/twitch/bot.js +354 -0
  52. package/lib/twitch/commands.js +302 -0
  53. package/lib/twitch/notifications.js +63 -0
  54. package/lib/utils/achievements.js +191 -0
  55. package/lib/utils/activity-log.js +182 -0
  56. package/lib/utils/agent-leaderboard.js +119 -0
  57. package/lib/utils/audit-logger.js +232 -0
  58. package/lib/utils/codebase-context.js +288 -0
  59. package/lib/utils/codebase-indexer.js +381 -0
  60. package/lib/utils/config-schema.js +230 -0
  61. package/lib/utils/context-compressor.js +172 -0
  62. package/lib/utils/correlation.js +63 -0
  63. package/lib/utils/cost-tracker.js +423 -0
  64. package/lib/utils/cron-scheduler.js +53 -0
  65. package/lib/utils/db-adapter.js +293 -0
  66. package/lib/utils/display.js +272 -0
  67. package/lib/utils/errors.js +116 -0
  68. package/lib/utils/format.js +134 -0
  69. package/lib/utils/intent-engine.js +464 -0
  70. package/lib/utils/mcp-client.js +238 -0
  71. package/lib/utils/model-ab-test.js +164 -0
  72. package/lib/utils/notify.js +122 -0
  73. package/lib/utils/persona-loader.js +80 -0
  74. package/lib/utils/pipeline-lock.js +73 -0
  75. package/lib/utils/pipeline.js +214 -0
  76. package/lib/utils/plugin-runner.js +234 -0
  77. package/lib/utils/rate-limiter.js +84 -0
  78. package/lib/utils/rbac.js +74 -0
  79. package/lib/utils/runner.js +1809 -0
  80. package/lib/utils/security.js +191 -0
  81. package/lib/utils/self-healer.js +144 -0
  82. package/lib/utils/skill-loader.js +255 -0
  83. package/lib/utils/spinner.js +132 -0
  84. package/lib/utils/stage-queue.js +50 -0
  85. package/lib/utils/state-machine.js +89 -0
  86. package/lib/utils/status-bar.js +327 -0
  87. package/lib/utils/token-estimator.js +101 -0
  88. package/lib/utils/ux-analyzer.js +101 -0
  89. package/lib/utils/webhook-emitter.js +83 -0
  90. package/lib/web/public/css/styles.css +417 -0
  91. package/lib/web/public/dark-mode.js +44 -0
  92. package/lib/web/public/hub/kanban.html +206 -0
  93. package/lib/web/public/index.html +45 -0
  94. package/lib/web/public/js/app.js +71 -0
  95. package/lib/web/public/js/ask.js +110 -0
  96. package/lib/web/public/js/dashboard.js +165 -0
  97. package/lib/web/public/js/deploy.js +72 -0
  98. package/lib/web/public/js/feature.js +79 -0
  99. package/lib/web/public/js/health.js +65 -0
  100. package/lib/web/public/js/logs.js +93 -0
  101. package/lib/web/public/js/review.js +123 -0
  102. package/lib/web/public/js/ws-client.js +82 -0
  103. package/lib/web/public/office/css/office.css +678 -0
  104. package/lib/web/public/office/index.html +148 -0
  105. package/lib/web/public/office/js/achievements-ui.js +117 -0
  106. package/lib/web/public/office/js/character.js +1056 -0
  107. package/lib/web/public/office/js/chat-bubbles.js +177 -0
  108. package/lib/web/public/office/js/cost-overlay.js +123 -0
  109. package/lib/web/public/office/js/day-night.js +68 -0
  110. package/lib/web/public/office/js/effects.js +632 -0
  111. package/lib/web/public/office/js/engine.js +146 -0
  112. package/lib/web/public/office/js/feature-ticket.js +216 -0
  113. package/lib/web/public/office/js/hub-client.js +60 -0
  114. package/lib/web/public/office/js/main.js +1757 -0
  115. package/lib/web/public/office/js/office-layout.js +1524 -0
  116. package/lib/web/public/office/js/pathfinding.js +144 -0
  117. package/lib/web/public/office/js/pixel-sprites.js +1454 -0
  118. package/lib/web/public/office/js/progress-bars.js +117 -0
  119. package/lib/web/public/office/js/replay.js +191 -0
  120. package/lib/web/public/office/js/sound-effects.js +91 -0
  121. package/lib/web/public/office/js/sprite-renderer.js +211 -0
  122. package/lib/web/public/office/js/stamina-system.js +89 -0
  123. package/lib/web/public/office/js/ui.js +107 -0
  124. package/lib/web/public/onboarding/index.html +243 -0
  125. package/lib/web/public/timeline/index.html +195 -0
  126. package/lib/web/routes/api.js +499 -0
  127. package/lib/web/routes/logs.js +20 -0
  128. package/lib/web/routes/metrics.js +99 -0
  129. package/lib/web/server.js +183 -0
  130. package/lib/web/ws/handler.js +65 -0
  131. package/package.json +67 -0
  132. package/templates/agent-architect.md +69 -0
  133. package/templates/agent-gemini-pm.md +49 -0
  134. package/templates/agent-gemini-reviewer.md +52 -0
  135. package/templates/copilot-instructions.md +36 -0
  136. package/templates/pipelines/mobile.json +27 -0
  137. package/templates/pipelines/nodejs-api.json +27 -0
  138. package/templates/pipelines/python.json +27 -0
  139. package/templates/pipelines/react.json +27 -0
  140. package/templates/pipelines/salesforce.json +27 -0
  141. package/templates/role-gemini.md +97 -0
  142. package/templates/skill-architect.md +114 -0
  143. package/templates/skill-browser-qa.md +50 -0
  144. package/templates/skill-bug-from-qa.md +58 -0
  145. package/templates/skill-chatbot.md +93 -0
  146. package/templates/skill-implement.md +78 -0
  147. package/templates/skill-openclaw.md +174 -0
  148. package/templates/skill-payment.md +110 -0
  149. package/templates/skill-pm-spec.md +77 -0
  150. package/templates/skill-requirement-capture.md +97 -0
  151. package/templates/skill-review.md +108 -0
  152. package/templates/skill-reviewer-qa.md +44 -0
  153. package/templates/skill-suggestion.md +45 -0
  154. package/templates/skill-template.md +142 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * New Feature / Bug Fix view.
3
+ */
4
+
5
+ export function renderFeature(container) {
6
+ container.innerHTML = `
7
+ <div class="card">
8
+ <div class="card-title">Submit New Feature / Bug Fix</div>
9
+
10
+ <div class="form-group">
11
+ <label>Type</label>
12
+ <select id="feat-type">
13
+ <option value="feature">Feature</option>
14
+ <option value="bug">Bug Fix</option>
15
+ </select>
16
+ </div>
17
+
18
+ <div class="form-group">
19
+ <label>Description (min 10 characters)</label>
20
+ <textarea id="feat-desc" placeholder="e.g. Add MYOB bank transaction inbound sync support" rows="4"></textarea>
21
+ </div>
22
+
23
+ <div class="form-group">
24
+ <label>Pipeline Mode</label>
25
+ <select id="feat-mode">
26
+ <option value="auto">Auto — run full pipeline, notify when done</option>
27
+ <option value="manual">Manual — step through each stage</option>
28
+ </select>
29
+ </div>
30
+
31
+ <div class="btn-group">
32
+ <button class="btn btn-primary" id="feat-submit">Submit Feature</button>
33
+ </div>
34
+
35
+ <div id="feat-result" style="margin-top: 16px; font-family: var(--mono); font-size: 13px;"></div>
36
+ </div>
37
+ `;
38
+
39
+ document.getElementById('feat-submit').addEventListener('click', submitFeature);
40
+ }
41
+
42
+ async function submitFeature() {
43
+ const btn = document.getElementById('feat-submit');
44
+ const desc = document.getElementById('feat-desc').value.trim();
45
+ const mode = document.getElementById('feat-mode').value;
46
+ const type = document.getElementById('feat-type').value;
47
+ const result = document.getElementById('feat-result');
48
+
49
+ if (desc.length < 10) {
50
+ result.innerHTML = '<span style="color: var(--red);">Description must be at least 10 characters.</span>';
51
+ return;
52
+ }
53
+
54
+ btn.disabled = true;
55
+ btn.innerHTML = '<span class="spinner"></span> Submitting...';
56
+ result.innerHTML = '<span style="color: var(--text-dim);">Creating feature inbox file...</span>';
57
+
58
+ try {
59
+ const res = await fetch('/api/feature', {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ description: desc, mode, type }),
63
+ });
64
+ const data = await res.json();
65
+
66
+ if (data.success) {
67
+ result.innerHTML = `<span style="color: var(--green);">Feature created: ${data.featureId}</span>`;
68
+ window.showToast('Feature submitted!', 'success');
69
+ document.getElementById('feat-desc').value = '';
70
+ } else {
71
+ result.innerHTML = `<span style="color: var(--red);">${data.error}</span>`;
72
+ }
73
+ } catch (err) {
74
+ result.innerHTML = `<span style="color: var(--red);">Network error: ${err.message}</span>`;
75
+ }
76
+
77
+ btn.disabled = false;
78
+ btn.textContent = 'Submit Feature';
79
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Health check view — shows AI tool + org availability.
3
+ */
4
+
5
+ export function renderHealth(container) {
6
+ container.innerHTML = `
7
+ <div class="card">
8
+ <div class="card-title">System Health Check</div>
9
+ <div id="health-content">
10
+ <div style="color: var(--text-dim);"><span class="spinner"></span> Checking...</div>
11
+ </div>
12
+ </div>
13
+ `;
14
+
15
+ loadHealth();
16
+ }
17
+
18
+ async function loadHealth() {
19
+ const el = document.getElementById('health-content');
20
+
21
+ try {
22
+ const data = await fetch('/api/health').then(r => r.json());
23
+
24
+ const row = (icon, label, detail) =>
25
+ `<div class="health-row">
26
+ <span class="health-icon ${icon ? 'health-ok' : 'health-fail'}">${icon ? '&#10003;' : '&#10007;'}</span>
27
+ <span class="health-label">${label}</span>
28
+ <span class="health-detail">${detail}</span>
29
+ </div>`;
30
+
31
+ let html = '';
32
+
33
+ // AI Tools
34
+ html += '<div style="color: var(--text-dim); font-size: 11px; margin: 8px 0 4px;">AI Tools</div>';
35
+ html += row(data.gemini?.available, 'gemini', data.gemini?.path || 'not found');
36
+ html += row(data.claude?.available, 'claude', data.claude?.path || 'not found');
37
+ html += row(data.copilot?.available, 'copilot', data.copilot?.path || 'not found');
38
+ html += row(data.gh?.available, 'gh', data.gh?.path || 'not found');
39
+
40
+ // Platform CLI
41
+ html += '<div style="color: var(--text-dim); font-size: 11px; margin: 12px 0 4px;">Platform CLI</div>';
42
+ html += row(data.sf?.available, 'sf', data.sf?.path || 'not found');
43
+
44
+ // Workflow
45
+ html += '<div style="color: var(--text-dim); font-size: 11px; margin: 12px 0 4px;">Workflow</div>';
46
+ const wf = data.workflow || {};
47
+ html += row(wf.aiWorkflow, '.ai-workflow/', wf.aiWorkflow ? 'exists' : 'missing');
48
+ html += row(wf.skills, '.claude/skills/', wf.skills ? 'exists' : 'missing');
49
+ html += row(wf.agents, '.claude/agents/', wf.agents ? 'exists' : 'missing');
50
+
51
+ // Pipeline
52
+ const p = data.pipeline || {};
53
+ html += '<div style="color: var(--text-dim); font-size: 11px; margin: 12px 0 4px;">Pipeline</div>';
54
+ html += `<div class="health-row">
55
+ <span class="health-icon" style="color: var(--cyan);">&#9670;</span>
56
+ <span class="health-label">stage</span>
57
+ <span class="health-detail">${p.stage || 'idle'} &middot; ${p.current_feature || 'none'}</span>
58
+ </div>`;
59
+
60
+ el.innerHTML = html;
61
+
62
+ } catch (err) {
63
+ el.innerHTML = `<span style="color: var(--red);">Health check failed: ${err.message}</span>`;
64
+ }
65
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Logs view — session log viewer with agent filter.
3
+ */
4
+
5
+ let currentFilter = 'all';
6
+
7
+ export function renderLogs(container) {
8
+ container.innerHTML = `
9
+ <div class="card">
10
+ <div class="card-title">Session Logs</div>
11
+
12
+ <div class="filter-tabs" id="log-filters">
13
+ <button class="filter-tab active" data-filter="all">All</button>
14
+ <button class="filter-tab" data-filter="GEMINI" style="color: var(--cyan);">Gemini</button>
15
+ <button class="filter-tab" data-filter="CLAUDE" style="color: var(--magenta);">Claude</button>
16
+ <button class="filter-tab" data-filter="COPILOT" style="color: var(--blue);">Copilot</button>
17
+ <button class="filter-tab" data-filter="PIPELINE" style="color: var(--yellow);">Pipeline</button>
18
+ </div>
19
+
20
+ <div id="log-content" class="log-viewer">Loading...</div>
21
+
22
+ <div style="margin-top: 8px; display: flex; justify-content: space-between; align-items: center;">
23
+ <span id="log-count" class="dim" style="font-size: 11px;"></span>
24
+ <button class="btn" id="log-refresh" style="font-size: 12px;">Refresh</button>
25
+ </div>
26
+ </div>
27
+ `;
28
+
29
+ // Filter tab handlers
30
+ document.getElementById('log-filters').addEventListener('click', (e) => {
31
+ if (!e.target.dataset.filter) return;
32
+ currentFilter = e.target.dataset.filter;
33
+ document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active'));
34
+ e.target.classList.add('active');
35
+ loadLogs();
36
+ });
37
+
38
+ document.getElementById('log-refresh').addEventListener('click', loadLogs);
39
+
40
+ // Listen for live log events
41
+ window.addEventListener('xconn', onLogUpdate);
42
+
43
+ loadLogs();
44
+ }
45
+
46
+ async function loadLogs() {
47
+ const contentEl = document.getElementById('log-content');
48
+ const countEl = document.getElementById('log-count');
49
+
50
+ try {
51
+ const data = await fetch('/api/logs?lines=200').then(r => r.json());
52
+ const lines = data.content.split('\n').filter(l => l.trim());
53
+
54
+ const filtered = currentFilter === 'all'
55
+ ? lines
56
+ : lines.filter(l => l.includes(`[${currentFilter}`));
57
+
58
+ contentEl.innerHTML = filtered.map(colorLine).join('\n');
59
+ countEl.textContent = `${filtered.length} line(s)${data.total ? ` of ${data.total} total` : ''}`;
60
+
61
+ // Auto-scroll to bottom
62
+ contentEl.scrollTop = contentEl.scrollHeight;
63
+ } catch (err) {
64
+ contentEl.textContent = 'Failed to load logs.';
65
+ }
66
+ }
67
+
68
+ function colorLine(line) {
69
+ let cls = '';
70
+ if (line.includes('[GEMINI')) cls = 'log-gemini';
71
+ else if (line.includes('[CLAUDE')) cls = 'log-claude';
72
+ else if (line.includes('[COPILOT')) cls = 'log-copilot';
73
+ else if (line.includes('[PIPELINE')) cls = 'log-pipeline';
74
+ else if (line.includes('[ERROR')) cls = 'log-error';
75
+ else if (line.includes('[SUCCESS')) cls = 'log-success';
76
+
77
+ const escaped = line.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
78
+ return `<span class="log-line ${cls}">${escaped}</span>`;
79
+ }
80
+
81
+ function onLogUpdate(e) {
82
+ if (e.detail.type !== 'log') return;
83
+ const { agent, message } = e.detail.data;
84
+
85
+ if (currentFilter !== 'all' && !agent.includes(currentFilter)) return;
86
+
87
+ const contentEl = document.getElementById('log-content');
88
+ if (!contentEl) return;
89
+
90
+ const line = `[${new Date().toLocaleTimeString()}] [${agent}] ${message}`;
91
+ contentEl.innerHTML += '\n' + colorLine(line);
92
+ contentEl.scrollTop = contentEl.scrollHeight;
93
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Review view — verdict display + approve/reject.
3
+ */
4
+
5
+ export function renderReview(container) {
6
+ container.innerHTML = `
7
+ <div class="card">
8
+ <div class="card-title">Latest Review</div>
9
+ <div id="review-content" style="font-family: var(--mono); font-size: 13px;">Loading...</div>
10
+ </div>
11
+
12
+ <div class="card" id="review-actions-card" style="display: none;">
13
+ <div class="card-title">Actions</div>
14
+ <div class="btn-group">
15
+ <button class="btn btn-success" id="review-approve">Approve</button>
16
+ <button class="btn btn-danger" id="review-reject">Reject</button>
17
+ </div>
18
+ <div id="reject-reason-wrap" style="display: none; margin-top: 12px;">
19
+ <div class="form-group">
20
+ <label>Rejection Reason</label>
21
+ <textarea id="reject-reason" placeholder="Describe what needs to be fixed..." rows="3"></textarea>
22
+ </div>
23
+ <button class="btn btn-danger" id="reject-confirm">Confirm Reject</button>
24
+ </div>
25
+ <div id="action-result" style="margin-top: 12px; font-family: var(--mono); font-size: 13px;"></div>
26
+ </div>
27
+
28
+ <div class="card">
29
+ <div class="card-title">Review File Content</div>
30
+ <div id="review-file" class="log-viewer" style="max-height: 600px;">Loading...</div>
31
+ </div>
32
+ `;
33
+
34
+ loadReview();
35
+
36
+ document.getElementById('review-approve').addEventListener('click', approveFeature);
37
+ document.getElementById('review-reject').addEventListener('click', () => {
38
+ document.getElementById('reject-reason-wrap').style.display = 'block';
39
+ });
40
+ document.getElementById('reject-confirm').addEventListener('click', rejectFeature);
41
+ }
42
+
43
+ async function loadReview() {
44
+ const contentEl = document.getElementById('review-content');
45
+ const fileEl = document.getElementById('review-file');
46
+ const actionsCard = document.getElementById('review-actions-card');
47
+
48
+ try {
49
+ const review = await fetch('/api/review/latest').then(r => r.json());
50
+
51
+ if (!review.found) {
52
+ contentEl.textContent = 'No reviews available yet.';
53
+ fileEl.textContent = 'Run a code review first.';
54
+ return;
55
+ }
56
+
57
+ // Verdict panel
58
+ const isApproved = review.verdict === 'APPROVED';
59
+ const verdictClass = isApproved ? 'verdict-approved' : 'verdict-rejected';
60
+ const color = isApproved ? 'var(--green)' : 'var(--red)';
61
+
62
+ contentEl.innerHTML = `
63
+ <div class="verdict-panel ${verdictClass}">
64
+ <div class="verdict-title" style="color: ${color};">${review.verdict}</div>
65
+ <div style="color: var(--text-dim); font-size: 12px;">${review.name}</div>
66
+ </div>
67
+ `;
68
+
69
+ // Show actions if review is complete
70
+ const status = await fetch('/api/status').then(r => r.json());
71
+ if (status.stage === 'review_complete') {
72
+ actionsCard.style.display = 'block';
73
+ }
74
+
75
+ // Full review content
76
+ fileEl.textContent = review.content;
77
+
78
+ } catch (err) {
79
+ contentEl.textContent = 'Failed to load review.';
80
+ fileEl.textContent = err.message;
81
+ }
82
+ }
83
+
84
+ async function approveFeature() {
85
+ const resultEl = document.getElementById('action-result');
86
+ resultEl.innerHTML = '<span class="spinner"></span> Approving...';
87
+
88
+ try {
89
+ const res = await fetch('/api/approve', { method: 'POST' });
90
+ const data = await res.json();
91
+ if (data.success) {
92
+ resultEl.innerHTML = '<span style="color: var(--green);">Feature approved!</span>';
93
+ window.showToast('Feature approved!', 'success');
94
+ } else {
95
+ resultEl.innerHTML = `<span style="color: var(--red);">${data.error}</span>`;
96
+ }
97
+ } catch (err) {
98
+ resultEl.innerHTML = `<span style="color: var(--red);">${err.message}</span>`;
99
+ }
100
+ }
101
+
102
+ async function rejectFeature() {
103
+ const reason = document.getElementById('reject-reason').value.trim() || 'Rejected via web dashboard';
104
+ const resultEl = document.getElementById('action-result');
105
+ resultEl.innerHTML = '<span class="spinner"></span> Rejecting...';
106
+
107
+ try {
108
+ const res = await fetch('/api/reject', {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({ reason }),
112
+ });
113
+ const data = await res.json();
114
+ if (data.success) {
115
+ resultEl.innerHTML = '<span style="color: var(--yellow);">Feature rejected. Dispatching fixes to Copilot.</span>';
116
+ window.showToast('Feature rejected', 'error');
117
+ } else {
118
+ resultEl.innerHTML = `<span style="color: var(--red);">${data.error}</span>`;
119
+ }
120
+ } catch (err) {
121
+ resultEl.innerHTML = `<span style="color: var(--red);">${err.message}</span>`;
122
+ }
123
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WebSocket client with auto-reconnect.
3
+ * Dispatches custom events on window for view modules to listen to.
4
+ */
5
+
6
+ let ws = null;
7
+ let reconnectTimer = null;
8
+ const RECONNECT_DELAY = 2000;
9
+
10
+ function connect() {
11
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
12
+ ws = new WebSocket(`${protocol}://${location.host}/ws`);
13
+
14
+ ws.onopen = () => {
15
+ updateIndicator(true);
16
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
17
+ };
18
+
19
+ ws.onclose = () => {
20
+ updateIndicator(false);
21
+ reconnectTimer = setTimeout(connect, RECONNECT_DELAY);
22
+ };
23
+
24
+ ws.onerror = () => ws.close();
25
+
26
+ ws.onmessage = (e) => {
27
+ try {
28
+ const msg = JSON.parse(e.data);
29
+ window.dispatchEvent(new CustomEvent('xconn', { detail: msg }));
30
+
31
+ // Update header badge on status changes
32
+ if (msg.type === 'status' && msg.data?.status) {
33
+ updateHeaderStatus(msg.data.status);
34
+ }
35
+ if (msg.type === 'clients') {
36
+ const el = document.getElementById('footer-clients');
37
+ if (el) el.textContent = `${msg.data.count} client(s)`;
38
+ }
39
+ } catch { /* ignore non-JSON */ }
40
+ };
41
+ }
42
+
43
+ function updateIndicator(connected) {
44
+ const dot = document.getElementById('ws-indicator');
45
+ if (!dot) return;
46
+ dot.className = connected ? 'ws-dot ws-connected' : 'ws-dot ws-disconnected';
47
+ dot.title = connected ? 'Connected' : 'Disconnected';
48
+ }
49
+
50
+ function updateHeaderStatus(status) {
51
+ const stageEl = document.getElementById('header-stage');
52
+ const featEl = document.getElementById('header-feature');
53
+ if (stageEl) {
54
+ const stage = status.stage || 'idle';
55
+ stageEl.textContent = stage;
56
+ stageEl.className = 'badge ' + getStageBadgeClass(stage);
57
+ }
58
+ if (featEl) {
59
+ featEl.textContent = status.current_feature || '';
60
+ }
61
+ }
62
+
63
+ function getStageBadgeClass(stage) {
64
+ if (stage === 'idle') return 'badge-idle';
65
+ if (stage === 'approved') return 'badge-approved';
66
+ if (stage === 'rejected') return 'badge-rejected';
67
+ if (stage === 'deployed') return 'badge-deployed';
68
+ if (stage === 'review_complete') return 'badge-review';
69
+ return 'badge-progress';
70
+ }
71
+
72
+ // Export for other modules
73
+ window.xconnWs = { connect };
74
+
75
+ // Auto-connect on load
76
+ connect();
77
+
78
+ // Fetch initial status for header
79
+ fetch('/api/status')
80
+ .then(r => r.json())
81
+ .then(updateHeaderStatus)
82
+ .catch(() => {});