Commit graph

1762 commits

Author SHA1 Message Date
KhalimCK
140383f18f
Merge pull request #392 from Hestia-Homes/feature/tag-select-all-matching
feat(tags): explicit select-all-matching in bulk tag mode
2026-07-14 13:31:30 +01:00
Khalim Conn-Kowlessar
5ed5a7f57c fix(tags): make "Select all N matching" a visible secondary button
Impeccable critique (P1): the escalation was a bare blue text-link on the bar's
pale-blue tint, no persistent underline — invisible next to the navy primary,
and it's the exact action users hunt for. Promote it to a secondary button
(white fill + brand-navy border + check icon, fills on hover) so it stands out
without competing with "Choose a tag"; demote "Clear selection" to a legible
neutral underlined link (was blue-on-blue).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:21:56 +00:00
Khalim Conn-Kowlessar
a7781eca81 fix(tags): header checkbox selects all loaded rows, not just the page
With a 7-row page size, the header "select all" (toggleAllPageRowsSelected)
only selected the visible 7 — so it never reached "all loaded", the condition
that reveals the "Select all N matching" escalation. It now selects every loaded
row (toggleAllRowsSelected):
- when everything matching is already loaded, that IS the whole set (tag via ids);
- when more match than are loaded (e.g. 250 loaded of 10,000), the escalation
  appears and routes through mode:"filter" — the server resolves and tags the
  entire matching set (getFilteredPropertyIds, chunked), so memory is never the
  ceiling and no huge id list is sent from the client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:12:05 +00:00
Khalim Conn-Kowlessar
99025ce4b3 feat(tags): explicit select-all-matching in bulk tag mode
The header checkbox only selects loaded rows, so bulk-tagging a portfolio with
more properties than are loaded (250-row window) silently tagged just the page.

Bulk mode is now explicit (Gmail pattern): nothing selected = no target (Choose
a tag is disabled), so a tag can't be applied to everything by accident. Tick
rows to target them; once every loaded row is ticked and more match, a "Select
all N matching" escalation switches the target to the whole matching set,
resolved server-side via the existing assignments filter mode (no backend
change). Any manual checkbox change drops the escalation back to the explicit
selection; changing filters / leaving the mode clears it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:34:37 +00:00
KhalimCK
fc5fc80d88
Merge pull request #389 from Hestia-Homes/feature/portfolio-tagging
feat: portfolio tagging system (ADR-0013)
2026-07-14 11:52:58 +01:00
Khalim Conn-Kowlessar
971767a62a fix(portfolio): don't show Run modelling twice in the no-plans state
The not-modelled banner and the toolbar both rendered a navy "Run modelling"
CTA whenever a portfolio had properties but no plans — two identical buttons.
The banner owns the action in that state, so the toolbar's Run modelling is now
hidden while the banner is up (it returns once everything is modelled or the
banner is dismissed). One primary CTA at a time, as intended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:30:58 +00:00
Khalim Conn-Kowlessar
af171f50d7 feat(tags): only show row-select checkboxes while bulk-tagging
The selection checkbox column was persistently visible. Now it appears only
when a bulk assign/remove mode is active (launched from the Tags menu) via a
select-less column set, and the selection clears on entering and leaving the
mode. The default table has no checkbox column; per-property tagging still uses
the inline Tags-cell popover, and the bulk bar's target still follows selection
→ filter → whole portfolio while the mode is open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:10:02 +00:00
Khalim Conn-Kowlessar
46421279fb feat(portfolio): address impeccable critique — no-plans state, contrast, focus
Design pass on the portfolio table (critique: 27/40; P0 was the unmodelled state).

- onboard: a mixed-safe "not modelled yet" nudge above the table — shown whenever
  any loaded property lacks a plan (so a part-modelled portfolio still gets it),
  wired to Run modelling. Turns a wall of empty cells into a next step without
  hiding real data for the rows that ARE modelled.
- harden: one empty-cell vocabulary (EmptyCell) replacing the four divergent
  renderings (blank div, slate-300 "—", italic "No cost", "Unknown"); AA-legible
  and aria-labelled. Empty EPC bubble is now a labelled placeholder ring, not an
  invisible div. Export failures surface an inline error instead of dying silently.
