netanalyzer/utils/merakiMatcher.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

100 lines
2.8 KiB
JavaScript

/**
* Device matching and Meraki client utilities.
* Centralizes the fragile name/description matching logic used across reports.
*/
/**
* Find the best matching Meraki client for a device using multiple strategies.
*
* identifiers can contain: name, deviceName, adyenName, UserName, DeviceFriendlyName, ip
*/
function findMatchingClient(merakiClients = [], identifiers = {}) {
if (!merakiClients.length) return null;
const names = [
identifiers.name,
identifiers.deviceName,
identifiers.adyenName,
identifiers.UserName,
identifiers.DeviceFriendlyName,
identifiers.printer_name,
identifiers.register_display_name,
]
.filter(Boolean)
.map(n => String(n).toLowerCase().trim());
const ipPrefix = identifiers.ip_address
? String(identifiers.ip_address).split('.')[0]
: identifiers.ip
? String(identifiers.ip).split('.')[0]
: null;
for (const client of merakiClients) {
if (!client?.description) continue;
const desc = client.description.toLowerCase().trim();
// Strategy 1: exact or substring match on any name
if (names.some(n => n && (desc === n || desc.includes(n) || n.includes(desc)))) {
return client;
}
// Strategy 2: IP prefix fallback
if (ipPrefix && desc.includes(ipPrefix)) {
return client;
}
// Strategy 3: check client.user field
if (client.user) {
const user = client.user.toLowerCase();
if (names.some(n => n && user.includes(n))) {
return client;
}
}
}
return null;
}
function getClientStatus(client) {
if (!client) return '❓ Unknown';
return client.status === 'Online' ? '✅ Online' : '❌ Offline';
}
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`;
}
/**
* Build a best-effort link to the Meraki client detail page.
* Note: This logic is environment-specific (dashboard URLs).
*/
function buildMerakiClientLink(merakiNetwork, client) {
if (!client || !merakiNetwork?.url) return '';
try {
const networkCode = merakiNetwork.url.split('/n/')[1]?.split('/')[0] || merakiNetwork.id;
const base = merakiNetwork.url.split('/n/')[0] || 'https://n976.dashboard.meraki.com';
// Different sections for switches vs APs vs clients
// Default to clients view
return `${base}/${merakiNetwork.name.replace(/ /g, '-')}/n/${networkCode}/manage/clients/${client.id}/overview`;
} catch (_e) {
return '';
}
}
module.exports = {
findMatchingClient,
getClientStatus,
formatLastSeen,
buildMerakiClientLink,
};