crewswarm 0.9.2 → 0.9.4

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 (228) hide show
  1. package/README.md +22 -9
  2. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js +1 -0
  3. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
  4. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
  5. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
  6. package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
  7. package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
  8. package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
  9. package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
  10. package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
  11. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
  12. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
  13. package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
  14. package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
  15. package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
  16. package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
  17. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
  18. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
  19. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
  20. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
  21. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
  22. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
  23. package/apps/dashboard/dist/index.html +135 -15
  24. package/apps/dashboard/dist/index.html.br +0 -0
  25. package/apps/dashboard/dist/index.html.gz +0 -0
  26. package/apps/vibe/README.md +2 -2
  27. package/apps/vibe/package.json +1 -1
  28. package/apps/vibe/server.mjs +101 -56
  29. package/crew-lead.mjs +34 -4
  30. package/lib/bridges/cli-executor.mjs +1 -1
  31. package/lib/bridges/gateway-ws.mjs +4 -0
  32. package/lib/browser/passthrough-stderr.js +1 -0
  33. package/lib/chat/project-messages.mjs +3 -5
  34. package/lib/cli-process-tracker.mjs +3 -2
  35. package/lib/contacts/identity-linker.mjs +1 -0
  36. package/lib/crew-judge/judge.mjs +19 -18
  37. package/lib/crew-lead/agent-manager.mjs +1 -1
  38. package/lib/crew-lead/background.mjs +14 -1
  39. package/lib/crew-lead/chat-handler.mjs +38 -1
  40. package/lib/crew-lead/http-server.mjs +106 -57
  41. package/lib/crew-lead/llm-caller.mjs +24 -8
  42. package/lib/crew-lead/prompts.mjs +14 -1
  43. package/lib/crew-lead/tools.mjs +3 -2
  44. package/lib/crew-lead/wave-dispatcher.mjs +19 -5
  45. package/lib/crew-lead/ws-router.mjs +219 -27
  46. package/lib/engines/crew-cli.mjs +1 -1
  47. package/lib/engines/engine-registry.mjs +14 -3
  48. package/lib/engines/rt-envelope.mjs +1 -0
  49. package/lib/engines/runners.mjs +28 -4
  50. package/lib/gemini-cli-passthrough-noise.mjs +1 -1
  51. package/lib/integrations/code-search.mjs +4 -3
  52. package/lib/memory/shared-adapter.mjs +23 -10
  53. package/lib/pipeline/manager.mjs +2 -1
  54. package/lib/runtime/config.mjs +1 -1
  55. package/lib/runtime/paths.mjs +12 -8
  56. package/lib/runtime/spending.mjs +2 -1
  57. package/package.json +42 -14
  58. package/scripts/capture-build-flow.mjs +118 -0
  59. package/scripts/coverage-report.mjs +209 -0
  60. package/scripts/coverage-summary.mjs +47 -0
  61. package/scripts/dashboard-validation.mjs +76 -0
  62. package/scripts/dashboard.mjs +1667 -551
  63. package/scripts/generate-openapi.mjs +683 -277
  64. package/scripts/live-bridge-matrix.mjs +79 -0
  65. package/scripts/live-cli-matrix.mjs +166 -0
  66. package/scripts/live-crewchat-check.mjs +42 -0
  67. package/scripts/live-engine-matrix.mjs +50 -0
  68. package/scripts/live-provider-failover-matrix.mjs +107 -0
  69. package/scripts/live-provider-matrix.mjs +228 -0
  70. package/scripts/restart-all-from-repo.sh +4 -4
  71. package/scripts/restart-service.sh +12 -9
  72. package/scripts/smoke-dispatch.mjs +4 -1
  73. package/scripts/test-blast-radius.mjs +204 -0
  74. package/scripts/test-report-summary.mjs +88 -0
  75. package/scripts/test-reporter.mjs +651 -0
  76. package/scripts/test-rerun.mjs +136 -0
  77. package/scripts/tmux-bridge +130 -0
  78. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js +0 -1
  79. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  80. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
  81. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  82. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  83. package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
  84. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  85. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js +0 -1
  86. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  87. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  88. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  89. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
  90. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  91. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js +0 -1
  92. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  93. package/apps/dashboard/index.html +0 -6529
  94. package/apps/dashboard/package.json +0 -15
  95. package/apps/dashboard/src/app.js +0 -2828
  96. package/apps/dashboard/src/app.js.br +0 -0
  97. package/apps/dashboard/src/app.js.gz +0 -0
  98. package/apps/dashboard/src/chat/chat-actions.js +0 -1847
  99. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  100. package/apps/dashboard/src/chat/unified-messages.js +0 -327
  101. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  102. package/apps/dashboard/src/cli-process.js +0 -208
  103. package/apps/dashboard/src/cli-process.js.br +0 -0
  104. package/apps/dashboard/src/cli-process.js.gz +0 -0
  105. package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
  106. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  107. package/apps/dashboard/src/core/api.js +0 -18
  108. package/apps/dashboard/src/core/api.js.br +0 -0
  109. package/apps/dashboard/src/core/dom.js +0 -228
  110. package/apps/dashboard/src/core/dom.js.br +0 -0
  111. package/apps/dashboard/src/core/state.js +0 -91
  112. package/apps/dashboard/src/core/state.js.br +0 -0
  113. package/apps/dashboard/src/core/task-manager.js +0 -134
  114. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  115. package/apps/dashboard/src/orchestration-status.js +0 -127
  116. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  117. package/apps/dashboard/src/setup-wizard.js +0 -562
  118. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  119. package/apps/dashboard/src/styles.css +0 -2085
  120. package/apps/dashboard/src/styles.css.br +0 -0
  121. package/apps/dashboard/src/styles.css.gz +0 -0
  122. package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
  123. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  124. package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
  125. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  126. package/apps/dashboard/src/tabs/comms-tab.js +0 -955
  127. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  128. package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
  129. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  130. package/apps/dashboard/src/tabs/engines-tab.js +0 -175
  131. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  132. package/apps/dashboard/src/tabs/memory-tab.js +0 -182
  133. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  134. package/apps/dashboard/src/tabs/models-tab.js +0 -450
  135. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  136. package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
  137. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  138. package/apps/dashboard/src/tabs/projects-tab.js +0 -663
  139. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  140. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  141. package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
  142. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  143. package/apps/dashboard/src/tabs/services-tab.js +0 -202
  144. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  145. package/apps/dashboard/src/tabs/settings-tab.js +0 -861
  146. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  147. package/apps/dashboard/src/tabs/skills-tab.js +0 -284
  148. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  149. package/apps/dashboard/src/tabs/spending-tab.js +0 -173
  150. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  151. package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
  152. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  153. package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
  154. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  155. package/apps/dashboard/src/tabs/usage-tab.js +0 -390
  156. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  157. package/apps/dashboard/src/tabs/waves-tab.js +0 -238
  158. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  159. package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
  160. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  161. package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
  162. package/apps/vibe/.crew/cost.json +0 -17
  163. package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
  164. package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
  165. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
  166. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
  167. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
  168. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
  169. package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
  170. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
  171. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
  172. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
  173. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
  174. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
  175. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
  176. package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
  177. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
  178. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
  179. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
  180. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
  181. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
  182. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
  183. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
  184. package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
  185. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
  186. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
  187. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
  188. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
  189. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
  190. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
  191. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
  192. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
  193. package/apps/vibe/.crew/sandbox.json +0 -7
  194. package/apps/vibe/.crew/session.json +0 -330
  195. package/apps/vibe/.crew/training-data.jsonl +0 -0
  196. package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
  197. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
  198. package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
  199. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
  200. package/apps/vibe/ARCHITECTURE.md +0 -3393
  201. package/apps/vibe/QUICK-REFERENCE.md +0 -211
  202. package/apps/vibe/ROADMAP.md +0 -41
  203. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
  204. package/apps/vibe/VISUAL-GUIDE.md +0 -378
  205. package/apps/vibe/capture-demo.mjs +0 -160
  206. package/apps/vibe/capture-full-demo.mjs +0 -255
  207. package/apps/vibe/capture-quickstart.mjs +0 -256
  208. package/apps/vibe/capture-vibe-assets.mjs +0 -71
  209. package/apps/vibe/capture-vibe-video.mjs +0 -260
  210. package/apps/vibe/check-buttons.js +0 -41
  211. package/apps/vibe/diagnose.html +0 -106
  212. package/apps/vibe/fix-buttons.js +0 -103
  213. package/apps/vibe/index.html +0 -3404
  214. package/apps/vibe/package-lock.json +0 -920
  215. package/apps/vibe/scripts/studio-pty-host.py +0 -117
  216. package/apps/vibe/src/main.js +0 -2940
  217. package/apps/vibe/src/register-all-languages.js +0 -98
  218. package/apps/vibe/start-studio.sh +0 -11
  219. package/apps/vibe/test/accessibility-tests.js +0 -77
  220. package/apps/vibe/test/browser-performance-audit.mjs +0 -205
  221. package/apps/vibe/test/performance-tests.js +0 -120
  222. package/apps/vibe/test/security-tests.js +0 -213
  223. package/apps/vibe/tests/e2e.local.mjs +0 -54
  224. package/apps/vibe/tests/server.smoke.mjs +0 -106
  225. package/apps/vibe/update_website.mjs +0 -74
  226. package/apps/vibe/vite.config.js +0 -19
  227. package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
  228. package/lib/engines/rt-envelope.mjs.backup-current +0 -870
