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>
197 lines
6.8 KiB
JavaScript
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;
|
|
}
|