collabSupport/services/renderers/callTestRenderer.js
jmcqueen 1eaabbcee1 Add /voicereport store digest and harden Webex CDR feed handling.
Groups cdr_feed legs by Correlation ID for call-level summaries, fixes report-column field parsing and Docker proxy routing, and adds /calltest post-call Twilio and CDR enrichment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 13:43:11 -04:00

241 lines
8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 (~23 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();
}