@@ -1,955 +0,0 @@
1
- import { getJSON, postJSON } from '../core/api.js';
2
- import { escHtml, showNotification } from '../core/dom.js';
3
-
4
- let showSettings = () => {};
5
- let showSettingsTab = () => {};
6
- let _waSavedContactNames = {};
7
- let _waSavedUserRouting = {};
8
- let _tgSavedContactNames = {};
9
- let _tgSavedUserRouting = {};
10
- let _tgSavedTopicRouting = {};
11
-
12
- export function initCommsTab(deps = {}) {
13
- showSettings = deps.showSettings || showSettings;
14
- showSettingsTab = deps.showSettingsTab || showSettingsTab;
15
- }
16
-
17
- export function showMessaging() {
18
- showSettings();
19
- showSettingsTab('comms');
20
- }
21
-
22
- export async function loadCommsTabData() {
23
- await Promise.allSettled([
24
- loadTgStatus(),
25
- loadTelegramSessions(),
26
- loadTgMessages(),
27
- loadTgConfig(),
28
- loadWaStatus(),
29
- loadWaConfig(),
30
- loadWaMessages(),
31
- ]);
32
- }
33
-
34
- export async function loadTgStatus() {
35
- try {
36
- const d = await getJSON('/api/telegram/status');
37
- const badge = document.getElementById('tgStatusBadge');
38
- if (!badge) return;
39
- if (d.running) {
40
- badge.textContent = d.botName ? '● @' + d.botName : '● running';
41
- badge.className = 'status-badge status-active';
42
- } else {
43
- badge.textContent = '● stopped';
44
- badge.className = 'status-badge status-stopped';
45
- }
46
- } catch {}
47
- }
48
-
49
- export function renderTgContactRows() {
50
- const listEl = document.getElementById('tgContactNamesList');
51
- if (!listEl) return;
52
- const raw = (document.getElementById('tgAllowedIds')?.value || '').trim();
53
- const ids = raw ? raw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n)) : [];
54
- listEl.innerHTML = '';
55
- if (!ids.length) return;
56
- const title = document.createElement('label');
57
- title.style.cssText = 'display:block;margin-bottom:6px;font-size:12px;color:var(--text-2);';
58
- title.textContent = 'Contact names & routing';
59
- listEl.appendChild(title);
60
- ids.forEach(id => {
61
- // Container for 2-line layout
62
- const container = document.createElement('div');
63
- container.style.cssText = 'margin-bottom:12px;padding:12px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;';
64
-
65
- // Line 1: Chat ID + Name
66
- const row1 = document.createElement('div');
67
- row1.style.cssText = 'display:grid;grid-template-columns:100px 1fr;gap:8px;margin-bottom:8px;align-items:center;';
68
-
69
- const span = document.createElement('span');
70
- span.style.cssText = 'font-size:11px;color:var(--text-3);font-family:monospace;';
71
- span.textContent = String(id);
72
-
73
- const input = document.createElement('input');
74
- input.id = 'tgContact-' + id;
75
- input.placeholder = 'Name (e.g. Jeff)';
76
- input.value = _tgSavedContactNames[String(id)] || '';
77
- input.style.cssText = 'font-size:12px;padding:6px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;color:var(--text-1);';
78
-
79
- row1.appendChild(span);
80
- row1.appendChild(input);
81
-
82
- // Line 2: Routing label + dropdown
83
- const row2 = document.createElement('div');
84
- row2.style.cssText = 'display:grid;grid-template-columns:100px 1fr;gap:8px;align-items:center;';
85
-
86
- const routeLabel = document.createElement('span');
87
- routeLabel.style.cssText = 'font-size:10px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em;';
88
- routeLabel.textContent = 'Routes to →';
89
-
90
- const routeSelect = document.createElement('select');
91
- routeSelect.id = 'tgRoute-' + id;
92
- routeSelect.style.cssText = 'font-size:12px;padding:6px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;color:var(--text-1);';
93
-
94
- // Get current routing for this chat ID
95
- const currentRoute = _tgSavedUserRouting[String(id)] || '';
96
-
97
- const agents = [
98
- 'crew-lead', 'crew-main', 'crew-coder', 'crew-pm', 'crew-qa',
99
- 'crew-fixer', 'crew-security', 'crew-frontend', 'crew-coder-front',
100
- 'crew-coder-back', 'crew-github', 'crew-copywriter', 'crew-researcher',
101
- 'crew-architect', 'crew-seo', 'crew-ml', 'crew-mega', 'crew-loco'
102
- ];
103
-
104
- // Default option
105
- const defaultOpt = document.createElement('option');
106
- defaultOpt.value = '';
107
- defaultOpt.textContent = '— default (crew-lead) —';
108
- routeSelect.appendChild(defaultOpt);
109
-
110
- // Agent options
111
- agents.forEach(agent => {
112
- const opt = document.createElement('option');
113
- opt.value = agent;
114
- opt.textContent = agent;
115
- if (agent === currentRoute) opt.selected = true;
116
- routeSelect.appendChild(opt);
117
- });
118
-
119
- // Update state immediately when routing changes
120
- routeSelect.addEventListener('change', (e) => {
121
- const newAgent = e.target.value;
122
- if (newAgent) {
123
- _tgSavedUserRouting[String(id)] = newAgent;
124
- } else {
125
- delete _tgSavedUserRouting[String(id)];
126
- }
127
- });
128
-
129
- row2.appendChild(routeLabel);
130
- row2.appendChild(routeSelect);
131
-
132
- container.appendChild(row1);
133
- container.appendChild(row2);
134
- listEl.appendChild(container);
135
- });
136
- }
137
-
138
- export async function loadTgConfig() {
139
- try {
140
- const d = await getJSON('/api/telegram/config');
141
- if (d.token) document.getElementById('tgTokenInput').value = d.token;
142
- const ids = d.allowedChatIds && d.allowedChatIds.length ? d.allowedChatIds : [];
143
- document.getElementById('tgAllowedIds').value = ids.join(', ');
144
- _tgSavedContactNames = d.contactNames || {};
145
- _tgSavedUserRouting = d.userRouting || {};
146
- _tgSavedTopicRouting = d.topicRouting || {};
147
- renderTgContactRows();
148
- renderTgTopicRouting();
149
- } catch {}
150
- }
151
-
152
- export function renderTgTopicRouting() {
153
- const container = document.getElementById('tgTopicRoutingContainer');
154
- if (!container) return;
155
-
156
- // Clear existing content
157
- container.innerHTML = '';
158
-
159
- // Title and explanation
160
- const header = document.createElement('div');
161
- header.style.cssText = 'margin-bottom:12px;';
162
- header.innerHTML = `
163
- <div style="font-size:13px;font-weight:600;margin-bottom:4px;">📌 Topic Routing (Optional)</div>
164
- <div style="font-size:11px;color:var(--text-3);line-height:1.4;">
165
- Route different topics in Forum groups to different agents.
166
- </div>
167
- `;
168
- container.appendChild(header);
169
-
170
- // Action buttons
171
- const btnRow = document.createElement('div');
172
- btnRow.style.cssText = 'display:flex;gap:8px;margin-bottom:12px;';
173
-
174
- const discoverBtn = document.createElement('button');
175
- discoverBtn.textContent = '🔍 Auto-discover Topics';
176
- discoverBtn.className = 'btn-ghost';
177
- discoverBtn.style.cssText = 'flex:1;font-size:12px;';
178
- discoverBtn.onclick = () => discoverTgTopics();
179
-
180
- const addGroupBtn = document.createElement('button');
181
- addGroupBtn.textContent = '➕ Add New Group';
182
- addGroupBtn.className = 'btn-ghost';
183
- addGroupBtn.style.cssText = 'flex:1;font-size:12px;';
184
- addGroupBtn.onclick = () => addTgNewGroup();
185
-
186
- btnRow.appendChild(discoverBtn);
187
- btnRow.appendChild(addGroupBtn);
188
- container.appendChild(btnRow);
189
-
190
- // Topics list container
191
- const listDiv = document.createElement('div');
192
- listDiv.id = 'tgTopicsList';
193
- listDiv.style.cssText = 'margin-bottom:12px;';
194
- container.appendChild(listDiv);
195
-
196
- // Render existing topics
197
- renderTgTopicsList();
198
-
199
- // Advanced: JSON editor (collapsed)
200
- const advancedToggle = document.createElement('details');
201
- advancedToggle.style.cssText = 'margin-top:12px;';
202
- advancedToggle.innerHTML = `
203
- <summary style="cursor:pointer;font-size:11px;color:var(--text-3);padding:6px 0;">
204
- ⚙️ Advanced: Edit JSON directly
205
- </summary>
206
- `;
207
-
208
- const textarea = document.createElement('textarea');
209
- textarea.id = 'tgTopicRoutingJson';
210
- textarea.placeholder = `{
211
- "-100123456789": {
212
- "5": "crew-coder",
213
- "8": "crew-copywriter"
214
- }
215
- }`;
216
- textarea.value = Object.keys(_tgSavedTopicRouting).length ? JSON.stringify(_tgSavedTopicRouting, null, 2) : '';
217
- textarea.style.cssText = 'width:100%;min-height:100px;font-family:monospace;font-size:11px;padding:8px;background:var(--bg-1);border:1px solid var(--border);border-radius:4px;color:var(--text-1);resize:vertical;margin-top:8px;';
218
- advancedToggle.appendChild(textarea);
219
-
220
- container.appendChild(advancedToggle);
221
- }
222
-
223
- export function renderTgTopicsList() {
224
- const listDiv = document.getElementById('tgTopicsList');
225
- if (!listDiv) return;
226
-
227
- listDiv.innerHTML = '';
228
-
229
- const agents = [
230
- 'crew-lead', 'crew-main', 'crew-coder', 'crew-pm', 'crew-qa',
231
- 'crew-fixer', 'crew-security', 'crew-frontend', 'crew-coder-front',
232
- 'crew-coder-back', 'crew-github', 'crew-copywriter', 'crew-researcher',
233
- 'crew-architect', 'crew-seo', 'crew-ml', 'crew-mega', 'crew-loco'
234
- ];
235
-
236
- // Group topics by chatId
237
- const groupedTopics = {};
238
- Object.entries(_tgSavedTopicRouting).forEach(([key, value]) => {
239
- if (key.startsWith('_')) return; // Skip comment fields
240
- if (typeof value === 'object') {
241
- Object.entries(value).forEach(([topicId, agent]) => {
242
- if (!groupedTopics[key]) groupedTopics[key] = [];
243
- groupedTopics[key].push({ topicId, agent });
244
- });
245
- } else {
246
- const [chatId, topicId] = key.split(':');
247
- if (!groupedTopics[chatId]) groupedTopics[chatId] = [];
248
- groupedTopics[chatId].push({ topicId, agent: value });
249
- }
250
- });
251
-
252
- if (Object.keys(groupedTopics).length === 0) {
253
- const emptyMsg = document.createElement('div');
254
- emptyMsg.style.cssText = 'padding:12px;text-align:center;color:var(--text-3);font-size:11px;background:var(--bg-1);border:1px dashed var(--border);border-radius:4px;';
255
- emptyMsg.textContent = 'No topics configured. Click "Auto-discover" or "Add Manually" above.';
256
- listDiv.appendChild(emptyMsg);
257
- return;
258
- }
259
-
260
- let globalIdx = 0;
261
-
262
- // Render each group
263
- Object.entries(groupedTopics).forEach(([chatId, topics]) => {
264
- // Group container (class for saveTgConfig selector)
265
- const groupContainer = document.createElement('div');
266
- groupContainer.className = 'tg-topic-group';
267
- groupContainer.style.cssText = 'margin-bottom:16px;padding:12px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;';
268
-
269
- // Group header with chat ID
270
- const groupHeader = document.createElement('div');
271
- groupHeader.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border);';
272
-
273
- const groupLabel = document.createElement('span');
274
- groupLabel.textContent = 'Group ID:';
275
- groupLabel.style.cssText = 'font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em;';
276
-
277
- const chatIdInput = document.createElement('input');
278
- chatIdInput.value = chatId;
279
- chatIdInput.className = 'tg-topic-group-chatid';
280
- chatIdInput.dataset.groupChatId = chatId;
281
- chatIdInput.style.cssText = 'flex:1;font-size:11px;padding:5px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:3px;color:var(--text-1);font-family:monospace;';
282
-
283
- // Add topic to this group button
284
- const addTopicBtn = document.createElement('button');
285
- addTopicBtn.textContent = '➕';
286
- addTopicBtn.className = 'btn-ghost';
287
- addTopicBtn.title = 'Add topic to this group';
288
- addTopicBtn.style.cssText = 'font-size:14px;padding:4px 8px;width:32px;height:28px;';
289
- addTopicBtn.addEventListener('click', (e) => {
290
- e.preventDefault();
291
- addTgTopicToGroup(chatId);
292
- });
293
-
294
- // Delete group button
295
- const deleteGroupBtn = document.createElement('button');
296
- deleteGroupBtn.textContent = '🗑';
297
- deleteGroupBtn.className = 'btn-ghost';
298
- deleteGroupBtn.title = 'Delete entire group and all topics';
299
- deleteGroupBtn.style.cssText = 'font-size:14px;padding:4px 8px;width:32px;height:28px;';
300
- deleteGroupBtn.addEventListener('click', (e) => {
301
- e.preventDefault();
302
- e.stopPropagation();
303
- if (window.confirm(`Delete group ${chatId} and all topics?`)) {
304
- delete _tgSavedTopicRouting[String(chatId)];
305
- renderTgTopicsList();
306
- showNotification('Group removed - click Save to persist');
307
- }
308
- });
309
-
310
- groupHeader.appendChild(groupLabel);
311
- groupHeader.appendChild(chatIdInput);
312
- groupHeader.appendChild(addTopicBtn);
313
- groupHeader.appendChild(deleteGroupBtn);
314
- groupContainer.appendChild(groupHeader);
315
-
316
- // Topic rows under this group
317
- topics.forEach(topic => {
318
- const row = document.createElement('div');
319
- row.style.cssText = 'display:grid;grid-template-columns:80px 1fr 36px;gap:8px;align-items:center;padding:6px 8px;margin-bottom:4px;';
320
- row.dataset.idx = globalIdx;
321
- row.dataset.chatId = chatId;
322
- row.dataset.originalTopicId = topic.topicId; // Track for renames
323
-
324
- // Topic ID input
325
- const topicIdInput = document.createElement('input');
326
- topicIdInput.value = topic.topicId;
327
- topicIdInput.placeholder = 'Topic 5';
328
- topicIdInput.className = 'tg-topic-id';
329
- topicIdInput.style.cssText = 'font-size:11px;padding:5px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:3px;color:var(--text-1);font-family:monospace;text-align:center;';
330
-
331
- // Agent dropdown
332
- const agentSelect = document.createElement('select');
333
- agentSelect.className = 'tg-topic-agent';
334
- agentSelect.style.cssText = 'font-size:11px;padding:5px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:3px;color:var(--text-1);';
335
- agents.forEach(agent => {
336
- const opt = document.createElement('option');
337
- opt.value = agent;
338
- opt.textContent = agent;
339
- if (agent === topic.agent) opt.selected = true;
340
- agentSelect.appendChild(opt);
341
- });
342
-
343
- // Update state immediately when agent changes
344
- agentSelect.addEventListener('change', (e) => {
345
- const newAgent = e.target.value;
346
- const topicIdInput = row.querySelector('.tg-topic-id');
347
- const topicId = topicIdInput ? topicIdInput.value.trim() : topic.topicId;
348
-
349
- if (topicId && _tgSavedTopicRouting[chatId]) {
350
- _tgSavedTopicRouting[chatId][topicId] = newAgent;
351
-
352
- // Update JSON editor too
353
- const topicJsonEl = document.getElementById('tgTopicRoutingJson');
354
- if (topicJsonEl) {
355
- topicJsonEl.value = JSON.stringify(_tgSavedTopicRouting, null, 2);
356
- }
357
- }
358
- });
359
-
360
- // Remove button
361
- const removeBtn = document.createElement('button');
362
- removeBtn.textContent = '🗑';
363
- removeBtn.className = 'btn-ghost';
364
- removeBtn.title = 'Remove this topic';
365
- removeBtn.style.cssText = 'font-size:14px;padding:4px;width:28px;height:28px;';
366
- removeBtn.addEventListener('click', (e) => {
367
- e.preventDefault();
368
- e.stopPropagation();
369
- if (window.confirm(`Remove topic ${topic.topicId}?`)) {
370
- // Direct delete from nested structure (fast - no rebuild)
371
- if (_tgSavedTopicRouting[chatId] && _tgSavedTopicRouting[chatId][topic.topicId]) {
372
- delete _tgSavedTopicRouting[chatId][topic.topicId];
373
-
374
- // If group is now empty, remove the group key
375
- if (Object.keys(_tgSavedTopicRouting[chatId]).length === 0) {
376
- delete _tgSavedTopicRouting[chatId];
377
- }
378
-
379
- // Re-render and update JSON editor
380
- renderTgTopicsList();
381
- showNotification(`Topic ${topic.topicId} removed - click Save to persist`);
382
- }
383
- }
384
- });
385
-
386
- row.appendChild(topicIdInput);
387
- row.appendChild(agentSelect);
388
- row.appendChild(removeBtn);
389
- groupContainer.appendChild(row);
390
-
391
- globalIdx++;
392
- });
393
-
394
- listDiv.appendChild(groupContainer);
395
- });
396
-
397
- // Keep JSON editor in sync when state changes (e.g. after delete/add)
398
- const topicJsonEl = document.getElementById('tgTopicRoutingJson');
399
- if (topicJsonEl) {
400
- topicJsonEl.value = Object.keys(_tgSavedTopicRouting).length ? JSON.stringify(_tgSavedTopicRouting, null, 2) : '';
401
- }
402
- }
403
-
404
- export function addTgNewGroup() {
405
- // Add a brand new group
406
- const newGroupId = prompt('Enter Group ID (e.g., -100123456789):');
407
- if (!newGroupId || !newGroupId.trim()) return;
408
-
409
- const groupId = newGroupId.trim();
410
- if (!_tgSavedTopicRouting[groupId]) {
411
- _tgSavedTopicRouting[groupId] = {};
412
- }
413
-
414
- // Add first topic to the new group
415
- const newTopicId = '1';
416
- _tgSavedTopicRouting[groupId][newTopicId] = 'crew-lead';
417
- renderTgTopicsList();
418
- showNotification(`Added group ${groupId}`);
419
- }
420
-
421
- export function addTgTopicToGroup(chatId) {
422
- // Add topic to specific group
423
- if (!_tgSavedTopicRouting[chatId]) {
424
- _tgSavedTopicRouting[chatId] = {};
425
- }
426
-
427
- // Generate new topic ID (find highest + 1)
428
- const existingIds = Object.keys(_tgSavedTopicRouting[chatId]).map(id => parseInt(id, 10)).filter(n => !isNaN(n));
429
- const newTopicId = existingIds.length > 0 ? String(Math.max(...existingIds) + 1) : '1';
430
-
431
- _tgSavedTopicRouting[chatId][newTopicId] = 'crew-lead';
432
- renderTgTopicsList();
433
- showNotification(`Added topic ${newTopicId} to group`);
434
- }
435
-
436
- export function addTgTopicRow() {
437
- // Legacy: Add to first existing group
438
- const existingChatIds = Object.keys(_tgSavedTopicRouting).filter(k => !k.startsWith('_'));
439
- if (existingChatIds.length === 0) {
440
- // No groups exist, create one
441
- addTgNewGroup();
442
- return;
443
- }
444
-
445
- // Add to first group
446
- addTgTopicToGroup(existingChatIds[0]);
447
- }
448
-
449
- export function removeTgGroup(chatId) {
450
- // Remove entire group from topic routing
451
- delete _tgSavedTopicRouting[String(chatId)];
452
- renderTgTopicsList();
453
- showNotification(`Removed group ${chatId} - don't forget to Save config!`);
454
- }
455
-
456
- export function removeTgTopicRow(idx) {
457
- // Rebuild topic routing without this entry
458
- const topics = [];
459
- Object.entries(_tgSavedTopicRouting).forEach(([key, value]) => {
460
- if (key.startsWith('_')) return;
461
- if (typeof value === 'object') {
462
- Object.entries(value).forEach(([topicId, agent]) => {
463
- topics.push({ chatId: key, topicId, agent });
464
- });
465
- } else {
466
- const [chatId, topicId] = key.split(':');
467
- topics.push({ chatId, topicId, agent: value });
468
- }
469
- });
470
-
471
- topics.splice(idx, 1);
472
-
473
- // Rebuild nested format
474
- _tgSavedTopicRouting = {};
475
- topics.forEach(t => {
476
- if (!_tgSavedTopicRouting[t.chatId]) _tgSavedTopicRouting[t.chatId] = {};
477
- _tgSavedTopicRouting[t.chatId][t.topicId] = t.agent;
478
- });
479
-
480
- renderTgTopicsList();
481
- }
482
-
483
- export async function discoverTgTopics() {
484
- try {
485
- const allTopics = await getJSON('/api/telegram/discover-topics');
486
- if (!allTopics.length) {
487
- showNotification('No topics found in logs. Send messages to topics first.', true);
488
- return;
489
- }
490
-
491
- // Group discovered topics by chatId
492
- const groupedDiscovered = {};
493
- allTopics.forEach(t => {
494
- if (!groupedDiscovered[t.chatId]) {
495
- groupedDiscovered[t.chatId] = [];
496
- }
497
- groupedDiscovered[t.chatId].push(t);
498
- });
499
-
500
- // Build modal with checkboxes per-TOPIC (not per-group)
501
- const existingGroups = Object.keys(_tgSavedTopicRouting).filter(k => !k.startsWith('_'));
502
-
503
- let modalHtml = '<div style="max-height:400px;overflow-y:auto;">';
504
- Object.entries(groupedDiscovered).forEach(([chatId, topics]) => {
505
- modalHtml += `
506
- <div style="background:var(--bg-1);border:1px solid var(--border);border-radius:6px;padding:12px;margin-bottom:12px;">
507
- <div style="font-weight:600;margin-bottom:8px;font-family:monospace;font-size:12px;">
508
- Group ${chatId}
509
- </div>
510
- <div style="margin-left:12px;">
511
- `;
512
-
513
- // Checkbox for each topic
514
- topics.forEach(t => {
515
- const topicId = String(t.threadId);
516
- const alreadyExists = _tgSavedTopicRouting[chatId]?.[topicId];
517
- const checked = !alreadyExists ? 'checked' : '';
518
- const disabledLabel = alreadyExists ? ' <span style="font-size:10px;color:var(--text-3);">(already configured)</span>' : '';
519
-
520
- modalHtml += `
521
- <div style="margin-bottom:6px;display:flex;align-items:center;gap:8px;">
522
- <input type="checkbox" class="discover-topic-check" data-chat-id="${chatId}" data-thread-id="${topicId}" ${checked} ${alreadyExists ? 'disabled' : ''}>
523
- <span style="font-size:11px;color:var(--text-2);">Topic ${topicId}${disabledLabel}</span>
524
- </div>
525
- `;
526
- });
527
-
528
- modalHtml += `
529
- </div>
530
- </div>
531
- `;
532
- });
533
- modalHtml += '</div>';
534
-
535
- // Simple confirmation with HTML
536
- const modal = document.createElement('div');
537
- modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:9999;';
538
- modal.innerHTML = `
539
- <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:20px;max-width:600px;width:90%;">
540
- <h3 style="margin:0 0 16px;font-size:16px;">Discovered Topics</h3>
541
- <div style="font-size:11px;color:var(--text-3);margin-bottom:12px;">
542
- ✓ Select which topics to add. Already configured topics are disabled.
543
- </div>
544
- ${modalHtml}
545
- <div style="display:flex;gap:8px;margin-top:16px;justify-content:flex-end;">
546
- <button class="btn-ghost" id="discoverCancel">Cancel</button>
547
- <button class="btn-primary" id="discoverConfirm">Add Selected</button>
548
- </div>
549
- </div>
550
- `;
551
- document.body.appendChild(modal);
552
-
553
- document.getElementById('discoverCancel').onclick = () => {
554
- document.body.removeChild(modal);
555
- };
556
-
557
- document.getElementById('discoverConfirm').onclick = () => {
558
- const checkedTopics = Array.from(modal.querySelectorAll('.discover-topic-check:checked'));
559
- let addedCount = 0;
560
-
561
- checkedTopics.forEach(checkbox => {
562
- const chatId = checkbox.dataset.chatId;
563
- const threadId = checkbox.dataset.threadId;
564
-
565
- if (!_tgSavedTopicRouting[chatId]) {
566
- _tgSavedTopicRouting[chatId] = {};
567
- }
568
-
569
- if (!_tgSavedTopicRouting[chatId][threadId]) {
570
- _tgSavedTopicRouting[chatId][threadId] = 'crew-lead';
571
- addedCount++;
572
- }
573
- });
574
-
575
- document.body.removeChild(modal);
576
- renderTgTopicsList();
577
- showNotification(`Added ${addedCount} new topic${addedCount !== 1 ? 's' : ''}! Set agents and click Save.`);
578
- };
579
- } catch (e) {
580
- showNotification('Error discovering topics: ' + e.message, true);
581
- }
582
- }
583
-
584
- export async function saveTgConfig() {
585
- const token = document.getElementById('tgTokenInput').value.trim();
586
- const idsRaw = document.getElementById('tgAllowedIds').value.trim();
587
- const allowedChatIds = idsRaw
588
- ? idsRaw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n))
589
- : [];
590
- if (!token) { showNotification('Enter a bot token first', true); return; }
591
- const contactNames = {};
592
- const userRouting = {};
593
-
594
- allowedChatIds.forEach(id => {
595
- // Contact name
596
- const nameEl = document.getElementById('tgContact-' + id);
597
- if (nameEl && nameEl.value.trim()) contactNames[String(id)] = nameEl.value.trim();
598
-
599
- // Per-user routing
600
- const routeEl = document.getElementById('tgRoute-' + id);
601
- if (routeEl && routeEl.value) {
602
- userRouting[String(id)] = routeEl.value;
603
- }
604
- });
605
-
606
- // Collect topic routing from in-memory state (already updated by delete/add)
607
- // Sync any edits from DOM inputs back to state (only update values, don't rebuild)
608
- const listDiv = document.getElementById('tgTopicsList');
609
- if (listDiv) {
610
- const groups = listDiv.querySelectorAll('.tg-topic-group');
611
- groups.forEach(groupContainer => {
612
- const chatIdInput = groupContainer.querySelector('[data-group-chat-id]');
613
- if (!chatIdInput) return;
614
- const originalChatId = chatIdInput.dataset.groupChatId;
615
- const newChatId = chatIdInput.value.trim();
616
-
617
- // If chatId changed, rename the key in state
618
- if (originalChatId && newChatId && originalChatId !== newChatId) {
619
- if (_tgSavedTopicRouting[originalChatId]) {
620
- _tgSavedTopicRouting[newChatId] = _tgSavedTopicRouting[originalChatId];
621
- delete _tgSavedTopicRouting[originalChatId];
622
- }
623
- }
624
-
625
- // Update topic IDs and agents from DOM (sync all visible rows)
626
- const targetChatId = newChatId || originalChatId;
627
- if (!targetChatId) return;
628
-
629
- // Make sure group exists in state
630
- if (!_tgSavedTopicRouting[targetChatId]) {
631
- _tgSavedTopicRouting[targetChatId] = {};
632
- }
633
-
634
- const rows = groupContainer.querySelectorAll('[data-chat-id]');
635
- rows.forEach(row => {
636
- const topicIdEl = row.querySelector('.tg-topic-id');
637
- const agentEl = row.querySelector('.tg-topic-agent');
638
- if (topicIdEl && agentEl) {
639
- const oldTopicId = row.dataset.originalTopicId || topicIdEl.value.trim();
640
- const newTopicId = topicIdEl.value.trim();
641
- const newAgent = agentEl.value;
642
-
643
- if (!newTopicId) return; // Skip empty topic IDs
644
-
645
- // If topic ID changed, update the key
646
- if (oldTopicId && newTopicId && oldTopicId !== newTopicId) {
647
- if (_tgSavedTopicRouting[targetChatId][oldTopicId]) {
648
- delete _tgSavedTopicRouting[targetChatId][oldTopicId];
649
- }
650
- _tgSavedTopicRouting[targetChatId][newTopicId] = newAgent;
651
- } else {
652
- // Update or create the agent mapping
653
- _tgSavedTopicRouting[targetChatId][newTopicId] = newAgent;
654
- }
655
- }
656
- });
657
- });
658
- }
659
-
660
- // Use the in-memory state (respects deletions)
661
- const topicRouting = { ..._tgSavedTopicRouting };
662
-
663
- // Also check JSON editor (advanced mode) if it has content
664
- const topicJsonEl = document.getElementById('tgTopicRoutingJson');
665
- if (topicJsonEl && topicJsonEl.value.trim()) {
666
- try {
667
- const jsonParsed = JSON.parse(topicJsonEl.value.trim());
668
- // Merge with form data (JSON takes precedence)
669
- Object.assign(topicRouting, jsonParsed);
670
- } catch (e) {
671
- // Ignore JSON parse errors if form data exists
672
- if (Object.keys(topicRouting).length === 0) {
673
- showNotification('Invalid topic routing JSON: ' + e.message, true);
674
- return;
675
- }
676
- }
677
- }
678
-
679
- _tgSavedContactNames = contactNames;
680
- _tgSavedUserRouting = userRouting;
681
- _tgSavedTopicRouting = topicRouting;
682
-
683
- await postJSON('/api/telegram/config', { token, targetAgent: 'crew-lead', allowedChatIds, contactNames, userRouting, topicRouting });
684
- showNotification('Telegram config saved');
685
- renderTgContactRows();
686
- renderTgTopicsList();
687
- }
688
-
689
- export async function startTgBridge() {
690
- const token = document.getElementById('tgTokenInput').value.trim();
691
- const body = { targetAgent: 'crew-lead' };
692
- if (token) body.token = token;
693
- const r = await postJSON('/api/telegram/start', body);
694
- if (r && r.error) { showNotification(r.error, true); return; }
695
- showNotification(r && r.message === 'Already running' ? 'Already running' : 'Telegram bridge starting...');
696
- setTimeout(loadTgStatus, 2000);
697
- }
698
-
699
- export async function stopTgBridge() {
700
- await postJSON('/api/telegram/stop', {});
701
- showNotification('Telegram bridge stopped');
702
- setTimeout(loadTgStatus, 1000);
703
- }
704
-
705
- export async function loadWaStatus() {
706
- try {
707
- const d = await getJSON('/api/whatsapp/status');
708
- const badge = document.getElementById('waStatusBadge');
709
- if (!badge) return;
710
- if (d.running) {
711
- badge.textContent = d.number ? '● +' + d.number : '● running';
712
- badge.className = 'status-badge status-active';
713
- } else {
714
- badge.textContent = '● stopped';
715
- badge.className = 'status-badge status-stopped';
716
- }
717
- const authEl = document.getElementById('waAuthStatus');
718
- if (authEl) {
719
- authEl.textContent = d.authSaved
720
- ? '✅ Auth saved — no QR scan needed on restart'
721
- : '⚠️ No auth saved — run npm run whatsapp in terminal to scan QR';
722
- }
723
- } catch {}
724
- }
725
-
726
- export function renderWaContactRows() {
727
- const listEl = document.getElementById('waContactNamesList');
728
- if (!listEl) return;
729
- const raw = (document.getElementById('waAllowedNumbers')?.value || '').trim();
730
- const numbers = raw ? raw.split(',').map(s => s.trim()).filter(Boolean) : [];
731
- listEl.innerHTML = '';
732
- if (!numbers.length) return;
733
- const title = document.createElement('label');
734
- title.style.cssText = 'display:block;margin-bottom:6px;font-size:12px;color:var(--text-2);';
735
- title.textContent = 'Contact names (address book)';
736
- listEl.appendChild(title);
737
- numbers.forEach(num => {
738
- const key = num.replace(/\D/g, '');
739
-
740
- // Container for 2-line layout
741
- const container = document.createElement('div');
742
- container.style.cssText = 'margin-bottom:12px;padding:12px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;';
743
-
744
- // Line 1: Number + Name
745
- const row1 = document.createElement('div');
746
- row1.style.cssText = 'display:grid;grid-template-columns:140px 1fr;gap:8px;margin-bottom:8px;align-items:center;';
747
-
748
- const span = document.createElement('span');
749
- span.style.cssText = 'font-size:11px;color:var(--text-3);font-family:monospace;';
750
- span.textContent = num;
751
-
752
- const input = document.createElement('input');
753
- input.id = 'waContact-' + key;
754
- input.placeholder = 'Name (e.g. Jeff)';
755
- input.value = _waSavedContactNames[key] || _waSavedContactNames[num] || '';
756
- input.style.cssText = 'font-size:12px;padding:6px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;color:var(--text-1);';
757
-
758
- row1.appendChild(span);
759
- row1.appendChild(input);
760
-
761
- // Line 2: Routing label + dropdown
762
- const row2 = document.createElement('div');
763
- row2.style.cssText = 'display:grid;grid-template-columns:140px 1fr;gap:8px;align-items:center;';
764
-
765
- const routeLabel = document.createElement('span');
766
- routeLabel.style.cssText = 'font-size:10px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em;';
767
- routeLabel.textContent = 'Routes to →';
768
-
769
- const routeSelect = document.createElement('select');
770
- routeSelect.id = 'waRoute-' + key;
771
- routeSelect.style.cssText = 'font-size:12px;padding:6px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;color:var(--text-1);';
772
-
773
- // Get current routing for this number (check multiple formats)
774
- const currentRoute = _waSavedUserRouting[num] || _waSavedUserRouting['+' + key] || _waSavedUserRouting[key] || '';
775
-
776
- const agents = [
777
- 'crew-lead', 'crew-main', 'crew-coder', 'crew-pm', 'crew-qa',
778
- 'crew-fixer', 'crew-security', 'crew-frontend', 'crew-coder-front',
779
- 'crew-coder-back', 'crew-github', 'crew-copywriter', 'crew-researcher',
780
- 'crew-architect', 'crew-seo', 'crew-ml', 'crew-mega', 'crew-loco'
781
- ];
782
-
783
- // Default option
784
- const defaultOpt = document.createElement('option');
785
- defaultOpt.value = '';
786
- defaultOpt.textContent = '— default (see above) —';
787
- routeSelect.appendChild(defaultOpt);
788
-
789
- // Agent options
790
- agents.forEach(agent => {
791
- const opt = document.createElement('option');
792
- opt.value = agent;
793
- opt.textContent = agent;
794
- if (agent === currentRoute) opt.selected = true;
795
- routeSelect.appendChild(opt);
796
- });
797
-
798
- // Update state immediately when routing changes
799
- routeSelect.addEventListener('change', (e) => {
800
- const newAgent = e.target.value;
801
- if (newAgent) {
802
- _waSavedUserRouting[num] = newAgent;
803
- } else {
804
- delete _waSavedUserRouting[num];
805
- }
806
- });
807
-
808
- row2.appendChild(routeLabel);
809
- row2.appendChild(routeSelect);
810
-
811
- container.appendChild(row1);
812
- container.appendChild(row2);
813
- listEl.appendChild(container);
814
- });
815
- }
816
-
817
- export async function loadWaConfig() {
818
- try {
819
- const d = await getJSON('/api/whatsapp/config');
820
- const n = document.getElementById('waAllowedNumbers');
821
- const t = document.getElementById('waTargetAgent');
822
- _waSavedContactNames = d.contactNames || {};
823
- _waSavedUserRouting = d.userRouting || {};
824
- if (n) n.value = (d.allowedNumbers || []).join(', ');
825
- if (t) t.value = d.targetAgent || 'crew-lead';
826
- renderWaContactRows();
827
- } catch {}
828
- }
829
-
830
- export async function saveWaConfig() {
831
- const numbersRaw = document.getElementById('waAllowedNumbers').value.trim();
832
- const allowedNumbers = numbersRaw ? numbersRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
833
- const targetAgent = (document.getElementById('waTargetAgent').value.trim()) || 'crew-lead';
834
- const contactNames = {};
835
- const userRouting = {};
836
-
837
- allowedNumbers.forEach(num => {
838
- const key = num.replace(/\D/g, '');
839
-
840
- // Contact name
841
- const nameEl = document.getElementById('waContact-' + key);
842
- if (nameEl && nameEl.value.trim()) contactNames[key] = nameEl.value.trim();
843
-
844
- // Per-user routing
845
- const routeEl = document.getElementById('waRoute-' + key);
846
- if (routeEl && routeEl.value) {
847
- userRouting[num] = routeEl.value;
848
- }
849
- });
850
-
851
- _waSavedContactNames = contactNames;
852
- _waSavedUserRouting = userRouting;
853
-
854
- await postJSON('/api/whatsapp/config', { allowedNumbers, targetAgent, contactNames, userRouting });
855
- showNotification('WhatsApp config saved');
856
- renderWaContactRows();
857
- }
858
-
859
- export async function startWaBridge() {
860
- const r = await postJSON('/api/whatsapp/start', {});
861
- if (r && r.error) { showNotification(r.error, true); return; }
862
- showNotification(r && r.message === 'Already running' ? 'Already running' : 'WhatsApp bridge starting…');
863
- setTimeout(loadWaStatus, 2000);
864
- }
865
-
866
- export async function stopWaBridge() {
867
- await postJSON('/api/whatsapp/stop', {});
868
- showNotification('WhatsApp bridge stopped');
869
- setTimeout(loadWaStatus, 1000);
870
- }
871
-
872
- export async function loadWaMessages() {
873
- const feed = document.getElementById('waMessageFeed');
874
- if (!feed) return;
875
- try {
876
- const msgs = await getJSON('/api/whatsapp/messages');
877
- if (!msgs.length) {
878
- feed.innerHTML = '<div class="meta" style="padding:20px;text-align:center;">No messages yet. Send a WhatsApp message to your linked number.</div>';
879
- return;
880
- }
881
- feed.innerHTML = msgs.slice(-50).reverse().map(m => {
882
- const isIn = m.direction === 'inbound';
883
- const time = m.ts ? new Date(m.ts).toLocaleTimeString() : '';
884
- const number = (m.jid || '').split('@')[0] || '';
885
- return '<div style="display:flex;gap:10px;padding:8px;background:var(--bg-2);border-radius:6px;align-items:flex-start;">' +
886
- '<span style="font-size:18px;">' + (isIn ? '📲' : '🤖') + '</span>' +
887
- '<div style="flex:1;min-width:0;">' +
888
- '<div style="font-size:11px;color:var(--text-3);margin-bottom:2px;">' +
889
- escHtml(isIn ? ('+' + number) : 'crewswarm') + (time ? ' · ' + time : '') +
890
- '</div>' +
891
- '<div style="font-size:13px;word-break:break-word;">' + escHtml((m.text || '').slice(0, 300)) + '</div>' +
892
- '</div>' +
893
- '</div>';
894
- }).join('');
895
- } catch {
896
- feed.innerHTML = '<div style="color:var(--text-3);font-size:12px;padding:8px;">Could not load messages.</div>';
897
- }
898
- }
899
-
900
- export async function loadTgMessages() {
901
- const feed = document.getElementById('tgMessageFeed');
902
- if (!feed) return;
903
- try {
904
- const msgs = await getJSON('/api/telegram/messages');
905
- if (!msgs.length) {
906
- feed.innerHTML = '<div class="meta" style="padding:20px;text-align:center;">No messages yet. Send something to your bot on Telegram.</div>';
907
- return;
908
- }
909
- feed.innerHTML = msgs.slice(-50).reverse().map(m => {
910
- const isIn = m.direction === 'inbound';
911
- const time = m.ts ? new Date(m.ts).toLocaleTimeString() : '';
912
- const who = isIn ? (m.firstName || m.username || 'User') : 'crewswarm';
913
- const icon = isIn ? '👤' : '⚡';
914
- return '<div class="card" style="padding:12px;gap:4px;display:flex;flex-direction:column;">' +
915
- '<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--text-3);">' +
916
- '<span>' + icon + ' ' + escHtml(who) + (m.username ? ' @' + escHtml(m.username) : '') + '</span>' +
917
- '<span>' + time + '</span></div>' +
918
- '<div style="font-size:13px;white-space:pre-wrap;">' + escHtml(m.text || '') + '</div>' +
919
- '</div>';
920
- }).join('');
921
- } catch {
922
- feed.innerHTML = '<div class="meta" style="padding:20px;color:var(--red-hi);">Error loading messages</div>';
923
- }
924
- }
925
-
926
- export async function loadTelegramSessions() {
927
- const box = document.getElementById('tgSessionsList');
928
- if (!box) return;
929
- const sessions = await getJSON('/api/telegram-sessions').catch(() => []);
930
- box.innerHTML = '';
931
- if (!sessions.length) {
932
- box.innerHTML = '<div style="color:var(--text-3);font-size:12px;padding:8px;">No Telegram sessions yet — send a message to your bot to start one.</div>';
933
- return;
934
- }
935
- for (const s of sessions) {
936
- const card = document.createElement('div');
937
- card.style.cssText = 'background:var(--bg-1);border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:10px;';
938
- const ago = s.lastTs ? Math.round((Date.now() - s.lastTs) / 60000) + 'm ago' : 'unknown';
939
- const msgLines = s.messages.slice(-6).map(m => {
940
- const color = m.role === 'user' ? 'var(--accent)' : 'var(--green)';
941
- const icon = m.role === 'user' ? '👤' : '🤖';
942
- const txt = String(m.content || '').slice(0, 100).replace(/</g, '&lt;');
943
- return '<div style="margin-bottom:4px;"><span style="color:' + color + ';">' + icon + '</span> <span>' + txt + '</span></div>';
944
- }).join('');
945
- card.innerHTML =
946
- '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
947
- '<span style="font-size:13px;font-weight:600;">chat ' + s.chatId + '</span>' +
948
- '<span style="font-size:11px;color:var(--text-3);">' + s.messageCount + ' msgs · ' + ago + '</span>' +
949
- '</div>' +
950
- '<div style="font-size:12px;color:var(--text-2);border-top:1px solid var(--border);padding-top:8px;max-height:120px;overflow-y:auto;">' +
951
- msgLines +
952
- '</div>';
953
- box.appendChild(card);
954
- }
955
- }