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>
137 lines
4 KiB
JavaScript
137 lines
4 KiB
JavaScript
// 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<object>}
|
||
*/
|
||
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;
|
||
});
|
||
}
|