- Initialize git repository - Add comprehensive .gitignore (protects .env and secrets) - Fix package.json (correct main entry, add metadata) - Expand .env.example with all required variables and comments - Add README.md with architecture, setup, and commands - Clean up empty scaffolding directories (logs removed, agents/models marked) - Backup previous .env file locally This establishes a safe foundation before further development.
344 lines
No EOL
16 KiB
JavaScript
344 lines
No EOL
16 KiB
JavaScript
const { getStoreLocation, getStoreRegisters, getStorePrinters, getStorePaymentTerminals } = require('../services/siw');
|
|
const { findMerakiNetwork, getMerakiDeviceAvailabilities, getMerakiClients } = require('../services/meraki');
|
|
const { formatStoreReport } = require('../utils/formatter');
|
|
|
|
function formatLastSeen(lastSeen) {
|
|
if (!lastSeen) return 'N/A';
|
|
const now = new Date();
|
|
const seen = new Date(lastSeen);
|
|
const diffMs = now - seen;
|
|
const diffMin = Math.floor(diffMs / 60000);
|
|
|
|
if (diffMin < 1) return 'Just now';
|
|
if (diffMin < 60) return `${diffMin} min ago`;
|
|
const hours = Math.floor(diffMin / 60);
|
|
return `${hours} hr ago`;
|
|
}
|
|
|
|
async function getStoreDetail(storeNumber) {
|
|
console.log(`🔍 Starting full analysis for store ${storeNumber}`);
|
|
|
|
let locationData, merakiNetwork, registers, printers, paymentTerminals, merakiClients = [], mdmDevices = [];
|
|
|
|
try {
|
|
// Phase 1: Fast lookups
|
|
[locationData, merakiNetwork] = await Promise.all([
|
|
getStoreLocation(storeNumber),
|
|
findMerakiNetwork(storeNumber)
|
|
]);
|
|
|
|
// Phase 2: Heavy parallel lookups
|
|
const parallelPromises = [
|
|
getStoreRegisters(storeNumber),
|
|
getStorePrinters(storeNumber),
|
|
getStorePaymentTerminals(storeNumber)
|
|
];
|
|
|
|
if (merakiNetwork) {
|
|
parallelPromises.push(
|
|
getMerakiClients(merakiNetwork.id),
|
|
require('../services/mdm').getMDMDevices(storeNumber)
|
|
);
|
|
}
|
|
|
|
const results = await Promise.all(parallelPromises);
|
|
|
|
registers = results[0];
|
|
printers = results[1];
|
|
paymentTerminals = results[2];
|
|
|
|
if (merakiNetwork) {
|
|
console.log(`Meraki Network:`)
|
|
console.log(JSON.stringify(merakiNetwork, null, 2))
|
|
merakiClients = results[3] || [];
|
|
mdmDevices = results[4] || [];
|
|
}
|
|
|
|
console.log(`✅ Fetched: ${registers?.length || 0} registers, ${printers?.length || 0} printers, ${paymentTerminals?.length || 0} payment terminals`);
|
|
if (merakiNetwork) {
|
|
console.log(`✅ Fetched ${merakiClients.length} Meraki clients`);
|
|
console.log(`✅ Fetched ${mdmDevices.length} MDM devices`);
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('❌ Error fetching data:', err.message);
|
|
}
|
|
|
|
let report = formatStoreReport(locationData, storeNumber);
|
|
|
|
// === Meraki Network Hardware ===
|
|
if (merakiNetwork) {
|
|
report += `**🌐 [${merakiNetwork.name}](${merakiNetwork.url})**\n\n`;
|
|
|
|
let activeDevices = [];
|
|
try {
|
|
const availabilities = await getMerakiDeviceAvailabilities(merakiNetwork.id);
|
|
activeDevices = availabilities.filter(d => d.status !== 'dormant');
|
|
} catch (e) {
|
|
console.error('Device availabilities failed:', e.message);
|
|
}
|
|
|
|
// Sort: Rear Switch → Front Switch → APs
|
|
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);
|
|
});
|
|
|
|
report += `**🛠️ Active Network Devices (${activeDevices.length})**\n`;
|
|
|
|
activeDevices.forEach(dev => {
|
|
const statusEmoji = dev.status === 'online' ? '✅' : '⚠️';
|
|
const deviceName = (dev.name || dev.serial || 'Unknown').trim();
|
|
|
|
report += `**${deviceName}** — ${statusEmoji} ${dev.status}`;
|
|
|
|
if (dev.serial && merakiNetwork.url) {
|
|
// Extract short network code from the URL (e.g. JCz-ucnd)
|
|
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`;
|
|
}
|
|
|
|
report += ` → [Dashboard](${link})`;
|
|
}
|
|
report += `\n`;
|
|
});
|
|
} else {
|
|
report += `\n⚠️ No matching Meraki network found.\n`;
|
|
}
|
|
|
|
// === Store Registers ===
|
|
if (registers && registers.length > 0) {
|
|
report += `\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();
|
|
|
|
const match = merakiClients.find(c =>
|
|
c?.description &&
|
|
(c.description.trim().toLowerCase() === name.toLowerCase() ||
|
|
c.description.includes(name))
|
|
);
|
|
|
|
const status = match
|
|
? (match.status === 'Online' ? '✅ Online' : '❌ Offline')
|
|
: '❓ Unknown';
|
|
|
|
const lastSeen = match ? formatLastSeen(match.lastSeen) : '';
|
|
const deviceName = match?.recentDeviceName ? ` via ${match.recentDeviceName}` : '';
|
|
const connection = match?.recentDeviceConnection ? ` (${match.recentDeviceConnection})` : '';
|
|
|
|
let link = '';
|
|
if (match && merakiNetwork?.url) {
|
|
const networkCode = merakiNetwork.url.split('/n/')[1]?.split('/')[0] || merakiNetwork.id;
|
|
link = ` → [Meraki Client](https://n976.dashboard.meraki.com/${merakiNetwork.name.replace(/ /g, '-')}/n/${networkCode}/manage/clients/${match.id}/overview)`;
|
|
}
|
|
|
|
report += `**${regNum}** — ${brand} — ${type} — ${status} — ${lastSeen}${deviceName}${connection}${link}\n`;
|
|
});
|
|
}
|
|
|
|
// === Store Printers (USB filtered) ===
|
|
const filteredPrinters = (printers || []).filter(p =>
|
|
(p.connection_type_name || '').toLowerCase() !== 'usb'
|
|
);
|
|
|
|
if (filteredPrinters.length > 0) {
|
|
report += `\n**🖨️ Store Printers (${filteredPrinters.length})**\n`;
|
|
|
|
filteredPrinters.forEach(printer => {
|
|
const name = (printer.printer_name || 'Unknown').trim();
|
|
const model = printer.printer_model_name || 'N/A';
|
|
|
|
const match = merakiClients.find(c =>
|
|
c?.description &&
|
|
(c.description.trim().toLowerCase() === name.toLowerCase() ||
|
|
c.description.includes(name))
|
|
);
|
|
|
|
const status = match
|
|
? (match.status === 'Online' ? '✅ Online' : '❌ Offline')
|
|
: '❓ Unknown';
|
|
|
|
const lastSeen = match ? formatLastSeen(match.lastSeen) : '';
|
|
const deviceName = match?.recentDeviceName ? ` via ${match.recentDeviceName}` : '';
|
|
const connection = match?.recentDeviceConnection ? ` (${match.recentDeviceConnection})` : '';
|
|
|
|
let link = '';
|
|
if (match && merakiNetwork?.url) {
|
|
const networkCode = merakiNetwork.url.split('/n/')[1]?.split('/')[0] || merakiNetwork.id;
|
|
link = ` → [Meraki Client](https://n976.dashboard.meraki.com/${merakiNetwork.name.replace(/ /g, '-')}/n/${networkCode}/manage/clients/${match.id}/overview)`;
|
|
}
|
|
|
|
report += `**${name}** — ${model} — ${status} — ${lastSeen}${deviceName}${connection}${link}\n`;
|
|
});
|
|
}
|
|
|
|
if (paymentTerminals && paymentTerminals.length > 0) {
|
|
report += `\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';
|
|
|
|
const match = merakiClients.find(c =>
|
|
c?.description &&
|
|
(c.description.includes(deviceName) ||
|
|
c.description.includes(adyenName) ||
|
|
(term.ip_address && c.description.includes(term.ip_address.split('.')[0])))
|
|
);
|
|
|
|
const status = match
|
|
? (match.status === 'Online' ? '✅ Online' : '❌ Offline')
|
|
: '❓ Unknown';
|
|
|
|
const lastSeen = match ? formatLastSeen(match.lastSeen) : '';
|
|
const recentDevice = match?.recentDeviceName ? ` via ${match.recentDeviceName}` : '';
|
|
const connection = match?.recentDeviceConnection ? ` (${match.recentDeviceConnection})` : '';
|
|
|
|
let link = '';
|
|
if (match && merakiNetwork?.url) {
|
|
const networkCode = merakiNetwork.url.split('/n/')[1]?.split('/')[0] || merakiNetwork.id;
|
|
link = ` → [Meraki Client](https://n976.dashboard.meraki.com/${merakiNetwork.name.replace(/ /g, '-')}/n/${networkCode}/manage/clients/${match.id}/overview)`;
|
|
}
|
|
|
|
report += `**${deviceName}** — ${adyenName} — ${model} — ${type} — ${status} — ${lastSeen}${recentDevice}${connection}${link}\n`;
|
|
});
|
|
}
|
|
|
|
if (mdmDevices && mdmDevices.length > 0) {
|
|
const servers = mdmDevices.filter(d => (d.UserName || d.DeviceFriendlyName || '').includes('SRV'));
|
|
const mobileRegs = mdmDevices.filter(d => (d.UserName || d.DeviceFriendlyName || '').includes('MR'));
|
|
const custDisplays = mdmDevices.filter(d => (d.UserName || d.DeviceFriendlyName || '').includes('CD'));
|
|
const iphones = mdmDevices.filter(d => (d.UserName || d.DeviceFriendlyName || '').includes('IPH'));
|
|
|
|
// === Store Servers ===
|
|
if (servers.length > 0) {
|
|
report += `\n**🖥️ Store Servers (${servers.length})**\n`;
|
|
|
|
servers.forEach(dev => {
|
|
const name = dev.UserName || dev.DeviceFriendlyName || 'Unknown Server';
|
|
const match = merakiClients.find(c =>
|
|
c?.description &&
|
|
(c.description.includes(name) || (dev.UserName && c.description.includes(dev.UserName)))
|
|
);
|
|
|
|
const status = match ? (match.status === 'Online' ? '✅ Online' : '❌ Offline') : '❓ Unknown';
|
|
const lastSeen = match ? formatLastSeen(match.lastSeen) : '';
|
|
const recentDevice = match?.recentDeviceName ? ` via ${match.recentDeviceName}` : '';
|
|
const connection = match?.recentDeviceConnection ? ` (${match.recentDeviceConnection})` : '';
|
|
|
|
let link = '';
|
|
if (match && merakiNetwork?.url) {
|
|
const networkCode = merakiNetwork.url.split('/n/')[1]?.split('/')[0] || merakiNetwork.id;
|
|
link = ` → [Meraki Client](https://n976.dashboard.meraki.com/${merakiNetwork.name.replace(/ /g, '-')}/n/${networkCode}/manage/clients/${match.id}/overview)`;
|
|
}
|
|
|
|
report += `**${name}** — ${status} — ${lastSeen}${recentDevice}${connection}${link}\n`;
|
|
});
|
|
}
|
|
|
|
// === Mobile Registers ===
|
|
if (mobileRegs.length > 0) {
|
|
report += `\n**📱 Mobile Registers (${mobileRegs.length})**\n`;
|
|
|
|
mobileRegs.forEach(dev => {
|
|
const name = dev.UserName || dev.DeviceFriendlyName || 'Unknown Mobile Register';
|
|
const match = merakiClients.find(c =>
|
|
c?.description &&
|
|
(c.description.includes(name) || (dev.UserName && c.description.includes(dev.UserName)))
|
|
);
|
|
|
|
const status = match ? (match.status === 'Online' ? '✅ Online' : '❌ Offline') : '❓ Unknown';
|
|
const lastSeen = match ? formatLastSeen(match.lastSeen) : '';
|
|
const recentDevice = match?.recentDeviceName ? ` via ${match.recentDeviceName}` : '';
|
|
const connection = match?.recentDeviceConnection ? ` (${match.recentDeviceConnection})` : '';
|
|
|
|
let link = '';
|
|
if (match && merakiNetwork?.url) {
|
|
const networkCode = merakiNetwork.url.split('/n/')[1]?.split('/')[0] || merakiNetwork.id;
|
|
link = ` → [Meraki Client](https://n976.dashboard.meraki.com/${merakiNetwork.name.replace(/ /g, '-')}/n/${networkCode}/manage/clients/${match.id}/overview)`;
|
|
}
|
|
|
|
report += `**${name}** — ${status} — ${lastSeen}${recentDevice}${connection}${link}\n`;
|
|
});
|
|
}
|
|
|
|
// === Customer Displays ===
|
|
if (custDisplays.length > 0) {
|
|
report += `\n**🖥️ Customer Displays (${custDisplays.length})**\n`;
|
|
|
|
custDisplays.forEach(dev => {
|
|
const name = dev.UserName || dev.DeviceFriendlyName || 'Unknown Display';
|
|
const match = merakiClients.find(c =>
|
|
c?.description &&
|
|
(c.description.includes(name) || (dev.UserName && c.description.includes(dev.UserName)))
|
|
);
|
|
|
|
const status = match ? (match.status === 'Online' ? '✅ Online' : '❌ Offline') : '❓ Unknown';
|
|
const lastSeen = match ? formatLastSeen(match.lastSeen) : '';
|
|
const recentDevice = match?.recentDeviceName ? ` via ${match.recentDeviceName}` : '';
|
|
const connection = match?.recentDeviceConnection ? ` (${match.recentDeviceConnection})` : '';
|
|
|
|
let link = '';
|
|
if (match && merakiNetwork?.url) {
|
|
const networkCode = merakiNetwork.url.split('/n/')[1]?.split('/')[0] || merakiNetwork.id;
|
|
link = ` → [Meraki Client](https://n976.dashboard.meraki.com/${merakiNetwork.name.replace(/ /g, '-')}/n/${networkCode}/manage/clients/${match.id}/overview)`;
|
|
}
|
|
|
|
report += `**${name}** — ${status} — ${lastSeen}${recentDevice}${connection}${link}\n`;
|
|
});
|
|
}
|
|
|
|
// === Store iPhones ===
|
|
if (iphones.length > 0) {
|
|
report += `\n**📱 Store iPhones (${iphones.length})**\n`;
|
|
|
|
iphones.forEach(dev => {
|
|
const name = dev.UserName || dev.DeviceFriendlyName || 'Unknown iPhone';
|
|
const match = merakiClients.find(c =>
|
|
c?.description &&
|
|
(c.description.includes(name) || (dev.UserName && c.description.includes(dev.UserName)))
|
|
);
|
|
|
|
const status = match ? (match.status === 'Online' ? '✅ Online' : '❌ Offline') : '❓ Unknown';
|
|
const lastSeen = match ? formatLastSeen(match.lastSeen) : '';
|
|
const recentDevice = match?.recentDeviceName ? ` via ${match.recentDeviceName}` : '';
|
|
const connection = match?.recentDeviceConnection ? ` (${match.recentDeviceConnection})` : '';
|
|
|
|
let link = '';
|
|
if (match && merakiNetwork?.url) {
|
|
const networkCode = merakiNetwork.url.split('/n/')[1]?.split('/')[0] || merakiNetwork.id;
|
|
link = ` → [Meraki Client](https://n976.dashboard.meraki.com/${merakiNetwork.name.replace(/ /g, '-')}/n/${networkCode}/manage/clients/${match.id}/overview)`;
|
|
}
|
|
|
|
report += `**${name}** — ${status} — ${lastSeen}${recentDevice}${connection}${link}\n`;
|
|
});
|
|
}
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
module.exports = { getStoreDetail }; |