Why this exists.
The asset management dashboard started as one HTML file. It now spans roughly eighty files across fifty properties, twelve portfolio groups, and six upstream data sources. That growth is healthy — but the seams are starting to show.
Numbers occasionally drift between the master view and a property page. A typo in an embedded data block can blank the whole dashboard. Refreshing data is a multi-file ritual that gets done late on Friday afternoons. None of these are critical. All of them compound.
This document describes the architecture that fixes those problems without rebuilding what already works. It defines where data lives, who is allowed to write to it, and how every dashboard view stays in sync as new information arrives.
What the system covers.
Fifty properties across three asset types and twelve portfolio groups, all reporting into one command center.
Asset types
- Manufactured housing communities — the bulk of the portfolio, organized into Collective, Lone Star, GP Homes, and other groups.
- Apartments — multifamily assets including Big Valley and 1809 on Cox.
- Storage — included in portfolio rollups where applicable.
Upstream data sources
- Main Street portal · live ops data, refreshed on a scheduled task.
- AppFolio cash flow · primary CapEx actuals source.
- General ledger exports · CapEx by GL bucket.
- GP Master Consolidated · 71-sheet workbook, financial system of record.
- Monday boards · DD checklists, homes roster, project tracking.
- Rent roll exports · occupancy and lot-level state.
Four rules that anchor every decision.
These are not aesthetic preferences. Each one prevents a specific failure mode that has already happened in the system.
Every architectural choice that follows traces back to one of these four. When two principles conflict — for example, when adding a new property forces a tradeoff between repeatability and isolation — the order above is the tiebreaker.
One workspace. Five top-level folders.
Predictable shape. Every file has a single job and a name you can guess from its location.
Each folder has one job.
Sources flow in. Scripts shape them. Pages render them.
The pipeline is the only writer to _data/. Views are read-only consumers. This is the single rule that makes the system survive growth.
Three stages, in order
- Sources land in
_sources/— exports drop in raw form, organized by upstream system. Nothing in this folder is ever edited by hand. - Scripts in
_scripts/read sources, write JSON to_data/— one ingest script per source, one build script per output. Everything is idempotent. - Views read JSON from
_data/— the master HTML and per-property pages all read the same files. They display, they don't store.
The pipeline contract
The orchestrator refresh.py calls every ingest and build script in order. It accepts a flag to skip stages (--skip mainstreet) and a --dry-run mode that reports what would change without writing.
refresh.py capex. The four downstream files always agree because they read from one upstream file.
How we prove the numbers match.
A dashboard that shows different numbers in different views loses trust the first time it happens. The system enforces consistency through three mechanisms.
Mechanism one — single source
Each metric lives in exactly one JSON file. Lakeside LTD CapEx exists in capex-actuals.json. The master view, the Lakeside detail page, and the portfolio rollup all read that same file. There is no second copy to drift from.
Mechanism two — validate.py
After every pipeline run, validate.py walks the data files and asserts known relationships:
- Master LTD ≈ sum of per-property LTD across all 50 properties.
- CapEx actuals total ≈ sum of GL CapEx buckets net of reclassifications.
- Property IDs match across
properties.json,capex-actuals.json, andcapex-plan.json. - Every property in
properties.jsonhas a corresponding HTML page inProperties/. - Live ops snapshot is no more than 24 hours old.
Failures print to console and write a flag file. The dashboard surfaces a small banner when validation has failed since the last successful run.
Mechanism three — append-only sources
Raw exports go into _sources/ with the export date in the filename. Nothing overwrites. If a number changes mysteriously, you can diff yesterday's GL against today's GL and find the difference in two minutes.
What runs when.
Different upstream systems refresh on different cadences. The schedule below keeps the dashboard fresh without overwhelming any one source.
Refresh cadence
- Main Street live ops · daily, late afternoon (after PM team reconciliations are complete).
- Rent roll · weekly, Monday morning.
- CapEx (GL + cash flow) · weekly, Monday morning.
- GP Master · monthly, after close.
- Monday boards · daily, when DD or homes data has changed.
- Validate.py · after every pull, automatically.
Live ops drives the top-line KPIs
The KPI cards at the top of the master dashboard read from mainstreet.json, not from static fields in the HTML. When the late-afternoon Main Street pull completes, the next page load reflects current portfolio occupancy, collections, and work-order status.
Command center plus fifty isolated detail pages.
The master HTML is the command center — portfolio rollups, KPIs, cross-property comparisons. Each property has its own detail page that loads independently.
The pattern
- One template, fifty instances. The Lakeside Landing page is the canonical template — six tabs (Overview, Plan, Actuals, Variance, Timeline, Forecast), shared design system, consistent navigation.
- Each page reads its own data slice. Lakeside loads
capex-plan.jsonfiltered to its property ID. A bug in Lakeside's data does not affect Lone Star's render. - Sub-pages where applicable. Properties with active homes inventory get a homes roster page. Properties in DD get a DD hub page. Each sub-page reads its own data file.
- The master links to the detail. Clicking a property card on the master command center opens the property page. The reverse link returns to the master with state preserved.
Tab pattern (Lakeside template)
- Overview. Property facts, KPIs, links to sub-pages.
- Plan. Original CapEx plan with baseline and timeline.
- Actuals. Cash flow and GL spend to date, with monthly newsfeed.
- Variance. Plan vs actual with both budgeted and unbudgeted lines.
- Timeline. Gantt-style schedule of work in progress.
- Forecast. Three pace scenarios — plan, run-rate, blended.
Five phases. About six hours of focused work.
The current system already does most of this work — the migration is mostly extraction and orchestration, not rebuilding. Phases can be paused between, but should be done in order.
asset-management-master.html into _data/*.{json,js}. Master HTML shrank from 2.14 MB → 879 KB (59%). The HTML is now a renderer._scripts/refresh.py and _scripts/regenerate_wrappers.py are live. Per-domain extract scripts use a shared _domain_extract.py. argv interface supports --only, --skip, --dry-run._scripts/stamp_property_pages.py reads properties.json + all _data/ sources and auto-stamps a detail page per property. Tabs render only when their data exists. 44 pages stamped, 4 protected (Lakeside/Miami/Montecarlo/Ford Homes), 48 total in routing.am-refresh-daily runs every afternoon at 4:30 PM (Main Street + validate), am-refresh-weekly runs Mondays at 4:30 PM (full refresh across GL, GP Master, rent roll, Monday, Main Street). Failures surface through the Scheduled tasks sidebar.Today vs after
| Scenario | Today | After |
|---|---|---|
| New CapEx GL drops in | Edit 4 files by hand. Hope nothing drifts. | Run refresh.py. Validate confirms reconciliation. |
| Lakeside file has a typo | Whole dashboard goes blank. | Lakeside page errors. Everything else renders. |
| New property added | Touch master HTML, JS map, plan file, and copy a template. | Add a row to properties.json. Pipeline stamps the page. |
| Live ops change at 2pm | Manual export, manual paste, refresh browser. | Scheduled pull at 4pm. Dashboard reflects it. |