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.
"$X rent + $Y charges" breakdown the property page already has — so it's obvious what the number represents.
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.
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:
| Place | Writes totalMonthlyRent? | Writes totalMonthlyRevenue? |
|---|---|---|
DashboardStats interface (types.ts) | yes | yes |
live producer computeDashboardStats | yes | yes |
METRIC_KEYS / METRIC_CATALOG | yes | NO — not a metric key at all |
buildSnapshot (snapshots/compute.ts:164) | yes | NO |
SANDBOX_ANCHORS | yes | NO |
DEFAULT_METRIC_TREND | yes | NO |
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.
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.
METRIC_KEYS in src/lib/data/types.ts — add 'totalMonthlyRevenue' next to 'totalMonthlyRent'.METRIC_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)".computeDashboardStats emits totalMonthlyRevenue (#5473). No change.buildSnapshot — 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.SANDBOX_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.DEFAULT_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).
insight-specs.ts) and property expand target (PropertyDetailClient.tsx:325): flip metricKey: 'totalMonthlyRent' → 'totalMonthlyRevenue' so the panel + trend show the same fees-inclusive figure as the headline."$117,798 rent + $5,585 charges" caption the property card already shows, via the shared rent-income-basis.ts helper so the wording can't drift between the two surfaces.The new series only accrues going forward — historical snapshots have no totalMonthlyRevenue. Two options, I'll take the second unless you say otherwise:
MetricSnapshot rows for real properties. More faithful, but the historical fees split isn't always reconstructable per past day — risks fabricating history.| File | Change |
|---|---|
src/lib/data/types.ts | add to METRIC_KEYS |
src/lib/data/metric-catalog.ts | catalog spec |
src/lib/domain/metrics/snapshots/compute.ts | buildSnapshot literal (step 4) |
…/snapshots/sandbox-builder.ts | anchor |
…/snapshots/synthesize.ts | trend default |
scripts/backfill-metric-snapshots.ts | anchors map |
src/lib/domain/dashboard/insight-specs.ts | expand metricKey + label |
…/properties/[id]/PropertyDetailClient.tsx | expand metricKey + label |
src/lib/domain/dashboard/rent-income-basis.ts | breakdown caption on dashboard headline |
| tests | coverage guard already enforces; add a regression that the expand series is fees-inclusive |
noi = rent − WO spend uses totalMonthlyRent. Do NOT swap it to the gross — verified it reads the rent-only key; leaving it alone. Same for the owner digest's rent row.expenseRatio. Its gross denominator already moved to the fees-inclusive figure in #5473 (opex ÷ gross should include ancillary). No further change; will pin with the existing test.en-US pin on the breakdown formatter (the stranded #5473 nit) so caption separators match the headline.npm run typecheck exit 0, targeted vitest green (coverage guard + new regression), eslint 0.[preview] build + screenshot both cards (dashboard + a property) expanded, showing matching totals + breakdown, in light and dark.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).