// services/phoneCollectorService.js // // Fan-out MPP phone probes over the DECT relay hub. import { getDectRelayHub, RelayErrorCodes } from './dectRelayHub.js'; import { buildMppProbeView } from '../integrations/cisco-mpp-phone/aggregateProbe.js'; import { logger } from '../utils/logger.js'; const LOG_SCOPE = 'phone:collector'; const DEFAULT_TIMEOUT_MS = Number(process.env.PHONE_COLLECT_TIMEOUT_MS) || 15_000; /** * @param {object[]} phones from discoverDeskPhones() * @param {object} [opts] * @returns {Promise} */ export async function probeAll(phones, opts = {}) { const list = Array.isArray(phones) ? phones : []; if (list.length === 0) return []; const hub = opts.hub || getDectRelayHub(); const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS; logger(LOG_SCOPE, `Fanning out phone-probe() to ${list.length} phone(s)`, 'debug'); const results = await Promise.all(list.map((phone) => probeOne(hub, phone, timeoutMs))); const okCount = results.filter((r) => r.ok).length; logger(LOG_SCOPE, `Phone probe finished: ${okCount}/${list.length} succeeded`, 'debug'); return results; } /** * @param {object[]} phones * @param {object} [opts] * @returns {Promise} */ export async function probeRawAll(phones, opts = {}) { const list = Array.isArray(phones) ? phones : []; if (list.length === 0) return []; const hub = opts.hub || getDectRelayHub(); const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS; logger(LOG_SCOPE, `Fanning out phone-probe-raw() to ${list.length} phone(s)`, 'debug'); const results = await Promise.all(list.map((phone) => probeRawOne(hub, phone, timeoutMs))); const okCount = results.filter((r) => r.ok).length; logger(LOG_SCOPE, `Phone probe-raw finished: ${okCount}/${list.length} succeeded`, 'debug'); return results; } export async function probeOne(hub, phone, timeoutMs = DEFAULT_TIMEOUT_MS) { if (!phone?.ip) { return { phone, ok: false, probes: null, statusJson: null, statusXml: null, parsed: null, verdict: null, elapsedMs: null, error: { code: 'NO_IP', message: 'phone has no IP address' }, }; } const started = Date.now(); try { const { result, elapsedMs } = await hub.phoneProbe(phone.ip, { timeoutMs }); const mpp = buildMppProbeView({ statusJson: result?.statusJson, statusXml: result?.statusXml, downloadStatusJson: result?.downloadStatusJson, nsJson: result?.nsJson, systemJson: result?.systemJson, probes: result?.probes, }); const summary = result?.summary || null; return { phone, ok: !!(summary?.statusJsonFound || summary?.statusXmlFound || mpp.parsed), probes: result?.probes || null, statusJson: result?.statusJson || null, statusXml: result?.statusXml || null, downloadStatusJson: result?.downloadStatusJson || null, nsJson: result?.nsJson || null, systemJson: result?.systemJson || null, cfgParsed: result?.cfgParsed || null, parsed: mpp.parsed, verdict: mpp.verdict, mpp, summary, elapsedMs: elapsedMs ?? (Date.now() - started), error: null, }; } catch (err) { return { phone, ok: false, probes: null, statusJson: null, statusXml: null, parsed: null, verdict: null, elapsedMs: Date.now() - started, error: { code: err?.code || 'UNKNOWN', message: err?.message || String(err), hint: hintFor(err?.code), }, }; } } export async function probeRawOne(hub, phone, timeoutMs = DEFAULT_TIMEOUT_MS) { if (!phone?.ip) { return { phone, ok: false, probes: null, statusJson: null, statusXml: null, parsed: null, verdict: null, byteLength: null, elapsedMs: null, error: { code: 'NO_IP', message: 'phone has no IP address' }, }; } const started = Date.now(); try { const { result, elapsedMs } = await hub.phoneProbeRaw(phone.ip, { timeoutMs }); const mpp = buildMppProbeView({ statusJson: result?.statusJson, statusXml: result?.statusXml, downloadStatusJson: result?.downloadStatusJson, nsJson: result?.nsJson, systemJson: result?.systemJson, probes: result?.probes, }); const summary = result?.summary || null; return { phone, ok: !!(summary?.statusJsonFound || summary?.statusXmlFound || result?.statusJson || result?.statusXml), probes: result?.probes || null, statusJson: result?.statusJson || null, statusXml: result?.statusXml || null, downloadStatusJson: result?.downloadStatusJson || null, nsJson: result?.nsJson || null, systemJson: result?.systemJson || null, byteLength: result?.byteLength ?? null, cfgParsed: result?.cfgParsed || null, parsed: mpp.parsed, verdict: mpp.verdict, mpp, summary, elapsedMs: elapsedMs ?? (Date.now() - started), error: null, }; } catch (err) { return { phone, ok: false, probes: null, statusJson: null, statusXml: null, parsed: null, verdict: null, byteLength: null, elapsedMs: Date.now() - started, error: { code: err?.code || 'UNKNOWN', message: err?.message || String(err), hint: hintFor(err?.code), }, }; } } function hintFor(code) { switch (code) { case RelayErrorCodes.NOT_CONNECTED: return 'DECT relay agent is not connected. Check that dect-relay-agent is running in the data center.'; case RelayErrorCodes.TIMEOUT: return 'Relay accepted the request but the phone did not respond in time.'; case RelayErrorCodes.DISCONNECTED: return 'Relay agent disconnected while this command was in flight. Try again.'; case 'AGENT_CONFIG': return 'Relay agent configuration is incomplete — check dect-relay-agent/.env and restart.'; default: return null; } }