collabSupport/services/phoneCollectorService.js
jmcqueen f7953b8eb5 Add MPP desk phone diagnostics follow-up to /phonestatus via relay.
Wire CP-78xx probe discovery, relay phone-probe commands, and a chat follow-up message so store desk phones get registration, switch, and provisioning detail alongside DECT and WAN diagnostics.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 18:01:01 -04:00

180 lines
5.8 KiB
JavaScript

// 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<object[]>}
*/
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<object[]>}
*/
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;
}
}