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