PRD — Client Dashboard (ops, per-client run state)
Status: draft for owner review · Author: Franky + Claude · Date: 2026-07-27 Related: #4941 (three-tier analytics), #4963 (Business tier), #4959/#4961 (Dashboard IA), #4823 (org/OSA cost trends), #1699 (analytics org scoping)
1. Summary
The platform-level dashboard exists: /ops/analytics (SA landing since #4961) answers "how is the whole operation doing" in four tiers — Now / Loop / Diagnose / Business(+cost ledger).
This PRD adds its client-scoped counterpart: pick one client, see that client's run state in the same tier language, plus client-only context the platform view cannot show (Specialist roster, engagement facts, channel health). Primary jobs: AM account reviews / QBR prep, escalation triage ("is it just this client?"), renewal conversations.
2. Requirement review (what changed vs. the original ask)
The original ask — "a client dashboard with Now → Loop → Diagnose → Business → cost tiers, client selectable" — is sound. Three corrections/additions from review:
R1 — Consolidation mandate (the important one). Client-scoped run-state fragments already live in three places: /ops/clients/{id} (detail), /ops/billing → Org Usage section (workload / LLM cost month / conversations / rate limits, org-picker gated), and /ops/billing → Client LLM Cost Trends section. A fourth surface would recreate the sprawl #4959 just cleaned up. The Client Dashboard must absorb the two org-scoped analytics sections from /ops/billing (billing returns to money-of-record only); /ops/clients/{id} keeps configuration/settings and links here for run state.
R2 — Tier semantics must be remapped, not copied. Per-client, some platform metrics change meaning or lose statistical validity:
- Loop metrics on one org can have tiny samples (a client with 12 tasks in 30d makes p95/independence-rate noise). Cells must carry
nand render a low-sample hint below a threshold (default n < 20) instead of implying precision. - "Cost" deserves its own tier per client (the ask's "Business → cost" split is right): Business = commercial shape (subscriptions, rate, engagement), Cost = LLM spend trends (#4823 series).
- Client-only context (Specialist roster + thresholds, channel health, engagement dates) has no platform-tier equivalent — added as part of Business.
R3 — Roles. The platform dashboard is SA-centric; the client dashboard's primary user is arguably the AM. AMs already see org-scoped workload/conversations/rate-limits on billing today. Cost and rate data are SA-only today (AM per-org cost visibility is a standing open question). Default: AM sees Now/Loop/Diagnose + non-money Business facts; Business rate + Cost tiers render for SA only, until the owner decides otherwise (OD-1).
3. Non-goals
- Client-facing dashboard (clients have their portal; this is an HP-staff surface).
- Margin / expert labour cost (#3196, open).
- Real-time streaming beyond the existing 30s
pollWhenVisiblepattern. - Cross-client comparison views (platform dashboard's job).
4. Entry & IA
- Route:
/ops/client-dashboard?org={orgId}— sidebar Dashboard group gains a third item, "Client dashboard" (AM + SA). No org selected → picker empty state (same pattern as billing's Org Usage).?org=in the URL makes views shareable/bookmarkable. - Deep links:
/ops/clientsrow action "View dashboard";/ops/clients/{id}header button. (Decision note: a nested/ops/clients/{id}/dashboardroute was considered — nicer breadcrumbs, but it splits entry from the Dashboard nav group and complicates the picker-first flow. Revisit if the picker page underperforms.) - Org picker: same component/behaviour as
/ops/billing(SA: all orgs; AM: their orgs via the existing scoping). - Period selector: 7/30/90d, top-right, same semantics as the platform page — flow metrics follow it, state metrics don't and say so (see R2/AC).
5. Tier spec
Scope note: every query in rows marked (org) filters by the selected orgId; nothing here crosses ADR-020 boundaries (all reads are org-scoped aggregates on tables the ops surfaces already read).
Now — "is this client on fire?"
| Metric | Source | Backend status |
|---|---|---|
| SLA breached · open (org) | /analytics/summary + orgId | needs ?orgId= (P2; the AnalyticsCaller scoping shape from #1699 already exists on the rollup endpoints) |
| Queue depth (pending/resolved, period) (org) | same | same |
| p95 / avg response latency (org) | same | same |
| Auto-resolve rate (org) | same | same |
Loop — "how is the machine performing for this client?"
| Metric | Source | Backend status |
|---|---|---|
| Scoreboard B (8 cells) (org) | /analytics/scoreboard-b + orgId | needs ?orgId= (P2) |
| Low-sample honesty | frontend | cells show n; n < 20 renders a "low sample" hint |
Diagnose — "what do I fix for this client?"
| Metric | Source | Backend status |
|---|---|---|
| Improvement 4-panel (draft quality / feedback / KB no-hit / golden-pass) (org) | existing rollup endpoints | ready — endpoints already take ?orgId=/?specialistId=; only the frontend cards need the param threaded (P1) |
| Escalation reasons / confidence distribution (org) | /analytics/summary + orgId | needs ?orgId= (P2) |
| Expert coverage for this client | expert_access by org (who reviews this client, grant scope) | small new read or filter param (P2) |
Business — "what is the commercial shape of this client?"
| Metric | Source | Backend status |
|---|---|---|
| Subscriptions (per-OSA rows, status, monthly rate) | existing Lago ops overview, filtered to org | ready (P1) |
| Engagement facts (start date, trial end, billing-cycle anchor day) | org fields (anchorDayFromOrg semantics) | ready (P1) |
| Specialist roster (OSAs + primary assignment) | existing org detail data | ready (P1) |
| Conversations vs plan limit / rate limits / workload | migrated from /ops/billing Org Usage (R1) | ready (P1) |
Channel health (per-channel last inbound/outbound, recent channel_failures) | new light read | P3 |
Cost — "what does this client cost us in LLM spend?"
| Metric | Source | Backend status |
|---|---|---|
| Daily trend / calendar months / billing cycles, org + per-OSA | #4823 getLlmCostTrends, getOrgLlmCost, osa-cycles | ready — pure reuse (P1); the /ops/billing Client LLM Cost Trends section migrates here (R1) |
6. Phasing
- P1 — shell + the ready tiers (no backend changes). Route, picker, period selector; Business + Cost tiers; improvement 4-panel with
orgIdthreaded; migrate the two org-scoped sections off /ops/billing. Ship value in one PR-sized chunk per section. - P2 — org-parameterize summary + scoreboard. Add
?orgId=to/analytics/summaryand/analytics/scoreboard-b(reusing the #1699 caller-scoping shape + its 404-outside-scope contract); light Now + Loop tiers; expert-coverage read. - P3 — client-only enrichments. Channel health; anything the first account reviews ask for.
7. Open decisions
- OD-1 (owner): AM visibility of money — rate totals + cost tiers. Default in this spec: SA-only, AM sees the rest. Standing "AM per-org cost visibility" question; deciding it here unblocks more than this page.
- OD-2: low-sample threshold value (default n < 20) and whether cells grey out or only annotate.
- OD-3: does
/ops/clients/{id}embed a mini summary strip (sparkline row linking here), or link only?
8. Acceptance criteria (P1)
- Sidebar Dashboard group shows "Client dashboard" (AM + SA); no org → picker empty state;
?org=persists selection. - With an org selected: Business + Cost + improvement panels render org-scoped data; period selector governs flow metrics; state metrics label their window ("live" / "current cycle").
- /ops/billing no longer renders Org Usage or Client LLM Cost Trends; both live here; billing keeps subscriptions/invoices/payment failures/cycle-cost column.
- AM sees no rate/cost data (per OD-1 default); SA sees all.
- Every new frontend section has a spec pinning org-scoping + role gating (the #4963 test shape).
9. Risks
- Endpoint 404-vs-403 contract: org-scoped analytics for AMs must follow the existing cross-org read rule (404 outside scope, #1472/#1699).
- Billing-page migration regressions: the moved sections have existing behaviours (AM-visible subset, SA-only cards) — port their tests along with the code.
- Sample-size misreading: without the low-sample hint, per-client Loop numbers will be quoted in QBRs with false confidence — treat the hint as launch-blocking for P2, not polish.