Commit Graph

113 Commits

Author SHA1 Message Date
e03b1e6235 feat: structured JSON output + suggestion rules in AI system prompt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 11:11:13 +05:30
2d18110786 feat: suggestion rules engine + caller context evaluation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 11:09:47 +05:30
a576552f8a feat: pre-fetched caller context replaces tool-based patient lookups
- CallerContextService: fetches lead profile, appointments, call history,
  activities in parallel. Caches in Redis (5 min TTL). Renders as
  human-readable KB section — no UUIDs exposed to the LLM.
- Caller resolution controller: prewarms context cache on resolve
  (fire-and-forget) so the AI stream has a cache hit.
- AI chat stream: injects caller context into system prompt KB instead
  of raw Lead ID. LLM answers patient questions from context, no tool
  calls needed for current caller data.
- Eliminates UUID hallucination: LLM never sees leadId or patientId,
  can't pass wrong ID to wrong tool parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 09:56:18 +05:30
b11f4ea336 feat: log backfill endpoint for desktop log panel
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- LogStreamService: ring buffer (500 entries) + getRecentLogs() method
- SupervisorController: GET /api/supervisor/logs/recent returns buffered
  log entries so the desktop log panel shows history on tab open, not
  just live stream

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 08:51:55 +05:30
96ae867288 feat: server log streaming via SSE for desktop log panel
- LogStreamService: singleton that extends ConsoleLogger, captures all
  NestJS log output into an RxJS Subject while preserving stdout
- main.ts: uses LogStreamService.instance as app logger
- supervisor.controller.ts: new @Sse('logs/stream') endpoint pipes
  log entries (timestamp, level, context, message) to connected clients

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 08:22:11 +05:30
9a016a2ed0 feat: real-time active call SSE — hold/unhold status for supervisor live monitor
- SupervisorService: added activeCallSubject (RxJS Subject), emits on all
  activeCalls Map mutations (Answered, Calling, Disconnect, Hold, Unhold)
- SupervisorController: new @Sse('active-calls/stream') endpoint
- OzonetelAgentController: callControl HOLD/UNHOLD updates activeCalls Map
  status via supervisor.updateCallStatus()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 05:45:14 +05:30
9cf0f69dde feat: SSE push for worklist updates — instant missed-call notifications
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
New worklist SSE stream replaces the 30s frontend poll. When the
missed-call webhook creates a Call record, it emits a worklist-updated
event via the supervisor's worklistSubject. All connected agents
receive the event immediately.

- supervisor.service.ts: worklistSubject + emitWorklistUpdate()
- supervisor.controller.ts: @Sse('worklist/stream') broadcast endpoint
- missed-call-webhook.controller.ts: emits after createCall() with
  callerPhone + callerName for toast notification
- worklist.module.ts: imports SupervisorModule (forwardRef)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.10-apr-16-disposition-sla-sse
2026-04-16 18:32:57 +05:30
a6f4c51ca9 fix: disposition for answered inbound calls + SLA timing wiring + backfill
Three related fixes:

1. Disposition for answered inbound calls
   Previously the dispose endpoint sent the agent's choice to Ozonetel
   but never wrote it back to the platform Call record. The webhook's
   pre-disposition value ("General Enquiry" → INFO_PROVIDED) persisted.
   Now: dispose endpoint finds the Call by UCID and updates disposition
   to the agent's actual selection.

2. SLA timing wiring (assignedAt / answeredAt / responseTimeS)
   patchCallTiming() existed but was never called. Now wired into
   handleCallEvent:
   - "Calling" event → writes assignedAt (ring start)
   - "Answered" event → writes answeredAt + computes responseTimeS
     (answeredAt - startedAt = caller wait time)
   Uses patchCallTimingByUcid helper that looks up Call by UCID.

