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

@@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service';
import { TelephonyConfigService } from '../config/telephony-config.service';
import { CallerResolutionService } from '../caller/caller-resolution.service';
// Ozonetel sends all timestamps in IST — convert to UTC for storage
export function istToUtc(istDateStr: string | null): string | null {
@@ -35,6 +36,7 @@ export class MissedQueueService implements OnModuleInit {
private readonly platform: PlatformGraphqlService,
private readonly ozonetel: OzonetelAgentService,
private readonly telephony: TelephonyConfigService,
private readonly caller: CallerResolutionService,
) {
this.pollIntervalMs = this.config.get<number>('missedQueue.pollIntervalMs', 30000);
}
@@ -90,26 +92,29 @@ export class MissedQueueService implements OnModuleInit {
const callTime = istToUtc(call.callTime) ?? new Date().toISOString();
try {
// Look up lead by phone number — strip +91 prefix for flexible matching
const phoneDigits = phone.replace(/^\+91/, '');
// Resolve caller via the shared service — covers the case
// where there's an existing patient but no lead yet (the
// service creates the lead on the fly and returns the name).
// Same source of truth as the webhook path.
let leadId: string | null = null;
let leadName: string | null = null;
try {
const leadResult = await this.platform.query<any>(
`{ leads(first: 1, filter: {
contactPhone: { primaryPhoneNumber: { like: "%${phoneDigits}" } }
}) { edges { node { id contactName { firstName lastName } patientId } } } }`,
);
const matchedLead = leadResult?.leads?.edges?.[0]?.node;
if (matchedLead) {
leadId = matchedLead.id;
const fn = matchedLead.contactName?.firstName ?? '';
const ln = matchedLead.contactName?.lastName ?? '';
leadName = `${fn} ${ln}`.trim() || null;
this.logger.log(`Matched missed call ${phone} → lead ${leadId} (${leadName})`);
const apiKey = this.config.get<string>('platform.apiKey') ?? '';
const auth = apiKey ? `Bearer ${apiKey}` : '';
const r = await this.caller.resolve(phone, auth);
if (r.isNew) {
// No existing Lead/Patient — write phone as leadName.
// Record creation is deferred to the first agent
// action (enquiry / appointment).
leadName = phone;
} else if (r.leadId) {
leadId = r.leadId;
const fullName = `${r.firstName} ${r.lastName}`.trim();
leadName = fullName || null;
this.logger.log(`Matched missed call ${phone} → lead ${leadId} (${leadName ?? 'no name'})`);
}
} catch (err) {
this.logger.warn(`Lead lookup failed for ${phone}: ${err}`);
this.logger.warn(`Caller resolution failed for ${phone}: ${err}`);
}
const existing = await this.platform.query<any>(