@mahe_pkm/buzl-capi 0.1.2 → 0.1.3

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.
@@ -50,7 +50,40 @@ document.addEventListener('DOMContentLoaded', () => {
50
50
  const copyScriptCode = document.getElementById('copyScriptCode');
51
51
  const scriptCodeDisplay = document.getElementById('scriptCodeDisplay');
52
52
 
53
+ // Custom Confirm Modal elements
54
+ const customConfirmModal = document.getElementById('customConfirmModal');
55
+ const confirmModalTitle = document.getElementById('confirmModalTitle');
56
+ const confirmModalMessage = document.getElementById('confirmModalMessage');
57
+ const confirmModalConfirmBtn = document.getElementById('confirmModalConfirmBtn');
58
+ const confirmModalCancelBtn = document.getElementById('confirmModalCancelBtn');
59
+ const closeConfirmModal = document.getElementById('closeConfirmModal');
60
+
61
+ // Toast Container
62
+ const toastContainer = document.getElementById('toastContainer');
63
+
64
+ // Verification Report & Export Elements
65
+ const btnExportJson = document.getElementById('btnExportJson');
66
+ const btnExportPdf = document.getElementById('btnExportPdf');
67
+ const kpiTotalChecks = document.getElementById('kpiTotalChecks');
68
+ const kpiPassedChecks = document.getElementById('kpiPassedChecks');
69
+ const kpiFailedChecks = document.getElementById('kpiFailedChecks');
70
+ const kpiCapiStatus = document.getElementById('kpiCapiStatus');
71
+
72
+ // CAPI Inspector Elements
73
+ const capiInspectStatusBadge = document.getElementById('capiInspectStatusBadge');
74
+ const capiInspectLatencyBadge = document.getElementById('capiInspectLatencyBadge');
75
+ const capiPayloadDisplay = document.getElementById('capiPayloadDisplay');
76
+ const capiResponseDisplay = document.getElementById('capiResponseDisplay');
77
+ const capiLogsDisplay = document.getElementById('capiLogsDisplay');
78
+ const btnCopyCapiPayload = document.getElementById('btnCopyCapiPayload');
79
+ const btnCopyCapiResponse = document.getElementById('btnCopyCapiResponse');
80
+ const btnCopyCapiLogs = document.getElementById('btnCopyCapiLogs');
81
+
82
+ let currentVerificationReport = null;
83
+ let currentCapiData = null;
84
+
53
85
  let scriptTemplateText = '';
86
+ let pendingConfirmCallback = null;
54
87
 
55
88
  const siteLocationInput = document.getElementById('siteLocation');
56
89
  const columnHierarchyPreview = document.getElementById('columnHierarchyPreview');
@@ -59,6 +92,575 @@ document.addEventListener('DOMContentLoaded', () => {
59
92
  let activeDynamicFields = [];
60
93
  let currentScanData = null;
61
94
 
95
+ // ==========================================================================
96
+ // In-App Toast Notification Utility (Token-driven)
97
+ // ==========================================================================
98
+ function showToast(title, message, type = 'info', duration = 3800) {
99
+ if (!toastContainer) return;
100
+
101
+ const toast = document.createElement('div');
102
+ toast.className = `toast toast-${type}`;
103
+
104
+ let iconSymbol = '#icon-brand-buzl';
105
+ if (type === 'success') iconSymbol = '#icon-check';
106
+ else if (type === 'error') iconSymbol = '#icon-x';
107
+
108
+ toast.innerHTML = `
109
+ <svg class="icon-sm toast-icon"><use href="${iconSymbol}"/></svg>
110
+ <div class="toast-body">
111
+ <div class="toast-title">${title}</div>
112
+ ${message ? `<div class="toast-message">${message}</div>` : ''}
113
+ </div>
114
+ `;
115
+
116
+ toastContainer.appendChild(toast);
117
+
118
+ setTimeout(() => {
119
+ toast.classList.add('toast-fadeout');
120
+ setTimeout(() => {
121
+ if (toast.parentNode) toast.parentNode.removeChild(toast);
122
+ }, 250);
123
+ }, duration);
124
+ }
125
+
126
+ // ==========================================================================
127
+ // Custom Confirmation Modal (WCAG 2.2 AA compliant)
128
+ // ==========================================================================
129
+ function showConfirmDialog({ title, message, confirmText = 'Confirm', onConfirm }) {
130
+ if (!customConfirmModal) {
131
+ if (confirm(`${title}\n\n${message}`)) {
132
+ if (onConfirm) onConfirm();
133
+ }
134
+ return;
135
+ }
136
+
137
+ if (confirmModalTitle) confirmModalTitle.textContent = title;
138
+ if (confirmModalMessage) confirmModalMessage.textContent = message;
139
+ if (confirmModalConfirmBtn) confirmModalConfirmBtn.textContent = confirmText;
140
+
141
+ pendingConfirmCallback = onConfirm;
142
+ customConfirmModal.classList.remove('hidden');
143
+ if (confirmModalConfirmBtn) confirmModalConfirmBtn.focus();
144
+ }
145
+
146
+ function hideConfirmDialog() {
147
+ if (customConfirmModal) customConfirmModal.classList.add('hidden');
148
+ pendingConfirmCallback = null;
149
+ }
150
+
151
+ if (confirmModalConfirmBtn) {
152
+ confirmModalConfirmBtn.addEventListener('click', () => {
153
+ const cb = pendingConfirmCallback;
154
+ hideConfirmDialog();
155
+ if (cb) cb();
156
+ });
157
+ }
158
+
159
+ if (confirmModalCancelBtn) confirmModalCancelBtn.addEventListener('click', hideConfirmDialog);
160
+ if (closeConfirmModal) closeConfirmModal.addEventListener('click', hideConfirmDialog);
161
+
162
+ // Keyboard accessibility: Escape to dismiss dialogs
163
+ document.addEventListener('keydown', (e) => {
164
+ if (e.key === 'Escape') {
165
+ if (customConfirmModal && !customConfirmModal.classList.contains('hidden')) hideConfirmDialog();
166
+ if (backupsModal && !backupsModal.classList.contains('hidden')) backupsModal.classList.add('hidden');
167
+ if (uninstallModal && !uninstallModal.classList.contains('hidden')) uninstallModal.classList.add('hidden');
168
+ if (scriptModal && !scriptModal.classList.contains('hidden')) scriptModal.classList.add('hidden');
169
+ }
170
+ });
171
+
172
+ // ==========================================================================
173
+ // Console Tabbed Navigation (WAI-ARIA Compliant with Arrow Key Navigation)
174
+ // ==========================================================================
175
+ const tabButtons = Array.from(document.querySelectorAll('.console-tab-btn'));
176
+ const tabContents = document.querySelectorAll('.console-tab-content');
177
+
178
+ function switchTab(tabId) {
179
+ tabButtons.forEach(btn => {
180
+ const isActive = btn.dataset.tab === tabId;
181
+ btn.classList.toggle('active', isActive);
182
+ btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
183
+ btn.setAttribute('tabindex', isActive ? '0' : '-1');
184
+ });
185
+
186
+ tabContents.forEach(content => {
187
+ content.classList.toggle('active', content.id === tabId);
188
+ });
189
+ }
190
+
191
+ // Initialize roving tabindex on tabs
192
+ tabButtons.forEach(btn => {
193
+ const isActive = btn.classList.contains('active');
194
+ btn.setAttribute('tabindex', isActive ? '0' : '-1');
195
+ });
196
+
197
+ tabButtons.forEach((btn, index) => {
198
+ btn.addEventListener('click', () => {
199
+ const target = btn.dataset.tab;
200
+ if (target) switchTab(target);
201
+ });
202
+
203
+ btn.addEventListener('keydown', (e) => {
204
+ let targetIndex = -1;
205
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
206
+ e.preventDefault();
207
+ targetIndex = (index + 1) % tabButtons.length;
208
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
209
+ e.preventDefault();
210
+ targetIndex = (index - 1 + tabButtons.length) % tabButtons.length;
211
+ } else if (e.key === 'Home') {
212
+ e.preventDefault();
213
+ targetIndex = 0;
214
+ } else if (e.key === 'End') {
215
+ e.preventDefault();
216
+ targetIndex = tabButtons.length - 1;
217
+ }
218
+
219
+ if (targetIndex !== -1) {
220
+ const targetBtn = tabButtons[targetIndex];
221
+ targetBtn.focus();
222
+ switchTab(targetBtn.dataset.tab);
223
+ }
224
+ });
225
+ });
226
+
227
+ // ==========================================================================
228
+ // Verification Report KPIs & CAPI Inspector Rendering
229
+ // ==========================================================================
230
+ function renderVerificationKpis(report) {
231
+ if (!report) return;
232
+ currentVerificationReport = report;
233
+
234
+ if (kpiTotalChecks) kpiTotalChecks.textContent = report.total || 0;
235
+ if (kpiPassedChecks) kpiPassedChecks.textContent = report.passedCount || 0;
236
+ if (kpiFailedChecks) kpiFailedChecks.textContent = report.failedCount || 0;
237
+
238
+ if (btnExportJson) btnExportJson.disabled = false;
239
+ if (btnExportPdf) btnExportPdf.disabled = false;
240
+ }
241
+
242
+ function renderCapiInspector(capi) {
243
+ if (!capi) return;
244
+ currentCapiData = capi;
245
+
246
+ // 1. Update Badges
247
+ if (capiInspectStatusBadge) {
248
+ if (capi.ok) {
249
+ capiInspectStatusBadge.className = 'badge badge-success';
250
+ capiInspectStatusBadge.textContent = capi.status ? `HTTP ${capi.status} OK` : 'Verified Active';
251
+ } else if (capi.tested && capi.status === 0) {
252
+ capiInspectStatusBadge.className = 'badge badge-neutral';
253
+ capiInspectStatusBadge.textContent = 'Offline / Standby';
254
+ } else if (capi.tested && !capi.ok) {
255
+ capiInspectStatusBadge.className = 'badge badge-danger';
256
+ capiInspectStatusBadge.textContent = capi.status ? `HTTP ${capi.status}` : 'Error';
257
+ } else if (capi.configured) {
258
+ capiInspectStatusBadge.className = 'badge badge-info';
259
+ capiInspectStatusBadge.textContent = 'Configured';
260
+ } else {
261
+ capiInspectStatusBadge.className = 'badge badge-neutral';
262
+ capiInspectStatusBadge.textContent = 'Disabled';
263
+ }
264
+ }
265
+
266
+ if (kpiCapiStatus) {
267
+ if (capi.ok) {
268
+ kpiCapiStatus.className = 'badge badge-success';
269
+ kpiCapiStatus.textContent = 'Connected';
270
+ } else if (capi.configured) {
271
+ kpiCapiStatus.className = 'badge badge-info';
272
+ kpiCapiStatus.textContent = 'Configured';
273
+ } else {
274
+ kpiCapiStatus.className = 'badge badge-neutral';
275
+ kpiCapiStatus.textContent = 'Inactive';
276
+ }
277
+ }
278
+
279
+ if (capiInspectLatencyBadge) {
280
+ if (capi.latencyMs > 0) {
281
+ capiInspectLatencyBadge.classList.remove('hidden');
282
+ capiInspectLatencyBadge.className = 'badge badge-neutral';
283
+ capiInspectLatencyBadge.textContent = `⚡ ${capi.latencyMs}ms`;
284
+ } else {
285
+ capiInspectLatencyBadge.classList.add('hidden');
286
+ }
287
+ }
288
+
289
+ // 2. Render CAPI Payload
290
+ if (capiPayloadDisplay) {
291
+ if (capi.payload) {
292
+ capiPayloadDisplay.textContent = JSON.stringify(capi.payload, null, 2);
293
+ } else {
294
+ capiPayloadDisplay.textContent = '// No CAPI payload generated yet. Enable Buzl CAPI and run verification.';
295
+ }
296
+ }
297
+
298
+ // 3. Render CAPI Response
299
+ if (capiResponseDisplay) {
300
+ if (capi.response) {
301
+ capiResponseDisplay.textContent = typeof capi.response === 'object'
302
+ ? JSON.stringify(capi.response, null, 2)
303
+ : String(capi.response);
304
+ } else if (capi.message) {
305
+ capiResponseDisplay.textContent = JSON.stringify({ message: capi.message, status: capi.status || 0 }, null, 2);
306
+ } else {
307
+ capiResponseDisplay.textContent = '// No CAPI server response recorded yet.';
308
+ }
309
+ }
310
+
311
+ // 4. Render Registered Console Logs
312
+ if (capiLogsDisplay) {
313
+ if (capi.formattedAuditLog) {
314
+ capiLogsDisplay.innerHTML = '';
315
+ const lines = capi.formattedAuditLog.split('\n');
316
+ lines.forEach(line => {
317
+ const row = document.createElement('div');
318
+ row.className = 'log-row';
319
+ if (line.startsWith('===') || line.startsWith('---')) {
320
+ row.classList.add('log-audit-border');
321
+ row.textContent = line;
322
+ } else if (line.includes('BUZL CAPI LIVE DISPATCH')) {
323
+ row.classList.add('log-audit-title');
324
+ row.textContent = line;
325
+ } else if (line.includes('[Executing')) {
326
+ row.classList.add('log-audit-status');
327
+ row.textContent = line;
328
+ } else if (line.includes('CAPI VERIFICATION RESULT')) {
329
+ row.classList.add('log-audit-banner');
330
+ row.textContent = line;
331
+ } else if (line.startsWith('- ')) {
332
+ const colonIdx = line.indexOf(':');
333
+ if (colonIdx > -1) {
334
+ const keyPart = line.slice(0, colonIdx + 1);
335
+ const valPart = line.slice(colonIdx + 1).trim();
336
+
337
+ const keySpan = document.createElement('span');
338
+ keySpan.className = 'log-audit-key';
339
+ keySpan.textContent = keyPart + ' ';
340
+
341
+ const valSpan = document.createElement('span');
342
+ if (valPart.includes('201 Created') || valPart.includes('200 OK')) {
343
+ valSpan.className = 'log-audit-201';
344
+ } else if (valPart.startsWith('http')) {
345
+ valSpan.className = 'log-audit-url';
346
+ } else if (valPart.startsWith('{') || valPart.startsWith('[')) {
347
+ valSpan.className = 'log-audit-json';
348
+ } else {
349
+ valSpan.className = 'log-audit-val';
350
+ }
351
+ valSpan.textContent = valPart;
352
+
353
+ row.appendChild(keySpan);
354
+ row.appendChild(valSpan);
355
+ } else {
356
+ row.textContent = line;
357
+ }
358
+ } else if (line.trim() === '') {
359
+ row.innerHTML = '&nbsp;';
360
+ } else {
361
+ row.classList.add('log-info');
362
+ row.textContent = line;
363
+ }
364
+ capiLogsDisplay.appendChild(row);
365
+ });
366
+ } else {
367
+ const logs = capi.logs || [];
368
+ if (logs.length > 0) {
369
+ capiLogsDisplay.innerHTML = '';
370
+ logs.forEach(logLine => {
371
+ const row = document.createElement('div');
372
+ row.className = 'log-row';
373
+ if (logLine.includes('[CAPI-INIT]')) row.classList.add('log-info');
374
+ else if (logLine.includes('[CAPI-RESPONSE]') || logLine.includes('[CAPI-ACK]')) row.classList.add('log-success');
375
+ else if (logLine.includes('[CAPI-WARN]')) row.classList.add('log-warn');
376
+ else if (logLine.includes('[CAPI-ERROR]')) row.classList.add('log-error');
377
+ else row.classList.add('log-info');
378
+
379
+ row.textContent = logLine;
380
+ capiLogsDisplay.appendChild(row);
381
+ });
382
+ } else {
383
+ capiLogsDisplay.innerHTML = '<div class="log-row log-info">[STANDBY] Waiting for verification or test dispatch events...</div>';
384
+ }
385
+ }
386
+ }
387
+ }
388
+
389
+ // ==========================================================================
390
+ // Inspector Sub-tab Navigation
391
+ // ==========================================================================
392
+ const inspectorTabButtons = document.querySelectorAll('.inspector-tab-btn');
393
+ const inspectorViews = {
394
+ payload: document.getElementById('inspectViewPayload'),
395
+ response: document.getElementById('inspectViewResponse'),
396
+ logs: document.getElementById('inspectViewLogs')
397
+ };
398
+
399
+ inspectorTabButtons.forEach(btn => {
400
+ btn.addEventListener('click', () => {
401
+ const target = btn.dataset.inspect;
402
+ inspectorTabButtons.forEach(b => {
403
+ const isActive = b.dataset.inspect === target;
404
+ b.classList.toggle('active', isActive);
405
+ b.setAttribute('aria-selected', isActive ? 'true' : 'false');
406
+ });
407
+
408
+ Object.keys(inspectorViews).forEach(key => {
409
+ if (inspectorViews[key]) {
410
+ inspectorViews[key].classList.toggle('hidden', key !== target);
411
+ }
412
+ });
413
+ });
414
+ });
415
+
416
+ // Copy Buttons for Inspector
417
+ if (btnCopyCapiPayload && capiPayloadDisplay) {
418
+ btnCopyCapiPayload.addEventListener('click', () => {
419
+ navigator.clipboard.writeText(capiPayloadDisplay.textContent).then(() => {
420
+ btnCopyCapiPayload.innerHTML = '<svg class="icon-xs" style="color:var(--color-success-accessible);"><use href="#icon-check"/></svg> <span>Copied!</span>';
421
+ showToast('Copied', 'CAPI JSON Payload copied to clipboard', 'success');
422
+ setTimeout(() => {
423
+ btnCopyCapiPayload.innerHTML = '<svg class="icon-xs"><use href="#icon-clipboard"/></svg> <span>Copy Payload</span>';
424
+ }, 2000);
425
+ });
426
+ });
427
+ }
428
+
429
+ if (btnCopyCapiResponse && capiResponseDisplay) {
430
+ btnCopyCapiResponse.addEventListener('click', () => {
431
+ navigator.clipboard.writeText(capiResponseDisplay.textContent).then(() => {
432
+ btnCopyCapiResponse.innerHTML = '<svg class="icon-xs" style="color:var(--color-success-accessible);"><use href="#icon-check"/></svg> <span>Copied!</span>';
433
+ showToast('Copied', 'CAPI Response copied to clipboard', 'success');
434
+ setTimeout(() => {
435
+ btnCopyCapiResponse.innerHTML = '<svg class="icon-xs"><use href="#icon-clipboard"/></svg> <span>Copy Response</span>';
436
+ }, 2000);
437
+ });
438
+ });
439
+ }
440
+
441
+ if (btnCopyCapiLogs && capiLogsDisplay) {
442
+ btnCopyCapiLogs.addEventListener('click', () => {
443
+ navigator.clipboard.writeText(capiLogsDisplay.textContent).then(() => {
444
+ btnCopyCapiLogs.innerHTML = '<svg class="icon-xs" style="color:var(--color-success-accessible);"><use href="#icon-check"/></svg> <span>Copied!</span>';
445
+ showToast('Copied', 'Console Logs copied to clipboard', 'success');
446
+ setTimeout(() => {
447
+ btnCopyCapiLogs.innerHTML = '<svg class="icon-xs"><use href="#icon-clipboard"/></svg> <span>Copy Logs</span>';
448
+ }, 2000);
449
+ });
450
+ });
451
+ }
452
+
453
+ // ==========================================================================
454
+ // Automated Verification Report Export: JSON & Printable PDF
455
+ // ==========================================================================
456
+ function escapeHtml(str) {
457
+ if (!str) return '';
458
+ return String(str)
459
+ .replace(/&/g, '&amp;')
460
+ .replace(/</g, '&lt;')
461
+ .replace(/>/g, '&gt;')
462
+ .replace(/"/g, '&quot;')
463
+ .replace(/'/g, '&#039;');
464
+ }
465
+
466
+ function exportReportJson() {
467
+ if (!currentVerificationReport) {
468
+ showToast('Export Error', 'Please run automated verification first to generate a report.', 'error');
469
+ return;
470
+ }
471
+
472
+ const exportData = {
473
+ reportTitle: 'BUZL CAPI Suite — Automated Verification & Compliance Audit Report',
474
+ version: '0.1.3',
475
+ generatedAt: new Date().toISOString(),
476
+ projectRoot: (liveStateRootDir ? liveStateRootDir.textContent : '') || 'Workspace Root',
477
+ summary: {
478
+ totalChecks: currentVerificationReport.total || 0,
479
+ passedChecks: currentVerificationReport.passedCount || 0,
480
+ failedChecks: currentVerificationReport.failedCount || 0,
481
+ allPassed: !!currentVerificationReport.allPassed,
482
+ passRate: currentVerificationReport.total ? Math.round((currentVerificationReport.passedCount / currentVerificationReport.total) * 100) + '%' : '0%'
483
+ },
484
+ activeConfiguration: getCurrentConfig(),
485
+ tests: currentVerificationReport.tests || [],
486
+ capiTelemetry: currentVerificationReport.capi || currentCapiData || { configured: false }
487
+ };
488
+
489
+ const jsonStr = JSON.stringify(exportData, null, 2);
490
+ const blob = new Blob([jsonStr], { type: 'application/json' });
491
+ const url = URL.createObjectURL(blob);
492
+ const a = document.createElement('a');
493
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
494
+ a.href = url;
495
+ a.download = `buzl-verification-report-${timestamp}.json`;
496
+ document.body.appendChild(a);
497
+ a.click();
498
+ document.body.removeChild(a);
499
+ URL.revokeObjectURL(url);
500
+
501
+ showToast('Report Exported', 'Saved as structured JSON document', 'success');
502
+ }
503
+
504
+ function exportReportPdf() {
505
+ if (!currentVerificationReport) {
506
+ showToast('Export Error', 'Please run automated verification first to generate a report.', 'error');
507
+ return;
508
+ }
509
+
510
+ const report = currentVerificationReport;
511
+ const capi = report.capi || currentCapiData || {};
512
+ const timestamp = new Date().toLocaleString();
513
+ const projectPath = (liveStateRootDir ? liveStateRootDir.textContent : '') || 'Workspace Root';
514
+ const passRate = report.total ? Math.round((report.passedCount / report.total) * 100) : 0;
515
+ const statusColor = report.allPassed ? '#087f5b' : '#b02316';
516
+
517
+ let testRowsHtml = '';
518
+ (report.tests || []).forEach((t, i) => {
519
+ const iconText = t.passed ? '✔ PASS' : '✖ FAIL';
520
+ const badgeStyle = t.passed
521
+ ? 'background:#e6fcf5; color:#087f5b; border:1px solid #b2f2bb;'
522
+ : 'background:#fff5f5; color:#b02316; border:1px solid #ffc9c9;';
523
+ testRowsHtml += `
524
+ <tr style="border-bottom:1px solid #e5eaef;">
525
+ <td style="padding:6px 10px; font-family:monospace; color:#5a6a85;">#${i + 1}</td>
526
+ <td style="padding:6px 10px; font-weight:600; color:#2a3547;">${escapeHtml(t.name)}</td>
527
+ <td style="padding:6px 10px; text-align:center;"><span style="padding:2px 8px; border-radius:4px; font-size:10px; font-weight:700; ${badgeStyle}">${iconText}</span></td>
528
+ <td style="padding:6px 10px; font-size:11px; color:#5a6a85;">${escapeHtml(t.detail || '—')}</td>
529
+ </tr>
530
+ `;
531
+ });
532
+
533
+ const capiPayloadStr = capi.payload ? JSON.stringify(capi.payload, null, 2) : 'No CAPI payload generated';
534
+ const capiResponseStr = capi.response ? JSON.stringify(capi.response, null, 2) : (capi.message || 'No response recorded');
535
+ const capiLogsStr = capi.formattedAuditLog || (capi.logs || []).join('\n') || 'No console logs registered';
536
+
537
+ const printableHtml = `
538
+ <!DOCTYPE html>
539
+ <html>
540
+ <head>
541
+ <meta charset="utf-8">
542
+ <title>Buzl Verification Audit Report - ${timestamp}</title>
543
+ <style>
544
+ @page { size: A4 portrait; margin: 12mm 15mm; }
545
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; color: #2a3547; line-height: 1.4; padding: 15px; font-size: 11px; }
546
+ .header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #004aad; padding-bottom: 12px; margin-bottom: 16px; }
547
+ .logo-badge { font-family: monospace; font-size: 16px; font-weight: 700; color: #004aad; }
548
+ .title { font-size: 18px; font-weight: 700; color: #2a3547; margin: 3px 0 2px; }
549
+ .meta { font-size: 11px; color: #5a6a85; }
550
+ .scorecard { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 16px; }
551
+ .kpi-box { border: 1px solid #b8c4d5; border-radius: 6px; padding: 8px 12px; text-align: center; background: #f8fafc; }
552
+ .kpi-title { font-size: 10px; text-transform: uppercase; color: #5a6a85; font-weight: 700; margin-bottom: 2px; }
553
+ .kpi-num { font-size: 18px; font-weight: 700; font-family: monospace; }
554
+ .section-title { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin: 16px 0 8px; color: #004aad; border-bottom: 1px solid #e5eaef; padding-bottom: 4px; }
555
+ table { width: 100%; border-collapse: collapse; margin-bottom: 14px; font-size: 11px; }
556
+ th { background: #f2f6fa; color: #2a3547; text-align: left; padding: 6px 10px; font-weight: 700; border-bottom: 1px solid #b8c4d5; font-size: 10px; text-transform: uppercase; }
557
+ pre { background: #f8fafc; border: 1px solid #b8c4d5; border-radius: 4px; padding: 8px 10px; font-size: 10px; font-family: monospace; max-height: 220px; overflow: hidden; white-space: pre-wrap; word-break: break-all; }
558
+ .logs-box { background: #1a2332; color: #e2e8f0; font-family: monospace; font-size: 10px; padding: 8px 10px; border-radius: 4px; line-height: 1.5; white-space: pre-wrap; }
559
+ .footer { margin-top: 24px; padding-top: 10px; border-top: 1px solid #e5eaef; display: flex; justify-content: space-between; font-size: 10px; color: #5a6a85; }
560
+ </style>
561
+ </head>
562
+ <body>
563
+ <div class="header">
564
+ <div>
565
+ <div class="logo-badge">BUZL / CAPI SUITE</div>
566
+ <div class="title">Automated Verification &amp; Compliance Audit Report</div>
567
+ <div class="meta">Target Project: <code>${projectPath}</code></div>
568
+ </div>
569
+ <div style="text-align:right;">
570
+ <div class="meta"><strong>Date:</strong> ${timestamp}</div>
571
+ <div class="meta"><strong>Status:</strong> <span style="color:${statusColor}; font-weight:700;">${report.allPassed ? 'VERIFIED PASS' : 'ISSUES DETECTED'}</span></div>
572
+ <div class="meta"><strong>Engine:</strong> v0.1.3 (WCAG 2.2 AA)</div>
573
+ </div>
574
+ </div>
575
+
576
+ <div class="scorecard">
577
+ <div class="kpi-box">
578
+ <div class="kpi-title">Total Checks</div>
579
+ <div class="kpi-num">${report.total}</div>
580
+ </div>
581
+ <div class="kpi-box">
582
+ <div class="kpi-title">Passed</div>
583
+ <div class="kpi-num" style="color:#087f5b;">${report.passedCount}</div>
584
+ </div>
585
+ <div class="kpi-box">
586
+ <div class="kpi-title">Failed</div>
587
+ <div class="kpi-num" style="color:${report.failedCount > 0 ? '#b02316' : '#5a6a85'};">${report.failedCount}</div>
588
+ </div>
589
+ <div class="kpi-box">
590
+ <div class="kpi-title">Compliance Score</div>
591
+ <div class="kpi-num" style="color:${statusColor};">${passRate}%</div>
592
+ </div>
593
+ </div>
594
+
595
+ <div class="section-title">1. Automated Verification Checks (${report.passedCount}/${report.total} Passed)</div>
596
+ <table>
597
+ <thead>
598
+ <tr>
599
+ <th style="width:30px;">#</th>
600
+ <th>Verification Item</th>
601
+ <th style="width:90px; text-align:center;">Result</th>
602
+ <th>Diagnostic Details</th>
603
+ </tr>
604
+ </thead>
605
+ <tbody>
606
+ ${testRowsHtml}
607
+ </tbody>
608
+ </table>
609
+
610
+ <div class="section-title">2. Buzl CAPI Live Telemetry &amp; Payload Inspector</div>
611
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
612
+ <div>
613
+ <div style="font-weight:700; margin-bottom:4px; font-size:10px; text-transform:uppercase; color:#5a6a85;">Dispatched CAPI Payload (JSON):</div>
614
+ <pre>${escapeHtml(capiPayloadStr)}</pre>
615
+ </div>
616
+ <div>
617
+ <div style="font-weight:700; margin-bottom:4px; font-size:10px; text-transform:uppercase; color:#5a6a85;">Server Acknowledgment / Response:</div>
618
+ <pre>${escapeHtml(capiResponseStr)}</pre>
619
+ </div>
620
+ </div>
621
+
622
+ <div class="section-title" style="margin-top:12px;">3. Registered Console Logs &amp; Audit Trail</div>
623
+ <div class="logs-box">${escapeHtml(capiLogsStr)}</div>
624
+
625
+ <div class="footer">
626
+ <div>Generated by Buzl CAPI Suite • Official Locations Compliance Engine</div>
627
+ <div>Certified Automated Verification Audit • Confidential</div>
628
+ </div>
629
+ </body>
630
+ </html>
631
+ `;
632
+
633
+ const printFrame = document.createElement('iframe');
634
+ printFrame.style.position = 'fixed';
635
+ printFrame.style.right = '0';
636
+ printFrame.style.bottom = '0';
637
+ printFrame.style.width = '0';
638
+ printFrame.style.height = '0';
639
+ printFrame.style.border = 'none';
640
+ document.body.appendChild(printFrame);
641
+
642
+ const doc = printFrame.contentWindow.document;
643
+ doc.open();
644
+ doc.write(printableHtml);
645
+ doc.close();
646
+
647
+ setTimeout(() => {
648
+ printFrame.contentWindow.focus();
649
+ printFrame.contentWindow.print();
650
+ setTimeout(() => {
651
+ if (printFrame.parentNode) printFrame.parentNode.removeChild(printFrame);
652
+ }, 1500);
653
+ }, 400);
654
+
655
+ showToast('Exporting PDF', 'Print dialog opened. Select "Save as PDF"', 'info');
656
+ }
657
+
658
+ if (btnExportJson) btnExportJson.addEventListener('click', exportReportJson);
659
+ if (btnExportPdf) btnExportPdf.addEventListener('click', exportReportPdf);
660
+
661
+ // ==========================================================================
662
+ // Column Hierarchy Preview
663
+ // ==========================================================================
62
664
  function renderColumnHierarchy() {
63
665
  if (!columnHierarchyPreview) return;
64
666
  columnHierarchyPreview.innerHTML = '';
@@ -87,7 +689,9 @@ document.addEventListener('DOMContentLoaded', () => {
87
689
  });
88
690
  }
89
691
 
90
- // 1. Fetch project scan & live state
692
+ // ==========================================================================
693
+ // 1. Fetch Project Scan & Live State
694
+ // ==========================================================================
91
695
  async function loadScan() {
92
696
  fileBadge.textContent = 'Scanning...';
93
697
  fileBadge.className = 'badge badge-neutral';
@@ -134,7 +738,7 @@ document.addEventListener('DOMContentLoaded', () => {
134
738
  label.className = 'field-checkbox-item';
135
739
  label.innerHTML = `
136
740
  <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>
741
+ <span><strong>${formatted}</strong> (${f.tag}) — <small class="helper-text">Col D</small></span>
138
742
  `;
139
743
 
140
744
  label.querySelector('input').addEventListener('change', (e) => {
@@ -189,6 +793,7 @@ document.addEventListener('DOMContentLoaded', () => {
189
793
  } catch (err) {
190
794
  fileBadge.textContent = 'Scan Error';
191
795
  fileBadge.className = 'badge badge-danger';
796
+ showToast('Scan Error', 'Could not scan local project files', 'error');
192
797
  }
193
798
  }
194
799
 
@@ -237,7 +842,11 @@ document.addEventListener('DOMContentLoaded', () => {
237
842
  items.forEach(item => {
238
843
  const chip = document.createElement('span');
239
844
  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>` : ''}`;
845
+ chip.innerHTML = `
846
+ <span class="status-dot ${item.active ? 'dot-active' : 'dot-inactive'}"></span>
847
+ <span>${item.text}</span>
848
+ ${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>` : ''}
849
+ `;
241
850
 
242
851
  const removeBtn = chip.querySelector('.chip-remove-btn');
243
852
  if (removeBtn) {
@@ -252,7 +861,10 @@ document.addEventListener('DOMContentLoaded', () => {
252
861
  }
253
862
  }
254
863
 
255
- async function removeSingleService(serviceKey) {
864
+ // ==========================================================================
865
+ // Surgical Removal of Single Service
866
+ // ==========================================================================
867
+ function removeSingleService(serviceKey) {
256
868
  const serviceNames = {
257
869
  gtm: 'Google Tag Manager (GTM)',
258
870
  meta: 'Meta Pixel & Client CAPI',
@@ -262,26 +874,30 @@ document.addEventListener('DOMContentLoaded', () => {
262
874
  whatsapp: 'WhatsApp Handoff'
263
875
  };
264
876
  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
877
 
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}`);
878
+ showConfirmDialog({
879
+ title: `Remove ${name}`,
880
+ message: `Are you sure you want to surgically 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.`,
881
+ confirmText: 'Remove Service',
882
+ onConfirm: async () => {
883
+ try {
884
+ const res = await fetch('/api/remove-service', {
885
+ method: 'POST',
886
+ headers: { 'Content-Type': 'application/json' },
887
+ body: JSON.stringify({ service: serviceKey })
888
+ });
889
+ const data = await res.json();
890
+ if (data.success) {
891
+ showToast('Service Removed', data.message, 'success');
892
+ loadScan();
893
+ } else {
894
+ showToast('Removal Failed', data.message, 'error');
895
+ }
896
+ } catch (err) {
897
+ showToast('Network Error', err.message, 'error');
898
+ }
281
899
  }
282
- } catch (err) {
283
- alert(`Network error removing ${name}: ${err.message}`);
284
- }
900
+ });
285
901
  }
286
902
 
287
903
  function applyLiveConfigToForm(data) {
@@ -326,6 +942,19 @@ document.addEventListener('DOMContentLoaded', () => {
326
942
  capiSwitch.dispatchEvent(new Event('change'));
327
943
  }
328
944
 
945
+ if (capiData && capiData.endpoint && !currentCapiData) {
946
+ renderCapiInspector({
947
+ configured: true,
948
+ tested: false,
949
+ endpoint: capiData.endpoint,
950
+ authUser: capiData.authUser,
951
+ logs: [
952
+ `[${new Date().toISOString().slice(11, 19)}] [CAPI-INFO] Loaded active CAPI configuration for endpoint: ${capiData.endpoint}`,
953
+ `[${new Date().toISOString().slice(11, 19)}] [CAPI-INFO] Ready for automated verification or synthetic test lead.`
954
+ ]
955
+ });
956
+ }
957
+
329
958
  // Google Sheets
330
959
  const sheetsSwitch = document.getElementById('enableSheets');
331
960
  const sheetsInput = document.getElementById('googleSheetUrl');
@@ -370,6 +999,7 @@ document.addEventListener('DOMContentLoaded', () => {
370
999
  waSwitch.checked = true;
371
1000
  waSwitch.dispatchEvent(new Event('change'));
372
1001
  }
1002
+ showToast('WhatsApp Applied', `Applied auto-detected number: ${detectedWa}`, 'success');
373
1003
  };
374
1004
  if (waHelper) {
375
1005
  waHelper.textContent = `Auto-fetches ${detectedWa} from form buttons, or specify fallback number.`;
@@ -393,7 +1023,9 @@ document.addEventListener('DOMContentLoaded', () => {
393
1023
  }
394
1024
  }
395
1025
 
1026
+ // ==========================================================================
396
1027
  // 2. Render Individual Discovered Forms with Per-Form Testing
1028
+ // ==========================================================================
397
1029
  function renderIndividualForms(forms, archetypes) {
398
1030
  if (!individualFormsList) return;
399
1031
  individualFormsList.innerHTML = '';
@@ -402,9 +1034,9 @@ document.addEventListener('DOMContentLoaded', () => {
402
1034
 
403
1035
  if (formsCountBadge) {
404
1036
  if (archetypes && archetypes.length > 0 && forms.length !== archetypes.length) {
405
- formsCountBadge.textContent = `${forms.length} Instances (${archetypes.length} Unique)`;
1037
+ formsCountBadge.textContent = `${forms.length} (${archetypes.length} unique)`;
406
1038
  } else {
407
- formsCountBadge.textContent = `${forms.length} Form(s)`;
1039
+ formsCountBadge.textContent = `${forms.length}`;
408
1040
  }
409
1041
  }
410
1042
 
@@ -417,7 +1049,7 @@ document.addEventListener('DOMContentLoaded', () => {
417
1049
  const card = document.createElement('div');
418
1050
  card.className = 'form-item-card';
419
1051
 
420
- const tagPills = f.inputs.map(i => `<span class="col-pill col-pill-dynamic" style="font-size: 10px;">${i.name}</span>`).join('');
1052
+ const tagPills = f.inputs.map(i => `<span class="col-pill col-pill-dynamic" style="font-size: 11px;">${i.name}</span>`).join('');
421
1053
 
422
1054
  // Build sample test input controls
423
1055
  const inputElements = f.inputs.map(i => {
@@ -446,7 +1078,7 @@ document.addEventListener('DOMContentLoaded', () => {
446
1078
  const isShared = f.isShared || (f.pages && f.pages.length > 1);
447
1079
  const pageInfo = isShared
448
1080
  ? `<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>`;
1081
+ : `<small style="color: var(--color-text-inverse); font-size: 11px;">in <code>${f.file || (f.pages && f.pages[0]) || 'page'}</code></small>`;
450
1082
 
451
1083
  const pagesList = (isShared && f.pages)
452
1084
  ? `<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px;">
@@ -473,7 +1105,7 @@ document.addEventListener('DOMContentLoaded', () => {
473
1105
  <div class="form-actions-row">
474
1106
  <button type="button" class="btn btn-primary btn-sm btn-test-this-form" data-form-id="${f.formId}">
475
1107
  <svg class="icon-xs"><use href="#icon-send"/></svg>
476
- <span>Test This Form (${f.selector})</span>
1108
+ <span>Test Form (${f.selector})</span>
477
1109
  </button>
478
1110
  <span class="form-test-result hidden"></span>
479
1111
  </div>
@@ -488,7 +1120,7 @@ document.addEventListener('DOMContentLoaded', () => {
488
1120
  testBtn.innerHTML = '<svg class="icon-xs icon-spin"><use href="#icon-refresh"/></svg> <span>Testing...</span>';
489
1121
  resultSpan.classList.remove('hidden');
490
1122
  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>';
1123
+ resultSpan.innerHTML = '<svg class="icon-xs icon-spin"><use href="#icon-refresh"/></svg> <span>Dispatching...</span>';
492
1124
 
493
1125
  // Collect fields
494
1126
  const fieldValues = {};
@@ -511,30 +1143,40 @@ document.addEventListener('DOMContentLoaded', () => {
511
1143
  });
512
1144
  const data = await res.json();
513
1145
  testBtn.disabled = false;
514
- testBtn.innerHTML = `<svg class="icon-xs"><use href="#icon-send"/></svg> <span>Test This Form (${f.selector})</span>`;
1146
+ testBtn.innerHTML = `<svg class="icon-xs"><use href="#icon-send"/></svg> <span>Test Form (${f.selector})</span>`;
515
1147
 
516
1148
  if (data.success && data.results) {
517
1149
  const ch = data.results.channels;
1150
+ if (ch && ch.buzlCapi) {
1151
+ renderCapiInspector(ch.buzlCapi);
1152
+ if (currentVerificationReport) {
1153
+ currentVerificationReport.capi = ch.buzlCapi;
1154
+ }
1155
+ }
518
1156
  const sheetOk = ch.googleSheets && ch.googleSheets.ok;
519
1157
  const capiOk = ch.buzlCapi && ch.buzlCapi.ok;
520
1158
 
521
1159
  if (sheetOk || capiOk) {
522
1160
  resultSpan.className = 'form-test-result badge badge-success';
523
1161
  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>`;
1162
+ resultSpan.innerHTML = `<svg class="icon-xs"><use href="#icon-check"/></svg> <span>Delivered! (Sheets: ${sheetOk ? rowLabel : 'Off'} | CAPI: ${capiOk ? 'Accepted' : 'Off'})</span>`;
1163
+ showToast('Form Test Success', `Payload successfully delivered for ${f.selector}`, 'success');
525
1164
  } else {
526
1165
  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>`;
1166
+ resultSpan.innerHTML = `<svg class="icon-xs"><use href="#icon-x"/></svg> <span>Failed: ${ch.googleSheets.message || ch.buzlCapi.message || 'Error'}</span>`;
1167
+ showToast('Form Test Warning', 'No active channels received payload', 'error');
528
1168
  }
529
1169
  } else {
530
1170
  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>`;
1171
+ resultSpan.innerHTML = `<svg class="icon-xs"><use href="#icon-x"/></svg> <span>Error: ${data.message || 'Failed'}</span>`;
1172
+ showToast('Form Test Error', data.message || 'Execution failed', 'error');
532
1173
  }
533
1174
  } catch (e) {
534
1175
  testBtn.disabled = false;
535
- testBtn.innerHTML = `<svg class="icon-xs"><use href="#icon-send"/></svg> <span>Test This Form (${f.selector})</span>`;
1176
+ testBtn.innerHTML = `<svg class="icon-xs"><use href="#icon-send"/></svg> <span>Test Form (${f.selector})</span>`;
536
1177
  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>`;
1178
+ resultSpan.innerHTML = `<svg class="icon-xs"><use href="#icon-x"/></svg> <span>Network Error: ${e.message}</span>`;
1179
+ showToast('Network Error', e.message, 'error');
538
1180
  }
539
1181
  });
540
1182
 
@@ -542,7 +1184,9 @@ document.addEventListener('DOMContentLoaded', () => {
542
1184
  });
543
1185
  }
544
1186
 
1187
+ // ==========================================================================
545
1188
  // 3. Named Backups Management
1189
+ // ==========================================================================
546
1190
  async function loadBackupsList() {
547
1191
  if (!backupsTableBody) return;
548
1192
  backupsTableBody.innerHTML = '<tr><td colspan="5" style="text-align: center;">Loading backups...</td></tr>';
@@ -555,7 +1199,7 @@ document.addEventListener('DOMContentLoaded', () => {
555
1199
  if (backupsCountBadge) backupsCountBadge.textContent = backups.length;
556
1200
 
557
1201
  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>';
1202
+ backupsTableBody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: var(--color-text-inverse); padding: 18px 0;">No snapshots found on disk.</td></tr>';
559
1203
  return;
560
1204
  }
561
1205
 
@@ -566,64 +1210,78 @@ document.addEventListener('DOMContentLoaded', () => {
566
1210
 
567
1211
  tr.innerHTML = `
568
1212
  <td><strong>${b.name || 'Snapshot'}</strong></td>
569
- <td><code style="font-size: 11px; color: var(--text-muted);">${b.dirName}</code></td>
1213
+ <td><code style="font-size: 11px; color: var(--color-brand-primary);">${b.dirName}</code></td>
570
1214
  <td>${formattedDate}</td>
571
1215
  <td><span class="badge badge-neutral">${b.filesCount} file(s)</span></td>
572
1216
  <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}">
1217
+ <button type="button" class="btn btn-secondary btn-xs btn-restore-backup" data-dir="${b.dirName}" data-name="${b.name}">
574
1218
  <svg class="icon-xs"><use href="#icon-refresh"/></svg>
575
1219
  <span>Restore</span>
576
1220
  </button>
577
- <button type="button" class="btn btn-danger-outline btn-sm btn-delete-backup" data-dir="${b.dirName}">
1221
+ <button type="button" class="btn btn-danger-outline btn-xs btn-delete-backup" data-dir="${b.dirName}">
578
1222
  <svg class="icon-xs"><use href="#icon-trash"/></svg>
579
1223
  <span>Delete</span>
580
1224
  </button>
581
1225
  </td>
582
1226
  `;
583
1227
 
584
- // Restore action
585
- tr.querySelector('.btn-restore-backup').addEventListener('click', async (e) => {
1228
+ // Restore action with in-app confirm
1229
+ tr.querySelector('.btn-restore-backup').addEventListener('click', (e) => {
586
1230
  const dir = e.currentTarget.dataset.dir;
587
1231
  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
1232
 
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
- }
1233
+ showConfirmDialog({
1234
+ title: `Restore Snapshot`,
1235
+ message: `Are you sure you want to restore "${name}" (${dir})?\n\nAll current HTML files will be replaced with this snapshot.`,
1236
+ confirmText: 'Restore Files',
1237
+ onConfirm: async () => {
1238
+ try {
1239
+ const rRes = await fetch('/api/restore', {
1240
+ method: 'POST',
1241
+ headers: { 'Content-Type': 'application/json' },
1242
+ body: JSON.stringify({ backupDirName: dir })
1243
+ });
1244
+ const rData = await rRes.json();
1245
+ showToast('Snapshot Restored', rData.message, 'success');
1246
+ backupsModal.classList.add('hidden');
1247
+ loadScan();
1248
+ } catch (err) {
1249
+ showToast('Restore Error', err.message, 'error');
1250
+ }
1251
+ }
1252
+ });
603
1253
  });
604
1254
 
605
- // Delete action
606
- tr.querySelector('.btn-delete-backup').addEventListener('click', async (e) => {
1255
+ // Delete action with in-app confirm
1256
+ tr.querySelector('.btn-delete-backup').addEventListener('click', (e) => {
607
1257
  const dir = e.currentTarget.dataset.dir;
608
- if (!confirm(`Are you sure you want to delete backup "${dir}"? This cannot be undone.`)) return;
609
1258
 
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
- }
1259
+ showConfirmDialog({
1260
+ title: `Delete Snapshot`,
1261
+ message: `Are you sure you want to permanently delete backup "${dir}"? This action cannot be undone.`,
1262
+ confirmText: 'Delete Snapshot',
1263
+ onConfirm: async () => {
1264
+ try {
1265
+ const dRes = await fetch('/api/delete-backup', {
1266
+ method: 'POST',
1267
+ headers: { 'Content-Type': 'application/json' },
1268
+ body: JSON.stringify({ backupDirName: dir })
1269
+ });
1270
+ const dData = await dRes.json();
1271
+ showToast('Snapshot Deleted', dData.message || 'Backup deleted', 'info');
1272
+ loadBackupsList();
1273
+ } catch (err) {
1274
+ showToast('Delete Failed', err.message, 'error');
1275
+ }
1276
+ }
1277
+ });
621
1278
  });
622
1279
 
623
1280
  backupsTableBody.appendChild(tr);
624
1281
  });
625
1282
  } catch (e) {
626
- backupsTableBody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: red;">Failed to load backups: ${e.message}</td></tr>`;
1283
+ backupsTableBody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: var(--color-danger-dark);">Failed to load backups: ${e.message}</td></tr>`;
1284
+ showToast('Backups Error', e.message, 'error');
627
1285
  }
628
1286
  }
629
1287
 
@@ -656,16 +1314,19 @@ document.addEventListener('DOMContentLoaded', () => {
656
1314
  btnCreateNamedBackup.disabled = false;
657
1315
  btnCreateNamedBackup.innerHTML = '<svg class="icon-sm"><use href="#icon-database"/></svg> <span>Save Snapshot</span>';
658
1316
  if (customBackupName) customBackupName.value = '';
1317
+ showToast('Snapshot Created', `Saved point-in-time snapshot on disk`, 'success');
659
1318
  loadBackupsList();
660
1319
  } catch (err) {
661
1320
  btnCreateNamedBackup.disabled = false;
662
1321
  btnCreateNamedBackup.innerHTML = '<svg class="icon-sm"><use href="#icon-database"/></svg> <span>Save Snapshot</span>';
663
- alert('Backup failed: ' + err.message);
1322
+ showToast('Backup Failed', err.message, 'error');
664
1323
  }
665
1324
  });
666
1325
  }
667
1326
 
1327
+ // ==========================================================================
668
1328
  // 4. Clean Tracking Removal / Uninstaller
1329
+ // ==========================================================================
669
1330
  if (btnUninstallTracking) {
670
1331
  btnUninstallTracking.addEventListener('click', () => {
671
1332
  uninstallModal.classList.remove('hidden');
@@ -678,21 +1339,21 @@ document.addEventListener('DOMContentLoaded', () => {
678
1339
  if (btnConfirmUninstall) {
679
1340
  btnConfirmUninstall.addEventListener('click', async () => {
680
1341
  btnConfirmUninstall.disabled = true;
681
- btnConfirmUninstall.textContent = '⏳ Removing all tracking...';
1342
+ btnConfirmUninstall.innerHTML = '<svg class="icon-xs icon-spin"><use href="#icon-refresh"/></svg> <span>Removing all tracking...</span>';
682
1343
 
683
1344
  try {
684
1345
  const res = await fetch('/api/remove-tracking', { method: 'POST' });
685
1346
  const data = await res.json();
686
1347
  btnConfirmUninstall.disabled = false;
687
- btnConfirmUninstall.textContent = 'Confirm & Remove All Tracking';
1348
+ btnConfirmUninstall.innerHTML = '<svg class="icon-xs"><use href="#icon-trash"/></svg> <span>Confirm &amp; Remove All Tracking</span>';
688
1349
  uninstallModal.classList.add('hidden');
689
1350
 
690
- alert(data.message);
1351
+ showToast('Tracking Removed', data.message, 'success');
691
1352
  loadScan();
692
1353
  } catch (e) {
693
1354
  btnConfirmUninstall.disabled = false;
694
- btnConfirmUninstall.textContent = 'Confirm & Remove All Tracking';
695
- alert('Removal failed: ' + e.message);
1355
+ btnConfirmUninstall.innerHTML = '<svg class="icon-xs"><use href="#icon-trash"/></svg> <span>Confirm &amp; Remove All Tracking</span>';
1356
+ showToast('Removal Failed', e.message, 'error');
696
1357
  }
697
1358
  });
698
1359
  }
@@ -708,7 +1369,9 @@ document.addEventListener('DOMContentLoaded', () => {
708
1369
  });
709
1370
  });
710
1371
 
1372
+ // ==========================================================================
711
1373
  // 5. Fetch Apps Script Template Code
1374
+ // ==========================================================================
712
1375
  async function loadScriptTemplate() {
713
1376
  try {
714
1377
  const res = await fetch('/api/apps-script');
@@ -734,21 +1397,147 @@ document.addEventListener('DOMContentLoaded', () => {
734
1397
  if (copyScriptCode) {
735
1398
  copyScriptCode.addEventListener('click', () => {
736
1399
  navigator.clipboard.writeText(scriptTemplateText).then(() => {
737
- copyScriptCode.innerHTML = '<svg class="icon-xs" style="color:var(--success)"><use href="#icon-check"/></svg> <span>Copied!</span>';
1400
+ copyScriptCode.innerHTML = '<svg class="icon-xs" style="color:var(--color-success-dark)"><use href="#icon-check"/></svg> <span>Copied!</span>';
1401
+ showToast('Copied to Clipboard', 'Google Apps Script code copied to clipboard', 'success');
738
1402
  setTimeout(() => {
739
1403
  copyScriptCode.innerHTML = '<svg class="icon-xs"><use href="#icon-clipboard"/></svg> <span>Copy Code</span>';
740
- }, 2000);
1404
+ }, 2200);
741
1405
  });
742
1406
  });
743
1407
  }
744
1408
 
1409
+ // ==========================================================================
1410
+ // Cross-Platform User Handbook Modal & PDF Print Integration
1411
+ // ==========================================================================
1412
+ const btnOpenHandbook = document.getElementById('btnOpenHandbook');
1413
+ const handbookModal = document.getElementById('handbookModal');
1414
+ const closeHandbookModal = document.getElementById('closeHandbookModal');
1415
+ const btnPrintHandbookPdf = document.getElementById('btnPrintHandbookPdf');
1416
+
1417
+ if (btnOpenHandbook && handbookModal) {
1418
+ btnOpenHandbook.addEventListener('click', () => {
1419
+ handbookModal.classList.remove('hidden');
1420
+ });
1421
+ }
1422
+
1423
+ if (closeHandbookModal && handbookModal) {
1424
+ closeHandbookModal.addEventListener('click', () => {
1425
+ handbookModal.classList.add('hidden');
1426
+ });
1427
+ }
1428
+
1429
+ // OS Tab Switching in Handbook Modal
1430
+ const handbookOsTabs = document.querySelectorAll('#handbookModal .console-tab-btn[data-os]');
1431
+ const osCommands = {
1432
+ powershell: {
1433
+ cmdWizard: 'npx @mahe_pkm/buzl-capi',
1434
+ cmdGui: 'npx @mahe_pkm/buzl-capi --gui',
1435
+ cmdBackup: 'npx @mahe_pkm/buzl-capi --backup "Pre-Launch Baseline"',
1436
+ cmdList: 'npx @mahe_pkm/buzl-capi --list-backups',
1437
+ cmdRestore: 'npx @mahe_pkm/buzl-capi --restore',
1438
+ cmdUninstall: 'npx @mahe_pkm/buzl-capi --uninstall',
1439
+ cmdNpxLatest: 'npx @mahe_pkm/buzl-capi@latest --gui',
1440
+ cmdGlobalInstall: 'npm install -g @mahe_pkm/buzl-capi@latest',
1441
+ cmdPublish2fa: 'npm publish --tag latest --otp=YOUR_6_DIGIT_OTP'
1442
+ },
1443
+ cmd: {
1444
+ cmdWizard: 'npx @mahe_pkm/buzl-capi',
1445
+ cmdGui: 'npx @mahe_pkm/buzl-capi --gui',
1446
+ cmdBackup: 'npx @mahe_pkm/buzl-capi --backup "Pre-Launch Baseline"',
1447
+ cmdList: 'npx @mahe_pkm/buzl-capi --list-backups',
1448
+ cmdRestore: 'npx @mahe_pkm/buzl-capi --restore',
1449
+ cmdUninstall: 'npx @mahe_pkm/buzl-capi --uninstall',
1450
+ cmdNpxLatest: 'npx @mahe_pkm/buzl-capi@latest --gui',
1451
+ cmdGlobalInstall: 'npm install -g @mahe_pkm/buzl-capi@latest',
1452
+ cmdPublish2fa: 'npm publish --tag latest --otp=YOUR_6_DIGIT_OTP'
1453
+ },
1454
+ macos: {
1455
+ cmdWizard: 'npx @mahe_pkm/buzl-capi',
1456
+ cmdGui: 'npx @mahe_pkm/buzl-capi --gui',
1457
+ cmdBackup: 'npx @mahe_pkm/buzl-capi --backup "Pre-Launch Baseline"',
1458
+ cmdList: 'npx @mahe_pkm/buzl-capi --list-backups',
1459
+ cmdRestore: 'npx @mahe_pkm/buzl-capi --restore',
1460
+ cmdUninstall: 'npx @mahe_pkm/buzl-capi --uninstall',
1461
+ cmdNpxLatest: 'npx @mahe_pkm/buzl-capi@latest --gui',
1462
+ cmdGlobalInstall: 'npm install -g @mahe_pkm/buzl-capi@latest',
1463
+ cmdPublish2fa: 'npm publish --tag latest --otp=YOUR_6_DIGIT_OTP'
1464
+ },
1465
+ linux: {
1466
+ cmdWizard: 'npx @mahe_pkm/buzl-capi',
1467
+ cmdGui: 'npx @mahe_pkm/buzl-capi --gui',
1468
+ cmdBackup: 'npx @mahe_pkm/buzl-capi --backup "Pre-Launch Baseline"',
1469
+ cmdList: 'npx @mahe_pkm/buzl-capi --list-backups',
1470
+ cmdRestore: 'npx @mahe_pkm/buzl-capi --restore',
1471
+ cmdUninstall: 'npx @mahe_pkm/buzl-capi --uninstall',
1472
+ cmdNpxLatest: 'npx @mahe_pkm/buzl-capi@latest --gui',
1473
+ cmdGlobalInstall: 'npm install -g @mahe_pkm/buzl-capi@latest',
1474
+ cmdPublish2fa: 'npm publish --tag latest --otp=YOUR_6_DIGIT_OTP'
1475
+ }
1476
+ };
1477
+
1478
+ handbookOsTabs.forEach(tab => {
1479
+ tab.addEventListener('click', () => {
1480
+ const os = tab.dataset.os;
1481
+ handbookOsTabs.forEach(t => t.classList.toggle('active', t === tab));
1482
+ const cmds = osCommands[os] || osCommands.powershell;
1483
+ Object.keys(cmds).forEach(id => {
1484
+ const el = document.getElementById(id);
1485
+ if (el) el.textContent = cmds[id];
1486
+ });
1487
+ });
1488
+ });
1489
+
1490
+ // Copy Buttons for Handbook Command Blocks
1491
+ document.querySelectorAll('.btn-copy-cli').forEach(btn => {
1492
+ btn.addEventListener('click', () => {
1493
+ const targetId = btn.dataset.target;
1494
+ const targetEl = document.getElementById(targetId);
1495
+ if (targetEl) {
1496
+ navigator.clipboard.writeText(targetEl.textContent).then(() => {
1497
+ btn.innerHTML = '<svg class="icon-xs" style="color:var(--color-success-accessible);"><use href="#icon-check"/></svg> <span>Copied!</span>';
1498
+ showToast('Copied', 'Command copied to clipboard', 'success');
1499
+ setTimeout(() => {
1500
+ btn.innerHTML = '<svg class="icon-xs"><use href="#icon-clipboard"/></svg> <span>Copy</span>';
1501
+ }, 2000);
1502
+ });
1503
+ }
1504
+ });
1505
+ });
1506
+
1507
+ // Print Handbook as PDF (100% vector copyable text via isolated iframe)
1508
+ if (btnPrintHandbookPdf) {
1509
+ btnPrintHandbookPdf.addEventListener('click', () => {
1510
+ const printFrame = document.createElement('iframe');
1511
+ printFrame.style.position = 'fixed';
1512
+ printFrame.style.right = '0';
1513
+ printFrame.style.bottom = '0';
1514
+ printFrame.style.width = '0';
1515
+ printFrame.style.height = '0';
1516
+ printFrame.style.border = 'none';
1517
+ printFrame.src = '/handbook';
1518
+ document.body.appendChild(printFrame);
1519
+
1520
+ printFrame.onload = () => {
1521
+ setTimeout(() => {
1522
+ printFrame.contentWindow.focus();
1523
+ printFrame.contentWindow.print();
1524
+ setTimeout(() => {
1525
+ if (printFrame.parentNode) printFrame.parentNode.removeChild(printFrame);
1526
+ }, 2000);
1527
+ }, 400);
1528
+ };
1529
+
1530
+ showToast('Exporting Handbook PDF', 'Print dialog opened. Select "Save as PDF"', 'info');
1531
+ });
1532
+ }
1533
+
745
1534
  // Toggle helpers
746
1535
  function setupToggle(switchId, sectionId) {
747
1536
  const sw = document.getElementById(switchId);
748
1537
  const sec = document.getElementById(sectionId);
749
1538
  if (!sw || !sec) return;
750
1539
  sw.addEventListener('change', () => {
751
- sec.style.opacity = sw.checked ? '1' : '0.4';
1540
+ sec.style.opacity = sw.checked ? '1' : '0.45';
752
1541
  sec.style.pointerEvents = sw.checked ? 'all' : 'none';
753
1542
  });
754
1543
  }
@@ -788,16 +1577,21 @@ document.addEventListener('DOMContentLoaded', () => {
788
1577
  };
789
1578
  }
790
1579
 
791
- // 6. Inject & Run Tests Form Submit
1580
+ // ==========================================================================
1581
+ // 6. Inject & Run Automated Verification Form Submit
1582
+ // ==========================================================================
792
1583
  form.addEventListener('submit', async (e) => {
793
1584
  e.preventDefault();
794
1585
 
795
1586
  btnSubmit.disabled = true;
796
1587
  btnSubmit.innerHTML = '<svg class="icon-sm icon-spin"><use href="#icon-refresh"/></svg> <span>Injecting &amp; Running Tests...</span>';
797
1588
  testSummaryBadge.textContent = 'Testing...';
798
- testSummaryBadge.className = 'badge badge-info';
1589
+ testSummaryBadge.className = 'tab-badge badge-info';
799
1590
  testConsole.innerHTML = '<p class="empty-state">Executing injection and running automated verification tests...</p>';
800
1591
 
1592
+ // Automatically switch to verification tab so developer sees real-time test output
1593
+ switchTab('tabVerification');
1594
+
801
1595
  const payload = getCurrentConfig();
802
1596
 
803
1597
  try {
@@ -814,7 +1608,12 @@ document.addEventListener('DOMContentLoaded', () => {
814
1608
  if (result.success && result.testReport) {
815
1609
  const report = result.testReport;
816
1610
  testSummaryBadge.textContent = `${report.passedCount}/${report.total} Passed`;
817
- testSummaryBadge.className = report.allPassed ? 'badge badge-success' : 'badge badge-danger';
1611
+ testSummaryBadge.className = report.allPassed ? 'tab-badge badge-success' : 'tab-badge badge-danger';
1612
+
1613
+ renderVerificationKpis(report);
1614
+ if (report.capi) {
1615
+ renderCapiInspector(report.capi);
1616
+ }
818
1617
 
819
1618
  testConsole.innerHTML = '';
820
1619
  report.tests.forEach(t => {
@@ -833,22 +1632,32 @@ document.addEventListener('DOMContentLoaded', () => {
833
1632
  testConsole.appendChild(item);
834
1633
  });
835
1634
 
1635
+ showToast(
1636
+ report.allPassed ? 'Configuration Applied' : 'Applied with Warnings',
1637
+ `${report.passedCount}/${report.total} automated tests passed.`,
1638
+ report.allPassed ? 'success' : 'error'
1639
+ );
1640
+
836
1641
  loadScan();
837
1642
  } else {
838
1643
  testSummaryBadge.textContent = 'Failed';
839
- testSummaryBadge.className = 'badge badge-danger';
1644
+ testSummaryBadge.className = 'tab-badge badge-danger';
840
1645
  testConsole.innerHTML = `<p class="test-icon fail">Error: ${result.message || 'Injection failed'}</p>`;
1646
+ showToast('Injection Failed', result.message || 'Check server logs', 'error');
841
1647
  }
842
1648
  } catch (err) {
843
1649
  btnSubmit.disabled = false;
844
1650
  btnSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-brand-buzl"/></svg> <span>Apply Configuration &amp; Run Automated Tests</span>';
845
1651
  testSummaryBadge.textContent = 'Network Error';
846
- testSummaryBadge.className = 'badge badge-danger';
1652
+ testSummaryBadge.className = 'tab-badge badge-danger';
847
1653
  testConsole.innerHTML = `<p class="test-icon fail">Error communicating with local server: ${err.message}</p>`;
1654
+ showToast('Network Error', err.message, 'error');
848
1655
  }
849
1656
  });
850
1657
 
1658
+ // ==========================================================================
851
1659
  // 7. Quick Multi-Channel Test Lead Button
1660
+ // ==========================================================================
852
1661
  if (btnTestSubmit) {
853
1662
  btnTestSubmit.addEventListener('click', async () => {
854
1663
  const config = getCurrentConfig();
@@ -885,6 +1694,12 @@ document.addEventListener('DOMContentLoaded', () => {
885
1694
 
886
1695
  if (data.success && data.results) {
887
1696
  const channels = data.results.channels;
1697
+ if (channels && channels.buzlCapi) {
1698
+ renderCapiInspector(channels.buzlCapi);
1699
+ if (currentVerificationReport) {
1700
+ currentVerificationReport.capi = channels.buzlCapi;
1701
+ }
1702
+ }
888
1703
  testDispatchResults.innerHTML = '';
889
1704
 
890
1705
  let anyFailed = false;
@@ -894,9 +1709,9 @@ document.addEventListener('DOMContentLoaded', () => {
894
1709
  row.className = 'dispatch-row';
895
1710
 
896
1711
  const labelMap = {
897
- googleSheets: 'Google Sheets',
898
- buzlCapi: 'Buzl CAPI',
899
- zoho: 'Zoho CRM'
1712
+ googleSheets: 'Google Sheets CRM',
1713
+ buzlCapi: 'Buzl CAPI Server',
1714
+ zoho: 'Zoho Web-to-Lead'
900
1715
  };
901
1716
  const serviceName = labelMap[key] || key;
902
1717
 
@@ -923,18 +1738,31 @@ document.addEventListener('DOMContentLoaded', () => {
923
1738
  liveTestBadge.textContent = anyFailed ? 'Issues Found' : 'All Channels OK';
924
1739
  liveTestBadge.className = anyFailed ? 'badge badge-danger' : 'badge badge-success';
925
1740
 
1741
+ showToast(
1742
+ anyFailed ? 'Test Lead Dispatched with Warnings' : 'Test Lead Dispatched',
1743
+ anyFailed ? 'One or more active channels reported issues.' : 'All active channels responded successfully.',
1744
+ anyFailed ? 'error' : 'success'
1745
+ );
1746
+
926
1747
  } else {
927
1748
  testDispatchResults.innerHTML = `<p class="dispatch-detail text-danger">${data.message || 'Failed to dispatch test lead'}</p>`;
1749
+ showToast('Dispatch Failed', data.message || 'Check server connection', 'error');
928
1750
  }
929
1751
  } catch (err) {
930
1752
  btnTestSubmit.disabled = false;
931
1753
  btnTestSubmit.innerHTML = '<svg class="icon-sm"><use href="#icon-send"/></svg> <span>Dispatch Quick Live Test Lead</span>';
932
1754
  testDispatchResults.innerHTML = `<p class="dispatch-detail text-danger">Error: ${err.message}</p>`;
1755
+ showToast('Network Error', err.message, 'error');
933
1756
  }
934
1757
  });
935
1758
  }
936
1759
 
937
- if (btnScan) btnScan.addEventListener('click', loadScan);
1760
+ if (btnScan) {
1761
+ btnScan.addEventListener('click', () => {
1762
+ loadScan();
1763
+ showToast('Scan Refreshed', 'Inspected workspace HTML files and active state', 'info', 2000);
1764
+ });
1765
+ }
938
1766
 
939
1767
  // Initialize
940
1768
  loadScan();