The CDR explorer in the TeleQuick console is a paged view over the cdrs table in ClickHouse, collapsed to one row per call. This page explains what the grid shows, what it does not show, and where the numbers come from, so that you can tell a data problem apart from a display artifact. For the underlying CDR schema and how rows get written, see Telemetry.

What one row represents

One row is one call, identified by call_id — not one CDR record and not one call leg. The columns are: The direction marker under Caller → Callee (↓ Inbound / ↑ Outbound) comes from the same direction field the direction filter uses.

Deduplication by call ID

The cdrs table is a plain MergeTree. MergeTree never collapses identical rows on its own, so when cdr_service replays or re-emits a record, the duplicate accumulates in the table. Nothing removes it at write time. The grid therefore deduplicates at query time. Rows are grouped by call_id and collapsed with aggregates:
  • caller, callee, direction, status, agent — argMax(..., starting_time), i.e. the value from the latest-starting record for that call.
  • starting_time, ending_time, duration, cost — max(...).
  • id — any(...), used only as a React row key.
Two consequences worth knowing:
  • The row you see may not correspond to any single stored record. It is a per-field reconciliation across every record carrying that call_id.
  • Filters run on the deduplicated values, not the raw rows. The WHERE clause is applied outside the grouping, so a replayed record cannot pull a call into the window, or push it out, on its own.
A migration of cdrs to ReplacingMergeTree is queued as a follow-up. Until then, query-time collapsing is the only thing making the grid one-row-per-call — a hand-written SELECT * FROM cdrs will show you the duplicates. Every filter is applied server-side, in one predicate. The grid rows, the pager total, and the aggregate figures are all computed from that same predicate, so they cannot disagree about which calls are in scope. Notes on the search box specifically:
  • It is a substring match, not a prefix match and not a fuzzy match. A partial Call ID or a partial number both work.
  • It is case-insensitive on every one of the four fields.
  • It is a single needle. Typing two terms searches for that whole string, including the space, not for either term.
  • Input is trimmed and debounced before it is sent, so the grid settles a moment after you stop typing rather than on each keystroke.
  • Because agent is one of the matched fields, searching an agent name will not surface AI-answered calls — their agent field is empty.
Relative range tokens such as now-7d / now are resolved to epoch milliseconds and rounded to the minute (from down, to up) before the request goes out. That rounding is what keeps the query key stable between renders; the view refetches on its own interval so a relative window stays live without the range shifting on every tick. Changing the org, the range, the direction, the status, or the search term resets you to the first page.

Summary tiles vs the range total

These two readouts are computed over different sets, and that is deliberate:
  • The four summary tiles — Total Calls, Total Duration, Total Cost, Avg Call Length — are computed in the browser from the rows currently loaded, which is at most one page. Total Calls is labelled “loaded window” for exactly this reason.
  • The pager line — “Showing x–y of N in range” — uses N from the server, which counts every deduplicated call matching the predicate across the whole range, not just the loaded page.
So on a range with more matches than fit on one page, the Total Calls tile will read lower than the pager total, and Total Duration and Total Cost describe only the page in front of you. Narrow the range or tighten the filters until the range total fits on one page if you need the tiles to describe the whole selection. Two smaller behaviours in the same footer:
  • With no matches, the footer reads “No records in the selected range” and the pager is hidden entirely — a “1 / 1” control over an empty table is noise.
  • While a fetch is in flight the footer reads “Loading…” and the Previous/Next buttons are disabled. The previous page’s rows stay on screen as placeholder data rather than blanking the grid.

AI and human handlers on the same call

The agent column on cdrs holds a single value, and it is empty for AI-answered calls. That is why the Agent cell renders — on calls an AI agent handled: there is nothing in that field to show. It is not a lookup failure. The real handler information lives in events_raw, which records a row per segment with a handler_kind of 'llm' or 'human', a handler_id, a handler_display_name, and talk_time_sec. The list procedure enriches each page from that table, keyed on call_id (the same id space as cdrs), producing per call:
  • the AI handler id and display name, plus summed AI talk time
  • the human handler id and display name, plus summed human talk time
Within each kind, the id and display name come from the latest-finishing segment (argMaxIf on seg_stop), while talk time is summed across all segments of that kind. A call that was transferred from an AI agent to a person therefore carries both sides, with talk time split between them. A call with no matching events_raw segment simply gets nulls for the split — an absent segment is not an error. This enrichment is deliberately scoped to the call_ids on the current page, which keeps it to one extra round-trip per page rather than a join across the whole range.

Dropping into ClickHouse for queries the grid cannot express

The grid exposes a fixed predicate: range, direction, status, and the four-field search. Anything outside that shape — grouping, joins against events_raw beyond the built-in handler split, arbitrary aggregate windows, or columns the grid does not render — belongs in ClickHouse directly. Query ClickHouse in the header calls admin.launchClickhouse with the currently active org. The procedure builds a launch URL scoped to your user id and that org id, and the console opens it in a new tab with noopener. The hand-off carries the org you are looking at, not an account-wide grant, and it carries your identity rather than a shared credential. The button is disabled while you have no active org selected and while the launch request is in flight. When you write queries there, remember the deduplication rule above: cdrs holds raw, potentially duplicated records. Group by call_id and reduce with argMax(..., starting_time) / max(...) if you want numbers that agree with the grid.
  • Telemetry — the CDR schema and how rows reach ClickHouse
  • Telephony Metrics — definitions for the rates you compute from CDRs