Why the bars stop at ~7 months when AppFolio holds 8 years, and the shape that fixes it. Drawn from the live code + prod row counts for Camellia.
All three bars plot the daily snapshot series. That store started 2026-02-28 and every row expires after 400 days — by design it can never hold 2018. Meanwhile all three monthly stores already hold 8 years of AppFolio history for Camellia (99 / 98 / 102 months) and nothing on the dashboard reads them for the bars.
Same move on each metric — and after Fable's check, all three are repoints: the data is already in the tables for every one of them. The one extra piece is a small monthly cron so the occupancy/revenue stores keep advancing (today they only fill from a one-off script and stop at June 2026). Crucially, the read is one server-built series per metric — not two fetches stitched in the browser — so the bar can never disagree with the number above it.
| Bar | Deep data already stored? | Change | Effort |
|---|---|---|---|
| NOI | ✅ 99 months (FinancialPeriod) | repoint the bar from the daily series to the monthly series — the drill-down already does this | small |
| Revenue / rents | ✅ 99 months (FinancialPeriod.income.rent) and 98 months (UnitRevenuePeriod) | repoint, same move as NOI | small |
| Occupancy | ✅ 102 months (UnitRentSnapshot, 2018-01 → 2026-06; occupancy present on 100% of rows) | add a read route · repoint · add a monthly cron so it keeps advancing past June 2026 | small–medium |
The 400-day TTL is on the daily store only (snapshots/compute.ts:28). It was put there to keep that store bounded, back when it was purely a live-dashboard cache. Two honest things about it:
getMetricHistory reads every daily row for a property and filters in memory, safe only because the TTL caps it at ~400 (analytics.ts:582). Drop the TTL and that must become a date-range query in the same diff, or loads slow down every year. That's why the TTL flip is its own PR with a one-line ADR note, not buried in the chart repoint.all uncapped, date-range reads, honest "since <month>" coverage per property. No UI change yet.TTL_DAYS, range-query the reader, ADR-0013 one-liner. "Preserves daily history from today forward."Verdict: CONFIRMED — WITH CHANGES. The direction is right and is already the data layer's own doctrine (monthly no-TTL stores are the history; daily is the short cache). Fable pushed back on three things, and it was right on each:
UnitRentSnapshot holds 12,240 rows / 102 months for Camellia, 2018-01 → 2026-06, occupancy on 100% of rows. The repo docs saying "backfill not yet run" are stale. So occupancy is a repoint, same as the other two.Also flagged: only Camellia has monthly stores today (16 other properties have none) — the series must fall back per property and label what arrived, never blank or lie. And ~14 other summary-table metrics (work orders, delinquency, tours…) have no monthly store at all — for those, dropping the TTL is the only lever, and it only helps from today forward.
getMetricHistory) reads the entire daily prefix and is only safe because the TTL bounds it — it must switch to a date-range query in the same PR, or dashboard loads get slower every year. Plus a one-line ADR-0013 amendment, since 400d is a stated retention policy. So: remove it, in its own PR, labelled "preserves daily history from today forward."