// 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 `'; } 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; }