@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.
- package/Buzl_GoogleAppsScript_Template.gs +948 -948
- package/CHANGELOG.md +109 -83
- package/LICENSE +21 -21
- package/README.md +339 -253
- package/USER_HANDBOOK.html +808 -0
- package/USER_HANDBOOK.md +842 -0
- package/assets/js/buzl-tracking.js +390 -390
- package/bin/cli.js +175 -155
- package/package.json +3 -1
- package/src/cli/terminal.js +388 -388
- package/src/core/injector.js +321 -321
- package/src/core/rollback.js +281 -281
- package/src/core/scanner.js +506 -506
- package/src/core/tester.js +666 -357
- package/src/gui/public/app.js +915 -87
- package/src/gui/public/index.html +620 -230
- package/src/gui/public/style.css +1603 -509
- package/src/gui/server.js +242 -232
- package/src/index.js +47 -47
- package/src/templates/GoogleAppsScript.gs +948 -948
- package/src/templates/buzl-tracking.js +475 -475
- package/src/templates/gtm-meta-snippets.js +149 -149
package/src/gui/public/app.js
CHANGED
|
@@ -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 = ' ';
|
|
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, '&')
|
|
460
|
+
.replace(/</g, '<')
|
|
461
|
+
.replace(/>/g, '>')
|
|
462
|
+
.replace(/"/g, '"')
|
|
463
|
+
.replace(/'/g, ''');
|
|
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 & 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 & 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 & 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
|
-
//
|
|
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">
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
}
|
|
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}
|
|
1037
|
+
formsCountBadge.textContent = `${forms.length} (${archetypes.length} unique)`;
|
|
406
1038
|
} else {
|
|
407
|
-
formsCountBadge.textContent = `${forms.length}
|
|
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:
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
|
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"
|
|
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-
|
|
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(--
|
|
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-
|
|
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-
|
|
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',
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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',
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1348
|
+
btnConfirmUninstall.innerHTML = '<svg class="icon-xs"><use href="#icon-trash"/></svg> <span>Confirm & Remove All Tracking</span>';
|
|
688
1349
|
uninstallModal.classList.add('hidden');
|
|
689
1350
|
|
|
690
|
-
|
|
1351
|
+
showToast('Tracking Removed', data.message, 'success');
|
|
691
1352
|
loadScan();
|
|
692
1353
|
} catch (e) {
|
|
693
1354
|
btnConfirmUninstall.disabled = false;
|
|
694
|
-
btnConfirmUninstall.
|
|
695
|
-
|
|
1355
|
+
btnConfirmUninstall.innerHTML = '<svg class="icon-xs"><use href="#icon-trash"/></svg> <span>Confirm & 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
|
-
},
|
|
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.
|
|
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
|
-
//
|
|
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 & 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 & 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
|
|
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)
|
|
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();
|