// services/renderers/callTestRenderer.js function fmtMs(ms) { if (ms == null || !Number.isFinite(ms)) return '—'; if (ms < 1000) return `${Math.round(ms)}ms`; return `${(ms / 1000).toFixed(1)}s`; } function fmtTime(iso) { if (!iso) return '—'; try { return new Date(iso).toISOString(); } catch { return String(iso); } } /** * @param {object} session */ export function renderCallTestStartedMarkdown(session) { const modeLabel = session.mode === 'store' ? 'Store path' : 'Direct dial'; const storePart = session.storeNum ? ` for store **${session.storeNum}**` : ''; return ( `**Call test started** (${modeLabel})${storePart}\n\n` + `- **Dialing:** ${session.dialNumber}\n` + `- **Test ID:** \`${session.testId}\`\n` + `- Results will post here when the call completes (~2–3 min).` ); } /** * @param {object} session */ export function renderCallTestResultMarkdown(session) { const modeLabel = session.mode === 'store' ? 'Store' : 'Direct'; const inProgress = session.result == null && (session.status === 'in_progress' || session.status === 'pending'); const pass = session.result === 'pass'; const warn = session.result === 'warn'; const icon = inProgress ? '⏳' : (pass || warn ? '✅' : '❌'); const headline = inProgress ? 'IN PROGRESS' : (pass || warn ? 'PASSED' : 'FAILED'); let md = `**${icon} Call test ${headline}** (${modeLabel})\n\n`; if (session.storeNum) md += `- **Store:** ${session.storeNum}\n`; md += `- **Dialed:** ${session.dialNumber}\n`; md += `- **Test ID:** \`${session.testId}\`\n`; if (session.callSid) md += `- **Twilio CallSid:** \`${session.callSid}\`\n`; if (session.resultReason) { md += `- **Result:** ${session.resultReason}\n`; } if (session.failedAt) { md += `- **Failed at:** ${session.failedAt}\n`; } const e = session.events || {}; md += '\n**Timings:**\n'; if (e.initiated) md += `- Initiated: ${fmtTime(e.initiated)}\n`; if (e.ringing) md += `- Ringing: ${fmtTime(e.ringing)}\n`; if (e.answered) md += `- Answered: ${fmtTime(e.answered)}\n`; if (!e.answered && e['in-progress']) md += `- In progress: ${fmtTime(e['in-progress'])}\n`; if (e.completed) md += `- Completed: ${fmtTime(e.completed)}\n`; if (e.answered && e.initiated) { md += `- Ring → answer: ${fmtMs(new Date(e.answered) - new Date(e.initiated))}\n`; } if (e.coreStartedAt) { md += `- Core started: ${fmtTime(e.coreStartedAt)}\n`; } if (e.completed && e.answered) { md += `- Answer → complete: ${fmtMs(new Date(e.completed) - new Date(e.answered))}\n`; } if (session.durationSec != null) { md += `- Total duration: ${session.durationSec}s\n`; } if (session.mode === 'store' && e.dtmfStepAt) { md += `- Entry → DTMF step: ${fmtTime(e.dtmfStepAt)}\n`; } return md.trim(); } /** * @param {object} session * @param {object} details from fetchCallResource */ export function renderCallTestTwilioDetailsMarkdown(session, details) { let md = `**Call test — Twilio details** (\`${session.testId}\`)\n\n`; if (!details?.available) { md += `_Unavailable:_ ${details?.reason || 'unknown'}\n`; if (session.callSid) md += `- **CallSid:** \`${session.callSid}\`\n`; return md.trim(); } const d = details.data || {}; md += `- **CallSid:** \`${d.sid || session.callSid}\`\n`; md += `- **Status:** ${d.status || '—'}\n`; md += `- **Duration:** ${d.duration != null ? `${d.duration}s` : '—'}\n`; if (d.startTime) md += `- **Start:** ${fmtTime(d.startTime)}\n`; if (d.endTime) md += `- **End:** ${fmtTime(d.endTime)}\n`; md += `- **From:** ${d.from || '—'}\n`; md += `- **To:** ${d.to || '—'}\n`; if (d.price != null) md += `- **Price:** ${d.price} ${d.priceUnit || ''}\n`.trim() + '\n'; if (d.answeredBy) md += `- **Answered by:** ${d.answeredBy}\n`; return md.trim(); } /** * @param {object} session * @param {object} insights combined summary + metrics */ export function renderCallTestTwilioInsightsMarkdown(session, insights) { let md = `**Call test — Twilio Insights** (\`${session.testId}\`)\n\n`; if (!insights?.available) { md += `_Unavailable:_ ${insights?.reason || 'Voice Insights not available'}\n`; md += '\n_Enable Voice Insights Advanced Features in Twilio Console for MOS/jitter/loss metrics._\n'; return md.trim(); } const s = insights.summary; if (s) { md += '**Summary**\n'; if (s.duration != null) md += `- Duration: ${s.duration}s\n`; if (s.connectDuration != null) md += `- Connect duration: ${s.connectDuration}s\n`; if (s.processingState) md += `- Processing: ${s.processingState}\n`; } const m = insights.metrics; if (m?.highlights) { md += '\n**Quality (carrier edge)**\n'; const h = m.highlights; if (h.maxJitter != null) md += `- Max jitter: ${h.maxJitter}\n`; if (h.maxPacketLoss != null) md += `- Max packet loss: ${h.maxPacketLoss}%\n`; if (h.minMos != null) md += `- Min MOS: ${h.minMos}\n`; } return md.trim(); } /** * @param {object} session * @param {object} cdrResult from fetchMatchedCdrForSession */ export function renderCallTestCdrMatchMarkdown(session, cdrResult) { let md = `**Call test — Webex CDR match** (\`${session.testId}\`)\n\n`; if (!cdrResult?.available) { md += `_CDR unavailable:_ ${cdrResult?.reason || 'unknown'}\n`; return md.trim(); } if (cdrResult.window) { md += `- **Window:** ${cdrResult.window.startTime} → ${cdrResult.window.endTime}\n`; } if (cdrResult.location) md += `- **Location:** ${cdrResult.location}\n`; md += `- **Records in window:** ${cdrResult.rawCount ?? '—'}\n`; const match = cdrResult.match; if (!match) { md += `\n_No matching leg found._ ${cdrResult.reason || ''}\n`; return md.trim(); } md += '\n**Matched leg**\n'; md += `- **Start:** ${fmtTime(match.start)}\n`; md += `- **Direction:** ${match.direction || '—'}\n`; md += `- **Duration:** ${match.duration ?? '—'}s\n`; md += `- **Status:** ${match.status || '—'}\n`; if (match.callingNumber) md += `- **Calling:** ${match.callingNumber}\n`; if (match.calledNumber) md += `- **Called:** ${match.calledNumber}\n`; if (match.score != null) md += `- **Match score:** ${match.score}\n`; return md.trim(); } /** * Append enrichment status for /calltest status. */ export function renderCallTestEnrichmentStatusMarkdown(session) { const e = session.enrichment || {}; const lines = ['\n**Enrichment:**']; const tw = e.twilio || {}; if (tw.detailsFetchedAt) { lines.push(`- Twilio details: ${tw.details?.available ? 'ready' : 'failed'}`); } else { lines.push('- Twilio details: pending'); } if (tw.insightsFetchedAt) { lines.push(`- Twilio Insights: ${tw.insights?.available ? 'ready' : 'unavailable'}`); } else { lines.push('- Twilio Insights: pending'); } const cdr = e.cdr; if (cdr?.fetchedAt) { lines.push(`- Webex CDR: ${cdr.match ? 'matched' : 'no match'}`); } else { lines.push('- Webex CDR: pending'); } return lines.join('\n'); } /** * @param {object} session * @param {object} cdr */ export function renderCallTestCdrMarkdown(session, cdr) { if (!cdr?.available) { return ( `**Call test CDR** (\`${session.testId}\`)\n\n` + `_Detailed call history unavailable:_ ${cdr?.reason || 'not configured'}` ); } const summary = cdr.summary || {}; let md = `**Call test CDR enrichment** (store **${session.storeNum}**)\n\n` + `- **Window:** ${cdr.startTime} → ${cdr.endTime}\n` + `- **Location:** ${cdr.location || '—'}\n` + `- **Matching legs:** ${summary.totalCalls ?? cdr.rawCount ?? 0}\n`; if (summary.inbound != null) md += `- Inbound legs: ${summary.inbound}\n`; if (summary.missed != null) md += `- Missed / failed legs: ${summary.missed}\n`; const samples = cdr.samples || []; if (samples.length) { md += '\n**Sample legs:**\n'; for (const s of samples.slice(0, 5)) { md += `- ${s.start || '—'} | ${s.direction || '—'} | ${s.duration ?? '—'}s | ${s.otherParty || s.phoneNumber || '—'}\n`; } } return md.trim(); }