netanalyzer/integrations/storeDetail.js
Joseph McQueen 0a46d25bd6 chore: project cleanup — dead code, logging, tests, WS hardening
- 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>
2026-06-24 17:21:22 -04:00

304 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 };