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,654 +0,0 @@
1
- /**
2
- * Contacts Tab — Unified contact management across WhatsApp, Telegram, etc.
3
- * Deps: getJSON, postJSON (core/api), escHtml, showNotification (core/dom), state (core/state)
4
- */
5
-
6
- import { getJSON, postJSON } from '../core/api.js';
7
- import { escHtml, showNotification } from '../core/dom.js';
8
- import { state } from '../core/state.js';
9
-
10
- let _contactsData = {};
11
- let _allContacts = [];
12
- let _currentFilters = {
13
- search: '',
14
- platform: '',
15
- sortBy: 'name',
16
- letter: 'all'
17
- };
18
-
19
- export function showContacts() {
20
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
21
- document.getElementById('contactsView').classList.add('active');
22
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
23
- const nav = document.getElementById('navContacts');
24
- if (nav) nav.classList.add('active');
25
- loadContacts();
26
- }
27
-
28
- // ── Load contacts ─────────────────────────────────────────────────────────────
29
-
30
- export async function loadContacts() {
31
- const list = document.getElementById('contactsList');
32
-
33
- list.innerHTML = '<div class="meta" style="padding:20px;">Loading contacts...</div>';
34
-
35
- try {
36
- const data = await getJSON('/api/contacts');
37
- const contacts = data.contacts || [];
38
-
39
- _contactsData = {};
40
- contacts.forEach(c => { _contactsData[c.contact_id] = c; });
41
- _allContacts = contacts;
42
-
43
- if (!contacts.length) {
44
- list.innerHTML = '<div class="meta" style="padding:20px;">No contacts yet. Click "+ New Contact" to add manually, or contacts are created automatically when someone messages the bot.</div>';
45
- document.getElementById('contactsCount').innerHTML = '';
46
- return;
47
- }
48
-
49
- applyFiltersAndRender();
50
-
51
- } catch(e) {
52
- list.innerHTML = '<div class="meta" style="padding:20px;color:var(--red-hi);">Failed to load contacts: ' + escHtml(e.message) + '</div>';
53
- }
54
- }
55
-
56
- // ── Filtering and Sorting ─────────────────────────────────────────────────────
57
-
58
- export function applyContactFilters() {
59
- const searchInput = document.getElementById('contactsSearch');
60
- const platformFilter = document.getElementById('contactsPlatformFilter');
61
- const sortBy = document.getElementById('contactsSortBy');
62
-
63
- _currentFilters.search = searchInput.value.toLowerCase().trim();
64
- _currentFilters.platform = platformFilter.value;
65
- _currentFilters.sortBy = sortBy.value;
66
-
67
- applyFiltersAndRender();
68
- }
69
-
70
- export function filterByLetter(letter) {
71
- _currentFilters.letter = letter;
72
-
73
- // Update active button
74
- document.querySelectorAll('.alpha-filter').forEach(btn => {
75
- btn.classList.remove('active');
76
- btn.style.background = 'var(--bg-1)';
77
- btn.style.color = 'var(--text-2)';
78
- });
79
- const activeBtn = document.querySelector(`[data-letter="${letter}"]`);
80
- if (activeBtn) {
81
- activeBtn.classList.add('active');
82
- activeBtn.style.background = 'var(--purple)';
83
- activeBtn.style.color = '#fff';
84
- }
85
-
86
- applyFiltersAndRender();
87
- }
88
-
89
- function applyFiltersAndRender() {
90
- let filtered = [..._allContacts];
91
-
92
- // Filter by search term
93
- if (_currentFilters.search) {
94
- filtered = filtered.filter(c => {
95
- const searchText = `${c.display_name} ${c.phone_number || ''} ${c.email || ''} ${c.notes || ''} ${JSON.stringify(c.preferences)} ${JSON.stringify(c.tags)}`.toLowerCase();
96
- return searchText.includes(_currentFilters.search);
97
- });
98
- }
99
-
100
- // Filter by platform
101
- if (_currentFilters.platform) {
102
- filtered = filtered.filter(c => {
103
- if (c.platform === _currentFilters.platform) return true;
104
- const links = c.platform_links || {};
105
- return !!links[_currentFilters.platform];
106
- });
107
- }
108
-
109
- // Filter by letter
110
- if (_currentFilters.letter !== 'all') {
111
- filtered = filtered.filter(c => {
112
- const firstChar = (c.display_name || '').charAt(0).toUpperCase();
113
- if (_currentFilters.letter === '#') {
114
- return !/[A-Z]/.test(firstChar);
115
- }
116
- return firstChar === _currentFilters.letter;
117
- });
118
- }
119
-
120
- // Sort
121
- if (_currentFilters.sortBy === 'name') {
122
- filtered.sort((a, b) => {
123
- const nameA = (a.display_name || a.phone_number || '').toLowerCase();
124
- const nameB = (b.display_name || b.phone_number || '').toLowerCase();
125
- return nameA.localeCompare(nameB);
126
- });
127
- } else if (_currentFilters.sortBy === 'recent') {
128
- filtered.sort((a, b) => b.last_seen - a.last_seen);
129
- } else if (_currentFilters.sortBy === 'messages') {
130
- filtered.sort((a, b) => b.message_count - a.message_count);
131
- }
132
-
133
- // Update count
134
- const countEl = document.getElementById('contactsCount');
135
- if (countEl) {
136
- const total = _allContacts.length;
137
- const showing = filtered.length;
138
- if (showing === total) {
139
- countEl.innerHTML = `Showing all ${total} contacts`;
140
- } else {
141
- countEl.innerHTML = `Showing ${showing} of ${total} contacts`;
142
- }
143
- }
144
-
145
- renderContactsList(filtered);
146
- }
147
-
148
- // ── Render ────────────────────────────────────────────────────────────────────
149
-
150
- function renderContactsList(contacts) {
151
- const list = document.getElementById('contactsList');
152
-
153
- if (!contacts.length) {
154
- list.innerHTML = '<div class="meta" style="padding:20px;">No contacts match your filters.</div>';
155
- return;
156
- }
157
-
158
- list.innerHTML = contacts.map(c => {
159
- const id = c.contact_id;
160
- const safeId = escHtml(id);
161
- const name = escHtml(c.display_name || c.phone_number || 'Unknown');
162
- const platformLinks = c.platform_links || {};
163
- const preferences = c.preferences || {};
164
- const tags = c.tags || [];
165
-
166
- // Platform badges
167
- const platforms = [];
168
- if (c.platform === 'whatsapp' || platformLinks.whatsapp) platforms.push('<span style="background:#25D366;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">WhatsApp</span>');
169
- if (c.platform === 'telegram' || platformLinks.telegram) platforms.push('<span style="background:#0088cc;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Telegram</span>');
170
- if (c.platform === 'twitter' || platformLinks.twitter) platforms.push('<span style="background:#1DA1F2;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Twitter</span>');
171
- if (c.platform === 'instagram' || platformLinks.instagram) platforms.push('<span style="background:#E4405F;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Instagram</span>');
172
- if (c.platform === 'tiktok' || platformLinks.tiktok) platforms.push('<span style="background:#000000;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">TikTok</span>');
173
- if (c.platform === 'slack' || platformLinks.slack) platforms.push('<span style="background:#4A154B;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Slack</span>');
174
- if (c.platform === 'web' || platformLinks.web) platforms.push('<span style="background:#666;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Web</span>');
175
- if (c.platform === 'website' || platformLinks.website) platforms.push('<span style="background:#666;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Website</span>');
176
-
177
- // Last seen
178
- const lastSeenText = formatRelativeTime(c.last_seen);
179
-
180
- // Preferences preview (compact)
181
- const prefPreview = [];
182
- if (preferences?.diet) prefPreview.push(`🍴 ${escHtml(preferences.diet)}`);
183
- if (preferences?.allergies?.length) prefPreview.push(`⚠️ ${escHtml(preferences.allergies.join(', '))}`);
184
- if (preferences?.spiceLevel) prefPreview.push(`🌶️ ${escHtml(preferences.spiceLevel)}`);
185
-
186
- // Tags
187
- const tagHtml = tags.length ? tags.map(t => `<span style="background:var(--bg-1);color:var(--text-3);padding:2px 6px;border-radius:4px;font-size:10px;">${escHtml(t)}</span>`).join(' ') : '';
188
-
189
- // Expandable details
190
- const phone = c.phone_number ? `<div><strong>Phone:</strong> ${escHtml(c.phone_number)}</div>` : '';
191
- const email = c.email ? `<div><strong>Email:</strong> ${escHtml(c.email)}</div>` : '';
192
- const notes = c.notes ? `<div style="margin-top:8px;"><strong>Notes:</strong> ${escHtml(c.notes)}</div>` : '';
193
- const allPrefs = preferences && Object.keys(preferences).length > 0
194
- ? `<div style="margin-top:8px;"><strong>Preferences:</strong> <pre style="font-size:11px;margin-top:4px;background:var(--bg-1);padding:8px;border-radius:4px;overflow:auto;">${escHtml(JSON.stringify(preferences, null, 2))}</pre></div>`
195
- : '';
196
-
197
- return `
198
- <div class="card contact-card" id="contact-card-${safeId}" data-contact-id="${safeId}">
199
- <div id="contact-view-${safeId}">
200
- <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
201
- <div style="flex:1;">
202
- <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px;">
203
- <strong style="font-size:15px;">${name}</strong>
204
- ${platforms.join(' ')}
205
- </div>
206
- <div class="meta" style="font-size:12px;">
207
- ${c.message_count} messages · Last seen: ${lastSeenText}
208
- </div>
209
- </div>
210
- </div>
211
-
212
- ${prefPreview.length ? `<div style="margin-bottom:10px;font-size:12px;color:var(--text-2);">${prefPreview.join(' · ')}</div>` : ''}
213
- ${tagHtml ? `<div style="margin-bottom:10px;display:flex;gap:4px;flex-wrap:wrap;">${tagHtml}</div>` : ''}
214
-
215
- <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
216
- <button data-action="send-message" data-id="${safeId}" class="btn-green" style="font-size:12px;">💬 Send Message</button>
217
- <button data-action="toggle-details" data-id="${safeId}" class="btn-ghost" style="font-size:12px;">📋 Details</button>
218
- <button data-action="edit" data-id="${safeId}" class="btn-ghost" style="font-size:12px;">✏️ Edit</button>
219
- <button data-action="delete" data-id="${safeId}" style="background:transparent;color:var(--text-3);border:1px solid var(--border);border-radius:6px;padding:4px 10px;cursor:pointer;font-size:12px;">🗑 Delete</button>
220
- </div>
221
- </div>
222
-
223
- <!-- Expandable details -->
224
- <div id="contact-details-${safeId}" style="display:none;margin-top:14px;padding-top:14px;border-top:1px solid var(--border);font-size:13px;color:var(--text-2);">
225
- ${phone}
226
- ${email}
227
- ${notes}
228
- ${allPrefs}
229
- </div>
230
-
231
- <!-- Edit form -->
232
- <div id="contact-edit-${safeId}" style="display:none;padding:12px;border-top:1px solid var(--border);margin-top:12px;">
233
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Display Name</label><input id="contact-name-${safeId}" type="text" value="${escHtml(c.display_name || '')}" style="margin-top:4px;width:100%;" /></div>
234
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Phone</label><input id="contact-phone-${safeId}" type="text" value="${escHtml(c.phone_number || '')}" style="margin-top:4px;width:100%;" /></div>
235
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Email</label><input id="contact-email-${safeId}" type="text" value="${escHtml(c.email || '')}" style="margin-top:4px;width:100%;" /></div>
236
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">📍 Location</label><input id="contact-location-${safeId}" type="text" value="${escHtml(c.last_location || '')}" placeholder="Grand Bend, Ontario, Canada" style="margin-top:4px;width:100%;" /></div>
237
- <div style="margin-bottom:12px;padding:12px;background:var(--bg-1);border-radius:6px;">
238
- <label style="font-size:12px;color:var(--text-2);display:block;margin-bottom:8px;font-weight:600;">🔗 Platform IDs</label>
239
- <div style="font-size:10px;color:var(--text-3);margin-bottom:12px;">Primary: ${escHtml(c.contact_id)}</div>
240
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
241
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Telegram</label><input id="platform-telegram-${safeId}" type="text" placeholder="12345678" value="${escHtml(platformLinks.telegram || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
242
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">WhatsApp</label><input id="platform-whatsapp-${safeId}" type="text" placeholder="1234@s.whatsapp.net" value="${escHtml(platformLinks.whatsapp || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
243
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Twitter</label><input id="platform-twitter-${safeId}" type="text" placeholder="@username" value="${escHtml(platformLinks.twitter || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
244
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Instagram</label><input id="platform-instagram-${safeId}" type="text" placeholder="@username" value="${escHtml(platformLinks.instagram || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
245
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">TikTok</label><input id="platform-tiktok-${safeId}" type="text" placeholder="@username" value="${escHtml(platformLinks.tiktok || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
246
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Slack</label><input id="platform-slack-${safeId}" type="text" placeholder="U01234ABC" value="${escHtml(platformLinks.slack || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
247
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Website</label><input id="platform-website-${safeId}" type="text" placeholder="https://example.com" value="${escHtml(platformLinks.website || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
248
- <div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Other</label><input id="platform-other-${safeId}" type="text" placeholder="Custom ID" value="${escHtml(platformLinks.other || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
249
- </div>
250
- </div>
251
-
252
- <!-- Preferences Section -->
253
- <div style="margin-bottom:12px;padding:12px;background:var(--bg-1);border-radius:6px;">
254
- <label style="font-size:12px;color:var(--text-2);display:block;margin-bottom:8px;font-weight:600;">🍴 Preferences</label>
255
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
256
- <div>
257
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Diet</label>
258
- <select id="pref-diet-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
259
- <option value="">None</option>
260
- <option value="omnivore" ${preferences?.diet === 'omnivore' ? 'selected' : ''}>Omnivore</option>
261
- <option value="vegetarian" ${preferences?.diet === 'vegetarian' ? 'selected' : ''}>Vegetarian</option>
262
- <option value="vegan" ${preferences?.diet === 'vegan' ? 'selected' : ''}>Vegan</option>
263
- <option value="pescatarian" ${preferences?.diet === 'pescatarian' ? 'selected' : ''}>Pescatarian</option>
264
- </select>
265
- </div>
266
- <div>
267
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Spice Level</label>
268
- <select id="pref-spice-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
269
- <option value="">Any</option>
270
- <option value="mild" ${preferences?.spiceLevel === 'mild' ? 'selected' : ''}>Mild</option>
271
- <option value="medium" ${preferences?.spiceLevel === 'medium' ? 'selected' : ''}>Medium</option>
272
- <option value="hot" ${preferences?.spiceLevel === 'hot' ? 'selected' : ''}>Hot</option>
273
- </select>
274
- </div>
275
- <div>
276
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Budget</label>
277
- <select id="pref-budget-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
278
- <option value="">Any</option>
279
- <option value="budget" ${preferences?.budget === 'budget' ? 'selected' : ''}>Budget ($)</option>
280
- <option value="moderate" ${preferences?.budget === 'moderate' ? 'selected' : ''}>Moderate ($$)</option>
281
- <option value="upscale" ${preferences?.budget === 'upscale' ? 'selected' : ''}>Upscale ($$$)</option>
282
- </select>
283
- </div>
284
- <div>
285
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Atmosphere</label>
286
- <select id="pref-atmosphere-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
287
- <option value="">Any</option>
288
- <option value="quiet" ${preferences?.atmosphere === 'quiet' ? 'selected' : ''}>Quiet</option>
289
- <option value="lively" ${preferences?.atmosphere === 'lively' ? 'selected' : ''}>Lively</option>
290
- <option value="romantic" ${preferences?.atmosphere === 'romantic' ? 'selected' : ''}>Romantic</option>
291
- <option value="family-friendly" ${preferences?.atmosphere === 'family-friendly' ? 'selected' : ''}>Family-friendly</option>
292
- </select>
293
- </div>
294
- <div style="grid-column:1/-1;">
295
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Allergies (comma-separated)</label>
296
- <input id="pref-allergies-${safeId}" type="text" placeholder="shellfish, peanuts, gluten, dairy" value="${escHtml((preferences?.allergies || []).join(', '))}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" />
297
- </div>
298
- <div style="grid-column:1/-1;">
299
- <label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Favorite Cuisines (comma-separated)</label>
300
- <input id="pref-cuisines-${safeId}" type="text" placeholder="Thai, Mexican, Italian, Japanese" value="${escHtml((preferences?.favCuisines || []).join(', '))}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" />
301
- </div>
302
- </div>
303
- </div>
304
-
305
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Notes</label><textarea id="contact-notes-${safeId}" rows="3" style="margin-top:4px;width:100%;">${escHtml(c.notes || '')}</textarea></div>
306
- <div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Tags (comma-separated)</label><input id="contact-tags-${safeId}" type="text" value="${escHtml(tags.join(', '))}" style="margin-top:4px;width:100%;" /></div>
307
- <div style="display:flex;gap:8px;">
308
- <button data-action="save-edit" data-id="${safeId}" class="btn-green" style="font-size:12px;">💾 Save</button>
309
- <button data-action="cancel-edit" data-id="${safeId}" class="btn-ghost" style="font-size:12px;">Cancel</button>
310
- </div>
311
- </div>
312
- </div>
313
- `;
314
- }).join('');
315
- }
316
-
317
- // ── Actions ───────────────────────────────────────────────────────────────────
318
-
319
- export function toggleContactDetails(contactId) {
320
- const detailsEl = document.getElementById('contact-details-' + contactId);
321
- if (!detailsEl) return;
322
- const isVisible = detailsEl.style.display !== 'none';
323
- detailsEl.style.display = isVisible ? 'none' : 'block';
324
- }
325
-
326
- export function toggleContactEdit(contactId) {
327
- const viewEl = document.getElementById('contact-view-' + contactId);
328
- const editEl = document.getElementById('contact-edit-' + contactId);
329
- const detailsEl = document.getElementById('contact-details-' + contactId);
330
- if (!viewEl || !editEl) return;
331
- const isEditing = editEl.style.display !== 'none';
332
- viewEl.style.display = isEditing ? '' : 'none';
333
- editEl.style.display = isEditing ? 'none' : 'block';
334
- if (detailsEl) detailsEl.style.display = 'none';
335
- }
336
-
337
- export async function saveContactEdit(contactId) {
338
- const name = document.getElementById('contact-name-' + contactId)?.value?.trim();
339
- const phone = document.getElementById('contact-phone-' + contactId)?.value?.trim();
340
- const email = document.getElementById('contact-email-' + contactId)?.value?.trim();
341
- const location = document.getElementById('contact-location-' + contactId)?.value?.trim();
342
- const notes = document.getElementById('contact-notes-' + contactId)?.value?.trim();
343
- const tagsStr = document.getElementById('contact-tags-' + contactId)?.value?.trim();
344
- const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : [];
345
-
346
- // Collect platform IDs from individual fields
347
- const platformLinks = {};
348
- const telegram = document.getElementById('platform-telegram-' + contactId)?.value?.trim();
349
- const whatsapp = document.getElementById('platform-whatsapp-' + contactId)?.value?.trim();
350
- const twitter = document.getElementById('platform-twitter-' + contactId)?.value?.trim();
351
- const instagram = document.getElementById('platform-instagram-' + contactId)?.value?.trim();
352
- const tiktok = document.getElementById('platform-tiktok-' + contactId)?.value?.trim();
353
- const slack = document.getElementById('platform-slack-' + contactId)?.value?.trim();
354
- const website = document.getElementById('platform-website-' + contactId)?.value?.trim();
355
- const other = document.getElementById('platform-other-' + contactId)?.value?.trim();
356
-
357
- if (telegram) platformLinks.telegram = telegram;
358
- if (whatsapp) platformLinks.whatsapp = whatsapp;
359
- if (twitter) platformLinks.twitter = twitter;
360
- if (instagram) platformLinks.instagram = instagram;
361
- if (tiktok) platformLinks.tiktok = tiktok;
362
- if (slack) platformLinks.slack = slack;
363
- if (website) platformLinks.website = website;
364
- if (other) platformLinks.other = other;
365
-
366
- // Collect preferences from form fields
367
- const preferences = {};
368
- const diet = document.getElementById('pref-diet-' + contactId)?.value?.trim();
369
- const spice = document.getElementById('pref-spice-' + contactId)?.value?.trim();
370
- const budget = document.getElementById('pref-budget-' + contactId)?.value?.trim();
371
- const atmosphere = document.getElementById('pref-atmosphere-' + contactId)?.value?.trim();
372
- const allergiesStr = document.getElementById('pref-allergies-' + contactId)?.value?.trim();
373
- const cuisinesStr = document.getElementById('pref-cuisines-' + contactId)?.value?.trim();
374
-
375
- if (diet) preferences.diet = diet;
376
- if (spice) preferences.spiceLevel = spice;
377
- if (budget) preferences.budget = budget;
378
- if (atmosphere) preferences.atmosphere = atmosphere;
379
- if (allergiesStr) preferences.allergies = allergiesStr.split(',').map(a => a.trim()).filter(Boolean);
380
- if (cuisinesStr) preferences.favCuisines = cuisinesStr.split(',').map(c => c.trim()).filter(Boolean);
381
-
382
- try {
383
- await postJSON('/api/contacts/update', {
384
- contactId,
385
- display_name: name,
386
- phone_number: phone,
387
- email,
388
- last_location: location,
389
- notes,
390
- tags,
391
- platform_links: platformLinks,
392
- preferences
393
- });
394
- showNotification('Contact saved');
395
- toggleContactEdit(contactId);
396
- loadContacts();
397
- } catch(e) {
398
- showNotification('Failed: ' + e.message, true);
399
- }
400
- }
401
-
402
- export async function deleteContact(contactId) {
403
- const contact = _contactsData[contactId];
404
- const name = contact ? contact.display_name : contactId;
405
- if (!confirm(`Delete contact "${name}"?\n\nMessage history will also be deleted.`)) return;
406
-
407
- try {
408
- await postJSON('/api/contacts/delete', { contactId });
409
- showNotification('Contact deleted');
410
- delete _contactsData[contactId];
411
- _allContacts = _allContacts.filter(c => c.contact_id !== contactId);
412
- applyFiltersAndRender();
413
- } catch(e) {
414
- showNotification('Failed: ' + e.message, true);
415
- }
416
- }
417
-
418
- // ── New Contact Modal ─────────────────────────────────────────────────────────
419
-
420
- export function newContact() {
421
- const modal = document.getElementById('modalOverlay') || createModalOverlay();
422
-
423
- modal.innerHTML = `
424
- <div style="background:var(--bg-card);border-radius:12px;padding:24px;max-width:500px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.4);">
425
- <h3 style="margin:0 0 16px 0;">➕ New Contact</h3>
426
-
427
- <div style="margin-bottom:12px;">
428
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Platform *</label>
429
- <select id="newContactPlatform" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;">
430
- <option value="telegram">Telegram</option>
431
- <option value="whatsapp">WhatsApp</option>
432
- <option value="twitter">Twitter</option>
433
- <option value="instagram">Instagram</option>
434
- <option value="tiktok">TikTok</option>
435
- <option value="slack">Slack</option>
436
- <option value="website">Website</option>
437
- <option value="web">Web</option>
438
- </select>
439
- </div>
440
-
441
- <div style="margin-bottom:12px;">
442
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Platform ID *</label>
443
- <input id="newContactId" type="text" placeholder="12345678 (Telegram), @username (Twitter/IG/TikTok), URL (Website)" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
444
- <div style="font-size:10px;color:var(--text-3);margin-top:4px;">
445
- • Telegram: chat ID (e.g., 12345678)<br>
446
- • WhatsApp: phone@s.whatsapp.net (e.g., 15551234567@s.whatsapp.net)<br>
447
- • Twitter/Instagram/TikTok: @username<br>
448
- • Website: URL (e.g., https://example.com)<br>
449
- • Slack: user ID (e.g., U01234ABC)
450
- </div>
451
- </div>
452
-
453
- <div style="margin-bottom:12px;">
454
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Display Name *</label>
455
- <input id="newContactName" type="text" placeholder="John Doe" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
456
- </div>
457
-
458
- <div style="margin-bottom:12px;">
459
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Phone</label>
460
- <input id="newContactPhone" type="text" placeholder="+1 310 905 0857" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
461
- </div>
462
-
463
- <div style="margin-bottom:12px;">
464
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Email</label>
465
- <input id="newContactEmail" type="text" placeholder="john@example.com" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
466
- </div>
467
-
468
- <div style="margin-bottom:16px;">
469
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Notes</label>
470
- <textarea id="newContactNotes" rows="3" placeholder="Important client, prefers morning calls..." style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;"></textarea>
471
- </div>
472
-
473
- <div style="display:flex;gap:8px;justify-content:flex-end;">
474
- <button onclick="document.getElementById('modalOverlay').style.display='none'" class="btn-ghost">Cancel</button>
475
- <button onclick="window.createContact()" class="btn-green">Create Contact</button>
476
- </div>
477
- </div>
478
- `;
479
-
480
- modal.style.display = 'flex';
481
- }
482
-
483
- export async function createContact() {
484
- const platform = document.getElementById('newContactPlatform').value;
485
- const platformId = document.getElementById('newContactId').value.trim();
486
- const displayName = document.getElementById('newContactName').value.trim();
487
- const phone = document.getElementById('newContactPhone').value.trim();
488
- const email = document.getElementById('newContactEmail').value.trim();
489
- const notes = document.getElementById('newContactNotes').value.trim();
490
-
491
- if (!platform || !platformId || !displayName) {
492
- showNotification('Platform, Platform ID, and Display Name are required', true);
493
- return;
494
- }
495
-
496
- const contactId = `${platform}:${platformId}`;
497
-
498
- try {
499
- await postJSON('/api/contacts/create', {
500
- contact_id: contactId,
501
- platform,
502
- display_name: displayName,
503
- phone_number: phone || null,
504
- email: email || null,
505
- notes: notes || null
506
- });
507
- showNotification('✅ Contact created');
508
- document.getElementById('modalOverlay').style.display = 'none';
509
- loadContacts();
510
- } catch(e) {
511
- showNotification('Failed: ' + e.message, true);
512
- }
513
- }
514
-
515
- // ── Send Message Modal ────────────────────────────────────────────────────────
516
-
517
- export function showSendMessageModal(contactId) {
518
- const contact = _contactsData[contactId];
519
- if (!contact) {
520
- showNotification('Contact not found', true);
521
- return;
522
- }
523
-
524
- const modal = document.getElementById('modalOverlay') || createModalOverlay();
525
-
526
- const platformLinks = contact.platform_links || {};
527
- const hasWhatsApp = contact.platform === 'whatsapp' || platformLinks.whatsapp;
528
- const hasTelegram = contact.platform === 'telegram' || platformLinks.telegram;
529
-
530
- modal.innerHTML = `
531
- <div style="background:var(--bg-card);border-radius:12px;padding:24px;max-width:500px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.4);">
532
- <h3 style="margin:0 0 16px 0;">💬 Send Message</h3>
533
- <div style="margin-bottom:12px;">
534
- <div style="font-size:13px;color:var(--text-2);margin-bottom:8px;">To: <strong>${escHtml(contact.display_name || contact.contact_id)}</strong></div>
535
- </div>
536
- <div style="margin-bottom:12px;">
537
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Platform</label>
538
- <select id="sendMessagePlatform" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;">
539
- ${hasWhatsApp ? '<option value="whatsapp">WhatsApp</option>' : ''}
540
- ${hasTelegram ? '<option value="telegram">Telegram</option>' : ''}
541
- ${hasWhatsApp && hasTelegram ? '<option value="both">Both</option>' : ''}
542
- </select>
543
- </div>
544
- <div style="margin-bottom:16px;">
545
- <label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Message</label>
546
- <textarea id="sendMessageText" rows="4" placeholder="Your message here..." style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;"></textarea>
547
- </div>
548
- <div style="display:flex;gap:8px;justify-content:flex-end;">
549
- <button onclick="document.getElementById('modalOverlay').style.display='none'" class="btn-ghost">Cancel</button>
550
- <button onclick="window.sendContactMessage('${escHtml(contactId)}')" class="btn-green">Send</button>
551
- </div>
552
- </div>
553
- `;
554
-
555
- modal.style.display = 'flex';
556
- }
557
-
558
- export async function sendContactMessage(contactId) {
559
- const platform = document.getElementById('sendMessagePlatform').value;
560
- const message = document.getElementById('sendMessageText').value.trim();
561
-
562
- if (!message) {
563
- showNotification('Message cannot be empty', true);
564
- return;
565
- }
566
-
567
- try {
568
- await postJSON('/api/contacts/send', { contactId, platform, message });
569
- showNotification('✅ Message sent');
570
- document.getElementById('modalOverlay').style.display = 'none';
571
- } catch(e) {
572
- showNotification('Failed: ' + e.message, true);
573
- }
574
- }
575
-
576
- // ── Helpers ───────────────────────────────────────────────────────────────────
577
-
578
- function createModalOverlay() {
579
- let modal = document.getElementById('modalOverlay');
580
- if (!modal) {
581
- modal = document.createElement('div');
582
- modal.id = 'modalOverlay';
583
- modal.style.cssText = 'display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:9999;align-items:center;justify-content:center;';
584
- modal.onclick = (e) => { if (e.target === modal) modal.style.display = 'none'; };
585
- document.body.appendChild(modal);
586
- }
587
- return modal;
588
- }
589
-
590
- function formatRelativeTime(timestamp) {
591
- const now = Date.now();
592
- const diff = now - timestamp;
593
- const minutes = Math.floor(diff / 60000);
594
- const hours = Math.floor(diff / 3600000);
595
- const days = Math.floor(diff / 86400000);
596
-
597
- if (minutes < 1) return 'just now';
598
- if (minutes < 60) return `${minutes}m ago`;
599
- if (hours < 24) return `${hours}h ago`;
600
- if (days < 7) return `${days}d ago`;
601
- return new Date(timestamp).toLocaleDateString();
602
- }
603
-
604
- // ── Event Handlers ────────────────────────────────────────────────────────────
605
-
606
- export function initContactsList() {
607
- document.addEventListener('click', (e) => {
608
- const action = e.target.getAttribute('data-action');
609
- const id = e.target.getAttribute('data-id');
610
- const letter = e.target.getAttribute('data-letter');
611
-
612
- if (action === 'toggle-details') toggleContactDetails(id);
613
- else if (action === 'edit') toggleContactEdit(id);
614
- else if (action === 'save-edit') saveContactEdit(id);
615
- else if (action === 'cancel-edit') toggleContactEdit(id);
616
- else if (action === 'delete') deleteContact(id);
617
- else if (action === 'send-message') showSendMessageModal(id);
618
- else if (action === 'newContact') newContact();
619
- else if (action === 'applyContactFilters') applyContactFilters();
620
- else if (action === 'filterByLetter') filterByLetter(letter);
621
- else if (action === 'loadContacts') loadContacts();
622
- });
623
-
624
- // Real-time search
625
- const searchInput = document.getElementById('contactsSearch');
626
- if (searchInput) {
627
- searchInput.addEventListener('input', () => {
628
- _currentFilters.search = searchInput.value.toLowerCase().trim();
629
- applyFiltersAndRender();
630
- });
631
- }
632
-
633
- // Platform filter change
634
- const platformFilter = document.getElementById('contactsPlatformFilter');
635
- if (platformFilter) {
636
- platformFilter.addEventListener('change', applyContactFilters);
637
- }
638
-
639
- // Sort change
640
- const sortBy = document.getElementById('contactsSortBy');
641
- if (sortBy) {
642
- sortBy.addEventListener('change', applyContactFilters);
643
- }
644
- }
645
-
646
- // Export to window for HTML onclick handlers
647
- if (typeof window !== 'undefined') {
648
- window.loadContacts = loadContacts;
649
- window.searchContacts = applyContactFilters;
650
- window.newContact = newContact;
651
- window.createContact = createContact;
652
- window.sendContactMessage = sendContactMessage;
653
- window.filterByLetter = filterByLetter;
654
- }