First to Site
Release 3.7

Release 3.7 Overview

UAT/staging auto-deploy, Mercure removal, optimistic lock iteration, invoice UX fixes, address canonicalisation

Release 3.7 tagged on 21 April 2026 as v3.7.0, with patches v3.7.1, v3.7.2, and v3.7.3 landing the same day. The cycle delivered a full UAT + staging auto-deploy pipeline, retired the unused Mercure real-time infrastructure (-1,518 lines), rolled back an over-aggressive optimistic lock that was producing 7-9 false-positive errors out of 10 saves, cleaned up invoice UX (layout, redirect, PO field), and pushed address format canonicalisation across every layer (dossier-api, Project entity, address autocomplete JS) after a long-standing Trello request.

Items Included

Deployment Pipeline

Preprod and production remained manual-only by deliberate design (fts1 is IP-allowlisted, so GitHub-hosted runners cannot SSH in), but UAT and staging moved to push-triggered auto-deploys. Engineers can now push to a feature branch, have it deployed to UAT for client sign-off, and track release health through the dev dashboard tabs that accurately describe the 5-environment / 3-host reality for the first time.

  1. UAT + staging auto-deploy workflows - .github/workflows/deploy-uat.yml and deploy-staging.yml run release:* on push. Authenticated via DEPLOY_SSH_KEY + DEPLOY_KNOWN_HOSTS secrets.
  2. Preprod stays manual - An attempt at auto-deploying preprod via self-hosted runner on fts1 was tried and reverted. fts1's IP allowlist makes manual the right default for preprod + production.
  3. Env-named symlinks - Release script swaps ~/<env> instead of hard-coded ~/live so each host's symlink name matches its env identity.
  4. post-release.sh restarts - Extended with UAT and staging branches driven by DEPLOY_ENV (falls back to LIVE_LINK_NAME).
  5. Dev dashboard rewrite - Release Preparation and Deployment tabs in scripts/dev-dashboard-server.ts rewritten to match current reality (5 envs, 3 hosts, PHP-FPM 8.3, ~/live symlinks, per-env npm scripts).
  6. release: script rename* - Dropped the :safe suffix now that there's only one release script per env. Deleted the dead scripts/release-prod-symlink.ts.

Data Integrity (Part II)

The optimistic-lock work shipped in v3.6 turned out to be too aggressive for the admin flow. This release keeps the entity-level Doctrine @Version protection but removes the explicit $em->lock() check that was firing on 70-90% of admin saves because background workers and hourly crons were bumping the version between form render and submit. The ordering portal API retains its lock since clients opt in by submitting a version field.

  1. Optimistic lock extended to ordering portal API (v3.7.0) - ProjectResource DTO gains a version field; ProjectProcessor::updateProject() calls $em->lock() when the client echoes a version. Returns HTTP 409 on conflict.
  2. Explicit form-side lock removed (v3.7.1) - The aggressive $em->lock() in ProjectCrudController::updateEntity() removed after users reported 7-9 false positives per 10 saves. Entity-level @Version still catches true concurrent-save races at the DB layer.

Mercure + ux-notify Removal

Mercure was installed in March 2023 and wired into NotificationService in January 2024 ("add real-time notification"), but no Mercure hub was ever deployed on any environment - every call to $hub->publish() has been silently failing for 16 months. The JS EventSource subscription had also been commented out. This release removes the entire stack cleanly: no user-visible change, one less phantom dependency, 1,518 fewer lines of code.

  1. Removed symfony/mercure-bundle + symfony/ux-notify - Transitively removes symfony/mercure, symfony/mercure-notifier, lcobucci/jwt.
  2. Removed MercureBundle + NotifyBundle - From both app/config/bundles.php and ordering/config/bundles.php.
  3. Deleted mercure.yaml - Both packages.
  4. Refactored NotificationService - HubInterface dependency and publishNotification() / runNotifications() / runOneNotification() / getNotificationPublicUrl() methods removed. The DB-backed notification dropdown still works; only the dead realtime layer is gone.
  5. Removed runRealTimeNotification endpoint - Its JS caller was commented out.
  6. Cleaned commented-out EventSource handler - ~60 lines of dead JS in custom-ea.js removed.

Invoice UX

Three targeted fixes to the admin invoice workflow plus a decision about where the PO number belongs in the data model.

  1. Invoice index page header layout - Template was overriding content_header_wrapper wholesale and fighting EasyAdmin's default flex layout; heading rendered past the right edge of the action buttons. Switched to overriding just the page_actions block.
  2. Invoice save redirect - After saving an invoice, users were redirected back to the new-invoice form URL instead of the project edit page. Query-string projectId was being dropped on POST; now pulled from the saved entity's getProject() relationship.
  3. PO Number field moved off Project - The purchaseOrder field exposed on the admin project new/edit forms in v3.6 was removed. PO number lives on the invoice (ProjectInvoice::$purchaseOrderNumber) and is visible on the invoice view modal.

Address Canonicalisation (Trello zH3TlNMp)

A long-standing Trello card (first raised June 2024, re-raised December 2025) asked for a consistent canonical address format across the platform, for example LOT 555 (5) KIELDER CRESCENT CLYDE 3978 rather than the raw LOT 555 5 KIELDER CRESCENT CLYDE 3978. This release applies it at every layer: the dossier-api response, the address autocomplete's input field on selection, the Project entity setter/getter, and the enrichment service.

  1. dossier-api canonicaliser - canonicaliseLotAddress() helper in both api.ts and parcel-summary.ts. Applied at the lookup_pending fallback and the street_address fallback paths where raw addresses were being echoed as lot_address.
  2. Address autocomplete selection - buildCanonicalLotAddress(address, lotNumber) method in both admin and ordering address_autocomplete_controller.js. Handles three cases: already canonical, LOT prefix without parens, or no LOT prefix at all.
  3. Project entity canonicaliser - Project::canonicaliseLotAddress() static helper applied in both setKnownAddress() (on write) and getKnownAddress() (on read) in both app/ and ordering/ entities. Legacy DB rows render canonically without a migration.
  4. Enrichment service - ProjectInsightEnrichmentService now prefers dossier-api's lot_address over the raw ezi_address when populating knownAddress.

Small UX & Feature Work

  1. Estate-specific title prediction - Estate::$titlePredictionDays column with per-estate override (falls back to TITLE_PREDICTION_DAYS env var, then 28-day default).
  2. Order submitted date toggle - Env-gated toggle to hide the order submitted date column by default.
  3. QA checklist builder disabled - Temporarily rolled back pending further work.
  4. Google API credentials rotation - Production DA Google API token rotated.
  5. Messenger worker runs without Symfony debug - Prevented debug-mode state leaking into async workers.
  6. Purchase Order Number on invoice view modal - Added to the read-only invoice view (between Customer Job Number and Line Items), mirrored across admin and ordering.

How To Read This Section

Each child page covers one deliverable area. The optimistic-locking page cross-references release 3.6's entry; the address-canonicalisation page references the Trello card that drove the work. Items marked with v3.7.x version tags indicate which patch release they shipped in.