Tenure read the property-row tenure column, null for new-approach properties.
Backfill propertyMeta.tenure from epc_property.tenure (mostly text labels like
"Rented Social"; numeric SAP codes 1/2/3 mapped to the same labels).
Local authority / constituency have no source in the new pipeline (not on
epc_property, null on the property row, energy_assessments doesn't cover these
properties) — left as-is pending the pipeline writing them to the property row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the overview page, Storeys read epc_property.number_of_storeys (null for some
properties) and Habitable rooms read the property-row number_of_rooms (null for
new-approach properties).
- Habitable rooms: backfill propertyMeta.numberOfRooms from
epc_property.habitable_rooms_count (always populated).
- Storeys: fall back to the main building part's floor-dimension count when
epc_property.number_of_storeys is null.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
epc_property stores property_type/built_form either as numeric RdSAP codes
(newer rows) or plain text ("House", "Detached" — most rows). The previous code
only mapped codes and fell through to dwelling_type for text rows, showing
"Semi-Detached house" instead of "House". Now: map numeric codes, pass text
through as-is, fall back to dwelling_type only when absent.
Also align AGE_BAND_YEARS with the backend ConstructionAgeBand enum: L is
2012–2022 (not "2012 onwards") and add M = 2023 onwards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
For new-approach properties the property-row text columns (property_type,
built_form, year_built) are null, and the condition report had no floor height,
so the assessment page showed Unknown for several general features.
- property-meta: backfill property type / built form / age from epc_property
(+ epc_building_part construction age band), mapping RdSAP codes to labels
(property_type 2 -> Flat, built_form 3 -> End-Terrace, age band F -> 1976–1982),
falling back to the raw value for unmapped codes.
- condition report: source floor height from the main building part's
epc_floor_dimension.room_height_m.
- formatGeneralFeatures: use ?? instead of || so a real 0 (open fireplaces /
extensions) shows as 0 rather than "Unknown".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getProperties selected p.current_sap_points / p.current_epc_rating directly,
which are null for new-approach properties (headline columns unwritten on the
row). That made the Current EPC column blank and, since the Expected EPC column
is currentSapPoints + recommendation SAP, made Expected EPC show 0.
Use the sapSql / epcBandSql baseline helpers (lodged_sap_score / lodged_epc_band
for new-approach properties, property row for legacy), matching the reporting
page. Verified for properties that have a baseline row; the rest populate once
the baseline backfill completes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both were per-property LATERAL lookups against tables with no index on the
join column (property_targets.property_id, funding_package.plan_id), so each
seq-scanned 150k-180k rows once per property — ~3.4s of the load for a
291-property portfolio came from property_targets alone.
Neither value is actually used by the portfolio table: the "Expected EPC" column
derives from currentSapPoints + recommendation SAP, and fundingScheme is fetched
separately by the proposals table. Drop both reads (kept as NULL to preserve the
PropertyWithRelations shape) and trim the plan LATERAL to the columns used.
291-property portfolio: ~3.5s -> ~85ms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getPropertyMeta self-fetched process.env.URL + /api/property-meta. That URL can
resolve to a different deployment (e.g. production), so the building-passport
pages were reading property meta from OLD code — the backfilled headline SAP/EPC
never arrived and the current rating rendered blank, regardless of caching.
Extract resolvePropertyMeta() and call it directly from both the route and
getPropertyMeta, so the page runs identical in-process logic. JSON round-trip
through serializeBigInt preserves the previous shape (bigints/dates as strings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getPropertyMeta self-fetched /api/property-meta with revalidate:60 (despite a
comment saying it shouldn't cache). Vercel's Data Cache served a stale response
from before the headline backfill, so the current EPC rating / SAP rendered
blank on the plans and plan-detail pages even though the API now returns them.
Use cache:"no-store" so backfilled headline values propagate immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getRecommendations read the retired plan_recommendations join table, which is
empty for new-approach plans, so the plan detail page showed no recommendations.
Read the recommendation table directly. Because recommendation.plan_id is
unindexed (26M rows), key off the plan's property_id (indexed) and filter
plan_id in memory — fast for any plan, default or not.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plans page computed the plan uplift as postSapPoints - currentSapPoints,
but current_sap_points is unwritten on the property row for new-approach
properties (null -> 0), so the uplift showed the full post score (e.g. +71.39)
and no current rating.
property-meta now backfills currentSapPoints / currentEpcRating from
property_baseline_performance (lodged_sap_score / lodged_epc_band) for
new-approach properties, fixing every propertyMeta consumer (plans page, hero
cards, building passport). e.g. 709634: current 60 (D), uplift now +11.39.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/api/properties timed out (15s) on just 12 properties: joining the 26M-row
recommendation table on the unindexed plan_id forced a full-table hash, and the
multi-million-row plan/funding joins compounded it.
Rewrite getProperties/getPropertiesCount to read the denormalised model (no
plan_recommendations / recommendation_materials): one-row LATERAL lookups for
target/plan/funding and a recommendation aggregate keyed by the INDEXED
recommendation.property_id, dropping the GROUP BY entirely. ~1s -> instant for a
12-property portfolio.
Also fix the reporting page for new-approach properties (current_sap_points /
current_epc_rating on the property row are unwritten): source avg SAP and the
EPC band distribution from property_baseline_performance (lodged_sap_score /
lodged_epc_band) via new sapSql / epcBandSql helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per domain semantics: lodged_* on property_baseline_performance is the
gov-registry (actual) figure, effective_* is the modelling-adjusted baseline.
Use the lodged columns for the "current" carbon/primary-energy display and the
individual cost columns (summed) for bills. Bills return NULL when no baseline
row exists, so they're excluded from AVG/SUM rather than shown as £0.
(property_baseline_performance is still unwritten DB-wide; this makes the reads
correct for when the pipeline populates it.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
For new-approach properties the modelling pipeline currently writes only
epc_property + epc_energy_element + plan/recommendation — property_baseline_
performance, epc_property_energy_performance metrics and the property-row
headline columns are not yet populated.
- layout: guard propertyMeta.detailsEpc?.estimated (detailsEpc can be absent)
- resolveConditionReport: degrade to an all-Unknown report instead of throwing
when the EPC graph/legacy row is missing (no more 500s)
- getProperties: join recommendation via recommendation.plan_id instead of the
retired plan_recommendations table, so Plan Cost / recommendation SAP populate
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The new modelling pipeline no longer writes property_details_epc. Route all
EPC reads through src/lib/services/epcSources.ts, which branches on the cutoff
(property.updated_at >= 2026-06-01): legacy rows keep reading
property_details_epc; new-approach rows read epc_property (+ children),
property_baseline_performance, and plan/recommendation.
- per-property: resolveConditionReport (building passport),
resolveDetailsEpcMeta (/api/property-meta)
- aggregates: shared carbon/bills/energy/estimated/expired/floor-area/
lodgement/mainfuel SQL fragments + newApproachJoins, wired into the
portfolio list + filters, reporting metrics, and both scenario metrics routes
estimated = predicted-only EPC. Gracefully omits gap fields for new
properties (mainfuel, energyTariff, floorHeight, installed-measures bill
adjustment, SAP-05 likely-downgrade).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a `source` discriminator to `epc_property` so a property can hold a
lodged and a predicted EPC at the same time, per the EPC Prediction
production-wiring branch in the Model repo (docs/MIGRATION_NOTE_predicted_epc_source.md).
- `source` text NOT NULL default 'lodged' — backfills every existing row as
a real lodged EPC.
- Unique index becomes (property_id, portfolio_id, source) so lodged +
predicted rows can coexist for the same property/portfolio.
- New (property_id, source) index — lodged/predicted reads filter on both.
Allowed values: 'lodged' | 'predicted'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>