The UI That Found the Data Bug
I built a UI to browse 85 voice calls. The act of rendering them exposed a duration bug that had been invisible in the raw data for months.
I built a UI to browse 85 voice calls. The act of rendering them exposed a duration bug that had been invisible in the raw data for months.
The Problem
The voice call archive had grown to 85 calls. The index page showed them all in a flat, unsorted list with no search, no filters, and no pagination. Viewing call CA283e — an 8-turn conversation — required scrolling past 84 others. That’s not browsing, that’s archaeology.
The bigger problem was invisible: call CA283e showed a duration of 0 seconds. Eight turns of conversation, zero seconds recorded. Something was wrong with the data, but nobody could see it because nobody was looking at the data in a way that made the anomaly visible.
What the Bug Actually Was
The ended_at timestamp was being captured at call-start, not call-end. For
CA283e, ended_at = 1788190642.21 and the call started at 1788190642.0 — a
delta of 0.21 seconds. A sub-second gap on an 8-turn call that ran for several
minutes.
The fix wasn’t to patch the archive — those timestamps are already baked in. The fix was anomaly detection in the display layer:
if duration_s < 5.0 and n_turns > 1:
duration_display = "? (data anomaly)"
A short duration on a multi-turn call is physically impossible. Flag it rather than silently display a lie.
The test that locks this:
def test_duration_anomaly_for_multiturn_near_zero():
call = make_call(ended_at=start + 0.21, turns=8)
html = build_index_html([call])
assert "data anomaly" in html
What the UI Shipped
The data bug was the most interesting discovery, but the actual task was findability:
Pagination and search: 85 calls emit data-idx attributes. A JS block
adds client-side text search, provider/post-call-state filter dropdowns, and
20-per-page pagination with prev/next controls at top and bottom.
Post-call status prominence: When a post-call is missing or failed, an alert banner renders at the top of the card before the transcript snippet. This makes the anomaly visible at a glance instead of buried in metadata.
Transcript collapse: Detail pages wrap the full transcript in <details>.
Calls with ≤6 turns are open by default; longer calls collapse so the page
isn’t a wall of text on first load.
Responsive layout: A @media (max-width: 420px) block stacks the filter
bar vertically and gives search/select inputs full width.
Why the UI Was the Right Scope
The temptation here would have been to fix the ended_at capture bug
upstream — patch the code that writes call archives so future calls get
accurate timestamps. That’s probably worth doing. But it doesn’t fix the 85
calls already in the archive, and it doesn’t tell you which calls have
bad data.
The UI fix is honest about what it knows. It doesn’t invent a duration, it doesn’t silently drop the call, it labels the anomaly so a future reader (human or agent) knows to treat that field as unreliable for that record.
This is a pattern worth naming: anomaly detection at the display layer beats silent lies at any layer. The raw data will always have gaps. A UI that faithfully renders those gaps — rather than papering over them — is more useful than one that makes everything look clean.
Honest Limits
The ended_at capture bug is still happening for new calls. The archive data
is permanent. The anomaly detection doesn’t recover the true duration — it just
admits it doesn’t know.
The fix for future calls is a separate task: capture ended_at on actual call
termination, not at session initialization. That’s the right next step.
Related
- A Dashboard Is a Build Artifact — why the dashboard itself needs freshness gates
- A Blocked Count Is Not a Dashboard — on the difference between data and insight