- 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>
243 lines
8.5 KiB
JavaScript
243 lines
8.5 KiB
JavaScript
const { getStoreLocation, getStorePaymentTerminals, getStorePrinters } = require('../services/siw');
|
|
const {
|
|
findMerakiNetwork,
|
|
getMerakiDeviceAvailabilities,
|
|
getMerakiClients,
|
|
} = require('../services/meraki');
|
|
const { getMDMDevices } = require('../services/mdm');
|
|
const { findMatchingClient, getClientStatus } = require('../utils/merakiMatcher');
|
|
const { createHealthReport } = require('../models/HealthReport');
|
|
const { MDM_DEVICE_TYPES, filterMdmByType } = require('../constants');
|
|
const logger = require('../utils/logger');
|
|
|
|
// Configurable scoring penalties (higher = worse impact)
|
|
const SCORING = {
|
|
noMerakiNetwork: 50,
|
|
switchOffline: 15,
|
|
apOffline: 5,
|
|
apAlerting: 3,
|
|
serverOffline: 35,
|
|
noMdmData: 20,
|
|
paymentTerminalOffline: 5,
|
|
printerOffline: 4,
|
|
// Mobile registers, customer displays and iPhones currently contribute to issues list only
|
|
};
|
|
|
|
async function getStoreHealth(storeNumber, _mode = 'default') {
|
|
logger.info('Running health analysis', { storeNumber });
|
|
|
|
let locationData,
|
|
merakiNetwork,
|
|
merakiClients = [],
|
|
mdmDevices = [],
|
|
paymentTerminals = [],
|
|
printers = [];
|
|
|
|
try {
|
|
[locationData, merakiNetwork] = await Promise.all([
|
|
getStoreLocation(storeNumber),
|
|
findMerakiNetwork(storeNumber),
|
|
]);
|
|
|
|
if (merakiNetwork) {
|
|
[merakiClients, mdmDevices, paymentTerminals, printers] = await Promise.all([
|
|
getMerakiClients(merakiNetwork.id),
|
|
getMDMDevices(storeNumber),
|
|
getStorePaymentTerminals(storeNumber),
|
|
getStorePrinters(storeNumber),
|
|
]);
|
|
}
|
|
} catch (err) {
|
|
logger.error('Health analysis error', { error: err.message });
|
|
}
|
|
|
|
const healthReport = createHealthReport(storeNumber, locationData?.name || 'Unknown Store');
|
|
|
|
// === Network Infrastructure ===
|
|
if (merakiNetwork) {
|
|
healthReport.addSection(
|
|
'**🌐 Network Infrastructure**',
|
|
`- Meraki Network: ✅ [${merakiNetwork.name}](${merakiNetwork.url})\n`
|
|
);
|
|
|
|
try {
|
|
const avail = await getMerakiDeviceAvailabilities(merakiNetwork.id);
|
|
const switches = avail.filter(d => d.productType === 'switch' && d.status !== 'dormant');
|
|
const aps = avail.filter(d => d.productType === 'wireless' && d.status !== 'dormant');
|
|
|
|
const swOnline = switches.filter(d => d.status === 'online').length;
|
|
const apOnline = aps.filter(d => d.status === 'online').length;
|
|
const apAlerting = aps.filter(d => d.status === 'alerting').length;
|
|
|
|
let netContent = `- Switching: ${swOnline}/${switches.length} online\n`;
|
|
netContent += `- Access Points: ${apOnline}/${aps.length} online`;
|
|
|
|
if (apAlerting > 0) {
|
|
netContent += ` (${apAlerting} alerting)`;
|
|
}
|
|
netContent += `\n\n`;
|
|
healthReport.addSection('', netContent); // append
|
|
|
|
// Scoring & Issues
|
|
if (swOnline < switches.length) {
|
|
const offlineSw = switches.length - swOnline;
|
|
healthReport.deduct(offlineSw * SCORING.switchOffline, `${offlineSw} switch(es) offline`);
|
|
}
|
|
|
|
if (apOnline < aps.length || apAlerting > 0) {
|
|
const totalApIssues = aps.length - apOnline;
|
|
|
|
if (apAlerting > 0) {
|
|
healthReport.deduct(
|
|
totalApIssues * SCORING.apAlerting,
|
|
`${apAlerting} access point(s) alerting (check cabling/power)`
|
|
);
|
|
} else {
|
|
healthReport.deduct(
|
|
totalApIssues * SCORING.apOffline,
|
|
`${totalApIssues} access point(s) offline`
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logger.error('Meraki availability error', { error: e.message });
|
|
}
|
|
} else {
|
|
healthReport.addSection('**🌐 Network Infrastructure**', `- Meraki Network: ❌ Not Found\n\n`);
|
|
healthReport.deduct(SCORING.noMerakiNetwork, 'No Meraki network found');
|
|
}
|
|
|
|
// === Core Systems ===
|
|
healthReport.addSection('**🖥️ POS Systems**', '');
|
|
|
|
if (mdmDevices && mdmDevices.length > 0) {
|
|
const servers = filterMdmByType(mdmDevices, MDM_DEVICE_TYPES.SERVER);
|
|
const mobileRegs = filterMdmByType(mdmDevices, MDM_DEVICE_TYPES.MOBILE_REGISTER);
|
|
const custDisplays = filterMdmByType(mdmDevices, MDM_DEVICE_TYPES.CUSTOMER_DISPLAY);
|
|
const iphones = filterMdmByType(mdmDevices, MDM_DEVICE_TYPES.IPHONE);
|
|
|
|
// Server (heavy penalty)
|
|
let serverStatus = '❓ Unknown';
|
|
if (servers.length > 0) {
|
|
const srv = servers[0];
|
|
const match = findMatchingClient(merakiClients, {
|
|
UserName: srv.UserName,
|
|
DeviceFriendlyName: srv.DeviceFriendlyName,
|
|
});
|
|
serverStatus = getClientStatus(match);
|
|
if (serverStatus === '❌ Offline') {
|
|
healthReport.deduct(
|
|
SCORING.serverOffline,
|
|
`Store Server ${srv?.UserName || srv?.DeviceFriendlyName} is offline`
|
|
);
|
|
}
|
|
}
|
|
healthReport.addSection('', `- Store Server: ${serverStatus}\n`);
|
|
|
|
// Mobile Registers
|
|
let mobileOnline = 0;
|
|
mobileRegs.forEach(reg => {
|
|
const name = (reg.UserName || reg.DeviceFriendlyName || '').trim();
|
|
const match = findMatchingClient(merakiClients, {
|
|
UserName: reg.UserName,
|
|
DeviceFriendlyName: reg.DeviceFriendlyName,
|
|
});
|
|
const status = getClientStatus(match);
|
|
if (status === '✅ Online') mobileOnline++;
|
|
if (status === '❌ Offline') healthReport.addIssue(`Mobile Register ${name} is offline`);
|
|
});
|
|
healthReport.addSection(
|
|
'',
|
|
`- Mobile Registers: ${mobileOnline}/${mobileRegs.length} online\n`
|
|
);
|
|
|
|
// Customer Displays & iPhones (lighter penalty)
|
|
let cdOnline = 0;
|
|
custDisplays.forEach(cd => {
|
|
const name = (cd.UserName || cd.DeviceFriendlyName || '').trim();
|
|
const match = findMatchingClient(merakiClients, {
|
|
UserName: cd.UserName,
|
|
DeviceFriendlyName: cd.DeviceFriendlyName,
|
|
});
|
|
const status = getClientStatus(match);
|
|
if (status === '✅ Online') cdOnline++;
|
|
if (status === '❌ Offline') healthReport.addIssue(`Customer Display ${name} is offline`);
|
|
});
|
|
healthReport.addSection('', `- Customer Displays: ${cdOnline}/${custDisplays.length} online\n`);
|
|
|
|
let iphoneOnline = 0;
|
|
iphones.forEach(phone => {
|
|
const name = (phone.UserName || phone.DeviceFriendlyName || '').trim();
|
|
const match = findMatchingClient(merakiClients, {
|
|
UserName: phone.UserName,
|
|
DeviceFriendlyName: phone.DeviceFriendlyName,
|
|
});
|
|
const status = getClientStatus(match);
|
|
if (status === '✅ Online') iphoneOnline++;
|
|
if (status === '❌ Offline') healthReport.addIssue(`iPhone ${name} is offline`);
|
|
});
|
|
healthReport.addSection('', `- Store iPhones: ${iphoneOnline}/${iphones.length} online\n\n`);
|
|
} else {
|
|
healthReport.addSection('', `- No MDM data available\n\n`);
|
|
healthReport.deduct(SCORING.noMdmData);
|
|
}
|
|
|
|
// === POS & Peripherals (with per-device penalty) ===
|
|
healthReport.addSection('**💳 POS Peripherals**', '');
|
|
|
|
// Payment Terminals
|
|
if (paymentTerminals && paymentTerminals.length > 0) {
|
|
let onlinePayments = 0;
|
|
paymentTerminals.forEach(term => {
|
|
const match = findMatchingClient(merakiClients, {
|
|
name: term.device_name,
|
|
adyenName: term.adyen_device_name,
|
|
ip_address: term.ip_address,
|
|
});
|
|
|
|
const terminalName = (term.device_name || term.adyen_device_name || 'unknown').trim();
|
|
const status = getClientStatus(match);
|
|
if (status === '✅ Online') onlinePayments++;
|
|
if (status === '❌ Offline') {
|
|
healthReport.deduct(
|
|
SCORING.paymentTerminalOffline,
|
|
`Payment Terminal ${terminalName} is offline`
|
|
);
|
|
}
|
|
});
|
|
|
|
healthReport.addSection(
|
|
'',
|
|
`- Payment Terminals: ${onlinePayments}/${paymentTerminals.length} online\n`
|
|
);
|
|
}
|
|
|
|
// Printers
|
|
const filteredPrinters = (printers || []).filter(
|
|
p => (p.connection_type_name || '').toLowerCase() !== 'usb'
|
|
);
|
|
|
|
if (filteredPrinters.length > 0) {
|
|
let onlinePrinters = 0;
|
|
filteredPrinters.forEach(printer => {
|
|
const name = (printer.printer_name || '').trim().toLowerCase();
|
|
const match = findMatchingClient(merakiClients, { name: printer.printer_name });
|
|
const status = getClientStatus(match);
|
|
if (status === '✅ Online') onlinePrinters++;
|
|
if (status === '❌ Offline') {
|
|
healthReport.deduct(SCORING.printerOffline, `Printer ${name} is offline`);
|
|
}
|
|
});
|
|
|
|
healthReport.addSection(
|
|
'',
|
|
`- Printers: ${onlinePrinters}/${filteredPrinters.length} online\n`
|
|
);
|
|
}
|
|
|
|
// Finalize and return
|
|
const report = healthReport.finalize();
|
|
return { summary: report.summary };
|
|
}
|
|
|
|
module.exports = { getStoreHealth };
|