collabSupport/services/renderers/mppPhoneDiagnosticsRenderer.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

204 lines
6.8 KiB
JavaScript

// services/renderers/mppPhoneDiagnosticsRenderer.js
//
// MPP desk phone diagnostics follow-up for /phonestatus (via relay).
import { formatDisplayTime } from '../../utils/time.js';
/**
* @param {Array} results probeAll() output
* @param {object} opts
* @param {string} opts.storeNum
* @param {boolean} [opts.verbose=false] Tier 3 fields
* @param {boolean} [opts.footer=true]
* @returns {string}
*/
export function renderMppPhoneDiagnosticsMarkdown(results, opts = {}) {
const { storeNum, verbose = false, footer = true } = opts;
const list = Array.isArray(results) ? results : [];
if (list.length === 0) return '';
let out = `**MPP Desk Phone Diagnostics — Store ${storeNum}**\n\n`;
for (const r of list) {
out += renderOnePhone(r, { verbose });
out += '\n';
}
if (footer) {
out += `\n*Phone diagnostics pulled at ${formatDisplayTime()} via the DECT relay.*`;
if (!verbose) {
out += ' Pass `verbose` or `debug` for extension/debug counters.';
}
out += ' Raw capture: `node scripts/probePhone.js raw <store>`';
}
return out.trim();
}
function renderOnePhone(r, { verbose }) {
const label = r.phone?.name || r.phone?.product || `Phone ${r.phone?.mac || '?'}`;
const ip = r.phone?.ip || r.parsed?.network?.ipv4 || '?';
if (!r.ok) {
return `⚠️ **${label}** (${ip}) — probe failed: ${r.error?.message || 'unknown error'}` +
(r.error?.hint ? `\n _${r.error.hint}_` : '');
}
const mpp = r.mpp || {};
const parsed = mpp.parsed || r.parsed || {};
const verdict = mpp.verdict || r.verdict || {};
const device = parsed.device || {};
const network = parsed.network || {};
const phoneStatus = parsed.phoneStatus || {};
const line1 = parsed.lines?.find((l) => l.index === 1) || parsed.lines?.[0];
const neighbor = mpp.networkNeighbor || {};
const download = mpp.download || {};
const system = mpp.system || {};
const icon = verdict.healthy ? '✅' : '⚠️';
const fwShort = shortenFirmware(device.firmware);
let out = `${icon} **${label}** (${ip})\n`;
out += ` ${device.product || '?'} · ${device.mac || '?'} · ${fwShort}\n`;
if (line1?.registrationState) {
out += ` Ext ${line1.index}: ${line1.registrationState}`;
if (line1.lastRegistrationIp) out += `${line1.lastRegistrationIp}`;
if (line1.lastRegistrationAt) out += ` (last ${line1.lastRegistrationAt})`;
out += '\n';
}
const uptime = phoneStatus.elapsedTime || '?';
const vlan = network.vlan ? `VLAN ${network.vlan}` : null;
const link = phoneStatus.linkSpeed || neighbor.portSpeed || null;
const netBits = [uptime !== '?' ? `uptime ${uptime}` : null, vlan, link].filter(Boolean);
if (netBits.length) out += ` ${netBits.join(' · ')}\n`;
if (neighbor.switchDevice) {
const swIp = neighbor.switchIp ? ` (${neighbor.switchIp})` : '';
out += ` Switch: ${neighbor.switchDevice} ${neighbor.switchPort || ''}${swIp}\n`;
}
if (parsed.rebootHistory?.length) {
out += ` Last reboot: ${formatReboot(parsed.rebootHistory[0])}\n`;
}
if (download.latestProvisioning) {
const prov = download.latestProvisioning;
const host = prov.url ? hostFromUrl(prov.url) : '?';
const result = prov.succeeded ? 'OK' : (prov.failed ? 'FAILED' : (prov.result || '?'));
out += ` Last resync: ${host} · ${result}\n`;
}
if (download.latestFirmwareUpgrade) {
const fw = download.latestFirmwareUpgrade;
if (fw.succeeded) {
out += ` Firmware upgrade: OK\n`;
} else if (fw.failed) {
out += ` Firmware upgrade: ${fw.result}\n`;
}
}
if (phoneStatus.dot1xStatus) {
out += ` 802.1X: ${phoneStatus.dot1xStatus}${phoneStatus.dot1xProtocol ? ` (${phoneStatus.dot1xProtocol})` : ''}\n`;
}
if (phoneStatus.ledLine1Color) {
out += ` Line 1 LED: ${phoneStatus.ledLine1Color}${phoneStatus.ledLine1Cadence ? ` ${phoneStatus.ledLine1Cadence}` : ''}\n`;
}
if (parsed.sip?.messagesSent || parsed.sip?.messagesRecv) {
out += ` SIP msgs: ${parsed.sip.messagesSent || 0} sent / ${parsed.sip.messagesRecv || 0} recv`;
if (phoneStatus.sipBytesSent) out += ` (${phoneStatus.sipBytesSent}B / ${phoneStatus.sipBytesRecv || 0}B)`;
out += '\n';
}
if (system.webServer) {
out += ` Web server: ${system.webServer}`;
if (system.proxyMode) out += ` · proxy ${system.proxyMode}`;
out += '\n';
}
if (line1?.messageWaiting && line1.messageWaiting !== 'No') {
out += ` Message waiting: ${line1.messageWaiting}\n`;
}
if (line1?.hotelingState && line1.hotelingState !== 'Disabled') {
out += ` Hoteling: ${line1.hotelingState}\n`;
}
if (Array.isArray(verdict.warnings) && verdict.warnings.length > 0) {
for (const w of verdict.warnings) {
out += ` ⚠️ ${w}\n`;
}
}
if (verbose) {
out += renderVerboseTier(r, parsed, neighbor, mpp.probes);
}
return out;
}
function renderVerboseTier(r, parsed, neighbor, probes) {
let out = '';
const otherLines = (parsed.lines || []).filter((l) => l.index !== 1);
for (const line of otherLines) {
out += ` Ext ${line.index}: ${line.registrationState || '?'}\n`;
}
const ps = parsed.phoneStatus || {};
if (ps.ipv6Status || ps.ipv6Address) {
out += ` IPv6: ${ps.ipv6Status || '?'} ${ps.ipv6Address || ''}\n`.trimEnd() + '\n';
}
if (ps.tr069Feature) out += ` TR-069: ${ps.tr069Feature}\n`;
if (ps.multicastRx != null || ps.multicastTx != null) {
out += ` Paging multicast: rx ${ps.multicastRx || 0} / tx ${ps.multicastTx || 0}\n`;
}
if (ps.streamingRx != null) out += ` XML streaming rx: ${ps.streamingRx}\n`;
const counters = neighbor.errorCounters || {};
const counterKeys = Object.keys(counters);
if (counterKeys.length > 0) {
out += ' Ethernet errors:\n';
for (const k of counterKeys.slice(0, 8)) {
out += ` ${k}: ${counters[k]}\n`;
}
}
const probeList = Array.isArray(probes) ? probes : [];
if (probeList.length > 0) {
out += ' Probe paths:\n';
out += ' ```\n';
out += ' STATUS BYTES PATH\n';
for (const p of probeList) {
const status = p.status == null ? 'ERR' : String(p.status);
out += ` ${status.padEnd(6)} ${String(p.sizeBytes || 0).padEnd(6)} ${p.path}\n`;
}
out += ' ```\n';
}
return out;
}
function shortenFirmware(fw) {
if (!fw || typeof fw !== 'string') return '?';
const m = fw.match(/(sip78xx[^\s]+)/i);
return m ? m[1] : (fw.length > 36 ? `${fw.slice(0, 36)}` : fw);
}
function formatReboot(raw) {
if (!raw) return '?';
const m = raw.match(/^(\w+)\(([^)]+)\)/);
return m ? `${m[1]} ${m[2]}` : raw;
}
function hostFromUrl(url) {
if (!url || typeof url !== 'string') return '?';
try {
const normalized = url.startsWith('http') ? url : `https://${url}`;
const u = new URL(normalized);
return u.hostname;
} catch {
return url.length > 30 ? `${url.slice(0, 30)}` : url;
}
}