- Delete unused dataMerger, formatter, Device model, dead service exports, and the empty agents/ + .gitkeep placeholders. - Extract STORE_MODES and MDM device-type filters into a shared constants.js. - Anchor bot regexes (^help|^store|^analyze) so "analyze store 305" no longer fires both handlers; replace catch-all noise. - Hoist inline require() calls in integrations to top-of-file imports. - Harden WebSocket server: Authorization header support, single-agent enforcement, bounded pending requests, server-level error handler, coalesced cache refresh in Meraki client. - Wrap Meraki/MDM network calls with withRetry; add request timeouts. - Migrate all console.* calls onto utils/logger.js (LOG_LEVEL aware); drive Webex framework logLevel from env. - Refactor storeDetail.js: shared renderClientLine + buildMdmSection helpers cut duplication roughly in half. - Refresh README structure, document LOG_LEVEL, add npm run agent script, add jest testMatch + new tests (handlers, HealthReport, Store, ws). Verified: npm run lint clean, 7 suites / 31 tests passing. Co-authored-by: Cursor <cursoragent@cursor.com>
304 lines
9.6 KiB
JavaScript
304 lines
9.6 KiB
JavaScript
const {
|
||
getStoreLocation,
|
||
getStoreRegisters,
|
||
getStorePrinters,
|
||
getStorePaymentTerminals,
|
||
} = require('../services/siw');
|
||
const {
|
||
findMerakiNetwork,
|
||
getMerakiDeviceAvailabilities,
|
||
getMerakiClients,
|
||
} = require('../services/meraki');
|
||
const { getMDMDevices } = require('../services/mdm');
|
||
const { createStore } = require('../models/Store');
|
||
const {
|
||
findMatchingClient,
|
||
getClientStatus,
|
||
formatLastSeen,
|
||
buildMerakiClientLink,
|
||
} = require('../utils/merakiMatcher');
|
||
const { STORE_MODES, MDM_DEVICE_TYPES, filterMdmByType } = require('../constants');
|
||
const logger = require('../utils/logger');
|
||
|
||
function shouldFetchForMode(mode, category) {
|
||
if (mode === STORE_MODES.IOS) {
|
||
return ['mdm', 'meraki', 'merakiClients'].includes(category);
|
||
}
|
||
if (mode === STORE_MODES.POS) {
|
||
return ['siw', 'mdm', 'meraki', 'merakiClients'].includes(category);
|
||
}
|
||
return true; // default fetches most things
|
||
}
|
||
|
||
async function getStoreDetail(storeNumber, mode = STORE_MODES.DEFAULT) {
|
||
logger.info('Starting store analysis', { storeNumber, mode });
|
||
|
||
let locationData = null,
|
||
merakiNetwork = null,
|
||
registers = [],
|
||
printers = [],
|
||
paymentTerminals = [],
|
||
merakiClients = [],
|
||
mdmDevices = [];
|
||
|
||
try {
|
||
// Phase 1: basic lookups
|
||
[locationData, merakiNetwork] = await Promise.all([
|
||
shouldFetchForMode(mode, 'location') ? getStoreLocation(storeNumber) : Promise.resolve(null),
|
||
shouldFetchForMode(mode, 'meraki') ? findMerakiNetwork(storeNumber) : Promise.resolve(null),
|
||
]);
|
||
|
||
// Phase 2: heavy data, fanned out in parallel
|
||
const dataPromises = [];
|
||
|
||
if (shouldFetchForMode(mode, 'siw')) {
|
||
dataPromises.push(
|
||
getStoreRegisters(storeNumber),
|
||
getStorePrinters(storeNumber),
|
||
getStorePaymentTerminals(storeNumber)
|
||
);
|
||
} else {
|
||
dataPromises.push(Promise.resolve([]), Promise.resolve([]), Promise.resolve([]));
|
||
}
|
||
|
||
dataPromises.push(
|
||
merakiNetwork && shouldFetchForMode(mode, 'merakiClients')
|
||
? getMerakiClients(merakiNetwork.id)
|
||
: Promise.resolve([])
|
||
);
|
||
|
||
dataPromises.push(
|
||
shouldFetchForMode(mode, 'mdm') ? getMDMDevices(storeNumber) : Promise.resolve([])
|
||
);
|
||
|
||
const [reg, prn, pay, clients, mdm] = await Promise.all(dataPromises);
|
||
registers = reg || [];
|
||
printers = prn || [];
|
||
paymentTerminals = pay || [];
|
||
merakiClients = clients || [];
|
||
mdmDevices = mdm || [];
|
||
|
||
logger.info('Store data fetched', { mode });
|
||
} catch (err) {
|
||
logger.error('Error fetching store data', { error: err.message });
|
||
}
|
||
|
||
let report = '';
|
||
|
||
if (mode === STORE_MODES.DEFAULT) {
|
||
report += createStore(locationData, storeNumber).toSummary();
|
||
report += await buildActiveNetworkDevices(merakiNetwork);
|
||
report += buildStoreServers(mdmDevices, merakiClients, merakiNetwork);
|
||
} else if (mode === STORE_MODES.POS) {
|
||
report += buildStoreServers(mdmDevices, merakiClients, merakiNetwork);
|
||
report += buildRegisters(registers, merakiClients, merakiNetwork);
|
||
report += buildMobileRegisters(mdmDevices, merakiClients, merakiNetwork);
|
||
report += buildPrinters(printers, merakiClients, merakiNetwork);
|
||
report += buildPaymentTerminals(paymentTerminals, merakiClients, merakiNetwork);
|
||
} else if (mode === STORE_MODES.IOS) {
|
||
report += buildIOSDevices(mdmDevices, merakiClients, merakiNetwork);
|
||
}
|
||
|
||
return report || 'No matching data for this view.';
|
||
}
|
||
|
||
// === Section Builders ===
|
||
|
||
/**
|
||
* Render a single "device → matched Meraki client" line.
|
||
* `prefixParts` is rendered before the status; `identifiers` is passed
|
||
* straight through to findMatchingClient.
|
||
*/
|
||
function renderClientLine({ prefixParts, identifiers, merakiClients, merakiNetwork }) {
|
||
const match = findMatchingClient(merakiClients, identifiers);
|
||
const status = getClientStatus(match);
|
||
const lastSeen = formatLastSeen(match?.lastSeen);
|
||
const recentDevice = match?.recentDeviceName ? ` via ${match.recentDeviceName}` : '';
|
||
const connection = match?.recentDeviceConnection ? ` (${match.recentDeviceConnection})` : '';
|
||
const link = match ? ` → [Meraki Client](${buildMerakiClientLink(merakiNetwork, match)})` : '';
|
||
const prefix = prefixParts.filter(Boolean).join(' — ');
|
||
return `${prefix} — ${status} — ${lastSeen}${recentDevice}${connection}${link}\n`;
|
||
}
|
||
|
||
async function buildActiveNetworkDevices(merakiNetwork) {
|
||
if (!merakiNetwork) return '\n⚠️ No matching Meraki network found.\n';
|
||
|
||
let out = `**🌐 [${merakiNetwork.name}](${merakiNetwork.url})**\n\n`;
|
||
|
||
let activeDevices = [];
|
||
try {
|
||
const availabilities = await getMerakiDeviceAvailabilities(merakiNetwork.id);
|
||
activeDevices = availabilities.filter(d => d.status !== 'dormant');
|
||
} catch (e) {
|
||
logger.error('Device availabilities failed', { error: e.message });
|
||
}
|
||
|
||
activeDevices.sort((a, b) => {
|
||
const nameA = (a.name || '').trim().toUpperCase();
|
||
const nameB = (b.name || '').trim().toUpperCase();
|
||
|
||
if (nameA.includes('SWR') || /SW.*R\d?$/.test(nameA)) return -1;
|
||
if (nameB.includes('SWR') || /SW.*R\d?$/.test(nameB)) return 1;
|
||
|
||
if (nameA.includes('SWF') || /SW.*F\d/.test(nameA)) return -1;
|
||
if (nameB.includes('SWF') || /SW.*F\d/.test(nameB)) return 1;
|
||
|
||
return nameA.localeCompare(nameB);
|
||
});
|
||
|
||
out += `**🛠️ Active Network Devices (${activeDevices.length})**\n`;
|
||
|
||
activeDevices.forEach(dev => {
|
||
const statusEmoji = dev.status === 'online' ? '✅' : '⚠️';
|
||
const deviceName = (dev.name || dev.serial || 'Unknown').trim();
|
||
|
||
out += `**${deviceName}** — ${statusEmoji} ${dev.status}`;
|
||
|
||
if (dev.serial && merakiNetwork.url) {
|
||
const urlParts = merakiNetwork.url.split('/n/');
|
||
const networkCode = urlParts[1] ? urlParts[1].split('/')[0] : merakiNetwork.id;
|
||
let link = `${urlParts[0]}/n/${networkCode}`;
|
||
|
||
if (dev.productType === 'switch') link += `/manage/switches/${dev.serial}/summary`;
|
||
else if (dev.productType === 'wireless')
|
||
link += `/manage/access_points/${dev.serial}/summary`;
|
||
else link += `/manage/${dev.serial}/summary`;
|
||
|
||
out += ` → [Dashboard](${link})`;
|
||
}
|
||
out += `\n`;
|
||
});
|
||
|
||
return out;
|
||
}
|
||
|
||
function buildMdmSection({
|
||
devices,
|
||
marker,
|
||
title,
|
||
fallbackName,
|
||
merakiClients,
|
||
merakiNetwork,
|
||
formatPrefix,
|
||
}) {
|
||
const filtered = filterMdmByType(devices, marker);
|
||
if (filtered.length === 0) return '';
|
||
|
||
let out = `\n**${title} (${filtered.length})**\n`;
|
||
|
||
filtered.forEach(dev => {
|
||
const name = dev.UserName || dev.DeviceFriendlyName || fallbackName;
|
||
out += renderClientLine({
|
||
prefixParts: formatPrefix ? formatPrefix(name, dev) : [`**${name}**`],
|
||
identifiers: {
|
||
UserName: dev.UserName,
|
||
DeviceFriendlyName: dev.DeviceFriendlyName,
|
||
name,
|
||
},
|
||
merakiClients,
|
||
merakiNetwork,
|
||
});
|
||
});
|
||
|
||
return out;
|
||
}
|
||
|
||
function buildStoreServers(mdmDevices, merakiClients, merakiNetwork) {
|
||
return buildMdmSection({
|
||
devices: mdmDevices,
|
||
marker: MDM_DEVICE_TYPES.SERVER,
|
||
title: '🖥️ Store Servers',
|
||
fallbackName: 'Unknown Server',
|
||
merakiClients,
|
||
merakiNetwork,
|
||
});
|
||
}
|
||
|
||
function buildMobileRegisters(mdmDevices, merakiClients, merakiNetwork) {
|
||
return buildMdmSection({
|
||
devices: mdmDevices,
|
||
marker: MDM_DEVICE_TYPES.MOBILE_REGISTER,
|
||
title: '📱 Mobile Registers',
|
||
fallbackName: 'Unknown Mobile Register',
|
||
merakiClients,
|
||
merakiNetwork,
|
||
});
|
||
}
|
||
|
||
function buildIOSDevices(mdmDevices, merakiClients, merakiNetwork) {
|
||
return buildMdmSection({
|
||
devices: mdmDevices,
|
||
marker: MDM_DEVICE_TYPES.IPHONE,
|
||
title: '📱 Store iPhones',
|
||
fallbackName: 'Unknown iPhone',
|
||
merakiClients,
|
||
merakiNetwork,
|
||
});
|
||
}
|
||
|
||
function buildRegisters(registers, merakiClients, merakiNetwork) {
|
||
if (!registers || registers.length === 0) return '';
|
||
|
||
let out = `\n**🖥️ Store Registers (${registers.length})**\n`;
|
||
|
||
registers.forEach(reg => {
|
||
const regNum = reg.register_number ? `Register ${reg.register_number}` : 'Register';
|
||
const brand = reg.brand_display_name || 'Unknown';
|
||
const type = reg.register_type_name || 'Unknown';
|
||
const name = (reg.register_display_name || reg.printer_name || 'Unknown').trim();
|
||
|
||
out += renderClientLine({
|
||
prefixParts: [`**${regNum}**`, brand, type],
|
||
identifiers: { name, register_display_name: name },
|
||
merakiClients,
|
||
merakiNetwork,
|
||
});
|
||
});
|
||
|
||
return out;
|
||
}
|
||
|
||
function buildPrinters(printers, merakiClients, merakiNetwork) {
|
||
const filtered = (printers || []).filter(
|
||
p => (p.connection_type_name || '').toLowerCase() !== 'usb'
|
||
);
|
||
if (filtered.length === 0) return '';
|
||
|
||
let out = `\n**🖨️ Store Printers (${filtered.length})**\n`;
|
||
|
||
filtered.forEach(printer => {
|
||
const name = (printer.printer_name || 'Unknown').trim();
|
||
const model = printer.printer_model_name || 'N/A';
|
||
out += renderClientLine({
|
||
prefixParts: [`**${name}**`, model],
|
||
identifiers: { name, printer_name: name },
|
||
merakiClients,
|
||
merakiNetwork,
|
||
});
|
||
});
|
||
|
||
return out;
|
||
}
|
||
|
||
function buildPaymentTerminals(paymentTerminals, merakiClients, merakiNetwork) {
|
||
if (!paymentTerminals || paymentTerminals.length === 0) return '';
|
||
|
||
let out = `\n**💳 Payment Terminals (${paymentTerminals.length})**\n`;
|
||
|
||
paymentTerminals.forEach(term => {
|
||
const deviceName = term.device_name || 'Unknown Terminal';
|
||
const adyenName = term.adyen_device_name || 'N/A';
|
||
const model = term.device_model_name || 'N/A';
|
||
const type = term.device_type_name || 'N/A';
|
||
out += renderClientLine({
|
||
prefixParts: [`**${deviceName}**`, adyenName, model, type],
|
||
identifiers: { deviceName, adyenName, ip_address: term.ip_address },
|
||
merakiClients,
|
||
merakiNetwork,
|
||
});
|
||
});
|
||
|
||
return out;
|
||
}
|
||
|
||
module.exports = { getStoreDetail };
|