Skip to main content

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/billingOrg Usage section (workload / LLM cost month / conversations / rate limits, org-picker gated), and /ops/billingClient 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 n and 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 pollWhenVisible pattern.
  • 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/clients row action "View dashboard"; /ops/clients/{id} header button. (Decision note: a nested /ops/clients/{id}/dashboard route 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?"

MetricSourceBackend status
SLA breached · open (org)/analytics/summary + orgIdneeds ?orgId= (P2; the AnalyticsCaller scoping shape from #1699 already exists on the rollup endpoints)
Queue depth (pending/resolved, period) (org)samesame
p95 / avg response latency (org)samesame
Auto-resolve rate (org)samesame

Loop — "how is the machine performing for this client?"

MetricSourceBackend status
Scoreboard B (8 cells) (org)/analytics/scoreboard-b + orgIdneeds ?orgId= (P2)
Low-sample honestyfrontendcells show n; n < 20 renders a "low sample" hint

Diagnose — "what do I fix for this client?"

MetricSourceBackend status
Improvement 4-panel (draft quality / feedback / KB no-hit / golden-pass) (org)existing rollup endpointsready — endpoints already take ?orgId=/?specialistId=; only the frontend cards need the param threaded (P1)
Escalation reasons / confidence distribution (org)/analytics/summary + orgIdneeds ?orgId= (P2)
Expert coverage for this clientexpert_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?"

MetricSourceBackend status
Subscriptions (per-OSA rows, status, monthly rate)existing Lago ops overview, filtered to orgready (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 dataready (P1)
Conversations vs plan limit / rate limits / workloadmigrated from /ops/billing Org Usage (R1)ready (P1)
Channel health (per-channel last inbound/outbound, recent channel_failures)new light readP3

Cost — "what does this client cost us in LLM spend?"

MetricSourceBackend status
Daily trend / calendar months / billing cycles, org + per-OSA#4823 getLlmCostTrends, getOrgLlmCost, osa-cyclesready — 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 orgId threaded; 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/summary and /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.