# Voice console: tenant dashboard

> How the tenant dashboard scopes and sources its numbers: inventory counts, today's call KPIs, de-duplicated CDR rows, entitlements, supervisor requests, and the analytics-unreachable banner.

The dashboard is the landing screen for an org in the TeleQuick console.
It mixes two unrelated data sources on one page: **inventory counts** that
come straight from the project database, and **call KPIs** that come from the
analytics store through the console's tRPC layer. Knowing which half a number
belongs to explains most "why does this say zero" questions.

Everything on the page is scoped to the currently selected org. The header
prints the org name and the raw tenant id it is querying with, so you can
confirm which tenant you are looking at before you read the numbers.

## What's on the dashboard

Top to bottom:

| Block | Contents |
| ----- | -------- |
| Header | Org name, tenant id, and a static "Live" indicator. |
| Plan / entitlement card | Tier, limits, and pool burn-down for this org. |
| Supervisor requests + manual supervise | The queue of calls an agent flagged for a human, plus a manual entry point. |
| Active calls | The list of calls currently in progress. |
| Resource KPIs | Trunks, Agents, Service Accounts, Team Members. |
| Call KPIs | Calls Today, Total Duration, Avg Call Length, Cost Today. |
| Recent Calls | The most recent calls for the tenant, one row per call. |

The pulsing "Live" dot in the header is a static presentation element. It is
not bound to a connection or health signal, so it does not go dark when the
analytics store is unreachable — watch for the warning banner instead.

## Where each number comes from

The four **resource KPIs** are exact row counts read directly from the project
database by the browser. Each is a count-only query filtered on the active
`org_id`, and row-level security additionally scopes the tables by your org
membership:

| Card | Table counted |
| ---- | ------------- |
| Trunks | `trunk` |
| Agents | `agent_config` |
| Service Accounts | `serviceaccount` |
| Team Members | `org_member` |

The four **call KPIs** come from a single tRPC call, `cdr.statsToday`, which
queries the `cdrs` table in the analytics store:

| Card | Field returned by `cdr.statsToday` |
| ---- | ---------------------------------- |
| Calls Today | `total_calls` |
| Total Duration | `total_duration` (seconds, formatted as `Xm SSs`) |
| Avg Call Length | `avg_duration` (seconds, formatted as `Xm SSs`) |
| Cost Today | `total_cost` (formatted with two decimals) |

**Recent Calls** comes from a second procedure, `cdr.recent`, over the same
table.

Because the two halves are independent, an analytics outage leaves the
inventory counts correct, and an unprovisioned tenant with no trunks can still
show call KPIs. While a query is in flight, its cards render an em dash
placeholder rather than a zero. Note that the resource cards and the call cards
share one loading flag, so both sets show placeholders until *both* the
database counts and `cdr.statsToday` have returned.

Neither CDR query polls. They run when the dashboard mounts and re-run when you
switch the active org.

## How call KPIs are de-duplicated and scoped to today

A call can produce more than one row in `cdrs` — the same `call_id` may be
re-inserted as the call progresses or as the pipeline retries. Aggregating the
raw rows would inflate `total_calls` and over-count `sum(duration)` and
`sum(cost)` by however many copies exist.

`cdr.statsToday` therefore aggregates in two stages:

1. An inner query selects from `cdrs` where `tenant_id` matches the org and
   `starting_time >= today()`, groups by `call_id`, and collapses each group to
   `max(duration)` and `max(cost)`.
2. The outer query aggregates that de-duplicated set: `count()` for
   `total_calls`, `sum(duration)`, `sum(cost)`, and `avg(duration)`.

Two consequences worth internalising:

- **`total_calls` counts distinct `call_id` values, not CDR rows.** If you
  compare against a raw row count in the analytics store, the dashboard will
  read lower. That is correct behaviour.
- **`avg_duration` averages over every de-duplicated call in the window**,
  with no filter on status or direction. Calls that never connected and carry a
  zero duration are included and pull the average down. Avg Call Length renders
  as an em dash when the computed value is zero.

"Today" is the analytics store's own `today()` function, evaluated server-side
when the query runs. The boundary is therefore the analytics server's date, not
the browser's clock or the viewer's timezone. A user in a timezone far from the
server's can see the counters reset at what looks like an odd local hour, and
two users in different timezones will see the same numbers at the same moment.

The org id is passed through a sanitiser before it is interpolated into the
query, and `cdr.statsToday` is an org-scoped procedure, so the caller must be
authorised for the org whose `orgId` it passes.

## Where the cost figure comes from

Cost Today is not computed by the console. Each CDR row carries a `cost` field;
the de-duplication step takes `max(cost)` per `call_id`, and the card shows the
sum across today's de-duplicated calls. Whatever rating the CDR pipeline wrote
onto the row is what you see.

The card formats the value with a `$` prefix and two decimal places. That prefix
is hardcoded in the dashboard's formatter — it does not read a currency field
off the CDR — so treat it as a display convention rather than a statement about
the billing currency.