3. Backfill maint endpoint: POST /api/maint/backfill-call-disposition-timing
   Walks calls for a given date, joins to CDR by UCID (both legs),
   patches disposition (from CDR's mapped value, always overwrites),
   timing fields (answeredAt, assignedAt, responseTimeS from CDR),
   and CDR-specific durations (handlingTimeS, acwDurationS, holdDurationS).
   Idempotent — safe to run multiple times.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:02:49 +05:30
2d8308bed8 fix: remove hardcoded Inbound_918041763265 campaign fallback
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
The default campaign name was hardcoded to 'Inbound_918041763265'.
After the Ozonetel campaigns were renamed (Inbound_918041763265 →
Global, Inbound_918041763400 → Ramaiah), agent login/dial would
break because the old name doesn't exist on Ozonetel anymore.

Campaign name now comes exclusively from the Agent entity's
campaignName field (per-agent) or the OZONETEL_CAMPAIGN_NAME env
var (per-workspace). No hardcoded fallback.
2026-04-16 17:33:35 +05:30
2666a10f48 fix: await Ozonetel logout + per-agent sipPassword + campaign name on missed calls
Three changes:

1. Await Ozonetel logout in /auth/logout — prevents race condition when
   agent re-logs in quickly via "Remember me". The fire-and-forget
   logoutAgent() left a window where the next loginAgent() arrived
   while Ozonetel was still processing the previous logout, leaving
   the agent stuck in "Telephony Unavailable". (#559)

2. Use agentConfig.sipPassword (from Agent entity) instead of
   OZONETEL_AGENT_PASSWORD env var for login/logout/force-ready.
   The env var was a single shared credential that ignored per-agent
   passwords. Removed hardcoded "Test123$" fallback. Force-ready
   now looks up the Agent entity by ozonetelAgentId to get the
   correct sipPassword + sipExtension.

3. Missed-calls worklist query now fetches campaign { id campaignName }
   so the frontend Branch column can show the campaign name instead
   of the raw DID phone number.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:54:08 +05:30
a00668c517 feat(ai): UUID-safe agent tools + lookup_lead_activities + tool logging
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Bug 553 (partial) — AI Panel 'Patient History' returned 'not in system'
even though the caller had 7 calls + an appointment. The model was
hallucinating instead of chaining lookup tools.

UUID safety: LLMs drop hyphens / swap chars on 36-char ids once the
context wears thin. To keep the model off the UUID path for 'this
caller' questions:
 - lookup_appointments, lookup_call_history, lookup_lead_activities
   now accept their id arguments OPTIONALLY
 - when omitted, the sidecar resolves leadId from ctx and patientId
   from the lead record (cached per-request)
 - new lookup_lead_activities tool rounds out the patient-history
   trio (call history + activity log + appointments)

System prompt (ccAgentHelper) tightened:
 - chain call history + activities + appointments for history questions
 - call lookup tools with NO arguments when using the current caller
 - don't re-type UUIDs seen in CURRENT CONTEXT
 - say 'feature not set up yet' when KB section is empty (packages,
   etc.) instead of 'I couldn't find that'

All agent tools now emit structured [AI-TOOL] trace lines with full
UUIDs printed — tail sidecar logs to see which tool the model chose,
whether the model passed an id or used the context fallback, and how
many records came back. If the model ever hallucinates a UUID, the
resolved= field on the log line will echo it and count=0 will flag
the miss immediately.
2026-04-16 05:39:08 +05:30
a1413aae40 fix(supervisor): sweep stale activeCalls before returning to Live Monitor
Bug 560: Live Call Monitor showed ghost calls with runaway timers when
the agent wasn't on a call. Cause — activeCalls Map only added on
'Answered' and deleted on 'Disconnect'; a missed Disconnect (sidecar
restart, Ozonetel subscription hiccup, network blip) left the entry
lingering forever.

getActiveCalls() now sweeps stale entries before returning:
 - drop if startTime is older than 30 minutes
 - drop if the mapped agent is currently ready / offline / paused
   (agent can't be on a call in any of those states)

Each sweep logs the reason so we can track how often this fires.
2026-04-16 05:38:52 +05:30
6adb3985cb feat(config): ui-flags endpoint driven by HELIX_SETUP_MANAGED
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Per-tenant flag that hides self-serve setup surfaces when the product team owns onboarding for a workspace. Set HELIX_SETUP_MANAGED=true on the sidecar env for that tenant; the frontend reads this endpoint at boot, hides the Settings nav + Setup banner, and blocks /settings/* routes. Setup-state APIs stay live so ops can still drive the wizard remotely.
2026-04-15 18:55:25 +05:30
67c41f4783 feat(maint): session-status endpoint for agent picker
Unlock Agent / Force Ready shortcuts used to read the target agentId from localStorage helix_agent_config — supervisors don't have that set and got 400 'agentId required'.

- SessionService.listLockedSessions() — SCAN over agent:session:*
- POST /api/maint/session-status returns { locked, free } by joining
  the platform Agent entities against Redis session locks
- orphan locks (Redis key with no matching Agent record) surface in
  the Locked bucket so the operator can still clear stale lock state
2026-04-15 18:55:18 +05:30
d459d6469a fix(worklist): include patientId in assigned-leads query
Scenario: unknown caller books appointment (creates Patient), calls again, caller resolver links Lead↔Patient. On the second call the frontend found the lead in the worklist cache but it lacked patientId — so Book Appt pills couldn't find the prior appointment. The resolver had the right patientId; the worklist didn't.

Adding patientId to the GraphQL selection so the cached row carries it end-to-end.
2026-04-15 18:55:08 +05:30
60d2329dd8 fix(call-attribution): resolve Ozonetel chain AgentNames to agent.id
Inbound transferred calls arrive with AgentName like 'RamaiahAdmin -> GlobalHealthX'. The webhook was persisting the raw chain string and leaving agentId null; the CDR enrichment cron then silently skipped 100% of rows because the bulk CDR keys on caller-leg UCID while the webhook stores monitorUCID — the join never matched.

- missed-call-webhook: split chain on ' -> ', take final handler,
  resolve via AgentLookupService (ozonetelAgentId + display name)
- cdr-enrichment: index CDR rows by both UCID and monitorUCID so
  the cron actually patches historical rows
- enrichment also parses chain in CDR AgentName as a second fallback
- spec: add CallerResolutionService + AgentLookupService mocks
2026-04-15 18:55:00 +05:30
f375e7736c fix(my-performance): LOGIN TIME uses AgentSession rollup, not Ozonetel summary
Ozonetel's summaryReport only tallies CLOSED login→logout pairs — an
agent who's still logged in reports 00:00:00, so the KPI card on My
Performance always showed 0s for the current session.

Our AgentSession rollup already caps open sessions at "now" when it
runs. Endpoint now:
  1. Triggers an on-demand rollupSessions(targetDate) to refresh the
     AgentSession row (no 15-min wait after login)
  2. Reads AgentSession and renders in the HH:MM:SS shape the frontend
     expects
  3. Falls back to Ozonetel's summaryReport when AgentSession is
     empty (brand-new agent, workspace missing AgentEvent entity)

Works transparently — same timeUtilization shape as before, frontend
unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:25:23 +05:30
96977e84a1 feat(maint): backfill-appointment-clinics endpoint
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Populates clinicId on historical Appointments that predate clinicId
persistence. Infers from DoctorVisitSlot records:

  1. Pull all appointments with clinicId=NULL
  2. Pre-load visit slots per unique doctorId (batched)
  3. For each appointment: match slots by weekday + time-window
  4. Single clinic → use it
  5. Multiple candidates → pick lex-order first (deterministic; logged)
  6. Last resort → any slot's clinic

No CDR or rate-limited calls — pure platform queries. Safe to re-run;
idempotent (only patches rows still missing clinicId).

POST /api/maint/backfill-appointment-clinics
Header: x-maint-otp: <OTP>

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:01:08 +05:30
00303df95b fix(slots): hide past slots today even on cache hit
Previous flow cached the unfiltered slot list AND applied the "hide
past slots" filter — but only on the fresh-fetch path. A cache hit
returned the stored list untouched, so by lunchtime agents saw morning
slots that had already passed.

Refactored into a post-cache filterPastSlotsForToday() helper applied
on both cache-hit and fresh paths. Cache stores the full day's slots
(keyed by doctorId + dayOfWeek), so same-weekday reuse across weeks
stays correct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:38:23 +05:30
34e053204f feat(leads): sidecar polling service for auto-assigning unassigned leads
Replaces the untrusted platform function path (SDK's lead-auto-assign
was written but never deployed to either workspace — all leads created
after seeding are orphan).

Polls every 60s:
  1. Fetch up to 100 unassigned leads (assignedAgent empty or null)
  2. Fetch platform Agents whose live SupervisorService state is
     ready/calling/in-call/acw (skip offline/break/training/unknown)
  3. Build open-lead count per agent (single paginated query)
  4. Assign each unassigned lead to the least-loaded active agent —
     writes agent.name into lead.assignedAgent to match the worklist
     filter (assignedAgent: { eq: agentName })

Catches every lead-creation path: CSV import, enquiry form,
missed-call webhook, widget, livekit. No platform changes needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:23:53 +05:30
98f5bc0347 fix(ai-chat): use correct Clinic schema in knowledge-base query
The Clinic entity has never had weekdayHours / saturdayHours /
sundayHours. Schema uses 7 booleans (openMonday..openSunday) + a single
opensAt/closesAt pair, and requiredDocuments is a RELATION
(ClinicRequiredDocumentConnection), not a scalar TEXT.

Query was failing silently since 2026-03-18 — AI chat knowledge base
was missing clinic info for a month.

Fix:
- Query the real fields: openMonday..openSunday, opensAt, closesAt
- Render "Open: Mon, Tue, ... HH:MM–HH:MM" + "Closed: Sat, Sun"
- Walk requiredDocuments.edges for documentType list instead of treating
  it as a string

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:31:00 +05:30
048545317d fix: set platform name on every entity create — patients/appts/calls/etc no longer "Untitled"
Audited all 23 sidecar create-mutation call sites; 7 were missing the
top-level data.name field that the platform uses as record title:

- caller-resolution.service.ts createPatient — full name from first/last
- maint.controller.ts createPatient (backfill-lead-patient-links) — same
- widget.service.ts createPatient (chat path + booking path) — full name
- widget.service.ts createAppointment — "<Patient> — <date>"
- worklist/missed-queue.service.ts createCall — "Missed — <phone>"
- rules-engine/actions/escalate.action.ts createPerformanceAlert —
  "<agent>: <message> (<value>)"
- supervisor/agent-history.service.ts createAgentEvent / createAgentSession

Cosmetic only — the app fetches fullName/agentName for display, so
end users never saw "Untitled". Fixes platform-side admin browsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:32:28 +05:30
8dcfa5a72f feat(performance-alerts): rules-engine-driven alerts, persisted as PerformanceAlert
Phase A+B of the alerts overhaul:

- New PerformanceFactsProvider exposes agent.idleMinutes (from
  AgentSession), agent.busyMinutes, agent.totalCallsToday,
  agent.bookedCallsToday, agent.conversionPercent
- Implement EscalateActionHandler (was a stub): persists a
  PerformanceAlert row, dedupes per agent+type+IST date so a 5-min
  cron can't spam, updates value if it changes
- New PerformanceConsumer: setInterval every 5 min, reads on_schedule
  rules referencing agent.* facts, evaluates per agent, dispatches
  escalate actions
- Two starter rules in hospital-starter.json: excessive-idle (>60min)
  and low-conversion (<15% with >10 calls today). NPS deferred — no
  source signal exists yet
- New PerformanceAlertsController: GET /api/supervisor/performance-alerts
  (active list), POST /:id/dismiss, POST /dismiss-all
- Rules engine now injects EscalateActionHandler via DI so the action
  has access to PlatformGraphqlService for persistence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:02:02 +05:30
5b40f49b65 feat(agent-lookup): resolve by Ozonetel display name too
Inbound webhook rows store agentName as Ozonetel's display string
("Ganesh Bandi", "GlobalHealthX") which doesn't match either
ozonetelAgentId or platform Agent.name. Add ozonetelDisplayName as a
third index — populated on each platform Agent with the Full Name from
the Ozonetel admin UI.

After this + setting display names on Global + Ramaiah agents:
backfill matched 122/136 (~90%) on Global; remaining are calls handled
by agents that don't exist on this workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:25:14 +05:30
fb616d47ee feat(maint): backfill-call-agents-by-name for historical Calls
Historical Calls pre-date UCID persistence, so the CDR-join enrichment
can't reach them. Fallback: parse agentName (may be "A -> B -> C"
transfer chain), take the final hop, resolve to Agent by ozonetelAgentId
(case-insensitive) or by Full Name. Preserve the full chain string in
transferredTo when it was actually chained.

No rate limit — pure platform queries, no CDR.

POST /api/maint/backfill-call-agents-by-name
Header: x-maint-otp: <OTP>

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:58:06 +05:30
6fd17acf78 fix(cdr-enrichment): 35s sleep between date fetches — Ozonetel caps at 2/min
The first boot hit 429 because two dates (today + yesterday) were
fetched back-to-back, and the dispose flow's fetchCdrByUCID shares the
same 2-req/min budget. 35s between dates keeps us clear of the cap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:49:14 +05:30
846c5f4c9b feat(calls): consolidate agent identity via Ozonetel CDR
Ozonetel's webhook AgentName is a transfer-chain display string — same
display can collide (two agents both named "GlobalHealthX" with distinct
agent IDs), and chained like "RamaiahAdmin -> Ganesh Bandi -> GlobalHealthX".
Team Performance was bucketing every unique raw string as a separate
"agent", producing 7 rows for 3 real agents.

Fix — authoritative agent link via CDR AgentID (unique):

- New AgentLookupService (platform module): case-insensitive
  ozonetelAgentId → Agent UUID cache, shared across webhook / dispose /
  enrichment / backfill paths
- Webhook + outbound-dispose now persist UCID on Call so CDR can join
- Outbound dispose resolves agent relation at create time and overwrites
  from CDR AgentID post-hoc (catches dial transfers)
- New CdrEnrichmentService: every 30 min fetches today + yesterday CDR,
  patches Calls missing agentId / transferredTo / transferType by UCID
  join. Well under Ozonetel's 2 req/min cap.
- Historical backfill maint endpoint: /api/maint/enrich-call-agents
  with configurable day window (default 2, max 15). Rate-limited at 35s
  between dates.

Call schema additions (synced on Global + Ramaiah): agent relation,
ucid, transferredTo, transferType. agentName remains for legacy/display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:43:28 +05:30
9472f83cd8 feat(supervisor): team-performance reads AgentSession first, Ozonetel as fallback
Phase 3 — wire the dashboard to the new metrics path without touching
the frontend. getTeamPerformance now:

1. Fetches AgentSession rows for the given IST date (keyed by agent UUID)
2. For each agent: uses AgentSession data rendered as HH:MM:SS if a row
   exists, otherwise falls back to Ozonetel summaryReport
3. Returns timeBreakdownSource so the frontend can optionally show which
   source was used

Frontend continues to parse the existing HH:MM:SS shape via parseTime()
— no UI change needed. Historical dates without AgentSession rows still
render via Ozonetel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:14:16 +05:30
6de1989536 feat(maint): backfill-agent-event-durations endpoint
Recomputes durationS on existing AgentEvent rows using the fixed
per-category pairing logic and re-runs the session rollup for every
affected date. Fixes the 0-second CALL_END durations written before the
slot-split fix.

Idempotent — only patches rows whose stored durationS differs from the
newly computed value. Safe to re-run.

POST /api/maint/backfill-agent-event-durations
Headers: x-maint-otp: <OTP>
Body:    { "date": "YYYY-MM-DD" }  (optional; defaults to today IST; use "all" to backfill every row)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:05:54 +05:30
2acba59963 fix(supervisor): separate pending slots per event category to pair CALL/ACW correctly
CALL and ACW overlap: an agent enters ACW before the CALL_END webhook
arrives. With a single shared pending slot, ACW_START would clobber the
pending CALL_START and CALL_END would compute 0-second duration against
the ACW_START timestamp. Verified in production data — 4/4 CALL_END rows
on Global had durationS=0.

Fix: one slot per category (pause/call/acw). Each END reads and clears
its own slot. READY and LOGOUT defensively flush all slots to avoid
leaking state across sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:53:23 +05:30
4eb8cb80b2 feat(supervisor): Phase 2 metrics ingest — AgentEvent/AgentSession rollup
- New AgentHistoryService: persistAgentEvent pairs START/END for durationS, patchCallTiming updates Call SLA fields
- Supervisor service wires handleCallEvent (CALL_START on Answered, CALL_END on Disconnect) and handleAgentEvent (LOGIN/LOGOUT/PAUSE/RESUME/ACW_START/ACW_END/READY) via priorState-aware mapping
- setInterval-based nightly-ish rollup: every 15min aggregates AgentEvent into AgentSession per IST day (idempotent upsert by agentId+date)
- Ozonetel dispose flow extracts HandlingTime/WrapupDuration/HoldDuration from CDR, patches Call timing fields
- Field names match platform truncation: durationS, loginDurationS, busyTimeS, idleTimeS, pauseTimeS, wrapupTimeS, avgHandlingTimeS, handlingTimeS, acwDurationS, holdDurationS, responseTimeS, sessionDate → date
- Skips cleanly on workspaces where AgentEvent entity isn't synced

Known issue: pending-pair map has single slot per agent, so ACW_START overwrites pending CALL_START and CALL_END computes 0s duration. Fix in followup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:49:15 +05:30
fbe782b5ac 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>
2026-04-15 06:49:02 +05:30
b6b597fdda fix: clinicId on all appointment paths + startedAt on call records
- AI chat book_appointment tool: accepts optional clinicId
- Widget booking: passes clinicId from request
- LiveKit agent: passes clinicId from doctor context if available
- Dispose endpoint: sets startedAt/endedAt on outbound call records
  (computed from durationSec). Fixes null timestamps in call history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:52:30 +05:30
a4ff052fef fix: stop auto-creating Unknown leads on caller resolve
Return empty IDs for unrecognized numbers instead of creating lead+patient.
Per PRD: 'System will not identify the patient — no summary shown.'
Records are created when agent books appointment or logs enquiry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:23:22 +05:30
5969441868 fix: map Ozonetel 'pause' webhook action to break state
Ozonetel sends action: 'pause' via webhook when agent is paused, but
mapOzonetelAction only handled 'AUX'. The 'pause' action fell through
to default (null), so the break SSE event was never emitted. The agent
UI stayed on 'Ready' while Ozonetel had the agent PAUSED.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:52:52 +05:30
01348123e6 fix: map HelixEngage Supervisor platform role to admin app role
Supervisor users were getting 'executive' role because only 'HelixEngage
Manager' was mapped to admin. This broke admin route access after the
RequireAdmin guard was added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 06:47:01 +05:30
d97d73dd1a fix: wrap raw base64 public key with PEM headers for Node crypto
Ozonetel returns raw base64 public key without PEM headers. Node's
crypto.publicEncrypt requires PEM format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:09:00 +05:30
7b178f9dc7 fix: remove ConfigThemeModule import — it's @Global, no import needed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:59:36 +05:30
3d790e51dc fix: circular dependency — forwardRef ConfigThemeModule in SupervisorModule
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:03:47 +05:30
1c3e42ad7c fix: non-null assertion on cachedToken return
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:50:10 +05:30
ea60787da0 feat(sidecar): supervisor barge endpoints — initiate, mode switch, end
Endpoints:
- GET  /api/supervisor/barge/sip-credentials — fetch SIP number from pool
- POST /api/supervisor/barge — initiate barge via Ozonetel apiId 63
- POST /api/supervisor/barge/mode — update mode (listen/whisper/barge)
- POST /api/supervisor/barge/end — cleanup session + Redis

SupervisorService extended with barge session tracking (in-memory Map).
Mode changes emit SSE events to agent: supervisor-whisper, supervisor-barge,
supervisor-left. Listen mode is silent (no event to agent).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:06:57 +05:30
c23792496b feat(sidecar): Ozonetel admin auth service — RSA login, JWT cache
- Node crypto RSA encryption (not jsencrypt — server-side)
- Pre-login public key fetch, encrypted login, JWT caching
- Auto-refresh before token expiry (decoded from JWT payload)
- Auth headers: Bearer token + userId + userName + isSuperAdmin
- Registered in SupervisorModule with ConfigThemeModule import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:05:24 +05:30
27a3fbcfed feat(config): add Ozonetel admin credentials to TelephonyConfig
- adminUsername + adminPassword in ozonetel section
- Masked in GET response, sentinel-stripped on update
- Env seeds: OZONETEL_ADMIN_USERNAME, OZONETEL_ADMIN_PASSWORD
- Used by supervisor barge/whisper/listen endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:03:51 +05:30
0f5bd7d61a ci: fix Teams notification — use Adaptive Card with curl
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-11 15:37:20 +05:30
f1313f0e2f ci: use Teams notification plugin
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-11 15:34:30 +05:30
44f1ec36e1 ci: add Woodpecker pipeline — unit tests + Teams notification
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-11 15:29:49 +05:30
4bd08a9b02 fix: remove defaultAgentId fallback — require agentId from caller
agent-state, dispose, dial, performance, force-ready, unlock-agent
all required agentId from the request body now. No silent fallback
to OZONETEL_AGENT_ID env var which caused cross-tenant operations
in multi-agent setups (Ramaiah operations hitting Global's agent).

OZONETEL_AGENT_ID removed from telephony env seed list. Hardcoded
fallbacks (agent3, Test123$, 521814) deleted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:10:31 +05:30
0248c4cad1 fix: #536 #538 performance metrics — filter CDR by agentId, add team call counts
#536: Performance endpoint now accepts agentId query param and filters
CDR to that agent only. Previously returned all agents' calls as one
agent's total. Fixed 'Unanswered' → 'NotAnswered' status filter.

#538: Team performance now includes per-agent call metrics (total,
inbound, outbound, answered, missed) from CDR data + teamTotals
aggregate. Previously only returned Ozonetel time breakdown without
any call counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:33:59 +05:30
be505b8d1f fix: #540 ignore call events for offline agents in live monitor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:27:09 +05:30
dbefa9675a feat: master data endpoint — cached departments, doctors, clinics
Redis-cached (5min TTL) lookups via /api/masterdata/departments,
/api/masterdata/doctors, /api/masterdata/clinics. Warms cache on
startup. Frontend dropdowns use these instead of hardcoded lists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:31:01 +05:30