// src/services/renderers/voiceDiagRenderer.js // // Renders the /voicediag CheckResult array into Webex-friendly // markdown. Pure function — no I/O — so it's usable from both the // chat command and (later) HTTP surfaces without recomputing. // // Layout: // // **Voice Diagnostic - Store 12345** (user: ae12345@ae.com — Store 12345) // // Errors (1) Warnings (2) Skipped (1) OK (5) // // **ERRORS** // - **Call Intercept**: Call intercept is ACTIVE — inbound calls... // • incomingType: INTERCEPT_ALL, outgoingType: ALLOW_ALL // // **WARNINGS** // - **Do Not Disturb**: DND is enabled — incoming calls silenced. // - **Call Forwarding**: 1 forwarding variant is active: ... // // **SKIPPED** // - **Call Waiting**: 403 on Call Waiting — likely missing scope ... // // **OK** (5 — hidden; pass `detailed` to see them) // // Fixable issues (2) — see confirmation cards below. // // Options // storeNum (required) header text // detailed (default false) — expands OK checks + shows the // `details` block under every check // emitFooter (default true) — trailing italic timestamp // // The renderer never emits an adaptive card itself. The caller // (commands/voiceDiag.js) walks the same results array to post cards. const SEVERITY_ORDER = ['error', 'warn', 'skipped', 'ok']; const SEVERITY_LABEL = { error: 'ERRORS', warn: 'WARNINGS', skipped: 'SKIPPED', ok: 'OK', }; /** * @param {Array<{id, label, status, message, details, remediation}>} results * @param {{ * storeNum: string, * personLabel?: string, * email?: string, * detailed?: boolean, * emitFooter?: boolean, * }} [opts] * @returns {string} markdown, whitespace-trimmed */ export function renderVoiceDiagMarkdown(results, opts = {}) { const { storeNum, personLabel = null, email = null, detailed = false, emitFooter = true, } = opts; const list = Array.isArray(results) ? results : []; let reply = `**Voice Diagnostic - Store ${storeNum}**`; if (personLabel || email) { const who = [personLabel, email].filter(Boolean).join(' — '); reply += ` (user: ${who})`; } reply += '\n\n'; if (list.length === 0) { reply += '_No checks were executed. Verify the store number and re-run._\n'; return reply.trim(); } const counts = countBySeverity(list); reply += summaryLine(counts) + '\n\n'; for (const severity of SEVERITY_ORDER) { const bucket = list.filter((r) => r.status === severity); if (bucket.length === 0) continue; // Hide the OK bucket from the console body unless detailed — // keeps the default output focused on what's actionable. if (severity === 'ok' && !detailed) continue; reply += `**${SEVERITY_LABEL[severity]}**\n`; for (const r of bucket) { reply += `- **${r.label}**: ${r.message}\n`; if (detailed && r.details && Object.keys(r.details).length > 0) { reply += ` - ${renderDetails(r.details)}\n`; } } reply += '\n'; } const fixable = list.filter((r) => r.status !== 'ok' && r.remediation); if (fixable.length > 0) { reply += `**Fixable issues (${fixable.length})** — see confirmation cards below.\n\n` + fixable.map((r) => `- ${r.label}: ${r.remediation.title}`).join('\n') + '\n\n'; } if (!detailed && counts.ok > 0) { reply += `_${counts.ok} OK check(s) hidden — pass \`detailed\` to include them._\n\n`; } if (emitFooter) { const now = new Date(); reply += `_Last checked: ${now.toISOString()}_\n`; } return reply.trim(); } function countBySeverity(list) { const counts = { error: 0, warn: 0, skipped: 0, ok: 0 }; for (const r of list) { if (Object.prototype.hasOwnProperty.call(counts, r.status)) { counts[r.status] += 1; } } return counts; } function summaryLine({ error, warn, skipped, ok }) { return `Errors (${error}) · Warnings (${warn}) · Skipped (${skipped}) · OK (${ok})`; } // Compact one-line rendering of the details object. Truncates arrays // past a small length so the chat message doesn't blow up on // verbose payloads (outgoingPermission's rule list, for example). function renderDetails(details) { if (details === null || details === undefined) return ''; if (typeof details !== 'object') return String(details); const parts = []; for (const [k, v] of Object.entries(details)) { parts.push(`${k}: ${formatValue(v)}`); } return parts.join(', '); } function formatValue(v) { if (v === null || v === undefined) return '—'; if (typeof v === 'boolean' || typeof v === 'number' || typeof v === 'string') { return String(v); } if (Array.isArray(v)) { if (v.length === 0) return '[]'; if (v.length <= 3) return `[${v.map(formatValue).join(', ')}]`; return `[${v.slice(0, 3).map(formatValue).join(', ')}, …+${v.length - 3}]`; } // Fall through: nested object — collapse to a JSON snippet. try { const s = JSON.stringify(v); return s.length > 120 ? `${s.slice(0, 117)}…` : s; } catch { return '[object]'; } }