@mahe_pkm/buzl-capi 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,942 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const form = document.getElementById('trackingForm');
3
+ const btnSubmit = document.getElementById('btnSubmit');
4
+ const btnScan = document.getElementById('btnScan');
5
+ const fileList = document.getElementById('fileList');
6
+ const fileBadge = document.getElementById('fileBadge');
7
+ const testConsole = document.getElementById('testConsole');
8
+ const testSummaryBadge = document.getElementById('testSummaryBadge');
9
+
10
+ // Live State Bar elements
11
+ const liveStateRootDir = document.getElementById('liveStateRootDir');
12
+ const liveStateChips = document.getElementById('liveStateChips');
13
+ const backupsCountBadge = document.getElementById('backupsCountBadge');
14
+
15
+ // Quick Test Lead elements
16
+ const btnTestSubmit = document.getElementById('btnTestSubmit');
17
+ const liveTestBadge = document.getElementById('liveTestBadge');
18
+ const testDispatchResults = document.getElementById('testDispatchResults');
19
+ const testLeadPhone = document.getElementById('testLeadPhone');
20
+ const testLeadName = document.getElementById('testLeadName');
21
+ const testLeadService = document.getElementById('testLeadService');
22
+
23
+ [testLeadPhone, testLeadName, testLeadService].forEach(el => {
24
+ if (el) el.addEventListener('input', () => { el.dataset.userEdited = 'true'; });
25
+ });
26
+
27
+ // Individual Form elements
28
+ const formsCountBadge = document.getElementById('formsCountBadge');
29
+ const individualFormsList = document.getElementById('individualFormsList');
30
+
31
+ // Backups Modal elements
32
+ const btnOpenBackups = document.getElementById('btnOpenBackups');
33
+ const backupsModal = document.getElementById('backupsModal');
34
+ const closeBackupsModal = document.getElementById('closeBackupsModal');
35
+ const btnCreateNamedBackup = document.getElementById('btnCreateNamedBackup');
36
+ const customBackupName = document.getElementById('customBackupName');
37
+ const backupsTableBody = document.getElementById('backupsTableBody');
38
+
39
+ // Uninstall Modal elements
40
+ const btnUninstallTracking = document.getElementById('btnUninstallTracking');
41
+ const uninstallModal = document.getElementById('uninstallModal');
42
+ const closeUninstallModal = document.getElementById('closeUninstallModal');
43
+ const btnCancelUninstall = document.getElementById('btnCancelUninstall');
44
+ const btnConfirmUninstall = document.getElementById('btnConfirmUninstall');
45
+
46
+ // Script Modal elements
47
+ const scriptModal = document.getElementById('scriptModal');
48
+ const openScriptModal = document.getElementById('openScriptModal');
49
+ const closeScriptModal = document.getElementById('closeScriptModal');
50
+ const copyScriptCode = document.getElementById('copyScriptCode');
51
+ const scriptCodeDisplay = document.getElementById('scriptCodeDisplay');
52
+
53
+ let scriptTemplateText = '';
54
+
55
+ const siteLocationInput = document.getElementById('siteLocation');
56
+ const columnHierarchyPreview = document.getElementById('columnHierarchyPreview');
57
+ const dynamicFieldsChecklist = document.getElementById('dynamicFieldsChecklist');
58
+
59
+ let activeDynamicFields = [];
60
+ let currentScanData = null;
61
+
62
+ function renderColumnHierarchy() {
63
+ if (!columnHierarchyPreview) return;
64
+ columnHierarchyPreview.innerHTML = '';
65
+
66
+ const cols = [
67
+ { name: 'Name', type: 'contact' },
68
+ { name: 'Location', type: 'contact' },
69
+ { name: 'Phone', type: 'contact' }
70
+ ];
71
+
72
+ activeDynamicFields.forEach(f => {
73
+ cols.push({ name: f, type: 'dynamic' });
74
+ });
75
+
76
+ const crmCols = ['Lead Stage', 'Event Time', 'Is Qualified', 'Qualified Date', 'Is Spam', 'Handled By', 'Comments'];
77
+ crmCols.forEach(c => cols.push({ name: c, type: 'crm' }));
78
+
79
+ const attrCols = ['Action Source', 'Source', 'UTM Source', 'UTM Campaign', 'Lead ID'];
80
+ attrCols.forEach(a => cols.push({ name: a, type: 'attrib' }));
81
+
82
+ cols.forEach((col, idx) => {
83
+ const pill = document.createElement('span');
84
+ pill.className = `col-pill col-pill-${col.type}`;
85
+ pill.textContent = `${idx + 1}. ${col.name}`;
86
+ columnHierarchyPreview.appendChild(pill);
87
+ });
88
+ }
89
+
90
+ // 1. Fetch project scan & live state
91
+ async function loadScan() {
92
+ fileBadge.textContent = 'Scanning...';
93
+ fileBadge.className = 'badge badge-neutral';
94
+
95
+ try {
96
+ const res = await fetch('/api/scan');
97
+ const data = await res.json();
98
+ currentScanData = data;
99
+
100
+ const dirCount = (data.directories && data.directories.length > 1) ? ` · ${data.directories.length} dirs` : '';
101
+ fileBadge.textContent = `${data.totalHtmlFiles} HTML File(s)${dirCount}`;
102
+ fileBadge.className = 'badge badge-info';
103
+ fileList.innerHTML = '';
104
+
105
+ // Update Live State Bar
106
+ updateLiveStateBar(data.liveState, data.rootDir);
107
+
108
+ // Auto-detect site location
109
+ if (data.detectedLocation && siteLocationInput && !siteLocationInput.dataset.userEdited) {
110
+ siteLocationInput.value = data.detectedLocation;
111
+ }
112
+
113
+ // Auto-detect phone and service for quick test dispatch
114
+ if (testLeadPhone && !testLeadPhone.dataset.userEdited) {
115
+ testLeadPhone.value = data.detectedWhatsapp || (data.liveState && data.liveState.whatsapp && data.liveState.whatsapp.detectedNumber) || '';
116
+ }
117
+ if (testLeadService && !testLeadService.dataset.userEdited) {
118
+ testLeadService.value = data.detectedService || 'General Inquiry';
119
+ }
120
+
121
+ // Populate dynamic fields checklist
122
+ if (dynamicFieldsChecklist && data.uniqueFields) {
123
+ dynamicFieldsChecklist.innerHTML = '';
124
+ const customFields = data.uniqueFields.filter(f => !f.isCore);
125
+
126
+ activeDynamicFields = customFields.map(f => f.name.charAt(0).toUpperCase() + f.name.slice(1));
127
+
128
+ if (customFields.length === 0) {
129
+ dynamicFieldsChecklist.innerHTML = '<span class="helper-text">No custom fields detected (core: Name, Phone).</span>';
130
+ } else {
131
+ customFields.forEach(f => {
132
+ const formatted = f.name.charAt(0).toUpperCase() + f.name.slice(1);
133
+ const label = document.createElement('label');
134
+ label.className = 'field-checkbox-item';
135
+ label.innerHTML = `
136
+ <input type="checkbox" checked data-field="${formatted}">
137
+ <span><strong>${formatted}</strong> (${f.tag}) — <small class="helper-text">Placed next to Phone (Col D)</small></span>
138
+ `;
139
+
140
+ label.querySelector('input').addEventListener('change', (e) => {
141
+ if (e.target.checked) {
142
+ if (!activeDynamicFields.includes(formatted)) activeDynamicFields.push(formatted);
143
+ } else {
144
+ activeDynamicFields = activeDynamicFields.filter(x => x !== formatted);
145
+ }
146
+ renderColumnHierarchy();
147
+ });
148
+
149
+ dynamicFieldsChecklist.appendChild(label);
150
+ });
151
+ }
152
+ }
153
+
154
+ renderColumnHierarchy();
155
+
156
+ // Render Discovered Project Files
157
+ if (data.files.length === 0) {
158
+ fileList.innerHTML = '<li class="file-item"><span class="name">No HTML files discovered in root</span></li>';
159
+ } else {
160
+ data.files.forEach(f => {
161
+ const li = document.createElement('li');
162
+ li.className = 'file-item';
163
+
164
+ const tags = [];
165
+ if (f.hasHead) tags.push('<span class="badge badge-neutral">&lt;head&gt;</span>');
166
+ if (f.hasBody) tags.push('<span class="badge badge-neutral">&lt;body&gt;</span>');
167
+ if (f.forms.length > 0) tags.push(`<span class="badge badge-success">${f.forms.length} form(s)</span>`);
168
+ if (f.existingGtmId) tags.push(`<span class="badge badge-info">${f.existingGtmId}</span>`);
169
+
170
+ li.innerHTML = `
171
+ <span class="name">${f.relativePath}</span>
172
+ <div class="file-tags">${tags.join('')}</div>
173
+ `;
174
+ fileList.appendChild(li);
175
+ });
176
+ }
177
+
178
+ // Render Individual Forms for independent testing
179
+ renderIndividualForms(data.forms || [], data.formArchetypes || []);
180
+
181
+ // Pre-fill inputs strictly from live project state
182
+ applyLiveConfigToForm(data);
183
+
184
+ // Refresh Backups Count
185
+ if (data.backups && backupsCountBadge) {
186
+ backupsCountBadge.textContent = data.backups.length;
187
+ }
188
+
189
+ } catch (err) {
190
+ fileBadge.textContent = 'Scan Error';
191
+ fileBadge.className = 'badge badge-danger';
192
+ }
193
+ }
194
+
195
+ function updateLiveStateBar(liveState, rootDir) {
196
+ if (!liveState) return;
197
+ if (liveStateRootDir) liveStateRootDir.textContent = rootDir || 'Project Root';
198
+
199
+ const locBadge = document.getElementById('siteLocationBadge');
200
+ if (locBadge) {
201
+ locBadge.innerHTML = currentScanData && currentScanData.detectedLocation
202
+ ? `<svg class="icon-xs"><use href="#icon-pin"/></svg> <span>${currentScanData.detectedLocation}</span>`
203
+ : '<svg class="icon-xs"><use href="#icon-pin"/></svg> <span>Location: Not detected</span>';
204
+ }
205
+
206
+ // Toggle per-card remove buttons
207
+ const btnGtm = document.getElementById('btnRemoveGtm');
208
+ if (btnGtm) btnGtm.classList.toggle('hidden', !(liveState.gtm && liveState.gtm.active));
209
+
210
+ const btnMeta = document.getElementById('btnRemoveMeta');
211
+ if (btnMeta) btnMeta.classList.toggle('hidden', !(liveState.meta && liveState.meta.active));
212
+
213
+ const btnCapi = document.getElementById('btnRemoveCapi');
214
+ if (btnCapi) btnCapi.classList.toggle('hidden', !(liveState.buzlCapi && liveState.buzlCapi.active));
215
+
216
+ const btnSheets = document.getElementById('btnRemoveSheets');
217
+ if (btnSheets) btnSheets.classList.toggle('hidden', !(liveState.googleSheets && liveState.googleSheets.active));
218
+
219
+ const btnZoho = document.getElementById('btnRemoveZoho');
220
+ if (btnZoho) btnZoho.classList.toggle('hidden', !(liveState.zoho && liveState.zoho.active));
221
+
222
+ const btnWa = document.getElementById('btnRemoveWhatsapp');
223
+ if (btnWa) btnWa.classList.toggle('hidden', !(liveState.whatsapp && liveState.whatsapp.active));
224
+
225
+ if (liveStateChips) {
226
+ liveStateChips.innerHTML = '';
227
+
228
+ const items = [
229
+ { serviceKey: 'gtm', label: 'GTM', active: liveState.gtm.active, text: liveState.gtm.id ? `GTM: ${liveState.gtm.id}` : 'GTM: Inactive' },
230
+ { serviceKey: 'meta', label: 'Meta', active: liveState.meta.active, text: liveState.meta.id ? `Meta: ${liveState.meta.id}` : 'Meta: Inactive' },
231
+ { serviceKey: 'capi', label: 'Buzl CAPI', active: liveState.buzlCapi.active, text: liveState.buzlCapi.active ? 'Buzl CAPI: Connected' : 'Buzl CAPI: Inactive' },
232
+ { serviceKey: 'sheets', label: 'Google Sheets', active: liveState.googleSheets.active, text: liveState.googleSheets.active ? 'Sheets CRM: Connected' : 'Sheets CRM: Inactive' },
233
+ { serviceKey: 'zoho', label: 'Zoho CRM', active: liveState.zoho.active, text: liveState.zoho.active ? 'Zoho CRM: Connected' : 'Zoho CRM: Inactive' },
234
+ { serviceKey: 'whatsapp', label: 'WhatsApp', active: liveState.whatsapp.active, text: liveState.whatsapp.active ? `WA: ${liveState.whatsapp.number}` : 'WA: Inactive' }
235
+ ];
236
+
237
+ items.forEach(item => {
238
+ const chip = document.createElement('span');
239
+ chip.className = `chip ${item.active ? 'chip-active' : 'chip-inactive'}`;
240
+ chip.innerHTML = `<span class="status-dot ${item.active ? 'dot-active' : 'dot-inactive'}"></span> <span>${item.text}</span> ${item.active ? `<button type="button" class="chip-remove-btn" data-service="${item.serviceKey}" title="Remove ${item.label}"><svg class="icon-xs"><use href="#icon-x"/></svg></button>` : ''}`;
241
+
242
+ const removeBtn = chip.querySelector('.chip-remove-btn');
243
+ if (removeBtn) {
244
+ removeBtn.addEventListener('click', (e) => {
245
+ e.stopPropagation();
246
+ removeSingleService(item.serviceKey);
247
+ });
248
+ }
249
+
250
+ liveStateChips.appendChild(chip);
251
+ });
252
+ }
253
+ }
254
+
255
+ async function removeSingleService(serviceKey) {
256
+ const serviceNames = {
257
+ gtm: 'Google Tag Manager (GTM)',
258
+ meta: 'Meta Pixel & Client CAPI',
259
+ capi: 'Buzl CAPI Server-Side Endpoint',
260
+ sheets: 'Google Sheets Direct CRM',
261
+ zoho: 'Zoho CRM Web-to-Lead',
262
+ whatsapp: 'WhatsApp Handoff'
263
+ };
264
+ const name = serviceNames[serviceKey] || serviceKey;
265
+ if (!confirm(`Are you sure you want to remove ${name} from this site?\n\nAll other active tracking and configurations will be safely preserved. An automatic safety snapshot will be created before removal.`)) {
266
+ return;
267
+ }
268
+
269
+ try {
270
+ const res = await fetch('/api/remove-service', {
271
+ method: 'POST',
272
+ headers: { 'Content-Type': 'application/json' },
273
+ body: JSON.stringify({ service: serviceKey })
274
+ });
275
+ const data = await res.json();
276
+ if (data.success) {
277
+ alert(data.message);
278
+ loadScan();
279
+ } else {
280
+ alert(`Failed to remove ${name}: ${data.message}`);
281
+ }
282
+ } catch (err) {
283
+ alert(`Network error removing ${name}: ${err.message}`);
284
+ }
285
+ }
286
+
287
+ function applyLiveConfigToForm(data) {
288
+ const live = data.liveState;
289
+ const cfg = data.existingConfig || {};
290
+
291
+ // GTM
292
+ const gtmSwitch = document.getElementById('enableGTM');
293
+ const gtmInput = document.getElementById('gtmId');
294
+ if (gtmInput) {
295
+ gtmInput.value = (live && live.gtm && live.gtm.id) || cfg.gtmId || '';
296
+ }
297
+ if (gtmSwitch) {
298
+ gtmSwitch.checked = !!(live && live.gtm && live.gtm.active);
299
+ gtmSwitch.dispatchEvent(new Event('change'));
300
+ }
301
+
302
+ // Meta Pixel
303
+ const metaSwitch = document.getElementById('enableMeta');
304
+ const metaInput = document.getElementById('metaPixelId');
305
+ if (metaInput) {
306
+ metaInput.value = (live && live.meta && live.meta.id) || cfg.metaPixelId || '';
307
+ }
308
+ if (metaSwitch) {
309
+ metaSwitch.checked = !!(live && live.meta && live.meta.active);
310
+ metaSwitch.dispatchEvent(new Event('change'));
311
+ }
312
+
313
+ // Buzl CAPI
314
+ const capiSwitch = document.getElementById('enableBuzlCapi');
315
+ const capiEndpoint = document.getElementById('buzlCapiEndpoint');
316
+ const capiUser = document.getElementById('buzlCapiUser');
317
+ const capiPass = document.getElementById('buzlCapiPass');
318
+
319
+ const capiData = (live && live.buzlCapi) || cfg.buzlCapi || {};
320
+ if (capiEndpoint && capiData.endpoint) capiEndpoint.value = capiData.endpoint;
321
+ if (capiUser) capiUser.value = capiData.authUser || '2BuzlmqpHJeVBow0dzR9gP3$uQLxIA';
322
+ if (capiPass) capiPass.value = (cfg.buzlCapi && cfg.buzlCapi.authPass) || 'dgAY%nH1MNPgOvGaYRg6ynomM3mbJgGjr%Z3FcPCJNzvm#KjV!I%Y9tf$bDacBgPIABuzl';
323
+
324
+ if (capiSwitch) {
325
+ capiSwitch.checked = !!(live && live.buzlCapi && live.buzlCapi.active);
326
+ capiSwitch.dispatchEvent(new Event('change'));
327
+ }
328
+
329
+ // Google Sheets
330
+ const sheetsSwitch = document.getElementById('enableSheets');
331
+ const sheetsInput = document.getElementById('googleSheetUrl');
332
+ if (sheetsInput && (live && live.googleSheets && live.googleSheets.url)) {
333
+ sheetsInput.value = live.googleSheets.url;
334
+ } else if (sheetsInput && cfg.googleSheetUrl) {
335
+ sheetsInput.value = cfg.googleSheetUrl;
336
+ }
337
+ if (sheetsSwitch) {
338
+ sheetsSwitch.checked = !!(live && live.googleSheets && live.googleSheets.active);
339
+ sheetsSwitch.dispatchEvent(new Event('change'));
340
+ }
341
+
342
+ // Zoho CRM
343
+ const zohoSwitch = document.getElementById('enableZoho');
344
+ const zohoXn = document.getElementById('zohoXnqsjsdp');
345
+ const zohoXm = document.getElementById('zohoXmiwtld');
346
+ const zohoEp = document.getElementById('zohoEndpoint');
347
+ if (cfg.zoho) {
348
+ if (zohoXn && cfg.zoho.xnQsjsdp) zohoXn.value = cfg.zoho.xnQsjsdp;
349
+ if (zohoXm && cfg.zoho.xmIwtLD) zohoXm.value = cfg.zoho.xmIwtLD;
350
+ if (zohoEp && cfg.zoho.endpoint) zohoEp.value = cfg.zoho.endpoint;
351
+ }
352
+ if (zohoSwitch) {
353
+ zohoSwitch.checked = !!(live && live.zoho && live.zoho.active);
354
+ zohoSwitch.dispatchEvent(new Event('change'));
355
+ }
356
+
357
+ // WhatsApp
358
+ const waSwitch = document.getElementById('enableWhatsapp');
359
+ const waNumber = document.getElementById('whatsappNumber');
360
+ const waBadge = document.getElementById('waDetectedBadge');
361
+ const waHelper = document.getElementById('waHelperText');
362
+
363
+ const detectedWa = data.detectedWhatsapp || (live && live.whatsapp && live.whatsapp.detectedNumber);
364
+ if (detectedWa && waBadge) {
365
+ waBadge.innerHTML = `<svg class="icon-xs"><use href="#icon-check"/></svg> <span>Auto-detected: ${detectedWa}</span>`;
366
+ waBadge.classList.remove('hidden');
367
+ waBadge.onclick = () => {
368
+ if (waNumber) waNumber.value = detectedWa;
369
+ if (waSwitch && !waSwitch.checked) {
370
+ waSwitch.checked = true;
371
+ waSwitch.dispatchEvent(new Event('change'));
372
+ }
373
+ };
374
+ if (waHelper) {
375
+ waHelper.textContent = `Auto-fetches ${detectedWa} from form buttons, or specify fallback number.`;
376
+ }
377
+ }
378
+
379
+ if (waNumber) {
380
+ if (live && live.whatsapp && live.whatsapp.number) {
381
+ waNumber.value = live.whatsapp.number;
382
+ } else if (cfg.whatsapp && cfg.whatsapp.number) {
383
+ waNumber.value = cfg.whatsapp.number;
384
+ } else if (detectedWa && !waNumber.value) {
385
+ waNumber.value = detectedWa;
386
+ }
387
+ }
388
+
389
+ if (waSwitch) {
390
+ const isWaActive = !!(live && live.whatsapp && live.whatsapp.active);
391
+ waSwitch.checked = isWaActive || (!!detectedWa && (live && live.whatsapp && live.whatsapp.active !== false));
392
+ waSwitch.dispatchEvent(new Event('change'));
393
+ }
394
+ }
395
+
396
+ // 2. Render Individual Discovered Forms with Per-Form Testing
397
+ function renderIndividualForms(forms, archetypes) {
398
+ if (!individualFormsList) return;
399
+ individualFormsList.innerHTML = '';
400
+
401
+ const listToRender = (archetypes && archetypes.length > 0) ? archetypes : forms;
402
+
403
+ if (formsCountBadge) {
404
+ if (archetypes && archetypes.length > 0 && forms.length !== archetypes.length) {
405
+ formsCountBadge.textContent = `${forms.length} Instances (${archetypes.length} Unique)`;
406
+ } else {
407
+ formsCountBadge.textContent = `${forms.length} Form(s)`;
408
+ }
409
+ }
410
+
411
+ if (!listToRender || listToRender.length === 0) {
412
+ individualFormsList.innerHTML = '<p class="empty-state">No forms found on HTML pages.</p>';
413
+ return;
414
+ }
415
+
416
+ listToRender.forEach((f, idx) => {
417
+ const card = document.createElement('div');
418
+ card.className = 'form-item-card';
419
+
420
+ const tagPills = f.inputs.map(i => `<span class="col-pill col-pill-dynamic" style="font-size: 10px;">${i.name}</span>`).join('');
421
+
422
+ // Build sample test input controls
423
+ const inputElements = f.inputs.map(i => {
424
+ let sampleVal = 'Test Value';
425
+ const lower = i.name.toLowerCase();
426
+ if (lower.includes('phone') || lower.includes('mobile')) {
427
+ sampleVal = f.detectedWhatsapp || (currentScanData && currentScanData.detectedWhatsapp) || '';
428
+ } else if (lower.includes('name')) {
429
+ sampleVal = `Test Lead ${idx + 1}`;
430
+ } else if (lower.includes('mail')) {
431
+ sampleVal = 'test-lead@example.com';
432
+ } else if (lower.includes('service') || lower.includes('inquiry') || lower.includes('treatment') || lower.includes('subject')) {
433
+ sampleVal = f.formTitle || f.id || (currentScanData && currentScanData.detectedService) || 'General Inquiry';
434
+ } else if (lower.includes('city') || lower.includes('loc')) {
435
+ sampleVal = (currentScanData && currentScanData.detectedLocation) || '';
436
+ }
437
+
438
+ return `
439
+ <div class="form-field-input-group">
440
+ <label>${i.name}</label>
441
+ <input type="text" data-field-name="${i.name}" value="${sampleVal}">
442
+ </div>
443
+ `;
444
+ }).join('');
445
+
446
+ const isShared = f.isShared || (f.pages && f.pages.length > 1);
447
+ const pageInfo = isShared
448
+ ? `<span class="badge badge-info" style="font-size: 10.5px;"><svg class="icon-xs"><use href="#icon-refresh"/></svg> <span>Appears on ${f.pages.length} pages</span></span>`
449
+ : `<small style="color: var(--text-dim); font-size: 11px;">in <code>${f.file || (f.pages && f.pages[0]) || 'page'}</code></small>`;
450
+
451
+ const pagesList = (isShared && f.pages)
452
+ ? `<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px;">
453
+ ${f.pages.map(p => `<span class="badge badge-neutral" style="font-family: monospace; font-size: 10px;">${p}</span>`).join('')}
454
+ </div>`
455
+ : '';
456
+
457
+ const waBadge = f.detectedWhatsapp
458
+ ? `<span class="badge badge-success" style="font-size: 10px; font-family: monospace;" title="Target WhatsApp Number for this form"><svg class="icon-xs"><use href="#icon-brand-whatsapp"/></svg> <span>WA: ${f.detectedWhatsapp}</span></span>`
459
+ : '';
460
+
461
+ card.innerHTML = `
462
+ <div class="form-item-header">
463
+ <div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
464
+ <span class="form-selector-tag">${f.selector}</span>
465
+ ${pageInfo}
466
+ ${waBadge}
467
+ </div>
468
+ <span class="badge badge-neutral">${f.inputCount || f.inputs.length} input(s)</span>
469
+ </div>
470
+ ${pagesList}
471
+ <div style="display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px;">${tagPills}</div>
472
+ <div class="form-fields-grid">${inputElements}</div>
473
+ <div class="form-actions-row">
474
+ <button type="button" class="btn btn-primary btn-sm btn-test-this-form" data-form-id="${f.formId}">
475
+ <svg class="icon-xs"><use href="#icon-send"/></svg>
476
+ <span>Test This Form (${f.selector})</span>
477
+ </button>
478
+ <span class="form-test-result hidden"></span>
479
+ </div>
480
+ `;
481
+
482
+ // Wire test click
483
+ const testBtn = card.querySelector('.btn-test-this-form');
484
+ const resultSpan = card.querySelector('.form-test-result');
485
+
486
+ testBtn.addEventListener('click', async () => {
487
+ testBtn.disabled = true;
488
+ testBtn.innerHTML = '<svg class="icon-xs icon-spin"><use href="#icon-refresh"/></svg> <span>Testing...</span>';
489
+ resultSpan.classList.remove('hidden');
490
+ resultSpan.className = 'form-test-result badge badge-info';
491
+ resultSpan.innerHTML = '<svg class="icon-xs icon-spin"><use href="#icon-refresh"/></svg> <span>Dispatching to Sheet &amp; CAPI...</span>';
492
+
493
+ // Collect fields
494
+ const fieldValues = {};
495
+ card.querySelectorAll('input[data-field-name]').forEach(inp => {
496
+ fieldValues[inp.dataset.fieldName] = inp.value.trim();
497
+ });
498
+
499
+ const targetPage = f.file || (f.pages && f.pages[0]) || 'index.html';
500
+
501
+ try {
502
+ const res = await fetch('/api/test-form', {
503
+ method: 'POST',
504
+ headers: { 'Content-Type': 'application/json' },
505
+ body: JSON.stringify({
506
+ formId: f.formId,
507
+ formFields: fieldValues,
508
+ pagePath: targetPage,
509
+ config: getCurrentConfig()
510
+ })
511
+ });
512
+ const data = await res.json();
513
+ testBtn.disabled = false;
514
+ testBtn.innerHTML = `<svg class="icon-xs"><use href="#icon-send"/></svg> <span>Test This Form (${f.selector})</span>`;
515
+
516
+ if (data.success && data.results) {
517
+ const ch = data.results.channels;
518
+ const sheetOk = ch.googleSheets && ch.googleSheets.ok;
519
+ const capiOk = ch.buzlCapi && ch.buzlCapi.ok;
520
+
521
+ if (sheetOk || capiOk) {
522
+ resultSpan.className = 'form-test-result badge badge-success';
523
+ const rowLabel = (ch.googleSheets.message.match(/Row \d+/) || [])[0] || 'OK';
524
+ resultSpan.innerHTML = `<svg class="icon-xs" style="vertical-align:text-bottom; margin-right:3px;"><use href="#icon-check"/></svg> <span>Delivered! (Sheets: ${sheetOk ? rowLabel : 'Off'} | CAPI: ${capiOk ? 'Accepted' : 'Off'})</span>`;
525
+ } else {
526
+ resultSpan.className = 'form-test-result badge badge-danger';
527
+ resultSpan.innerHTML = `<svg class="icon-xs" style="vertical-align:text-bottom; margin-right:3px;"><use href="#icon-x"/></svg> <span>Failed: ${ch.googleSheets.message || ch.buzlCapi.message || 'Error'}</span>`;
528
+ }
529
+ } else {
530
+ resultSpan.className = 'form-test-result badge badge-danger';
531
+ resultSpan.innerHTML = `<svg class="icon-xs" style="vertical-align:text-bottom; margin-right:3px;"><use href="#icon-x"/></svg> <span>Error: ${data.message || 'Failed'}</span>`;
532
+ }
533
+ } catch (e) {
534
+ testBtn.disabled = false;
535
+ testBtn.innerHTML = `<svg class="icon-xs"><use href="#icon-send"/></svg> <span>Test This Form (${f.selector})</span>`;
536
+ resultSpan.className = 'form-test-result badge badge-danger';
537
+ resultSpan.innerHTML = `<svg class="icon-xs" style="vertical-align:text-bottom; margin-right:3px;"><use href="#icon-x"/></svg> <span>Network Error: ${e.message}</span>`;
538
+ }
539
+ });
540
+
541
+ individualFormsList.appendChild(card);
542
+ });
543
+ }
544
+
545
+ // 3. Named Backups Management
546
+ async function loadBackupsList() {
547
+ if (!backupsTableBody) return;
548
+ backupsTableBody.innerHTML = '<tr><td colspan="5" style="text-align: center;">Loading backups...</td></tr>';
549
+
550
+ try {
551
+ const res = await fetch('/api/backups');
552
+ const data = await res.json();
553
+ const backups = data.backups || [];
554
+
555
+ if (backupsCountBadge) backupsCountBadge.textContent = backups.length;
556
+
557
+ if (backups.length === 0) {
558
+ backupsTableBody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: var(--text-muted);">No backups found on disk.</td></tr>';
559
+ return;
560
+ }
561
+
562
+ backupsTableBody.innerHTML = '';
563
+ backups.forEach(b => {
564
+ const tr = document.createElement('tr');
565
+ const formattedDate = b.timestamp ? new Date(b.timestamp).toLocaleString() : 'N/A';
566
+
567
+ tr.innerHTML = `
568
+ <td><strong>${b.name || 'Snapshot'}</strong></td>
569
+ <td><code style="font-size: 11px; color: var(--text-muted);">${b.dirName}</code></td>
570
+ <td>${formattedDate}</td>
571
+ <td><span class="badge badge-neutral">${b.filesCount} file(s)</span></td>
572
+ <td style="display: flex; gap: 6px;">
573
+ <button type="button" class="btn btn-secondary btn-sm btn-restore-backup" data-dir="${b.dirName}" data-name="${b.name}">
574
+ <svg class="icon-xs"><use href="#icon-refresh"/></svg>
575
+ <span>Restore</span>
576
+ </button>
577
+ <button type="button" class="btn btn-danger-outline btn-sm btn-delete-backup" data-dir="${b.dirName}">
578
+ <svg class="icon-xs"><use href="#icon-trash"/></svg>
579
+ <span>Delete</span>
580
+ </button>
581
+ </td>
582
+ `;
583
+
584
+ // Restore action
585
+ tr.querySelector('.btn-restore-backup').addEventListener('click', async (e) => {
586
+ const dir = e.currentTarget.dataset.dir;
587
+ const name = e.currentTarget.dataset.name;
588
+ if (!confirm(`Are you sure you want to restore "${name}" (${dir})? All current HTML files will be replaced with this snapshot.`)) return;
589
+
590
+ try {
591
+ const rRes = await fetch('/api/restore', {
592
+ method: 'POST',
593
+ headers: { 'Content-Type': 'application/json' },
594
+ body: JSON.stringify({ backupDirName: dir })
595
+ });
596
+ const rData = await rRes.json();
597
+ alert(rData.message);
598
+ backupsModal.classList.add('hidden');
599
+ loadScan();
600
+ } catch (err) {
601
+ alert('Restore failed: ' + err.message);
602
+ }
603
+ });
604
+
605
+ // Delete action
606
+ tr.querySelector('.btn-delete-backup').addEventListener('click', async (e) => {
607
+ const dir = e.currentTarget.dataset.dir;
608
+ if (!confirm(`Are you sure you want to delete backup "${dir}"? This cannot be undone.`)) return;
609
+
610
+ try {
611
+ const dRes = await fetch('/api/delete-backup', {
612
+ method: 'POST',
613
+ headers: { 'Content-Type': 'application/json' },
614
+ body: JSON.stringify({ backupDirName: dir })
615
+ });
616
+ const dData = await dRes.json();
617
+ loadBackupsList();
618
+ } catch (err) {
619
+ alert('Delete failed: ' + err.message);
620
+ }
621
+ });
622
+
623
+ backupsTableBody.appendChild(tr);
624
+ });
625
+ } catch (e) {
626
+ backupsTableBody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: red;">Failed to load backups: ${e.message}</td></tr>`;
627
+ }
628
+ }
629
+
630
+ if (btnOpenBackups) {
631
+ btnOpenBackups.addEventListener('click', () => {
632
+ backupsModal.classList.remove('hidden');
633
+ loadBackupsList();
634
+ });
635
+ }
636
+
637
+ if (closeBackupsModal) {
638
+ closeBackupsModal.addEventListener('click', () => {
639
+ backupsModal.classList.add('hidden');
640
+ });
641
+ }
642
+
643
+ if (btnCreateNamedBackup) {
644
+ btnCreateNamedBackup.addEventListener('click', async () => {
645
+ const name = customBackupName ? customBackupName.value.trim() : '';
646
+ btnCreateNamedBackup.disabled = true;
647
+ btnCreateNamedBackup.innerHTML = '<svg class="icon-sm icon-spin"><use href="#icon-refresh"/></svg> <span>Saving...</span>';
648
+
649
+ try {
650
+ const res = await fetch('/api/backup', {
651
+ method: 'POST',
652
+ headers: { 'Content-Type': 'application/json' },
653
+ body: JSON.stringify({ name })
654
+ });
655
+ const data = await res.json();
656
+ btnCreateNamedBackup.disabled = false;
657
+ btnCreateNamedBackup.innerHTML = '<svg class="icon-sm"><use href="#icon-database"/></svg> <span>Save Snapshot</span>';
658
+ if (customBackupName) customBackupName.value = '';
659
+ loadBackupsList();
660
+ } catch (err) {
661
+ btnCreateNamedBackup.disabled = false;
662
+ btnCreateNamedBackup.innerHTML = '<svg class="icon-sm"><use href="#icon-database"/></svg> <span>Save Snapshot</span>';
663
+ alert('Backup failed: ' + err.message);
664
+ }
665
+ });
666
+ }
667
+
668
+ // 4. Clean Tracking Removal / Uninstaller
669
+ if (btnUninstallTracking) {
670
+ btnUninstallTracking.addEventListener('click', () => {
671
+ uninstallModal.classList.remove('hidden');
672
+ });
673
+ }
674
+
675
+ if (closeUninstallModal) closeUninstallModal.addEventListener('click', () => uninstallModal.classList.add('hidden'));
676
+ if (btnCancelUninstall) btnCancelUninstall.addEventListener('click', () => uninstallModal.classList.add('hidden'));
677
+
678
+ if (btnConfirmUninstall) {
679
+ btnConfirmUninstall.addEventListener('click', async () => {
680
+ btnConfirmUninstall.disabled = true;
681
+ btnConfirmUninstall.textContent = '⏳ Removing all tracking...';
682
+
683
+ try {
684
+ const res = await fetch('/api/remove-tracking', { method: 'POST' });
685
+ const data = await res.json();
686
+ btnConfirmUninstall.disabled = false;
687
+ btnConfirmUninstall.textContent = 'Confirm & Remove All Tracking';
688
+ uninstallModal.classList.add('hidden');
689
+
690
+ alert(data.message);
691
+ loadScan();
692
+ } catch (e) {
693
+ btnConfirmUninstall.disabled = false;
694
+ btnConfirmUninstall.textContent = 'Confirm & Remove All Tracking';
695
+ alert('Removal failed: ' + e.message);
696
+ }
697
+ });
698
+ }
699
+
700
+ // 4.5. Selective Single Service Removal Buttons
701
+ document.querySelectorAll('.btn-remove-service').forEach(btn => {
702
+ btn.addEventListener('click', (e) => {
703
+ e.preventDefault();
704
+ const service = btn.dataset.service;
705
+ if (service) {
706
+ removeSingleService(service);
707
+ }
708
+ });
709
+ });
710
+
711
+ // 5. Fetch Apps Script Template Code
712
+ async function loadScriptTemplate() {
713
+ try {
714
+ const res = await fetch('/api/apps-script');
715
+ const data = await res.json();
716
+ scriptTemplateText = data.code;
717
+ scriptCodeDisplay.textContent = scriptTemplateText;
718
+ } catch (e) {}
719
+ }
720
+
721
+ if (openScriptModal) {
722
+ openScriptModal.addEventListener('click', (e) => {
723
+ e.preventDefault();
724
+ scriptModal.classList.remove('hidden');
725
+ });
726
+ }
727
+
728
+ if (closeScriptModal) {
729
+ closeScriptModal.addEventListener('click', () => {
730
+ scriptModal.classList.add('hidden');
731
+ });
732
+ }
733
+
734
+ if (copyScriptCode) {
735
+ copyScriptCode.addEventListener('click', () => {
736
+ navigator.clipboard.writeText(scriptTemplateText).then(() => {
737
+ copyScriptCode.innerHTML = '<svg class="icon-xs" style="color:var(--success)"><use href="#icon-check"/></svg> <span>Copied!</span>';
738
+ setTimeout(() => {
739
+ copyScriptCode.innerHTML = '<svg class="icon-xs"><use href="#icon-clipboard"/></svg> <span>Copy Code</span>';
740
+ }, 2000);
741
+ });
742
+ });
743
+ }
744
+
745
+ // Toggle helpers
746
+ function setupToggle(switchId, sectionId) {
747
+ const sw = document.getElementById(switchId);
748
+ const sec = document.getElementById(sectionId);
749
+ if (!sw || !sec) return;
750
+ sw.addEventListener('change', () => {
751
+ sec.style.opacity = sw.checked ? '1' : '0.4';
752
+ sec.style.pointerEvents = sw.checked ? 'all' : 'none';
753
+ });
754
+ }
755
+
756
+ setupToggle('enableGTM', 'gtmSection');
757
+ setupToggle('enableMeta', 'metaSection');
758
+ setupToggle('enableBuzlCapi', 'buzlCapiSection');
759
+ setupToggle('enableSheets', 'sheetsSection');
760
+ setupToggle('enableZoho', 'zohoSection');
761
+ setupToggle('enableWhatsapp', 'whatsappSection');
762
+
763
+ // Collect Current Configuration
764
+ function getCurrentConfig() {
765
+ const isCapiEnabled = document.getElementById('enableBuzlCapi').checked;
766
+ const isZohoEnabled = document.getElementById('enableZoho').checked;
767
+ const zohoFormId = isZohoEnabled ? document.getElementById('zohoXnqsjsdp').value.trim() : '';
768
+
769
+ return {
770
+ gtmId: document.getElementById('enableGTM').checked ? document.getElementById('gtmId').value.trim() : '',
771
+ enableDeferred: document.getElementById('enableDeferred').checked,
772
+ metaPixelId: document.getElementById('enableMeta').checked ? document.getElementById('metaPixelId').value.trim() : '',
773
+ trackMetaLeadEvent: document.getElementById('trackMetaLead').checked,
774
+ googleSheetUrl: document.getElementById('enableSheets').checked ? document.getElementById('googleSheetUrl').value.trim() : '',
775
+ siteLocation: siteLocationInput ? siteLocationInput.value.trim() : '',
776
+ dynamicFields: activeDynamicFields,
777
+ buzlCapi: {
778
+ endpoint: isCapiEnabled ? document.getElementById('buzlCapiEndpoint').value.trim() : '',
779
+ authUser: isCapiEnabled ? document.getElementById('buzlCapiUser').value.trim() : '',
780
+ authPass: isCapiEnabled ? document.getElementById('buzlCapiPass').value.trim() : ''
781
+ },
782
+ zoho: {
783
+ endpoint: (isZohoEnabled && zohoFormId) ? document.getElementById('zohoEndpoint').value : '',
784
+ xnQsjsdp: zohoFormId,
785
+ xmIwtLD: isZohoEnabled ? document.getElementById('zohoXmiwtld').value.trim() : ''
786
+ },
787
+ whatsappNumber: document.getElementById('enableWhatsapp').checked ? document.getElementById('whatsappNumber').value.trim() : ''
788
+ };
789
+ }
790
+
791
+ // 6. Inject & Run Tests Form Submit
792
+ form.addEventListener('submit', async (e) => {
793
+ e.preventDefault();
794
+
795
+ btnSubmit.disabled = true;
796
+ btnSubmit.innerHTML = '<svg class="icon-sm icon-spin"><use href="#icon-refresh"/></svg> <span>Injecting &amp; Running Tests...</span>';
797
+ testSummaryBadge.textContent = 'Testing...';
798
+ testSummaryBadge.className = 'badge badge-info';
799
+ testConsole.innerHTML = '<p class="empty-state">Executing injection and running automated verification tests...</p>';
800
+
801
+ const payload = getCurrentConfig();
802
+
803
+ try {
804
+ const res = await fetch('/api/inject', {
805
+ method: 'POST',
806
+ headers: { 'Content-Type': 'application/json' },
807
+ body: JSON.stringify(payload)
808
+ });
809
+ const result = await res.json();
810
+
811
+ btnSubmit.disabled = false;
812
+ btnSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-brand-buzl"/></svg> <span>Apply Configuration &amp; Run Automated Tests</span>';
813
+
814
+ if (result.success && result.testReport) {
815
+ const report = result.testReport;
816
+ testSummaryBadge.textContent = `${report.passedCount}/${report.total} Passed`;
817
+ testSummaryBadge.className = report.allPassed ? 'badge badge-success' : 'badge badge-danger';
818
+
819
+ testConsole.innerHTML = '';
820
+ report.tests.forEach(t => {
821
+ const item = document.createElement('div');
822
+ item.className = 'test-item';
823
+ const icon = t.passed
824
+ ? '<svg class="test-status-svg pass"><use href="#icon-check"/></svg>'
825
+ : '<svg class="test-status-svg fail"><use href="#icon-x"/></svg>';
826
+ item.innerHTML = `
827
+ ${icon}
828
+ <div class="test-text">
829
+ <div>${t.name}</div>
830
+ ${t.detail ? `<div class="test-detail">${t.detail}</div>` : ''}
831
+ </div>
832
+ `;
833
+ testConsole.appendChild(item);
834
+ });
835
+
836
+ loadScan();
837
+ } else {
838
+ testSummaryBadge.textContent = 'Failed';
839
+ testSummaryBadge.className = 'badge badge-danger';
840
+ testConsole.innerHTML = `<p class="test-icon fail">Error: ${result.message || 'Injection failed'}</p>`;
841
+ }
842
+ } catch (err) {
843
+ btnSubmit.disabled = false;
844
+ btnSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-brand-buzl"/></svg> <span>Apply Configuration &amp; Run Automated Tests</span>';
845
+ testSummaryBadge.textContent = 'Network Error';
846
+ testSummaryBadge.className = 'badge badge-danger';
847
+ testConsole.innerHTML = `<p class="test-icon fail">Error communicating with local server: ${err.message}</p>`;
848
+ }
849
+ });
850
+
851
+ // 7. Quick Multi-Channel Test Lead Button
852
+ if (btnTestSubmit) {
853
+ btnTestSubmit.addEventListener('click', async () => {
854
+ const config = getCurrentConfig();
855
+ const phone = testLeadPhone.value.trim() || (currentScanData && currentScanData.detectedWhatsapp) || '';
856
+ const name = testLeadName.value.trim() || 'Test Lead';
857
+ const service = testLeadService.value.trim() || (currentScanData && currentScanData.detectedService) || 'General Inquiry';
858
+ const loc = (siteLocationInput ? siteLocationInput.value.trim() : '') || (currentScanData && currentScanData.detectedLocation) || '';
859
+
860
+ btnTestSubmit.disabled = true;
861
+ btnTestSubmit.innerHTML = '<svg class="icon-sm icon-spin"><use href="#icon-refresh"/></svg> <span>Sending Live Test Lead...</span>';
862
+ liveTestBadge.textContent = 'Testing...';
863
+ liveTestBadge.className = 'badge badge-info';
864
+ testDispatchResults.classList.remove('hidden');
865
+ testDispatchResults.innerHTML = '<p class="dispatch-detail">Dispatching synthetic test lead across active channels...</p>';
866
+
867
+ try {
868
+ const res = await fetch('/api/test-submit', {
869
+ method: 'POST',
870
+ headers: { 'Content-Type': 'application/json' },
871
+ body: JSON.stringify({
872
+ config,
873
+ lead: {
874
+ phone,
875
+ name,
876
+ service,
877
+ location: loc
878
+ }
879
+ })
880
+ });
881
+
882
+ const data = await res.json();
883
+ btnTestSubmit.disabled = false;
884
+ btnTestSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-send"/></svg> <span>Dispatch Quick Live Test Lead</span>';
885
+
886
+ if (data.success && data.results) {
887
+ const channels = data.results.channels;
888
+ testDispatchResults.innerHTML = '';
889
+
890
+ let anyFailed = false;
891
+
892
+ for (const [key, ch] of Object.entries(channels)) {
893
+ const row = document.createElement('div');
894
+ row.className = 'dispatch-row';
895
+
896
+ const labelMap = {
897
+ googleSheets: 'Google Sheets',
898
+ buzlCapi: 'Buzl CAPI',
899
+ zoho: 'Zoho CRM'
900
+ };
901
+ const serviceName = labelMap[key] || key;
902
+
903
+ let badgeHtml = '';
904
+ if (!ch.tested) {
905
+ badgeHtml = `<span class="badge badge-neutral">Disabled</span>`;
906
+ } else if (ch.ok) {
907
+ badgeHtml = `<span class="badge badge-success"><svg class="icon-xs"><use href="#icon-check"/></svg> <span>Success</span></span>`;
908
+ } else {
909
+ anyFailed = true;
910
+ badgeHtml = `<span class="badge badge-danger"><svg class="icon-xs"><use href="#icon-x"/></svg> <span>Failed</span></span>`;
911
+ }
912
+
913
+ row.innerHTML = `
914
+ <div>
915
+ <div class="dispatch-service">${serviceName}</div>
916
+ <div class="dispatch-detail">${ch.message}</div>
917
+ </div>
918
+ <div>${badgeHtml}</div>
919
+ `;
920
+ testDispatchResults.appendChild(row);
921
+ }
922
+
923
+ liveTestBadge.textContent = anyFailed ? 'Issues Found' : 'All Channels OK';
924
+ liveTestBadge.className = anyFailed ? 'badge badge-danger' : 'badge badge-success';
925
+
926
+ } else {
927
+ testDispatchResults.innerHTML = `<p class="dispatch-detail text-danger">${data.message || 'Failed to dispatch test lead'}</p>`;
928
+ }
929
+ } catch (err) {
930
+ btnTestSubmit.disabled = false;
931
+ btnTestSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-send"/></svg> <span>Dispatch Quick Live Test Lead</span>';
932
+ testDispatchResults.innerHTML = `<p class="dispatch-detail text-danger">Error: ${err.message}</p>`;
933
+ }
934
+ });
935
+ }
936
+
937
+ if (btnScan) btnScan.addEventListener('click', loadScan);
938
+
939
+ // Initialize
940
+ loadScan();
941
+ loadScriptTemplate();
942
+ });