collabSupport/services/renderers/mppPhoneDiagnosticsRenderer.js
jmcqueen 7dc326c404 Split voice commands into status vs diag and redesign MPP phone output.
Replace /phonestatus follow-ups with /voicestatus, /wanstatus, /phonediag, and /dectdiag; extend /voicediag with relay probes and section-based MPP diagnostics.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 13:55:41 -04:00

197 lines
6.8 KiB
JavaScript

// services/renderers/mppPhoneDiagnosticsRenderer.js
//
// MPP desk phone diagnostics for /phonediag (via relay).
import { formatDisplayTime } from '../../utils/time.js';
import {
shortenFirmware,
formatHeaderStatusLine,
headerIconForLine,
isLineRegistered,
formatRebootEntry,
formatProvisioningResync,
formatFirmwareUpgrade,
collectIssues,
} from './mppPhoneFormatters.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 device = parsed.device || {};
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 issues = collectIssues(mpp, parsed);
let out = renderPhoneHeader({ label, ip, device, phoneStatus, neighbor, line1 });
out += renderRegistrationSection(line1, phoneStatus);
out += renderSwitchSection(neighbor);
out += renderProvisioningSection(parsed, download);
out += renderIssuesSection(issues);
if (verbose) {
out += renderVerboseTier(r, parsed, neighbor, mpp.probes, line1);
}
return out;
}
function renderPhoneHeader({ label, ip, device, phoneStatus, neighbor, line1 }) {
const icon = headerIconForLine(line1);
const fwShort = shortenFirmware(device.firmware);
let out = `${icon} **${label}** · \`${ip}\`\n`;
out += `_${device.product || '?'} · ${device.mac || '?'} · ${fwShort}_\n`;
const statusLine = formatHeaderStatusLine(phoneStatus, neighbor);
if (statusLine) out += `_${statusLine}_\n`;
return `${out}\n`;
}
function renderRegistrationSection(line1, phoneStatus) {
const bullets = [];
if (line1?.registrationState) {
let reg = `Ext ${line1.index}: ${line1.registrationState}`;
if (line1.lastRegistrationIp) reg += `${line1.lastRegistrationIp}`;
if (line1.lastRegistrationAt) reg += ` (last ${line1.lastRegistrationAt})`;
bullets.push(reg);
}
if (line1 && !isLineRegistered(line1) && phoneStatus.ledLine1Color) {
const led = `${phoneStatus.ledLine1Color}${phoneStatus.ledLine1Cadence ? ` ${phoneStatus.ledLine1Cadence}` : ''}`;
bullets.push(`Line 1 LED: ${led}`);
}
if (line1?.messageWaiting && line1.messageWaiting !== 'No') {
bullets.push(`Message waiting: ${line1.messageWaiting}`);
}
if (line1?.hotelingState && line1.hotelingState !== 'Disabled') {
bullets.push(`Hoteling: ${line1.hotelingState}`);
}
if (bullets.length === 0) return '';
return `**Registration**\n${bullets.map((b) => `${b}`).join('\n')}\n\n`;
}
function renderSwitchSection(neighbor) {
if (!neighbor?.switchDevice) return '';
const swIp = neighbor.switchIp ? ` · \`${neighbor.switchIp}\`` : '';
const port = neighbor.switchPort || '';
return `**Switch**\n${neighbor.switchDevice} · ${port}${swIp}\n\n`;
}
function renderProvisioningSection(parsed, download) {
const bullets = [];
if (parsed.rebootHistory?.length) {
const reboot = formatRebootEntry(parsed.rebootHistory[0]);
if (reboot) bullets.push(`Last reboot: ${reboot}`);
}
const resync = formatProvisioningResync(download.latestProvisioning);
if (resync) bullets.push(resync);
const fw = formatFirmwareUpgrade(download.latestFirmwareUpgrade);
if (fw) bullets.push(fw);
if (bullets.length === 0) return '';
return `**Provisioning**\n${bullets.map((b) => `${b}`).join('\n')}\n\n`;
}
function renderIssuesSection(issues) {
if (!Array.isArray(issues) || issues.length === 0) return '';
return `**Issues**\n${issues.map((i) => `${i}`).join('\n')}\n\n`;
}
function renderVerboseTier(r, parsed, neighbor, probes, line1) {
let out = '';
const otherLines = (parsed.lines || []).filter((l) => l.index !== 1);
if (otherLines.length > 0) {
out += '**Extensions**\n';
for (const line of otherLines) {
out += `• Ext ${line.index}: ${line.registrationState || '?'}\n`;
}
out += '\n';
}
if (parsed.sip?.messagesSent || parsed.sip?.messagesRecv) {
const ps = parsed.phoneStatus || {};
out += `**SIP counters**\n${parsed.sip.messagesSent || 0} sent / ${parsed.sip.messagesRecv || 0} recv`;
if (ps.sipBytesSent) out += ` (${ps.sipBytesSent}B / ${ps.sipBytesRecv || 0}B)`;
out += '\n\n';
}
if (isLineRegistered(line1) && line1 && parsed.phoneStatus?.ledLine1Color) {
const ps = parsed.phoneStatus;
const led = `${ps.ledLine1Color}${ps.ledLine1Cadence ? ` ${ps.ledLine1Cadence}` : ''}`;
out += `**LED**\n• Line 1: ${led}\n\n`;
}
const ps = parsed.phoneStatus || {};
if (ps.ipv6Status || ps.ipv6Address) {
out += `**IPv6**\n${ps.ipv6Status || '?'} ${ps.ipv6Address || ''}\n\n`;
}
if (ps.tr069Feature) out += `**TR-069**\n${ps.tr069Feature}\n\n`;
if (ps.multicastRx != null || ps.multicastTx != null) {
out += `**Paging multicast**\n• rx ${ps.multicastRx || 0} / tx ${ps.multicastTx || 0}\n\n`;
}
if (ps.streamingRx != null) out += `**XML streaming**\n• rx ${ps.streamingRx}\n\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`;
}
out += '\n';
}
const probeList = Array.isArray(probes) ? probes : [];
if (probeList.length > 0) {
out += '**Probe paths**\n```\nSTATUS 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;
}