Dashboard history — where the charts read from

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.

Today — the charts read the one store that can't hold history

SYSTEM OF RECORD · APPFOLIO OUR STORES · propflow-prod DASHBOARD CHARTS twelve_month_income_statement NOI + rent, true per-month columns gross_potential_rent_enhanced per-unit revenue bridge, monthly rent_roll_itemized · as-of date occupancy + rent at any past month live rent roll / entities today's state, read once a day FinancialPeriod · monthly 99 mo · 2018-05 → 2026-07 · no TTL UnitRevenuePeriod · monthly 98 mo · 2018-05 → 2026-06 · no TTL UnitRentSnapshot · monthly 102 mo · 2018-01 → 2026-06 · no TTL MetricSnapshot · DAILY 170 rows · since 2026-02-28 400-day TTL — self-deletes NOI bar shows ~7 months Revenue / rents bar shows ~7 months Occupancy bar shows ~7 months — backfill already done - - - what the charts read today: the ONE store that can't keep history

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.

Proposed — bars read the monthly stores; daily is for "right now"

SYSTEM OF RECORD · APPFOLIO OUR STORES · propflow-prod DASHBOARD CHARTS twelve_month_income_statement NOI + rent, true per-month columns gross_potential_rent_enhanced per-unit revenue bridge, monthly rent_roll_itemized · as-of date + a monthly cron so it keeps advancing live rent roll / entities today's state, read once a day FinancialPeriod · monthly 99 mo · no TTL · NOI + income.rent UnitRevenuePeriod · monthly 98 mo · no TTL UnitRentSnapshot · monthly 102 mo · no TTL · + read route + live writer MetricSnapshot · DAILY "right now" + recent granularity drop the TTL → accumulates forever NOI bar goes back to 2018 Revenue / rents bar goes back to 2018 Occupancy bar goes back to 2018 — already there ━ the change: each bar reads its own no-TTL monthly store; the daily store keeps the "today" number and recent detail

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.

What each bar needs

BarDeep data already stored?ChangeEffort
NOI✅ 99 months (FinancialPeriod)repoint the bar from the daily series to the monthly series — the drill-down already does thissmall
Revenue / rents✅ 99 months (FinancialPeriod.income.rent) and 98 months (UnitRevenuePeriod)repoint, same move as NOIsmall
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 2026small–medium

The TTL — your call, and why it's right

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:

The catch (Fable confirmed it): one reader assumes the store is small — 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.

Ship order

  1. Monthly series route — one server-built series per metric (NOI, revenue, occupancy) from the monthly stores, all uncapped, date-range reads, honest "since <month>" coverage per property. No UI change yet.
  2. Repoint the three bars (collapsed sparkline, big trend, drill-down) to it; delete the client-side stitching; window token rides the request. Closes #5260 for these three. For the daily-only metrics, stop offering a lying "All time".
  3. Live writer for the occupancy/revenue stores — a sliding 3-month re-derive on the existing nightly financials cron, so they advance past June 2026.
  4. TTL removal — separate PR: drop TTL_DAYS, range-query the reader, ADR-0013 one-liner. "Preserves daily history from today forward."
  5. Docs truth-up (the stale "backfill not run" lines, the NOI catalog provenance).

Independent check — Fable 5

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:

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.

On the TTL — Fable agrees with you, with two conditions. Remove it: the dollars are negligible (≈$0.01/month per year retained at today's 18 rows/day; ~$0.45/month even at 1,000 properties). But (1) it does nothing for 2018–2025 — that history comes only from the monthly stores; and (2) one reader (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."
PropFlow Docs