- a11y: the quick-filter clear is no longer a <span role=button> nested inside a
  <button> — pill is a container with sibling open/clear buttons.
- distill: exactly one navy CTA at a time — Add properties leads an empty
  portfolio, Run modelling leads a populated one; the other demotes to neutral.
- colorize: black tokens in PropertyFilters + the Edit-Columns checkboxes now use
  the brand navy/brandmidblue scale (was raw black, drifting off the toolbar).
- cleanup: removed the unreachable delete-property modal (no DELETE endpoint,
  onDeleteProperty never called). Real property deletion is its own slice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:50:27 +00:00
Khalim Conn-Kowlessar
5872e7c335 feat(tags): consolidate bulk-tag actions into a toolbar Tags menu
Replaces the isolated "Assign tags from file" button with a single Tags ▾
dropdown in the toolbar (grouped with Export / Add properties), offering the
three bulk paths: upload from file, assign a tag, remove a tag.

Assign/Remove enter a lightweight contextual mode (TagActionBar) whose target
follows the live table — the row selection if any, else the active filter set,
else the whole portfolio — so ticking rows or changing filters re-scopes it in
place, and it supports removal, not just assignment. Row checkboxes stay visible
at all times; a "N selected" chip now sits by the results count. The upload
modal gains downloadable CSV templates (Property Ref / UPRN).

Removes TagBulkBar (superseded by TagsMenu + TagActionBar).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:17:18 +00:00
Khalim Conn-Kowlessar
47fd733b45 fix(tags): address PR #389 review — server guards, error handling, reuse
- Reject `tagIds` in the modelling-runs trigger route (400) until the distributor
  resolves tag_ids — the RunModellingClient guard was UI-only, so a direct POST
  (or a future caller) could dispatch a tagged run that silently models the
  unscoped superset (preview≠run, ADR-0008). Defence-in-depth now.
- Narrow the tags POST catch to Postgres 23505 (new isUniqueViolation helper) so
  transient/other DB errors surface as 500, not a misleading duplicate-name 409.
- Wrap the PATCH rename in the same unique-violation handler — the pre-check is
  racy, so a concurrent rename collision now returns 409, not an uncaught 500.
- TagsCard reuses usePortfolioTags / portfolioTagsKey / Tag instead of a copied
  query + key, so every tag surface stays in sync if the key changes.
- Scope DataTable getRowId to selection-enabled tables only, leaving other
  consumers (plan table) on TanStack's default row identity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:50:10 +00:00