## Recent Calls: one row per call

`cdr.recent` takes a `limit` between 1 and 20 (default 5); the dashboard asks
for 5. Like the KPI query it groups `cdrs` by `call_id`, so each call appears
exactly once even when several rows were written for it. Within a group:

| Column | How it is resolved across duplicate rows |
| ------ | ---------------------------------------- |
| Call ID | the group's `call_id` (truncated to 16 characters in the table) |
| From / To | `caller` / `callee` from the row with the latest `starting_time` |
| Direction | from the row with the latest `starting_time` |
| Duration | `max(duration)` across the group |
| Status | from the row with the latest `starting_time` |
| Started | `max(starting_time)` |

Results are ordered by start time, newest first.

Unlike the KPI cards, **this query has no date filter**. It returns the tenant's
most recent calls whatever day they happened on. So an org with no calls today
will show `0` under Calls Today while Recent Calls still lists yesterday's
traffic. The row count in the panel header ("N shown") is the number of rows
returned, capped by the requested limit — it is not a total.

Status is rendered with a coloured dot: green for `completed`, red for
`failed`, and neutral grey for any other value the CDR carries. Durations under
a minute display as seconds; zero, missing, or negative durations display as an
em dash. The "Started" column is formatted with the browser's local time, so it
will not necessarily agree with the server-side day boundary used by the KPI
cards.

When the table is empty and there is no error, the panel prompts you to place a
call through your trunk. When the CDR stats query failed, the same empty state
reads "Call metrics unavailable" instead.

## Entitlements and plan limits

The entitlement card sits directly under the header and summarises the org's
tier, its limits, and pool burn-down against those limits. It is rendered only
when an org is selected, and it takes the org id as its only input.

The card **hides itself** in two situations:

- the entitlement service is not configured (typical in a local dev
  environment), or
- the tenant has not been provisioned yet.

So a missing plan card on the dashboard is a configuration or provisioning
signal, not evidence that the org has no plan.

## Supervisor requests and live call supervision

Three supervision surfaces are embedded on the dashboard so that they are
discoverable without navigating away:

- **Supervisor requests** — the queue of calls that an agent has flagged for
  human attention. Agents raise these through the `request_supervisor` LLM tool
  during a call. The panel polls for new requests every 5 seconds and is empty
  most of the time; an entry appearing here means an agent asked for a human
  now.
- **Manual supervise** — an entry point for attaching to a call yourself
  without waiting for an agent to raise a request.
- **Active calls** — the list of calls currently in progress for the tenant.
  It exists on this screen as a discoverability surface for supervision: you
  find the live call here, then supervise it.

All three render only once an org is selected. Their data is separate from the
CDR queries, so they are unaffected by an analytics outage.

## When call metrics are unavailable

If `cdr.statsToday` fails, an amber banner appears between the call KPI cards
and Recent Calls:

> Call metrics are temporarily unavailable — the analytics service is
> unreachable.

The banner appends the underlying error message in parentheses. It is driven
solely by the stats query's error, and it means the console could not get a
usable answer out of the analytics store for this tenant.

What to check, in order:

1. **Is the rest of the page fine?** If the resource KPIs (Trunks, Agents,
   Service Accounts, Team Members) still show real numbers, your session and
   org scoping are healthy and the problem is confined to the analytics path.
2. **Read the parenthesised error.** A connection or timeout error points at
   the analytics store or the network path from the console's backend to it. An
   authorisation error points at the org-scoped procedure rejecting the caller
   for this `orgId`.
3. **Reload or switch orgs and back.** The CDR queries do not poll, so a
   transient failure persists on screen until the query re-runs.
4. **Do not read the zeros as truth.** When the query errors, the call KPI
   cards fall back to zeros and dashes. Treat every number in the call KPI row
   and the Recent Calls panel as unknown while the banner is up, and confirm
   call volume from your own CDR export or metrics pipeline before acting on it.

The banner never appears for inventory problems. If a resource count looks
wrong, the cause is on the database side — org membership, row-level security,
or the `org_id` on the records — not the analytics store.

## Why these numbers differ from other screens

The dashboard's window and source are specific to this screen, and other KPI
surfaces in the console deliberately use different ones. The agent-suite admin
Overview strip, for example, reports a **rolling 24-hour** window from the
observability event spine rather than `cdrs`, counts distinct call ids with
`uniqExact`, and computes its answer-time percentile only over answered
segments.

So the two screens can legitimately disagree:

- **Different window** — calendar day on the analytics server versus the last
  24 hours from now.
- **Different table** — `cdrs` versus the raw event stream.
- **Different population** — the dashboard's average includes unanswered calls;
  the Overview strip's ring-time percentile excludes them.

When you need to reconcile a discrepancy, compare like for like: window,
source table, and whether unanswered calls are in the denominator.
