@mahe_pkm/buzl-capi 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Buzl_GoogleAppsScript_Template.gs +948 -0
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/assets/js/buzl-tracking.js +390 -0
- package/bin/cli.js +155 -0
- package/package.json +54 -0
- package/src/cli/terminal.js +388 -0
- package/src/core/injector.js +321 -0
- package/src/core/rollback.js +281 -0
- package/src/core/scanner.js +506 -0
- package/src/core/tester.js +357 -0
- package/src/gui/public/app.js +942 -0
- package/src/gui/public/index.html +634 -0
- package/src/gui/public/style.css +1130 -0
- package/src/gui/server.js +232 -0
- package/src/index.js +47 -0
- package/src/templates/GoogleAppsScript.gs +948 -0
- package/src/templates/buzl-tracking.js +475 -0
- package/src/templates/gtm-meta-snippets.js +149 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML, Form & Site Location Scanner
|
|
3
|
+
* Scans directories recursively to discover HTML files, existing tags, form fields, and site location
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_IGNORED_DIRS = [
|
|
9
|
+
'node_modules',
|
|
10
|
+
'.git',
|
|
11
|
+
'.github',
|
|
12
|
+
'Dev_CAPi',
|
|
13
|
+
'dist',
|
|
14
|
+
'build',
|
|
15
|
+
'.next',
|
|
16
|
+
'.nuxt',
|
|
17
|
+
'vendor',
|
|
18
|
+
'partials',
|
|
19
|
+
'includes',
|
|
20
|
+
'components'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Recursively find all HTML files
|
|
25
|
+
*/
|
|
26
|
+
function findHtmlFiles(dir, ignoredDirs = DEFAULT_IGNORED_DIRS) {
|
|
27
|
+
let results = [];
|
|
28
|
+
try {
|
|
29
|
+
const list = fs.readdirSync(dir);
|
|
30
|
+
for (const file of list) {
|
|
31
|
+
const fullPath = path.join(dir, file);
|
|
32
|
+
const stat = fs.statSync(fullPath);
|
|
33
|
+
|
|
34
|
+
if (stat && stat.isDirectory()) {
|
|
35
|
+
if (file === '.buzl' || file.startsWith('.buzl') || ignoredDirs.includes(file)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
results = results.concat(findHtmlFiles(fullPath, ignoredDirs));
|
|
39
|
+
} else if (file.toLowerCase().endsWith('.html') || file.toLowerCase().endsWith('.htm')) {
|
|
40
|
+
results.push(fullPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(`[Scanner] Error reading directory ${dir}:`, err.message);
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sanitize and normalize WhatsApp phone numbers to international digits format
|
|
51
|
+
*/
|
|
52
|
+
function sanitizeWhatsappNumber(raw) {
|
|
53
|
+
if (!raw) return null;
|
|
54
|
+
const digits = String(raw).replace(/\D/g, '');
|
|
55
|
+
if (!digits) return null;
|
|
56
|
+
// If 10 digits starting with [6-9] (Indian mobile like 9591318811), prepend country code 91
|
|
57
|
+
if (digits.length === 10 && /^[6-9]/.test(digits)) {
|
|
58
|
+
return '91' + digits;
|
|
59
|
+
}
|
|
60
|
+
// If 11 digits starting with 0, replace 0 with 91
|
|
61
|
+
if (digits.length === 11 && digits.startsWith('0')) {
|
|
62
|
+
return '91' + digits.slice(1);
|
|
63
|
+
}
|
|
64
|
+
if (digits.length >= 10 && digits.length <= 15) {
|
|
65
|
+
return digits;
|
|
66
|
+
}
|
|
67
|
+
return digits;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract WhatsApp numbers from markup strings or attributes
|
|
72
|
+
*/
|
|
73
|
+
function extractWhatsappFromText(text) {
|
|
74
|
+
if (!text) return null;
|
|
75
|
+
// 1. wa.me/919591318811
|
|
76
|
+
const waMeMatch = text.match(/wa\.me\/(\+?\d+)/i);
|
|
77
|
+
if (waMeMatch) return sanitizeWhatsappNumber(waMeMatch[1]);
|
|
78
|
+
|
|
79
|
+
// 2. api.whatsapp.com/send?phone=919591318811
|
|
80
|
+
const apiMatch = text.match(/api\.whatsapp\.com\/send\?[^"'>]*phone=(\+?\d+)/i);
|
|
81
|
+
if (apiMatch) return sanitizeWhatsappNumber(apiMatch[1]);
|
|
82
|
+
|
|
83
|
+
// 3. data-whatsapp="919591318811" or data-wa, data-phone, data-number, data-buzl-wa
|
|
84
|
+
const dataAttrMatch = text.match(/data-(?:whatsapp|wa|phone|number|buzl-wa)=["'](\+?\d+)["']/i);
|
|
85
|
+
if (dataAttrMatch) return sanitizeWhatsappNumber(dataAttrMatch[1]);
|
|
86
|
+
|
|
87
|
+
// 4. tel:+919591318811
|
|
88
|
+
const telMatch = text.match(/href=["']tel:(\+?\d+)["']/i);
|
|
89
|
+
if (telMatch) return sanitizeWhatsappNumber(telMatch[1]);
|
|
90
|
+
|
|
91
|
+
// 5. JS variable like WA_NUMBER = '919591318811'
|
|
92
|
+
const jsVarMatch = text.match(/(?:WA_NUMBER|waNumber|whatsapp_number)\s*=\s*["'](\+?\d+)["']/i);
|
|
93
|
+
if (jsVarMatch) return sanitizeWhatsappNumber(jsVarMatch[1]);
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Auto-detect site location from metadata or markup
|
|
100
|
+
*/
|
|
101
|
+
function detectSiteLocation(rootDir, htmlContent = '') {
|
|
102
|
+
// 1. Check website_context.json
|
|
103
|
+
const ctxPath = path.join(rootDir, 'website_context.json');
|
|
104
|
+
if (fs.existsSync(ctxPath)) {
|
|
105
|
+
try {
|
|
106
|
+
const ctx = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
|
|
107
|
+
if (ctx.address) {
|
|
108
|
+
// Extract area and city from address (e.g. "Sahakar Nagar, Byatarayanapura, Bengaluru")
|
|
109
|
+
const parts = ctx.address.split(',').map(s => s.trim()).filter(s => !s.toLowerCase().includes('floor') && !/\d{6}/.test(s));
|
|
110
|
+
if (parts.length >= 2) {
|
|
111
|
+
return parts.slice(-3).join(', ');
|
|
112
|
+
}
|
|
113
|
+
return ctx.address;
|
|
114
|
+
}
|
|
115
|
+
if (ctx.location) return ctx.location;
|
|
116
|
+
if (ctx.city) return ctx.city;
|
|
117
|
+
} catch (e) {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. Check assets/data/business.json
|
|
121
|
+
const bizPath = path.join(rootDir, 'assets', 'data', 'business.json');
|
|
122
|
+
if (fs.existsSync(bizPath)) {
|
|
123
|
+
try {
|
|
124
|
+
const biz = JSON.parse(fs.readFileSync(bizPath, 'utf8'));
|
|
125
|
+
if (biz.location) return biz.location;
|
|
126
|
+
if (biz.address) {
|
|
127
|
+
if (biz.address.area && biz.address.city) return `${biz.address.area}, ${biz.address.city}`;
|
|
128
|
+
if (biz.address.city) return biz.address.city;
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 3. Scan HTML for <address> or hero kicker text
|
|
134
|
+
if (htmlContent) {
|
|
135
|
+
const kickerM = htmlContent.match(/class=["'][^"']*(?:hero__kicker|kicker)[^"']*["'][^>]*>([\s\S]*?)<\/p>/i);
|
|
136
|
+
if (kickerM) {
|
|
137
|
+
const plain = kickerM[1].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
138
|
+
if (plain && plain.length < 100) return plain;
|
|
139
|
+
}
|
|
140
|
+
const addrM = htmlContent.match(/<address\b[^>]*>([\s\S]*?)<\/address>/i);
|
|
141
|
+
if (addrM) {
|
|
142
|
+
const plain = addrM[1].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
143
|
+
if (plain && plain.length < 120) return plain;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Analyze a single HTML file for head, body, forms, inputs in order
|
|
152
|
+
*/
|
|
153
|
+
function analyzeHtmlFile(filePath, rootDir = null) {
|
|
154
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
155
|
+
const relativePath = rootDir ? path.relative(rootDir, filePath).replace(/\\/g, '/') : path.basename(filePath);
|
|
156
|
+
|
|
157
|
+
const hasHead = /<head[\s>]/i.test(content);
|
|
158
|
+
const hasBody = /<body[\s>]/i.test(content);
|
|
159
|
+
|
|
160
|
+
// Check existing tracking tags
|
|
161
|
+
const hasGTM = /googletagmanager\.com/i.test(content) || /GTM-[A-Z0-9]+/i.test(content);
|
|
162
|
+
const hasMeta = /connect\.facebook\.net/i.test(content) || /fbq\(/i.test(content);
|
|
163
|
+
const hasBuzl = /buzl-tracking/i.test(content) || /BuzlTracker/i.test(content) || /BUZL_TRACKING_/i.test(content);
|
|
164
|
+
|
|
165
|
+
// Extract existing IDs if present
|
|
166
|
+
const gtmMatch = content.match(/GTM-[A-Z0-9]+/i);
|
|
167
|
+
const existingGtmId = gtmMatch ? gtmMatch[0] : null;
|
|
168
|
+
|
|
169
|
+
const metaMatch = content.match(/fbq\(\s*['"]init['"]\s*,\s*['"](\d+)['"]/i);
|
|
170
|
+
const existingMetaPixelId = metaMatch ? metaMatch[1] : null;
|
|
171
|
+
|
|
172
|
+
// Extract existing Buzl configuration if previously injected
|
|
173
|
+
let existingConfig = null;
|
|
174
|
+
const configMatch = content.match(/window\.__BUZL_CONFIG__\s*=\s*(\{[\s\S]*?\});/);
|
|
175
|
+
if (configMatch) {
|
|
176
|
+
try {
|
|
177
|
+
existingConfig = JSON.parse(configMatch[1]);
|
|
178
|
+
} catch (e) {}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Detect forms and inputs in exact DOM order
|
|
182
|
+
const forms = [];
|
|
183
|
+
const formRegex = /<form\b([^>]*)>([\s\S]*?)<\/form>/gi;
|
|
184
|
+
let match;
|
|
185
|
+
|
|
186
|
+
while ((match = formRegex.exec(content)) !== null) {
|
|
187
|
+
const formAttrs = match[1] || '';
|
|
188
|
+
const formInner = match[2] || '';
|
|
189
|
+
|
|
190
|
+
const idMatch = formAttrs.match(/id=['"]([^'"]+)['"]/i);
|
|
191
|
+
const actionMatch = formAttrs.match(/action=['"]([^'"]+)['"]/i);
|
|
192
|
+
const formId = idMatch ? idMatch[1] : `form_${forms.length + 1}`;
|
|
193
|
+
const action = actionMatch ? actionMatch[1] : '';
|
|
194
|
+
|
|
195
|
+
// Extract all interactive form elements in DOM order: <input>, <select>, <textarea>
|
|
196
|
+
const inputs = [];
|
|
197
|
+
const elemRegex = /<(input|select|textarea)\b([^>]*)>/gi;
|
|
198
|
+
let elemMatch;
|
|
199
|
+
|
|
200
|
+
while ((elemMatch = elemRegex.exec(formInner)) !== null) {
|
|
201
|
+
const tag = elemMatch[1].toLowerCase();
|
|
202
|
+
const attrs = elemMatch[2];
|
|
203
|
+
|
|
204
|
+
const nameM = attrs.match(/name=['"]([^'"]+)['"]/i);
|
|
205
|
+
const typeM = attrs.match(/type=['"]([^'"]+)['"]/i);
|
|
206
|
+
const idM = attrs.match(/id=['"]([^'"]+)['"]/i);
|
|
207
|
+
|
|
208
|
+
const type = typeM ? typeM[1].toLowerCase() : (tag === 'select' ? 'select' : tag === 'textarea' ? 'textarea' : 'text');
|
|
209
|
+
if (['submit', 'button', 'reset', 'hidden'].includes(type)) continue;
|
|
210
|
+
|
|
211
|
+
const name = nameM ? nameM[1] : (idM ? idM[1] : '');
|
|
212
|
+
if (name) {
|
|
213
|
+
inputs.push({
|
|
214
|
+
tag,
|
|
215
|
+
name,
|
|
216
|
+
type,
|
|
217
|
+
id: idM ? idM[1] : ''
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Inspect form and its submit buttons for WhatsApp number
|
|
223
|
+
let detectedWhatsapp = extractWhatsappFromText(formAttrs);
|
|
224
|
+
|
|
225
|
+
if (!detectedWhatsapp) {
|
|
226
|
+
const btnRegex = /<(button|input|a)\b([^>]*)>([\s\S]*?)<\/\1>|<(input)\b([^>]*)>/gi;
|
|
227
|
+
let btnM;
|
|
228
|
+
while ((btnM = btnRegex.exec(formInner)) !== null) {
|
|
229
|
+
const btnAttrs = btnM[2] || btnM[5] || '';
|
|
230
|
+
const num = extractWhatsappFromText(btnAttrs);
|
|
231
|
+
if (num) {
|
|
232
|
+
detectedWhatsapp = num;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (btnAttrs.includes('type="hidden"') && /name=["'](?:whatsapp|wa|phone_to)["']/i.test(btnAttrs)) {
|
|
236
|
+
const valM = btnAttrs.match(/value=["'](\+?\d+)["']/i);
|
|
237
|
+
if (valM) {
|
|
238
|
+
detectedWhatsapp = sanitizeWhatsappNumber(valM[1]);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
forms.push({
|
|
246
|
+
id: formId,
|
|
247
|
+
action: action,
|
|
248
|
+
inputCount: inputs.length,
|
|
249
|
+
inputs: inputs,
|
|
250
|
+
detectedWhatsapp: detectedWhatsapp || null
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Collect all WhatsApp numbers across the page (buttons, links, floating CTAs, script vars)
|
|
255
|
+
const detectedWhatsappNumbers = [];
|
|
256
|
+
const waRegex = /(?:wa\.me\/|api\.whatsapp\.com\/send\?[^"'>]*phone=|data-(?:whatsapp|wa|phone|buzl-wa)=["']|href=["']tel:)(\+?\d+)/gi;
|
|
257
|
+
let waM;
|
|
258
|
+
while ((waM = waRegex.exec(content)) !== null) {
|
|
259
|
+
const cleanNum = sanitizeWhatsappNumber(waM[1]);
|
|
260
|
+
if (cleanNum && !detectedWhatsappNumbers.includes(cleanNum)) {
|
|
261
|
+
detectedWhatsappNumbers.push(cleanNum);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const jsWaRegex = /(?:WA_NUMBER|waNumber|whatsapp_number)\s*=\s*["'](\+?\d+)["']/gi;
|
|
266
|
+
while ((waM = jsWaRegex.exec(content)) !== null) {
|
|
267
|
+
const cleanNum = sanitizeWhatsappNumber(waM[1]);
|
|
268
|
+
if (cleanNum && !detectedWhatsappNumbers.includes(cleanNum)) {
|
|
269
|
+
detectedWhatsappNumbers.push(cleanNum);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// If a form didn't have an explicit button number, associate the page's detected number as fallback
|
|
274
|
+
if (detectedWhatsappNumbers.length > 0) {
|
|
275
|
+
forms.forEach(f => {
|
|
276
|
+
if (!f.detectedWhatsapp) {
|
|
277
|
+
f.detectedWhatsapp = detectedWhatsappNumbers[0];
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
filePath,
|
|
284
|
+
relativePath,
|
|
285
|
+
hasHead,
|
|
286
|
+
hasBody,
|
|
287
|
+
hasGTM,
|
|
288
|
+
existingGtmId,
|
|
289
|
+
hasMeta,
|
|
290
|
+
existingMetaPixelId,
|
|
291
|
+
existingConfig,
|
|
292
|
+
hasBuzl,
|
|
293
|
+
forms,
|
|
294
|
+
detectedWhatsappNumbers,
|
|
295
|
+
content
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Scan entire project directory
|
|
301
|
+
*/
|
|
302
|
+
function scanProject(rootDir) {
|
|
303
|
+
const htmlFiles = findHtmlFiles(rootDir);
|
|
304
|
+
const analyzedFiles = htmlFiles.map(f => analyzeHtmlFile(f, rootDir));
|
|
305
|
+
|
|
306
|
+
const totalForms = analyzedFiles.reduce((acc, f) => acc + f.forms.length, 0);
|
|
307
|
+
const filesWithGtm = analyzedFiles.filter(f => f.hasGTM).length;
|
|
308
|
+
const filesWithMeta = analyzedFiles.filter(f => f.hasMeta).length;
|
|
309
|
+
|
|
310
|
+
// Collect unique form fields in order of discovery
|
|
311
|
+
const uniqueFields = [];
|
|
312
|
+
const fieldSet = new Set();
|
|
313
|
+
|
|
314
|
+
analyzedFiles.forEach(f => {
|
|
315
|
+
f.forms.forEach(form => {
|
|
316
|
+
form.inputs.forEach(input => {
|
|
317
|
+
const lowerName = input.name.toLowerCase();
|
|
318
|
+
if (!fieldSet.has(lowerName)) {
|
|
319
|
+
fieldSet.add(lowerName);
|
|
320
|
+
uniqueFields.push({
|
|
321
|
+
name: input.name,
|
|
322
|
+
type: input.type,
|
|
323
|
+
tag: input.tag,
|
|
324
|
+
isCore: ['name', 'phone', 'location', 'bizname', 'bizphone', 'bizlocation'].includes(lowerName)
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Detect site location from first HTML file or metadata
|
|
332
|
+
const firstHtmlContent = analyzedFiles.length > 0 ? analyzedFiles[0].content : '';
|
|
333
|
+
const detectedLocation = detectSiteLocation(rootDir, firstHtmlContent);
|
|
334
|
+
|
|
335
|
+
// Strip large content strings from final return
|
|
336
|
+
const cleanFiles = analyzedFiles.map(({ content, ...rest }) => rest);
|
|
337
|
+
|
|
338
|
+
// Extract first found existingConfig
|
|
339
|
+
const existingConfig = (analyzedFiles.find(f => f.existingConfig && Object.keys(f.existingConfig).length > 0) || {}).existingConfig || null;
|
|
340
|
+
|
|
341
|
+
// Compute live state of the site
|
|
342
|
+
const { listBackups } = require('./rollback');
|
|
343
|
+
const backups = listBackups(rootDir);
|
|
344
|
+
|
|
345
|
+
const gtmId = analyzedFiles.find(f => f.existingGtmId)?.existingGtmId || (existingConfig && existingConfig.gtmId) || null;
|
|
346
|
+
const metaPixelId = analyzedFiles.find(f => f.existingMetaPixelId)?.existingMetaPixelId || (existingConfig && existingConfig.metaPixelId) || null;
|
|
347
|
+
|
|
348
|
+
const capiCfg = existingConfig && existingConfig.buzlCapi;
|
|
349
|
+
const hasCapi = !!(capiCfg && capiCfg.endpoint && capiCfg.authUser);
|
|
350
|
+
|
|
351
|
+
const sheetUrl = existingConfig && existingConfig.googleSheetUrl;
|
|
352
|
+
const hasSheets = !!(sheetUrl && sheetUrl.trim().length > 0);
|
|
353
|
+
|
|
354
|
+
const zohoCfg = existingConfig && existingConfig.zoho;
|
|
355
|
+
const hasZoho = !!(zohoCfg && zohoCfg.xnQsjsdp && zohoCfg.xnQsjsdp.trim().length > 0);
|
|
356
|
+
|
|
357
|
+
const waCfg = existingConfig && existingConfig.whatsapp;
|
|
358
|
+
const hasWhatsapp = !!(waCfg && waCfg.number);
|
|
359
|
+
|
|
360
|
+
// Aggregate detected WhatsApp numbers across all forms & page buttons
|
|
361
|
+
const waCountMap = new Map();
|
|
362
|
+
analyzedFiles.forEach(f => {
|
|
363
|
+
f.forms.forEach(form => {
|
|
364
|
+
if (form.detectedWhatsapp) {
|
|
365
|
+
waCountMap.set(form.detectedWhatsapp, (waCountMap.get(form.detectedWhatsapp) || 0) + 1);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
(f.detectedWhatsappNumbers || []).forEach(num => {
|
|
369
|
+
waCountMap.set(num, (waCountMap.get(num) || 0) + 1);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
let detectedWhatsapp = null;
|
|
374
|
+
let maxCount = 0;
|
|
375
|
+
for (const [num, count] of waCountMap.entries()) {
|
|
376
|
+
if (count > maxCount) {
|
|
377
|
+
maxCount = count;
|
|
378
|
+
detectedWhatsapp = num;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Collect all forms across files with detailed identifiers & archetype grouping
|
|
383
|
+
const allDiscoveredForms = [];
|
|
384
|
+
const formSignatureMap = new Map();
|
|
385
|
+
|
|
386
|
+
analyzedFiles.forEach(f => {
|
|
387
|
+
f.forms.forEach((form, idx) => {
|
|
388
|
+
const selector = form.id.startsWith('form_') ? `form:nth-of-type(${idx + 1})` : `#${form.id}`;
|
|
389
|
+
const inputNames = form.inputs.map(i => i.name).sort().join('|');
|
|
390
|
+
const signature = `${form.id}::${inputNames}`;
|
|
391
|
+
|
|
392
|
+
if (!formSignatureMap.has(signature)) {
|
|
393
|
+
formSignatureMap.set(signature, {
|
|
394
|
+
formId: form.id,
|
|
395
|
+
selector: selector,
|
|
396
|
+
action: form.action,
|
|
397
|
+
inputs: form.inputs,
|
|
398
|
+
inputCount: form.inputs.length,
|
|
399
|
+
detectedWhatsapp: form.detectedWhatsapp || detectedWhatsapp || null,
|
|
400
|
+
pages: [f.relativePath],
|
|
401
|
+
isShared: false
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
const existing = formSignatureMap.get(signature);
|
|
405
|
+
if (!existing.pages.includes(f.relativePath)) {
|
|
406
|
+
existing.pages.push(f.relativePath);
|
|
407
|
+
existing.isShared = true;
|
|
408
|
+
}
|
|
409
|
+
if (!existing.detectedWhatsapp && form.detectedWhatsapp) {
|
|
410
|
+
existing.detectedWhatsapp = form.detectedWhatsapp;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
allDiscoveredForms.push({
|
|
415
|
+
formId: form.id,
|
|
416
|
+
file: f.relativePath,
|
|
417
|
+
selector: selector,
|
|
418
|
+
action: form.action,
|
|
419
|
+
inputs: form.inputs,
|
|
420
|
+
inputCount: form.inputs.length,
|
|
421
|
+
detectedWhatsapp: form.detectedWhatsapp || detectedWhatsapp || null
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const formArchetypes = Array.from(formSignatureMap.values());
|
|
427
|
+
const directories = Array.from(new Set(cleanFiles.map(f => {
|
|
428
|
+
const dir = path.dirname(f.relativePath).replace(/\\/g, '/');
|
|
429
|
+
return dir === '.' ? '/' : `/${dir}`;
|
|
430
|
+
}))).sort();
|
|
431
|
+
|
|
432
|
+
const cleanDirName = path.basename(rootDir).toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
433
|
+
const detectedDomain = cleanDirName || 'landing-page';
|
|
434
|
+
|
|
435
|
+
let detectedService = '';
|
|
436
|
+
for (const form of allDiscoveredForms) {
|
|
437
|
+
const sInput = (form.inputs || []).find(i => {
|
|
438
|
+
const lower = (i.name || '').toLowerCase();
|
|
439
|
+
return lower.includes('service') || lower.includes('inquiry') || lower.includes('treatment') || lower.includes('course') || lower.includes('department');
|
|
440
|
+
});
|
|
441
|
+
if (sInput) {
|
|
442
|
+
detectedService = form.id || form.selector || 'Website Inquiry';
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!detectedService && allDiscoveredForms.length > 0) {
|
|
447
|
+
detectedService = allDiscoveredForms[0].id || 'General Inquiry';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const liveState = {
|
|
451
|
+
gtm: { active: !!gtmId, id: gtmId },
|
|
452
|
+
meta: { active: !!metaPixelId, id: metaPixelId },
|
|
453
|
+
buzlCapi: {
|
|
454
|
+
active: hasCapi,
|
|
455
|
+
endpoint: (capiCfg && capiCfg.endpoint) || '',
|
|
456
|
+
authUser: (capiCfg && capiCfg.authUser) || ''
|
|
457
|
+
},
|
|
458
|
+
googleSheets: { active: hasSheets, url: sheetUrl || '' },
|
|
459
|
+
zoho: { active: hasZoho, endpoint: (zohoCfg && zohoCfg.endpoint) || '', xnQsjsdp: (zohoCfg && zohoCfg.xnQsjsdp) || '' },
|
|
460
|
+
whatsapp: {
|
|
461
|
+
active: hasWhatsapp,
|
|
462
|
+
number: (waCfg && waCfg.number) || '',
|
|
463
|
+
detectedNumber: detectedWhatsapp || ''
|
|
464
|
+
},
|
|
465
|
+
detectedLocation: detectedLocation || '',
|
|
466
|
+
detectedDomain: detectedDomain,
|
|
467
|
+
detectedService: detectedService || 'General Inquiry',
|
|
468
|
+
forms: allDiscoveredForms,
|
|
469
|
+
formArchetypes: formArchetypes,
|
|
470
|
+
directories: directories,
|
|
471
|
+
backups: backups.map(b => ({
|
|
472
|
+
dirName: b.dirName,
|
|
473
|
+
name: b.name,
|
|
474
|
+
timestamp: b.timestamp,
|
|
475
|
+
filesCount: b.filesCount
|
|
476
|
+
}))
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
rootDir,
|
|
481
|
+
totalHtmlFiles: htmlFiles.length,
|
|
482
|
+
files: cleanFiles,
|
|
483
|
+
totalForms,
|
|
484
|
+
totalUniqueForms: formArchetypes.length,
|
|
485
|
+
directories,
|
|
486
|
+
filesWithGtm,
|
|
487
|
+
filesWithMeta,
|
|
488
|
+
uniqueFields,
|
|
489
|
+
detectedLocation: detectedLocation || '',
|
|
490
|
+
detectedWhatsapp,
|
|
491
|
+
detectedDomain,
|
|
492
|
+
detectedService: detectedService || 'General Inquiry',
|
|
493
|
+
existingConfig,
|
|
494
|
+
liveState,
|
|
495
|
+
forms: allDiscoveredForms,
|
|
496
|
+
formArchetypes: formArchetypes,
|
|
497
|
+
backups: liveState.backups
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
module.exports = {
|
|
502
|
+
findHtmlFiles,
|
|
503
|
+
analyzeHtmlFile,
|
|
504
|
+
detectSiteLocation,
|
|
505
|
+
scanProject
|
|
506
|
+
};
|