// services/dectStatus/captureRawXml.js // // Fetch raw status.xml from store DECT bases via the on-prem relay. // Used by GET /api/dect/raw-xml/:storeNumber and scripts/fetchDectStatusXml.js. import { collectPhoneStatus } from '../phoneService.js'; import { discoverDectBases } from '../dectDiscovery.js'; import { collectRawAll } from '../dectCollectorService.js'; import { getDectRelayHub } from '../dectRelayHub.js'; import { logger } from '../../utils/logger.js'; const LOG_SCOPE = 'dect:capture'; /** * @param {string} storeNum * @param {object} [opts] * @param {string} [opts.baseFilter] optional IP or MAC filter * @returns {Promise} */ export async function captureDectRawXml(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; } let relay = null; try { relay = getDectRelayHub().status(); } catch (err) { logger(LOG_SCOPE, `Relay status unavailable: ${err.message}`, 'warn'); relay = { connected: false }; } const phoneData = await collectPhoneStatus(store); const { bases, warnings: discoveryWarnings } = discoverDectBases(phoneData || {}); let targets = bases; const baseFilter = (opts.baseFilter || '').trim(); if (baseFilter) { targets = filterBases(bases, baseFilter); if (targets.length === 0) { const err = new Error(`No discovered base matched "${baseFilter}" for store ${store}`); err.code = 'BASE_NOT_FOUND'; err.knownBases = bases.map((b) => ({ ip: b.ip, mac: b.mac, name: b.name })); throw err; } } const results = await collectRawAll(targets); return { storeNum: store, relay, discoveryWarnings, bases: results.map((r) => ({ mac: r.base?.mac || null, ip: r.base?.ip || null, webexId: r.base?.webexId || null, name: r.base?.name || null, ok: r.ok, byteLength: r.byteLength, rawXml: r.rawXml, sectionInventory: r.sectionInventory, parsed: r.data, verdict: r.verdict, elapsedMs: r.elapsedMs, error: r.error, })), }; } function filterBases(bases, raw) { const needle = String(raw).trim().toLowerCase(); const needleMac = needle.replace(/[^0-9a-f]/g, ''); return bases.filter((b) => { if (b.ip && b.ip.toLowerCase() === needle) return true; if (b.mac) { const macHex = b.mac.replace(/[^0-9a-fA-F]/g, '').toLowerCase(); if (macHex === needleMac && needleMac.length === 12) return true; if (b.mac.toLowerCase() === needle) return true; } if (b.name && b.name.toLowerCase().includes(needle)) return true; return false; }); }