TL;DR
The recent "unified conversation thread" work (PRs #561–#563) is well built and renders cleanly — zero console errors, good empty states, inline recording player. But it shipped four event renderers and, measured across all 5,279 conversations, two render for literally zero conversations (inbound-call, appointment) and a third (blast-reply) is deduped to ~zero by design. The fixes are mostly data-wiring + presentation, not a rebuild.
Method: headless browser with a short-lived SUPERADMIN session minted on prod & invalidated after (verified 0 rows); plus SQL run against prod that reproduces the exact filter logic in lib/leads/*-events.ts. No messages/dispositions/writes.
1 · Event-kind coverage across all 5,279 conversations
| Event kind | Threads | % of inbox | Status |
|---|---|---|---|
| SMS | 5,279 | 100% | ✓ healthy |
| Blast (outbound) | 5,258 | 99.6% | ⚠ in every thread |
| Call — softphone | 789 | 14.9% | ✓ works |
| Call — dialer | 317 | 6.0% | ✓ works |
| Call — inbound | 0 | 0% | ✗ dead — table empty |
| Blast reply (blast-in) | ~0 | ~0% | ⚠ deduped to SMS |
| Appointment | 0 | 0% | ✗ dead — never linked |
| Appointment if linked | 203 | 3.8% | realistic ceiling |
A kind "renders" iff it passes both its per-source fetch filter and the conversation-scoping filter in conversation-events.ts.
2 · It renders cleanly (the build is good)
3 · Finding A — Appointment rows: 0% rendered, wrong label dead + incorrect
2,590 appointments exist; 0 carry a conversation_id, so the filter (conversation-events.ts:94) drops all of them. Root cause: the main booking path book-appointment/route.ts:203 never sets conversationId (only the AI path does, and it's booked ~none).
Label bug: real statuses are completed (2,455) and proposed (135) — but the helper maps updatedAt > createdAt → "Rescheduled". So 2,454 completed appointments would all render as an amber "Rescheduled," which is wrong. There's no handling for completed/no_show.
Reality check: even fully linked, only 203 conversations (3.8%) would ever show an appointment — most appts belong to leads with no inbox conversation. This is a correctness/dead-code issue more than a high-volume feature.
4 · Finding B — Blast bubble is in 99.6% of threads, but it's one bubble polish
Blasts are fetched lead-wide, so the lead's outreach shows in every conversation. My first guess ("wall of blasts") was wrong — the data says blasts/lead is p50=1, p90=2, max=4; only 27 conversations have a lead with >3 blasts, none over 10. It's almost always the single originating outreach at the top — useful context.
The real issue is visual weight: the dashed-indigo bubble + the BLAST · <FULL CAMPAIGN NAME> label repeats the campaign already shown in the header chip and lead panel. Lighten it; keep it.
5 · Finding C — ~3 of 4 call rows are non-connects noise
| Source | Rendered | 0 sec | 1–9 sec | ≥30 sec | % under 10s |
|---|---|---|---|---|---|
| Softphone | 1,874 | 195 | 1,120 | 351 | 74.1% |
| Dialer | 316 | 6 | 148 | 45 | 76.6% |
Each — including 195 literal 0-second softphone calls — gets a full-width bordered card heavier than the actual reply bubble. Collapse sub-threshold attempts into "N call attempts · no answer," reserve the full card (+ player) for connected calls, and surface disposition (often blank).
6 · Findings D–F — dead inbound branch, blast-in dedup, no "load earlier"
- Inbound-call branch is dead —
inbound_callshas 0 rows; the wholesource:"inbound"path never runs. Remove it, or wire the upstream capture. - blast-in deduped to ~0 — 7,449 blast_replies exist, but replies thread as
ConversationMessageand a 2-min body-window dedup suppresses the blast-in version, so they show as plain SMS. Verify no legitimate reply is dropped, then simplify. - Event fetch capped at 100, single-shot, no "load earlier." Low impact today (threads are short) but a latent silent-truncation risk for long-lived leads.
7 · Prioritised recommendations
| # | Action | Effort | Value | Why (data) |
|---|---|---|---|---|
| 1 | Collapse non-connect call rows + show disposition | M | High | 74–77% of call cards are <10s |
| 2 | Lighten the seed blast bubble (label + treatment) | S | Med-High | 99.6% of threads; redundant label |
| 3 | Appointments: link conversationId + fix status map, or remove | M | Med | 0% now, 3.8% ceiling, mislabels |
| 4 | Remove or wire the dead inbound-call branch | S | Med | inbound_calls empty |
| 5 | Verify + simplify blast-in dedup | S | Low-Med | 7,449 replies, ~0 rendered |
| 6 | "Load earlier" for long threads | S | Low | latent truncation at 100 |
| 7 | Emil pass: calls/appts as quiet system markers | M | Med | system cards out-weight messages |
Start with #1 + #2 — highest value-to-effort, strongest data, pure presentation (no schema/migration). #3 is the biggest correctness cleanup.
docs/audits/inbox-unified-timeline-review-2026-06-03.md · docs/screenshots/inbox-prod-review/. Served gated behind the stage login.