Skip to content

fix: presence badge shows ONLINE when agent is offline#339

Merged
coygg merged 13 commits into
masterfrom
fix/presence-badge-status
Apr 2, 2026
Merged

fix: presence badge shows ONLINE when agent is offline#339
coygg merged 13 commits into
masterfrom
fix/presence-badge-status

Conversation

@coygg
Copy link
Copy Markdown
Collaborator

@coygg coygg commented Apr 2, 2026

Session restore was overriding DB offline status with stale sessionStorage. Now only restores if last heartbeat was within 60s (actual page refresh). Badge also guards against connectionState mismatch.

Summary by CodeRabbit

  • New Features

    • Added "ON CALL" presence badge state
  • Bug Fixes

    • More accurate session restoration on reload
    • Improved voicemail flow reliability and loop prevention
    • Stronger TTL enforcement for unbridged/ghost calls
  • Improvements

    • Refined offline/degraded banner and presence badge display
    • Added voicemail recording watchdogs and better recording error handling

coygg added 12 commits March 26, 2026 08:24
… stripe-retry, rate-limit

- archiveRecordingToStorage: 21 tests covering happy path, extension detection,
  upload failure, signedUrl failure, call_update_failures insert path,
  failure insert error logging, and missing callRecord skip
- health-checks.ts: 23 tests (0% -> 100%) — checkTelnyxCCA, checkTelnyxCredentialConnection,
  checkSupabaseReachability, EXPECTED_WEBHOOK_URL derivation, all error/timeout paths
- stripe-retry.ts: 11 tests (41% -> 100%) — isStripeConnectionError, withStripeRetry
  happy path, connection error passthrough, 429 retry with retry-after cap, status fallback
- rate-limit.ts: 12 tests added (64% -> 93%) — isRateLimited, peekRateLimit,
  getRateLimitRetryAfterSecs, refundRateLimit, clearRateLimit

Total: 975 -> 1030 tests (+55), all passing
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
policyjar Ready Ready Preview, Comment Apr 2, 2026 8:08pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Refactored presence UI/state mapping and tightened session-restore heartbeat logic; centralized voicemail prompting into a shared helper, added unbridged-call TTL enforcement across call flows, and introduced voicemail recording watchdogs and failure dispositions.

Changes

Cohort / File(s) Summary
Header / Presence UI
src/components/layout/header.tsx
Introduced presenceBadgeState mapping status+connectionState to online/offline/degraded/on_call. Replaced variant-driven badge rendering and banner logic with showOfflineBanner/showDegradedBanner. Reformatted handleStatusChange into a useCallback with explicit res.ok check and inner try/catch for server persistence.
Presence Hook
src/hooks/usePresence.ts
Tightened session-restore on reload to only restore when stored status === 'available'. Added HEARTBEAT_RECENCY_MS = 60_000 and switched heartbeat-age gating to that constant. Minor formatting/heartbeat-age refactor.
Webhook Handlers / Voicemail & TTL
src/lib/webhooks/handlers.ts
Centralized voicemail prompting into startVoicemailFlow(...), standardizing call record updates and prompt encoding. Added enforceUnbridgedCallTtl(...) and wired it into routeToACD, handleSpeakEnded, handlePlaybackEnded. Updated recovery to avoid voicemail routing loops and added delayed watchdog (30s) to detect/stall voicemail recording, setting disposition on failures and force-hanging up when needed. Minor TTL/context computation refactors.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Handler as Webhook Handler
    participant DB as Database
    participant CallCtrl as Call Control Service

    rect rgba(100, 150, 200, 0.5)
    Note over Client,CallCtrl: Voicemail flow via startVoicemailFlow
    end

    Client->>Handler: Incoming call / hangup triggers
    activate Handler

    Handler->>DB: updateCallRecord(voicemail: true, disposition: VOICEMAIL_PENDING)
    activate DB
    DB-->>Handler: Updated
    deactivate DB

    Handler->>CallCtrl: speakMessage(with voicemail_mode + voicemail_started_at)
    activate CallCtrl

    rect rgba(150, 100, 100, 0.5)
    Note over Handler: Start 30s watchdog to verify recording started
    end

    alt Prompt delivered
        CallCtrl-->>Handler: Prompt completed
        Handler->>DB: check recording state / disposition
        DB-->>Handler: recording_started/ended/bridged
        alt Recording OK
            Handler->>CallCtrl: continue (or bridge)
        else Recording never started / stuck
            Handler->>CallCtrl: forceHangup
        end
    else Prompt failed or TTL exceeded
        CallCtrl-->>Handler: error
        Handler->>DB: set disposition VOICEMAIL_RECORD_START_FAILED / VOICEMAIL_PROMPT_FAILED
        Handler->>CallCtrl: hangup
    end

    deactivate CallCtrl
    deactivate Handler
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A rabbit hops through presence light,
Badges blink: online, degraded, on_call bright.
Voicemail helpers tidy the flow,
Watchdogs listen, timers go—
I nibble bugs and guard the night. 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the core issue being fixed: a presence badge display bug where the agent showed as ONLINE despite being offline.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/presence-badge-status

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86122613eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/components/layout/header.tsx Outdated
Comment on lines +187 to +188
const showOfflineBanner = status === 'offline'
const showDegradedBanner = !showOfflineBanner && connectionState === 'degraded'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Show offline reconnect banner on heartbeat outages

