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>
The 0222-0225 batch backfilled plan_id/material_* onto recommendation
inside drizzle's single migration transaction. On the production table
(~26M rows) that held an AccessExclusiveLock for hours, blocked unrelated
migrations, exhausted EBS throughput credits, and could not report
progress or resume.
Split schema DDL from data backfill:
- 0222/0224: keep only instant metadata-only DDL (ADD COLUMN + FK
NOT VALID); drop the inline CREATE INDEX.
- 0223/0225: backfills removed (now no-ops pointing to the script).
- New src/app/db/backfill-recommendation-denormalization.ts: idempotent,
resumable, batched (committed per batch) backfill that then builds the
indexes CONCURRENTLY and validates the FKs online.
- CONTEXT.md: document recommendation->plan (1:1) and recommendation->
material (now at most 1; legacy multi-material rows reconciled).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-Property fact layer deferred by ADR-0004: one row per
(property, building_part, override_component) holding the resolved
landlord-override enum as a denormalised text snapshot, plus the raw
spreadsheet description it resolved from.
Schema only — no writer yet. The bulk_upload_finaliser application will
populate it (recalculate-on-rerun via upsert on the unique key). Design
and rationale (snapshot-not-FK, drop source, recalculate semantics) in
docs/design/bulk-upload-finaliser.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Verify classification" acknowledgement flag for ADR-0004 Step 1.
Gates Finalise whenever an upload has classifier columns, independent of
multi-entry, so it lives in its own column rather than on
multiEntryOrdering.
Plain additive column (boolean NOT NULL DEFAULT false), no data backfill,
so it applies cleanly with either `drizzle-kit migrate` or `push`. The
feature that reads/writes it lands separately on
feature/frontend_landlord_overrides.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Additive nullable jsonb column for the user-confirmed building-part ordering
(ADR-0004, issue #297), generated off main. No data migration. The jsonb shape
type is co-located with the column so the schema is self-contained.
Split out as its own migration PR so the DB change can be approved and deployed
independently of the feature's app code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Additive nullable jsonb column for multi-entry building-part detection
(ADR-0004), generated off main. No data migration. The jsonb shape type is
co-located with the column so the schema is self-contained.
This is the only migration the multi-entry feature needs that isn't already
on main (the column_mapping inversion, sub_task.service, and the
landlord_overrides tables are all already merged). Split out so the DB change
can be approved and deployed independently of the feature's app code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Splits the DB migration artifacts off the frontend branch so they can
land independently:
- 0215_invert_column_mapping: one-shot data migration inverting
bulk_address_uploads.column_mapping from header->field to field->header
(drops 'skip' entries). One-shot — see file header and ADR-0003.
- 0216_add_subtask_service: adds sub_task.service (nullable text) to tag
which pipeline a subtask belongs to (address2uprn vs
landlord_description_overrides).
Includes the subtask.ts schema source for 0216 so drizzle's snapshot
stays consistent. No app code depends on these yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The risk drill-down's coordinator-stage table showed "Yes" for every row,
which carried no useful signal. It also missed properties where the
coordinator wrote a comment without setting the growth flag.
Include rows where dampmould_growth is "yes" (case-insensitive) OR the
comment is populated, and render the comment in the cell — truncated
with a popover for the full text, or a "no note from coordinator"
placeholder when the row is here only because the flag was ticked.
Also drop the typo in the schema property name
(damnp -> damp); SQL column unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same email now contains a 6-digit code and a magic link, both backed by a
single verificationToken row. After submitting their email, the user
lands on /auth/verify-code with a single-input form (inputmode=numeric,
autocomplete=one-time-code, auto-submit on 6 digits or paste) and can
either type the code or use the link from the email. Either path
consumes the same row — single-use, replace-on-resend.
This is the structural fix for the silent-quarantine pattern observed
with Atkins and Sustainable Building UK: corporate gateways are happier
with short transactional content than long opaque token URLs, and a
code can't be broken by SafeLinks-style URL rewriting. The link path is
preserved so users whose email gets through unmangled keep one-click UX.
Security:
- Codes are 6-digit, crypto.randomInt-generated, stored as sha256
hashed against NEXTAUTH_SECRET on the same row as the link token
- 5-attempt lockout per code (attempts column); 6th attempt with the
correct code still fails
- Per-email send rate limit: 5/hour fixed window (authRateLimits
table); 6th send returns an error
- Code + link share a 10-minute window (maxAge dropped from 1h)
- Resending replaces any prior token rows for the identifier so only
the latest send is ever live
Implementation:
- verificationCode.ts holds generateCode + hashCode + the pure
evaluateCodeAttempt decision tree; 9 unit tests cover every branch
of the verification outcome (no-such-row, expired, locked-out, ok,
wrong-with-newAttempts, locked-out-still-rejects-correct-code)
- sendVerificationRequest now hashes the URL token the same way
/verify/[token]/page.tsx does, applies the rate limit + records the
code + replaces older rows in two transactions
- CredentialsProvider (id: "email-code") calls evaluateCodeAttempt
inside a transaction, handles all 5 outcomes, creates the user on
first successful code (parity with the magic-link callback path)
- oauthId backfill in the signIn callback is now guarded on
account.type === "oauth" so the credentials flow doesn't pollute
oauthProvider with "email-code"
- Migration is additive: code_hash nullable, attempts default 0; new
authRateLimits table is independent. In-flight tokens at deploy time
keep working via the link path.
Vercel preview deployment is the test surface; a Mailpit + Cypress E2E
loop is intentionally deferred per the lean-setup plan in docs/wip/
auth-email-code-fallback-plan.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>