// services/phoneStatus/capturePhoneProbe.js // // Fetch MPP phone probe data from store desk phones via the on-prem relay. import { collectPhoneStatus } from '../phoneService.js'; import { discoverDeskPhones, manualPhoneTarget } from '../phoneDiscovery.js'; import { probeRawAll, probeRawOne } from '../phoneCollectorService.js'; import { getDectRelayHub } from '../dectRelayHub.js'; import { logger } from '../../utils/logger.js'; const LOG_SCOPE = 'phone:capture'; function relayStatus() { try { return getDectRelayHub().status(); } catch (err) { logger(LOG_SCOPE, `Relay status unavailable: ${err.message}`, 'warn'); return { connected: false }; } } function mapProbeResult(r) { return { mac: r.phone?.mac || null, ip: r.phone?.ip || null, webexId: r.phone?.webexId || null, name: r.phone?.name || null, product: r.phone?.product || null, ok: r.ok, byteLength: r.byteLength, statusJson: r.statusJson || null, statusXml: r.statusXml || null, downloadStatusJson: r.downloadStatusJson || null, nsJson: r.nsJson || null, systemJson: r.systemJson || null, cfgParsed: r.cfgParsed || null, probes: r.probes, parsed: r.parsed, verdict: r.verdict, mpp: r.mpp || null, summary: r.summary, elapsedMs: r.elapsedMs, error: r.error, }; } /** * Probe a single phone IP via the connected relay (bot process only). * @param {string} targetIp */ export async function capturePhoneProbeDirect(targetIp) { const ip = String(targetIp || '').trim(); if (!ip) { const err = new Error('targetIp is required'); err.code = 'INVALID_IP'; throw err; } if (!process.env.DECT_RELAY_AGENT_TOKEN) { const err = new Error('DECT_RELAY_AGENT_TOKEN is not configured on this bot'); err.code = 'RELAY_NOT_CONFIGURED'; throw err; } const relay = relayStatus(); const phone = manualPhoneTarget(ip); const result = await probeRawOne(getDectRelayHub(), phone); return { relay, phones: [mapProbeResult(result)], }; } /** * @param {string} storeNum * @param {object} [opts] * @param {string} [opts.phoneFilter] * @returns {Promise} */ export async function capturePhoneProbe(storeNum, opts = {}) { const store = String(storeNum || '').trim(); if (!store || !/^\d{2,4}$/.test(store)) { const err = new Error('storeNum must be a 2–4 digit store number'); err.code = 'INVALID_STORE'; throw err; } if (!process.env.DECT_RELAY_AGENT_TOKEN) { const err = new Error('DECT_RELAY_AGENT_TOKEN is not configured on this bot'); err.code = 'RELAY_NOT_CONFIGURED'; throw err; } const relay = relayStatus(); const phoneData = await collectPhoneStatus(store); const { phones, warnings: discoveryWarnings, inventory } = discoverDeskPhones(phoneData || {}); let targets = phones; const phoneFilter = (opts.phoneFilter || '').trim(); if (phoneFilter) { targets = filterPhones(phones, phoneFilter); if (targets.length === 0) { const err = new Error(`No discovered phone matched "${phoneFilter}" for store ${store}`); err.code = 'PHONE_NOT_FOUND'; err.knownPhones = phones.map((p) => ({ ip: p.ip, mac: p.mac, name: p.name, product: p.product })); throw err; } } const results = await probeRawAll(targets); return { storeNum: store, relay, discoveryWarnings, inventory, phones: results.map(mapProbeResult), }; } function filterPhones(phones, raw) { const needle = String(raw).trim().toLowerCase(); const needleMac = needle.replace(/[^0-9a-f]/g, ''); return phones.filter((p) => { if (p.ip && p.ip.toLowerCase() === needle) return true; if (p.mac) { const macHex = p.mac.replace(/[^0-9a-fA-F]/g, '').toLowerCase(); if (macHex === needleMac && needleMac.length === 12) return true; if (p.mac.toLowerCase() === needle) return true; } if (p.name && p.name.toLowerCase().includes(needle)) return true; if (p.product && p.product.toLowerCase().includes(needle)) return true; return false; }); }