fix+feat: morning QA fixes, worklist pagination, misc sidecar improvements

- caller-resolution: drop cache, use indexed phone filter (lead.contactPhone.primaryPhoneNumber.like)
- worklist: externalize page size (WORKLIST_PAGE_SIZE × WORKLIST_MAX_PAGES), paginate getMissedCalls/getAssignedLeads/getPendingFollowUps
- maint: unlock-agent, force-ready, backfill-caller-resolution, clear-analysis-cache, fix-timestamps
- ozonetel agent.service: force logout+re-login on "already logged in"
- ai chat: context expansion
- livekit-agent: updates
- widget: session handling
- masterdata: clinic list cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 06:49:02 +05:30
parent b6b597fdda
commit fbe782b5ac
17 changed files with 685 additions and 269 deletions

View File

@@ -317,4 +317,108 @@ export class MaintController {
this.logger.log(`[MAINT] Backfill complete: ${linked} linked, ${created} patients created, ${apptLinked} appointments linked, ${skipped} skipped`);
return { status: 'ok', leads: { total: leads.length, linked, created, skipped }, appointments: { total: appointments.length, linked: apptLinked } };
}
// Backfill Call records that lost their identity at ingest (missed-call
// webhook / poller / dispose flow before the caller-resolution wiring).
// Routes each phone through CallerResolutionService so the same code
// path the live system uses also fixes historical rows. Idempotent —
// safe to re-run; only patches calls that are currently missing
// leadName / patientId / leadId.
@Post('backfill-caller-resolution')
async backfillCallerResolution() {
this.logger.log('[MAINT] Backfill caller resolution — patching Calls + Leads via resolver');
const apiKey = process.env.PLATFORM_API_KEY ?? '';
const auth = apiKey ? `Bearer ${apiKey}` : '';
if (!auth) throw new HttpException('PLATFORM_API_KEY not configured', 500);
let callsScanned = 0;
let callsPatched = 0;
let callsSkipped = 0;
let leadsResolved = 0;
let resolveErrors = 0;
// Phone → resolved cache so multiple calls from the same number
// only resolve once during this run.
const resolvedByPhone = new Map<string, { leadId: string; patientId: string; firstName: string; lastName: string }>();
// Page through all calls in chunks of 200. We're after rows where
// leadName is empty OR leadId is null OR patientId is missing.
let cursor: string | null = null;
let hasNext = true;
while (hasNext) {
const pageQuery = cursor
? `{ calls(first: 200, after: "${cursor}") { edges { cursor node { id leadId leadName callerNumber { primaryPhoneNumber } } } pageInfo { hasNextPage endCursor } } }`
: `{ calls(first: 200) { edges { cursor node { id leadId leadName callerNumber { primaryPhoneNumber } } } pageInfo { hasNextPage endCursor } } }`;
let page: any;
try {
page = await this.platform.query<any>(pageQuery);
} catch (err) {
this.logger.warn(`[MAINT] calls page query failed: ${err}`);
break;
}
const edges = page?.calls?.edges ?? [];
hasNext = page?.calls?.pageInfo?.hasNextPage ?? false;
cursor = page?.calls?.pageInfo?.endCursor ?? null;
for (const edge of edges) {
const call = edge.node;
callsScanned++;
const phoneRaw = call.callerNumber?.primaryPhoneNumber ?? '';
const phone10 = phoneRaw.replace(/\D/g, '').slice(-10);
const needsName = !call.leadName || call.leadName === '';
const needsLead = !call.leadId;
if (!phone10 || phone10.length < 10) { callsSkipped++; continue; }
if (!needsName && !needsLead) { callsSkipped++; continue; }
let resolved = resolvedByPhone.get(phone10) ?? null;
if (!resolved) {
try {
const r = await this.callerResolution.resolve(phone10, auth);
resolved = {
leadId: r.leadId,
patientId: r.patientId,
firstName: r.firstName,
lastName: r.lastName,
};
resolvedByPhone.set(phone10, resolved);
leadsResolved++;
} catch (err) {
this.logger.warn(`[MAINT] resolve failed for ${phone10}: ${err}`);
resolveErrors++;
callsSkipped++;
continue;
}
}
const fullName = `${resolved.firstName} ${resolved.lastName}`.trim();
const updateParts: string[] = [];
if (needsLead && resolved.leadId) updateParts.push(`leadId: "${resolved.leadId}"`);
if (needsName && fullName) updateParts.push(`leadName: "${fullName.replace(/"/g, '\\"')}"`);
if (updateParts.length === 0) { callsSkipped++; continue; }
try {
await this.platform.query<any>(
`mutation { updateCall(id: "${call.id}", data: { ${updateParts.join(', ')} }) { id } }`,
);
callsPatched++;
} catch (err) {
this.logger.warn(`[MAINT] updateCall failed for ${call.id}: ${err}`);
callsSkipped++;
}
// Throttle so the platform isn't hammered
await new Promise((r) => setTimeout(r, 100));
}
}
this.logger.log(`[MAINT] Backfill caller resolution complete: scanned=${callsScanned} patched=${callsPatched} skipped=${callsSkipped} uniquePhones=${resolvedByPhone.size} leadsResolved=${leadsResolved} resolveErrors=${resolveErrors}`);
return {
status: 'ok',
calls: { scanned: callsScanned, patched: callsPatched, skipped: callsSkipped },
phones: { unique: resolvedByPhone.size, resolved: leadsResolved, errors: resolveErrors },
};
}
}