showOfflineBanner is now tied only to status === 'offline', so when heartbeats fail and connectionState transitions to offline (which usePresence does after repeated failures) while status is still available/busy/on_call, the reconnect banner is suppressed. In that state the user is actually disconnected but loses the explicit warning and recovery CTA, which can leave them stranded until they notice another signal.

Useful? React with 👍 / 👎.

Comment on lines +181 to +185
: status === 'available' && connectionState !== 'offline'
? connectionState === 'degraded'
? 'degraded'
: 'online'
: 'offline'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid labeling busy/away agents as offline

The new badge mapping only treats available as online/degraded, so busy and away now always render as 🔴 OFFLINE even when the connection is healthy. This misreports presence state and conflicts with the status selector (which still shows Busy/Away), making operators look disconnected when they are intentionally in a non-available state.

Useful? React with 👍 / 👎.

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 2, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/layout/header.tsx`:
- Around line 187-188: Update the banner visibility logic in the Header
component so an offline connection always shows an offline/connection-issue
banner: set showOfflineBanner to true when connectionState === 'offline' OR
status === 'offline' (instead of only status === 'offline'), and set
showDegradedBanner to true only when connectionState === 'degraded' and
connectionState !== 'offline' (or use !showOfflineBanner && connectionState ===
'degraded') to avoid conflicting banners; adjust the banner rendering message to
distinguish connectionState 'offline' (connection issue) from status 'offline'
(manual status) and use the existing symbols showOfflineBanner,
showDegradedBanner, connectionState, and status in header.tsx.

In `@src/hooks/usePresence.ts`:
- Around line 605-618: Hoist the HEARTBEAT_RECENCY_MS constant out of the block
and define it at module scope alongside the other presence-related constants for
consistency and discoverability; update the code that currently defines
HEARTBEAT_RECENCY_MS inside usePresence.ts (the block that computes
heartbeatAgeMs and checks parsed?.status, ageMs, and heartbeatAgeMs before
calling updateStatus) to reference the newly exported/top-level
HEARTBEAT_RECENCY_MS constant instead of the inline value, keeping the same
numeric value (60_000) and leaving the restore logic and calls to updateStatus
unchanged.

In `@src/lib/webhooks/handlers.ts`:
- Around line 3008-3026: The watchdog currently aborts active voicemails because
it checks !recording_url (which is only set after upload); update the timeout
logic in the setTimeout block so it only force-hangs up when recording never
started or truly timed out: fetch and check a reliable flag such as
recording_started or recording_started_at (or recording_status) from the calls
row instead of recording_url, or compute the timeout using the voicemail max
length (max_length) plus a small grace period from the call row and only run the
watchdog after that interval; change references in this block
(createAdminClient(), admin.from('calls').select(...),
hangupCall(callControlId)) to use the new check so in-progress recordings aren’t
aborted, and ensure handleRecordingSaved() still writes the
recording_started/recording_started_at field so the watchdog can detect started
recordings.
- Around line 1488-1496: The TTL handler currently returns true even if
hangupCall(callControlId) fails, causing callers to treat the webhook as
handled; update the handler so that on hangupCall failure it re-throws the
caught error (or otherwise propagates it) instead of logging and returning true.
Locate the catch block around hangupCall(callControlId) and replace the current
pattern that logs and returns true with logic that logs the error and then throw
the error (or re-throw the original err) so the webhook caller will retry
instead of acknowledging a failed forced hangup.
- Around line 1512-1541: startVoicemailFlow currently continues even if
updateCallRecordWithRetry failed and leaves the call in VOICEMAIL_PENDING when
speakMessage fails; make the transition atomic by aborting if the pending-state
write fails: if updateCallRecordWithRetry returns falsy or throws, immediately
hang up the call via hangupCall(callControlId) and return false (do not proceed
to encodeClientState/speakMessage). Additionally, if speakMessage fails, persist
a terminal disposition (e.g., updateCallRecordWithRetry with disposition
'VOICEMAIL_FAILED' or similar) before calling hangupCall and returning false so
the DB reflects final state (this ensures handleRecordingSaved can reliably
resolve callRecord.id/tenant_id).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c02cbf3c-b0ce-4bad-8a3d-3ff189caf660

📥 Commits

Reviewing files that changed from the base of the PR and between c7a59d7 and 8612261.

📒 Files selected for processing (3)
  • src/components/layout/header.tsx
  • src/hooks/usePresence.ts
  • src/lib/webhooks/handlers.ts

Comment thread src/components/layout/header.tsx Outdated
Comment thread src/hooks/usePresence.ts
Comment thread src/lib/webhooks/handlers.ts Outdated
Comment thread src/lib/webhooks/handlers.ts Outdated
Comment thread src/lib/webhooks/handlers.ts Outdated
@coygg coygg dismissed coderabbitai[bot]’s stale review April 2, 2026 20:10

Findings addressed or minor - merging.

@coygg coygg merged commit 6f437bc into master Apr 2, 2026
4 of 5 checks passed
coygg added a commit that referenced this pull request Apr 2, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4679a14f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/usePresence.ts
if (
parsed?.status &&
parsed.status !== 'offline' &&
parsed?.status === 'available' &&
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore non-offline manual statuses on page reload

Restricting session restore to parsed?.status === 'available' causes agents who refresh while busy or away to be forced to offline: beforeunload/pagehide still sends the offline beacon, init() then reads currentStatus === 'offline', and this guard now blocks restoring the prior manual status. This is a regression from the previous parsed.status !== 'offline' behavior and will unexpectedly drop busy/away agents offline on every reload.

Useful? React with 👍 / 👎.

@coygg coygg deleted the fix/presence-badge-status branch June 1, 2026 07:04
coygg added a commit that referenced this pull request Jun 1, 2026
* test: full audit coverage — archiveRecordingToStorage, health-checks, stripe-retry, rate-limit

- archiveRecordingToStorage: 21 tests covering happy path, extension detection,
  upload failure, signedUrl failure, call_update_failures insert path,
  failure insert error logging, and missing callRecord skip
- health-checks.ts: 23 tests (0% -> 100%) — checkTelnyxCCA, checkTelnyxCredentialConnection,
  checkSupabaseReachability, EXPECTED_WEBHOOK_URL derivation, all error/timeout paths
- stripe-retry.ts: 11 tests (41% -> 100%) — isStripeConnectionError, withStripeRetry
  happy path, connection error passthrough, 429 retry with retry-after cap, status fallback
- rate-limit.ts: 12 tests added (64% -> 93%) — isRateLimited, peekRateLimit,
  getRateLimitRetryAfterSecs, refundRateLimit, clearRateLimit

Total: 975 -> 1030 tests (+55), all passing

* fix: address CodeRabbit test quality findings on PR #336

* fix: strengthen test assertions per CodeRabbit review on PR #336

* fix: add missing assertions in archive-recording tests

* Harden agent presence heartbeat and surface offline state

* fix: address CodeRabbit findings on PR #337

* fix presence stale-check error handling and codrabbit test findings

* fix: final test assertion hardening per CodeRabbit

* Fix ghost voicemail loops and add hangup safeguards

* fix: tighten presence restore and badge offline guard

* fix: address CodeRabbit presence and voicemail reliability findings
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant