Rent + Fees consistency across both revenue surfaces

Plan · persist totalMonthlyRevenue through the metric snapshot pipeline so the dashboard and property-page revenue numbers agree everywhere — headline, expand panel, and trend chart — and both spell out the rent-vs-fees split. Draft for Gera; no review needed, sharing before implementation.

The goal, in one line

Both surfaces show the same total (rent + fees) in every view, and the dashboard card gets the same "$X rent + $Y charges" breakdown the property page already has — so it's obvious what the number represents.

Where things actually stand (receipts)

PR #5473 (merged a6e6f987, on origin/main, prod has promoted since) already unified the two headline cards: both read one producer (computeDashboardStats), one figure (totalMonthlyRevenue = rent + fees), one basis caption. That part is live.

What it did not fix is the expand / trend panel you get when you click into either card. That panel plots a different number — rent-only — and that's the real source of the mismatch you're seeing.

Why the expand panel disagrees

The headline is computed live. The expand/trend panel reads history from MetricSnapshot — and the snapshot pipeline never persists totalMonthlyRevenue. Only rent-only totalMonthlyRent is written:

PlaceWrites totalMonthlyRent?Writes totalMonthlyRevenue?
DashboardStats interface (types.ts)yesyes
live producer computeDashboardStatsyesyes
METRIC_KEYS / METRIC_CATALOGyesNO — not a metric key at all
buildSnapshot (snapshots/compute.ts:164)yesNO
SANDBOX_ANCHORSyesNO
DEFAULT_METRIC_TRENDyesNO
expand target (insight-specs / PropertyDetailClient:325)both point metricKey: 'totalMonthlyRent' — rent-only

So the expand view literally has no fees-inclusive series to plot. That's why #5473 shipped it captioned "Trend shows rent only, not fees" — a stopgap, not the fix.

The fix

Promote totalMonthlyRevenue from a bare DashboardStats field to a fully-registered metric, so it flows through the whole snapshot pipeline exactly like totalMonthlyRent does — then repoint the expand/trend panels at it and add the breakdown label to the dashboard headline.

Part A — register the metric (the 6-step "add a metric" change)

  1. 1METRIC_KEYS in src/lib/data/types.ts — add 'totalMonthlyRevenue' next to 'totalMonthlyRent'.
  2. 2METRIC_CATALOG in metric-catalog.ts — mirror the totalMonthlyRent spec: category:'portfolio', kind:'currency', producer:'dashboard-stats', perPropertyAware:true; description says "rent + ancillary income (parking, pet rent, laundry, late fees, utilities recovery, interest)".
  3. 3producer — already done. computeDashboardStats emits totalMonthlyRevenue (#5473). No change.
  4. 4buildSnapshot — the cleanMetrics({…}) literal at snapshots/compute.ts:164 gains totalMonthlyRevenue: stats.totalMonthlyRevenue. This is the load-bearing one — the CLAUDE.md note calls out that omitting it is the SILENT failure (trend renders empty forever). The coverage guard metric-snapshot-coverage.test.ts will red until it's here.
  5. 5SANDBOX_ANCHORS in sandbox-builder.ts — add a plausible anchor (~101500, i.e. rent anchor + a fees delta) so demo/sandbox lanes light up and the coverage guard passes.
  6. 6DEFAULT_METRIC_TREND in synthesize.ts — add totalMonthlyRevenue: 'good'.

Plus the backfill anchors map in scripts/backfill-metric-snapshots.ts (line ~248) — add the key or the backfill is a no-op for it. And the Clara type-mirror if the field crosses into agents/clara/lib/data (ADR-0021 anti-drift — #5473 already mirrored the field, verify).

Part B — repoint the surfaces

Part C — trend history

The new series only accrues going forward — historical snapshots have no totalMonthlyRevenue. Two options, I'll take the second unless you say otherwise:

  1. Backfill the fees-inclusive series into historical MetricSnapshot rows for real properties. More faithful, but the historical fees split isn't always reconstructable per past day — risks fabricating history.
  2. Let it accrue forward (recommended). Trend shows rent+fees from the first snapshot after deploy; earlier points backfill from rent-only where that's all we have, clearly the honest floor. No invented fees history. Sandbox/test properties get the full synthetic series via the anchor from step 5, so demos are complete immediately.

Files to touch

FileChange
src/lib/data/types.tsadd to METRIC_KEYS
src/lib/data/metric-catalog.tscatalog spec
src/lib/domain/metrics/snapshots/compute.tsbuildSnapshot literal (step 4)
…/snapshots/sandbox-builder.tsanchor
…/snapshots/synthesize.tstrend default
scripts/backfill-metric-snapshots.tsanchors map
src/lib/domain/dashboard/insight-specs.tsexpand metricKey + label
…/properties/[id]/PropertyDetailClient.tsxexpand metricKey + label
src/lib/domain/dashboard/rent-income-basis.tsbreakdown caption on dashboard headline
testscoverage guard already enforces; add a regression that the expand series is fees-inclusive

Risks & how I'll de-risk

Verification before I call it mergeable

Out of scope (already tracked)

The three pre-existing producers surfaced in #5473 stay as filed follow-ups, not part of this PR: /properties grid (w7vwayGA), Clara chat's get_portfolio_stats (SkzXmlPR), owner digest basis label (LGOnUgJz).

PropFlow Docs