Khalim Conn-Kowlessar
7cd33ab18d Merge remote-tracking branch 'origin/main' into feature/portfolio-tagging
# Conflicts:
#	src/app/portfolio/[slug]/components/PropertyFilters.tsx
#	src/app/portfolio/[slug]/utils.ts
#	src/app/utils/propertyFilters.ts
2026-07-13 19:49:47 +00:00
Khalim Conn-Kowlessar
e80803c50a fix(tags): fall back to row index in DataTable getRowId for idless rows
The shared DataTable is also used by the plan table, whose PlanWithTotals rows
have no `id` (they carry planId/propertyId). A bare String(row.id) keyed every
plan row as "undefined" — colliding React keys. Key by id when present, else by
index (restoring the pre-change default for idless consumers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:16:51 +00:00
Khalim Conn-Kowlessar
1d12fa1baf refactor(tags): one calm, AA-safe chip vocabulary across every surface
Impeccable pass. Replaces the solid full-saturation chips (a heavy-accent
inconsistency vs the settings card's soft chips) with a single quiet vocabulary
per PRODUCT.md: a soft tint of the Tag's freeform hex, a colour dot, and dark
hue-preserving ink whose contrast on the tint is guaranteed ≥4.5:1 for any
colour (new tagChipTokens, TDD — yellow and every hue verified). Settings,
table, filter, bulk bar, upload, and run UI now share one TagChip. Also drops a
stray dev scratch file and gitignores the pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:09:27 +00:00
Khalim Conn-Kowlessar
9749e8525b feat(tags): tags multi-select in the Run modelling filter
Adds a Tags FilterColumn (per-portfolio options from GET /tags, colour dots,
counts) feeding filters.tagIds; folds it into filterCount/clearFilters and
removes the "Tags are coming" copy. The in-app preview already counts by tag
membership, so the selection previews exactly. The run button is guarded while
tags are selected — the distributor can't resolve tag_ids yet (backend ask,
ADR-0013), so previewing is honest but running stays disabled until it lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:00:12 +00:00
Khalim Conn-Kowlessar
f98e36ad87 feat(tags): bulk tag assignment — selection, filter-driven, and file upload
Adds row multi-select to the property table (new checkbox column + stable
id-keyed selection surviving pagination) with a bulk bar that tags the
selection ({mode:"properties"}) or every property matching the active filter
({mode:"filter"}, whole set resolved server-side). Plus a Bulk tag assignment
upload modal: client-side SheetJS → parseTagIdentifierRows → pick a target tag
→ {mode:"identifiers"} → match summary (matched / already-tagged / unmatched).
Selection is filter-scoped and clears on filter change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:14:30 +00:00
Khalim Conn-Kowlessar
5abf4a3a10 feat(tags): dynamic Tags filter in the property-table filter panel
Adds a "Tags" field to PropertyFilters with per-portfolio options fetched
via GET /tags plus a synthetic "Untagged" bucket. Emits enum_one_of with
value = JSON of tag ids (+ "__untagged__"); the server SQL already resolves
this (any-of, ADR-0013). Condition chips label by tag name and render
"(deleted tag)" for dangling ids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:58:19 +00:00
Khalim Conn-Kowlessar
b9d7bb4bba feat(tags): property-table Tags column + inline assign popover
Chips with "+N" overflow in a default-visible, toggle-able column; the
cell doubles as an assign popover that toggles membership via
POST …/assignments {mode:"properties"}, optimistic across every cached
property-table page and reconciled on settle. Freeform-hex chips get an
auto-contrast foreground (new pure colour util, TDD). Shared usePortfolioTags
hook keeps settings/table/filter in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:52:48 +00:00
KhalimCK
f801025ce3
Merge pull request #387 from Hestia-Homes/feature/tags-schema
feat(db): portfolio_tag + property_tag tables (tagging system)
2026-07-13 17:26:13 +01:00
Khalim Conn-Kowlessar
c5d58dbc96 feat(tags): table query + tag filter, filter-driven bulk assign, preview join, settings UI
Backend, end to end (ADR-0013):
- getProperties now returns each property's tags (name-ordered JSON); the
  PropertyWithRelations type gains a `tags: PropertyTag[]` field.
- A "tags" property-table filter (EXISTS/NOT-EXISTS on property_tag, any-of +
  an "Untagged" bucket) — PK-indexed by property_id, so no join added.
- getFilteredPropertyIds resolves the whole matching set for filter-driven Bulk
  tag assignment; the assignments route's `filter` mode uses it.
- Run filter: previewModellingRun joins property_tag for tag_ids; the two
  modelling routes accept tagIds. Distributor ask documented in
  docs/backend-asks/tag-ids-run-filter.md.

UI:
- Settings → Tags: create (name + colour), edit, delete-with-count-confirm,
  list with property counts (TanStack v4). Sidebar link added.

Typecheck clean; 431 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:23:49 +00:00
KhalimCK
aa38c916d2
Merge pull request #384 from Hestia-Homes/feature/portfolio-list-override-descriptors
feat(portfolio): resolve list descriptors via landlord-override precedence
2026-07-13 17:09:40 +01:00
Khalim Conn-Kowlessar
88c0c5b13d feat(tags): CRUD + assignment routes, bulk-file parser, run-filter tag_ids
- Run filter gains an optional tag_ids key (ADR-0013): normaliseRunFilters
  coerces/dedupes ids, buildRunRecord + parseRunConfig round-trip it, and
  selectionSummary counts it. Pure, TDD (9 new cases).
- parseTagIdentifierRows (pure, TDD): a CSV/Excel 2D array → the identifier
  column to match on (landlord_property_id or uprn, header-detected), trimmed,
  deduped, capped at 10k.
- Routes (write-gated via shared getPortfolioRole/canWriteTags):
  GET/POST /tags (list w/ counts, create), PATCH/DELETE /tags/[id] (edit,
  delete+cascade), POST /tags/[id]/assignments (properties add/remove +
  identifiers bulk-match with a summary). Names validated by validateTagDraft.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:04:28 +00:00
Khalim Conn-Kowlessar
ed1dae4e99 feat(tags): pure Tag draft validation/normalisation (TDD)
validateTagDraft: trims + requires a name, caps it at 30 chars, rejects a
case-insensitive duplicate against the portfolio's existing tag names (the
caller excludes the tag's own name on edit), and validates + lowercases a
freeform hex colour (#rgb or #rrggbb). The DB's case-insensitive unique index
remains the ultimate guard; this gives a friendly pre-check for the tag routes
and settings UI. 8 behaviours, built red-green. ADR-0013.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:50:09 +00:00
Khalim Conn-Kowlessar
af81827b14 docs: ADR-0013 tagging system + CONTEXT (Bulk tag assignment, Run filter tags)
Captures the grill-with-docs outcome: flat app-owned Tags, four assignment
paths (incl. in-app bulk upload, no FastAPI), tags as a Run-filter key extending
ADR-0008, hard-delete + cascade, reporting-by-tag deferred to the reporting
redesign. Design only — no implementation yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:45:14 +00:00
Khalim Conn-Kowlessar
4a841c20d4 docs(context): add Tag glossary entry (grill WIP)
Flat, portfolio-owned, many-to-many label for grouping Properties for bulk
actions. Captured during the tagging grill-with-docs session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:45:14 +00:00
Khalim Conn-Kowlessar
0a26f9ed93 feat(db): portfolio_tag + property_tag tables (tagging system)
The schema substrate for the portfolio tagging system (ADR-0013): a flat,
portfolio-owned Tag (name + freeform hex colour, case-insensitively unique per
portfolio) and the many-to-many property_tag membership. Both FKs cascade so a
tag delete drops its memberships and a property/portfolio delete drops its tags.
A tag_id-leading index backs the reverse lookup (chips, filter, run resolution).

Migration only — not yet applied to prod. Feature code stacks on this branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:44:26 +00:00
KhalimCK
84a485923c
Merge pull request #386 from Hestia-Homes/feature/epc-main-heating-detail-index
feat(db): index epc_main_heating_detail(epc_property_id)
2026-07-13 16:22:16 +01:00
Khalim Conn-Kowlessar
0873fbc5c1 docs(context): Main Fuel reads EPC detail; Wall/Roof/Heating filter by bucket
Updates the portfolio Property-table flagged-ambiguity note for the two changes
in the previous commit: Main Fuel now reads epc_main_heating_detail beneath the
override (no longer override-only for new-approach), and Wall/Roof/Heating are
now filterable via coarse prefix/substring buckets while still displaying the
full free-text description.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:38:42 +00:00
Khalim Conn-Kowlessar
576b8a0b33 feat(db): index epc_main_heating_detail(epc_property_id)
The portfolio list resolves Main Fuel per row via a correlated subquery on
epc_main_heating_detail.epc_property_id (mainfuelSql, ADR-0012). The table has
no index on that column, so the subquery seq-scans it once per property in the
count/export path. At ~41k rows it stays in memory (a 32k-property Main-Fuel
count measures ~1.2s), but this index makes it an index probe (~50ms) and
guards against larger new-approach portfolios.

Migration only — not yet applied to prod. Companion to PR #384.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:36:38 +00:00
Khalim Conn-Kowlessar
3b04de622b feat(portfolio): Main Fuel EPC fallback + filterable Wall/Roof/Heating
Main Fuel no longer reads "Unknown" for new-approach properties. The new EPC
graph stores the main fuel as a JSONB scalar on
epc_main_heating_detail.main_fuel_type — a numeric RdSAP main_fuel code (~95%)
or an EPR text string (~5%). mainfuelSql now reads it (lodged over predicted)
instead of NULL; resolvedMainFuelSql keeps the override → EPC → 'Unknown'
precedence. The RdSAP code table (Model epc_codes.csv, cross-checked against
prod) + the EPR strings fold into MAINFUEL_OPTIONS, so the column label and the
filter work unchanged. resolveMainFuel gains the EPC branch as its pure twin;
coverage tests assert every code + string + override value is folded.
Verified: portfolio 824 now resolves code 26 → "Mains Gas".

Wall Type / Roof Type / Heating System become filterable. Their resolved values
are free-text (override vocab / epc_energy_element.description / legacy column)
with high cardinality, so exact dbValues can't scale — each coarse bucket is a
set of case-insensitive needles matched by prefix (wall/roof) or substring
(heating). classifyDescriptor is the pure twin of the SQL bucketing in
buildConditionSql; the "Unknown" bucket matches the complement of every known
needle. Buckets validated against the full prod vocabulary (heating Unknown =
28/640k rows). Wired through FilterField, FIELD_OPTIONS, buildConditionSql,
EPC_JOIN_FILTER_FIELDS and the count-query override joins; columns stay
free-text display. Coverage tests assert every Wall/Roof/MainHeatingSystem
override value buckets to a real, non-Unknown label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:30:50 +00:00
Khalim Conn-Kowlessar
0131586a68 docs(context): note Wall/Roof/Heating join the override-resolved list columns
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:37:11 +00:00
Khalim Conn-Kowlessar
4f40c0b13d feat(portfolio): smaller list fetch, larger export, wall/roof/heating columns
Three property-list improvements:

1. Fetch 250 rows initially, not 1000 (DEFAULT_LIMIT/DISPLAY_LIMIT). The table
   paginates at 7/page and isn't bulk-scrolled, so the join-heavy query stays
   fast; load-more still pulls the rest on demand.

2. Export now fetches its own batch (up to EXPORT_LIMIT) for the current filters
   instead of dumping the on-screen rows — so the small display fetch doesn't cap
   it — and the cap is raised 1000 -> 6000. Export button shows a loading state.

3. Add Wall Type / Roof Type / Heating System optional columns, resolved through
   the same override precedence as the others (override -> EPC epc_energy_element
   description, lodged over predicted -> legacy property_details_epc -> 'Unknown').
   Per-part components (wall/roof) read the main building part. New generic
   resolveOverrideDescriptor (+5 tests) with SQL twins in epcSources. Columns are
   display-only (no filter — the wall/roof override vocab is too large for chips)
   and included in the CSV export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:36:26 +00:00
KhalimCK
3148ab8bba
Merge pull request #385 from Hestia-Homes/feature/plan-default-plan-index
perf(plan): property-leading index for the default-plan LATERAL (portfolio list load)
2026-07-13 14:04:57 +01:00
Khalim Conn-Kowlessar
8d9927b115 perf(plan): add property-leading partial index for the default-plan lookup
The property-list query (getProperties) resolves each property's latest default
plan via a LATERAL. The existing plan indexes lead with (portfolio_id,
scenario_id, …); with scenario_id unconstrained the lookup scans the whole
portfolio's plans per property. On portfolio 796 (~32k properties) EXPLAIN ANALYZE
showed this LATERAL at 11.5s of an 11.9s query (2.97M shared buffer hits,
11.5ms × 1000 loops).

idx_plan_property_default_latest leads with property_id (partial on is_default)
so the lookup becomes a direct index seek + LIMIT 1 — expected ~12s -> sub-second.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 09:06:13 +00:00
Khalim Conn-Kowlessar
b393b5efe0 refactor(modelling): previewModellingRun consumes shared epcSources fragments
Slice 6. The override → EPC-derived → 'Unknown' rule was inlined in
previewModellingRun and duplicated by the portfolio list. Replace the inline
COALESCE expressions and property_overrides joins with the shared
resolvedPropertyTypeSql / resolvedBuiltFormSql / *OverrideJoin fragments — a
byte-identical substitution — so the modelling preview and the list are provably
in sync (the divergence ADR-0008 warns about is now structurally impossible).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 00:00:50 +00:00
Khalim Conn-Kowlessar
78db9815ba feat(portfolio): remove Tenure from the property list column + filter
Slice 5. Tenure was broken for new-approach (read p.tenure, NULL) but has no
override component and its epc_property.tenure values don't reconcile with the
filter dbValues, so it's removed from the list rather than fixed (ADR-0012):
the optional column, the filter field, TENURE_OPTIONS, and the CSV column. The
per-property building passport (resolvePropertyMeta/resolvePropertyDescriptors)
still resolves tenure, and property.tenure is retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:56:42 +00:00
Khalim Conn-Kowlessar
fc2f7d5b30 feat(portfolio): resolve Main Fuel column + filter via override precedence
Slice 4. The new EPC graph exposes no main-fuel string (documented gap), so for
new-approach properties the landlord override is the only source. resolveMainFuel
returns the raw value (override → legacy EPC → 'Unknown'); the cell's
resolveEnumLabel folds it to a chip. MAINFUEL_OPTIONS.dbValues extended with the
override vocabulary so overridden fuels are both displayable and filterable —
guarded by a coverage test over MainFuelValues. resolvedMainFuelSql mirrors it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:51:40 +00:00
Khalim Conn-Kowlessar
558e20f2fe feat(portfolio): band-native Construction Age column + filter (was Year Built)
Slice 3. Construction age is modelled as an RdSAP band, not a year (ADR-0012) —
the only representation that faithfully carries an 'Unknown' override. resolve-
ConstructionAge owns override(letter)→label / EPC band(letter)→label / legacy
year→band bucketing / 'Unknown', with 10 unit tests incl. band boundaries.
resolvedConstructionAgeSql mirrors it (emits the band label). YEAR_BUILT_OPTIONS
becomes the band labels + Unknown, guarded against label/en-dash drift by a new
epcSources.test. Column + filter + CSV header renamed to 'Construction Age'
(internal 'yearBuilt' key retained to avoid a wide rename).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:35:14 +00:00
Khalim Conn-Kowlessar
e2ab52e4f9 feat(portfolio): resolve Built Form column + filter via override precedence
Slice 2. Same shape as slice 1: resolveBuiltForm + builtFormOverrideJoin /
resolvedBuiltFormSql, wired into getProperties SELECT, the filter colMap, and the
count query's join set (builtForm added to EPC_JOIN_FILTER_FIELDS). BUILT_FORM_OPTIONS
gains Unknown (Not Recorded now matches only an explicit override of that literal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:26:07 +00:00
Khalim Conn-Kowlessar
3bf816130c feat(portfolio): resolve Property Type column + filter via override precedence
Slice 1 of the portfolio-list descriptor fix. Property Type was read from the
raw property row (p.property_type), which is NULL for new-approach properties
(updated_at >= 2026-06-01), so the column and filter broke for new portfolios.

- New pure resolver resolvePropertyType (descriptorResolution.ts, mirrors the
  provenance.ts split) owning override → EPC-derived (lodged over predicted,
  RdSAP codes mapped, legacy fallback) → 'Unknown', with 9 unit tests.
- SQL twin resolvedPropertyTypeSql + propertyTypeOverrideJoin in epcSources.ts.
- Wired into getProperties SELECT, the filter colMap, and the count query's
  conditional join set (propertyType added to EPC_JOIN_FILTER_FIELDS).
- PROPERTY_TYPE_OPTIONS gains Park home + Unknown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:21:22 +00:00
Khalim Conn-Kowlessar
6871aff62a docs(portfolio): ADR-0012 — list resolves descriptors via override precedence
Records the decision to make the portfolio property-list override-aware for
Property Type / Built Form / Construction Age / Main Fuel, extending ADR-0008's
override → EPC-derived → Unknown precedence to a second surface via shared
epcSources fragments. Adds a CONTEXT.md Flagged-ambiguities note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:21:21 +00:00
KhalimCK
a88c6abf54
Merge pull request #382 from Hestia-Homes/feature/scenario-fabric-first-ui
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
feat(scenarios): surface and set fabric-first across scenario pages
2026-07-09 17:46:34 +01:00
Khalim Conn-Kowlessar
48bfac490f style(scenarios): make the fabric-first toggle distinctive
The plain bordered checkbox blended into the Measures step. Give it the
emerald "Fabric first" identity used by the list/detail indicators — a
layers icon, an "Optimisation order" eyebrow, and a state-reactive tint
so the same green signals fabric-first across create, list and detail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 14:18:05 +00:00
Khalim Conn-Kowlessar
2f3e4b4c8d feat(scenarios): surface and set fabric-first across scenario pages
Adds the fabric-first constraint to the app-authored scenario flow:

- Create: a "Fabric first" toggle in the Measures step (Step 2) with an
  explanation, mirrored in the Step 3 review and carried through the
  "Use as template" (?from=) flow.
- List card: a "Fabric first" chip in the constraints box, shown only
  when the scenario is fabric-first.
- Detail page: an always-shown "Fabric first" config row (chip +
  explanation, or "Off").

Domain layer (TDD): validateScenarioConfig carries the flag and defaults
it to false; scenarioToInsertRow persists it; findExactDuplicate treats
it as part of the immutable config identity, so two scenarios differing
only in fabric-first are distinct. Threaded through ScenarioListItem,
both query mappers, the create route, and the new-scenario loader.

Backend already reads scenario.fabric_first for id-based modelling runs,
so no dispatch changes are needed.

Docs: adds a "Fabric first" glossary entry to CONTEXT.md, softens the
stale "only measure-constraint" line, and pins the distinction from the
"Fabric only" quick-start.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 13:51:55 +00:00
KhalimCK
e48a5f4ae2
Merge pull request #381 from Hestia-Homes/feature/scenario-fabric-first
feat(db): add fabric_first flag to scenario
2026-07-09 12:37:01 +01:00
Khalim Conn-Kowlessar
9e57596b64 feat(db): add fabric_first flag to scenario
Adds `fabric_first boolean NOT NULL DEFAULT false` to the scenario table
via the drizzle model, with the generated migration (0268).

Migration to be run via `npm run migration:migrate` once merged to main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 11:32:58 +00:00
KhalimCK
9fa11b0b6d
Merge pull request #379 from Hestia-Homes/fix/hmo-classification-code
fix(postcode-search): classify RH (HMO) family as addable residential
2026-07-09 11:54:12 +01:00
Khalim Conn-Kowlessar
fac425762e fix(postcode-search): classify RH (HMO) family as addable residential
7 Leyton Avenue (UPRN 100031252876) came back from OS Places as RH03
("HMO Not Further Divided") and rendered as "Non-residential", so it
couldn't be added from the postcode-search "add properties" journey.

classifyOsCode decided "residential?" purely on an RD prefix, dropping the
whole RH (HMO) family — even though RD07 (the RD-family HMO code) was
already mapped to House. Extend the residential check to RH and map
RH01/RH02/RH03 to House (built form unknowable), consistent with RD07.

Updates ADR-0007's enumerated mapping table to stay truthful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 10:33:07 +00:00
KhalimCK
8bbcd77974
Merge pull request #376 from Hestia-Homes/feature/bulk-document-download
feat(live-reporting): bulk document download (Documents tab)
2026-07-08 17:40:53 +01:00
Khalim Conn-Kowlessar
098a5ccffc fix(live-reporting): bulk download poller no longer hangs after completion
The status poll only declared "ready" when it could parse presigned_url out
of sub_task.outputs, so a finished job whose link used a different key (or
whose completion was only reflected in status) sat on "preparing" forever.

- Derive terminal state from completion/failure *status* (task or sub_task,
  incl. jobCompleted), not just the parsed link.
- Tolerate link-key variants (presigned_url / presignedUrl / download_url /
  url; package_s3_key / s3_key).
- When completed but the link can't be surfaced in-app, show a "ready — check
  your email" card instead of hanging.
- Ease the poll cadence to 8s (assembly takes minutes; email is the real
  channel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:34:00 +00:00
Khalim Conn-Kowlessar
7a7f7e157e feat(live-reporting): select page vs all pages in bulk download
The header tick now selects the current page (its intuitive meaning). When
the whole page is ticked and more filtered rows exist, a Gmail-style banner
offers "Select all N across pages" (and "Clear selection" once everything
is selected) — so users can grab a page-sized chunk without selecting the
entire project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:04:53 +00:00