Release Notes
This page tracks user-visible changes shipped to the Formation app — new features, behaviour changes, and notable fixes. Internal refactors that don’t change how the app looks or works are omitted.
Versions match the Version shown on the Settings → System Info page.
Fix: scheme size now excludes in-progress Extension developments
Section titled “Fix: scheme size now excludes in-progress Extension developments”The headline Scheme Size (Gross / Net) on a scheme used to include every Extension development (type A.04) regardless of status, while the Total by use table on the same scheme detail page correctly excluded Extensions that weren’t yet Complete. The two figures could disagree by the size of any in-progress Extension. The headline now applies the same rule as the breakdown: Extensions only contribute once their status is Complete (E). The Reduction rule is unchanged (counts when Under Construction D or Complete E).
Effects you may notice: schemes with in-progress Extensions will show a smaller headline size on the schemes list, on scheme cards, and inside investment-event size auto-population for non-Strata events. Duplicate detection also scores schemes by size, so a few near-duplicate matches may shift. The per-use breakdown is unchanged. See Schemes → Auto-aggregated sizes for the full per-type / per-status table.
Fix: lookup-table edits now refresh dropdowns immediately
Section titled “Fix: lookup-table edits now refresh dropdowns immediately”Edits made through Admin → Lookup Tables (rename a tier, add a building type, re-order a role, etc.) used to keep showing the old values in dropdowns for up to 6 minutes — the cache invalidation interceptor was running at the wrong point in the save lifecycle and never actually cleared the cache, so dropdowns served stale data until the per-key TTL expired. The change is now reflected on the next page load.
Fix: occupier event rent-free vs lease length validation
Section titled “Fix: occupier event rent-free vs lease length validation”The cross-field check that compared Rent Free against Lease Length wasn’t accounting for the different units — Lease Length is captured in years, Rent Free in months — and was rejecting valid combinations (e.g. a 10-year lease with an 18-month rent-free period). The validator now compares months against years × 12, so a 10-year lease permits up to 120 months rent-free.
Development closure cascade is now reversible
Section titled “Development closure cascade is now reversible”Setting a Development to a Closed status fires the existing cascade — every other Development on the scheme inherits the same Closed status + closed date, and every OccupierEvent on the scheme is marked retired. That part is unchanged. New behaviour: if you then change that same Development out of Closed (e.g. you picked Closed by mistake and switch it back to Complete), the cascade reverses. Siblings that were closed by the same cascade flip back to the new status with their closed date cleared, and OccupierEvents whose retired date matches that cascade un-retire.
Reversal is bounded by the closed date: developments that were closed independently earlier (different closed date) stay closed. Same for occupier events with a different retired date. If the cascade-victim rows have been manually edited since (e.g. their closed date was changed by hand), the reversal won’t recognise them as victims and they stay closed — those can be reopened by hand.
Admin → Lookup Tables: edit lookup data in-app (Admin-only)
Section titled “Admin → Lookup Tables: edit lookup data in-app (Admin-only)”The Admin → Lookup Tables sidebar entry opens a new in-app editor for the 27 lookup tables that drive dropdowns throughout the app (building types, market types, statuses, currencies, role tables, attribute categories, etc.). Pick a table on the left; the right pane shows its rows. Inline rename, create new rows, delete, and drag-to-reorder rows up and down to set their display order. Deleting a row requires that no other record references it; the server’s rejection lists the affected rows.
Most rows are fully editable, including the machine-readable code. The exception is DevelopmentStatus rows whose code starts with B / C / D / E / F — those codes are referenced by completeness scoring and scheme-total exclusion rules, so the Code field is disabled on those rows with a tooltip explaining why. The visible name and sort order stay editable on every row.
Replaces the placeholder pages at /tier-types and /admin-boundary-types, which were never built out. Admin-only — non-admin users won’t see the sidebar entry.
Internal: generic admin write surface for every enum/lookup table
Section titled “Internal: generic admin write surface for every enum/lookup table”A single endpoint /admin/enums/{table}/{id} now handles Create / Patch / Delete for all 27 admin-editable lookup tables (building types, market types, role tables, statuses, currencies, etc.). Replaces the per-type /AdminBoundaryTypes and /TierTypes write routes; the IsSystem code-immutability check, FK-usage delete guard, and admin-only auth gate now apply uniformly across every editable enum, not just the two from PR 2. Admins also can’t patch IsSystem itself in either direction — that would let them forge or unfreeze a system row. The Admin UI in the next release sits on top of this single surface. No user-facing UI change yet.
Internal: every enum-table edit now reaches the audit log
Section titled “Internal: every enum-table edit now reaches the audit log”Edits to lookup / enum tables (rename a tier, re-order a role, etc.) now appear in /AuditLogs alongside every other entity change — same Create / Update / Delete rows, same before/after JSON, same user attribution. Most of the 28 enum tables already did this via the existing BaseEntity audit interceptor; JobType was the odd one out (its model never inherited BaseEntity) and is now wired in. Visible immediately for the two enum tables that have admin write endpoints (AdminBoundaryType, TierType); the remaining 26 will become editable when PR 4 lands the generic enum write service. No user-facing UI change in this release.
See Technical → Database → Enum tables and behaviour flags → Audit trail.
Internal: enum-table IsSystem flag + admin-only write gate + delete-with-usage guard
Section titled “Internal: enum-table IsSystem flag + admin-only write gate + delete-with-usage guard”The AdminBoundaryType and TierType write endpoints are now Admin-role only — non-admin sign-ins get 403 on POST/PATCH/PUT/DELETE. Every lookup / enum table also carries a new IsSystem column: rows shipped with the product (every row in the DB today) are stamped IsSystem = 1; admin-created rows will pick up 0 from the column default. When IsSystem = 1, the row’s *Code column is rejected for change — the visible name and sort order stay editable, but the machine-readable code is frozen. Before a delete, the handler runs a metadata-driven probe across every FK pointing at the table and refuses the delete with a friendly listing if any dependent rows exist (“Cannot delete tier ‘Tier 1’ because it is referenced by: 7 BuildingType, 2 DevelopmentStatus.”). The same guards will reach the rest of the enum tables when PR 4 introduces the generic enum write service. No user-facing UI change in this release.
See Technical → Database → Enum tables and behaviour flags → System rows.
Internal: enum-table versioning columns (Slowly Changing Dimension Type 2)
Section titled “Internal: enum-table versioning columns (Slowly Changing Dimension Type 2)”Every lookup / enum table now carries the same lifecycle columns: IsActive, Version, CreatedBy, ValidFrom, ValidTo. Ten tables that didn’t have them gain the columns (the others already did, unused). EnumService filters every read by ValidTo IS NULL so superseded rows are hidden from dropdowns — no behaviour change today because every existing row has ValidTo = NULL, but it’s the foundation the in-app Admin UI’s Save-as-new-version flow will sit on top of. No user-visible change in this release.
See Technical → Database → Enum tables and behaviour flags → Versioning.
Internal: enum business rules now flag-driven
Section titled “Internal: enum business rules now flag-driven”The business rules that branch on specific enum rows (“Closed” status, “Investor”/“Vendor” roles, “Operator” role, “Common” market type, “Whole”/“Share”/“Strata” ownership, “Extension”/“Reduction” development types) no longer match the row’s visible Name or Code. Each load-bearing row carries a typed flag (IsClosedStatus, RoleKind, IsOperatorRole, IsCatchAll, OwnershipKind, IsExtension, etc.) and the rules query the flag. This is foundation work for the in-app Admin UI for managing lookup tables — admins can rename and re-order rows without breaking completeness scoring, cascade behaviour, list-view aggregation, or role filters. No user-facing behaviour change in this release.
See Technical → Database → Enum tables and behaviour flags for the convention and the table of where each flag is read.
Per-scheme Share % on multi-scheme investment events
Section titled “Per-scheme Share % on multi-scheme investment events”When an investment event spans more than one scheme, each scheme now has its own Share % input next to it in the Schemes list. Shares auto-distribute equally on add/remove (two schemes → 50/50, three → 33.33 each, etc.), and edits stick between count changes — same shape as the existing investor-share rule. A single-scheme event has no share input. Individual values are bounded 0–100, and the total across all linked schemes can’t exceed 100% (the form shows a running total under the list and the server re-checks on save). Partial allocations are still allowed.
See Investment Events → Scheme shares auto-distribute equally.
Portfolio sector-markets: Market dropdown scoped to the picked Sector
Section titled “Portfolio sector-markets: Market dropdown scoped to the picked Sector”On the Portfolio edit form’s sector-market rows, the Market dropdown now only lists markets that exist within the picked Sector. Pick Industrial and only Industrial markets appear; pick Retail and only retail markets do. Sub-sectors (e.g. Logistics under Industrial) inherit their parent sector’s market list. Sectors without a dedicated MarketType (currently Hotels, Health Care, Other) fall back to the Common markets so the dropdown isn’t empty. Switching the Sector to one with no overlap clears the previously-picked Market — same behaviour as the existing Country reset.
See Portfolios → Market dropdown is scoped to the picked Sector.
”Replaces Owner” auto-retires the prior investment event
Section titled “”Replaces Owner” auto-retires the prior investment event”Picking a prior investment event in the Replaces Owner field now automatically ticks Is Closed? on that prior event, with the Closed Date set to the new event’s date — no need to open the prior event and tick it by hand. The cascade is one-way: clearing the link later doesn’t re-open the prior event.
See Investment Events → “Replaces Owner” auto-retires the prior event.
Scheme closure is now derived from its Developments
Section titled “Scheme closure is now derived from its Developments”The Is Closed? tick on the Scheme edit form has been removed. A scheme now reads as Closed when every development on it sits on the Closed status, and the badge takes its date from the developments. Setting a single development to Closed automatically copies the status and closed date to every other development on the scheme, and ticks Is Closed? (with the same date) on every occupier event linked to that scheme. Investor events are deliberately left untouched. The cascade is one-way — to re-open a scheme, edit each affected record manually.
See Schemes → Scheme closure is derived from Developments.
Yield Type column on the Investment Events list
Section titled “Yield Type column on the Investment Events list”The Investment Events list now has a Yield Type column next to Yield (%) showing the unit the yield was recorded in — Gross / Net / Initial / Cap Rate. The column is included in Excel exports alongside the yield value so analysts can disambiguate without going back to the source event.
Statistic columns: Unit of measure filtered by Statistic
Section titled “Statistic columns: Unit of measure filtered by Statistic”When adding a Statistic column on a scheme, the Unit of measure dropdown now narrows to units that make sense for the picked Statistic — Rent only allows Rent units (e.g. £/sqft/year), Vacancy & Occupancy only allows Size units (sqm, sqft, etc.). Switching the Statistic clears the previously-picked unit so an out-of-category value can’t slip through. Rate sub-statistics (Occupancy Rate, Vacancy Rate) still display as % regardless of the picked unit, as before.
See Schemes → Statistic columns are typed by unit category for the full rule.
WALT (Weighted Average Lease Term) on investment events
Section titled “WALT (Weighted Average Lease Term) on investment events”Investment events now carry a WALT field, always expressed in years. The edit form has a WALT (years) input under the Yield row, and the detail card surfaces it next to Yield when a value is set. The field is optional — leave it blank when the deal has no calculated WALT (vacant possession, ground rent, etc.).
Fix: new addresses now show their Markets immediately
Section titled “Fix: new addresses now show their Markets immediately”Previously, when you created a brand-new address with a map pin, the Markets column on the Address list stayed blank until you attached at least one scheme — even though the underlying spatial sync had already linked every market boundary the address fell in. The list-view filter was scoping the column to the MarketTypes represented by the address’s schemes, which is the right behaviour once schemes exist but unhelpful before any are added.
The column now shows every linked market while the address has no schemes; the scope-by-scheme behaviour kicks back in as soon as the first scheme lands. No change for addresses that already have schemes.
Confidential investment events and portfolios
Section titled “Confidential investment events and portfolios”Investment events and portfolios now carry an Is Confidential? flag, ticked from the edit form. Confidential records display a Confidential badge on their card and a new (off-by-default) Is Confidential column appears on the matching list page — turn it on from the column picker to filter on it.
The flag only affects Excel exports. When a confidential record is included in an export of the Investment Events or Portfolio list, the Yield and Price cells (currencies plus, for investments, the local value) are left blank instead of carrying the number — formulas like SUM and AVERAGE over the column still work, they just skip the redacted rows. Every other column exports as usual, and the in-app display is unchanged.
See the per-entity business rules for the exact field lists: Investment Events, Portfolios.
Activity log: clickable links to Schemes, Investments and Occupiers
Section titled “Activity log: clickable links to Schemes, Investments and Occupiers”The Entity column on the activity log (Settings → Activity Log) now links Scheme, Investment Event and Occupier Event rows. Clicking the link opens the address detail page with the matching card pre-selected — the same deep-link the per-card Share action produces. Address, Company and Portfolio rows continue to link to their own detail pages as before.
Admin Boundaries
Section titled “Admin Boundaries”A new Admin Boundaries column appears on the Address, Scheme, Company, Occupier Event, and Investment Event list pages — listing the admin areas each address sits in, ordered by tier (Country → Region → County → City → District) and comma-separated. The column supports the same hierarchical filter as Market: open the filter to pick boundaries from the same parent / child tree admins use when editing. The links update automatically whenever an address’s location changes or a boundary’s polygon is edited; no manual upkeep.
Admin Boundaries themselves are managed by Admins via a new /admin-boundaries area. The full in-app polygon editor follows in the next release; in this one, admins can manage boundaries (and the underlying Admin Boundary Type / Tier Type dictionaries) via the OData / write API. Default tiers (Country → Region → County → City → District) are seeded on first deploy.
See Admin Boundaries for the rules that govern how addresses auto-link, the tier hierarchy, and how the list column is composed.
Share % field always visible on investor and owner lists
Section titled “Share % field always visible on investor and owner lists”Previously the per-company Share % input on the Investor list (on investment events) and the Owner list (on schemes) only appeared once two or more parties were added. A single party can legitimately hold a partial share — e.g. one investor at 75% with the remainder unallocated — so the field is now visible whenever the role has at least one entry. The auto-distribute behaviour (equal split when the party count changes) is unchanged. The “must equal 100%” warning now also fires for a single party at less than 100% under Whole ownership.
Faster, more accurate map view
Section titled “Faster, more accurate map view”The map on every list page (Addresses, Schemes, Companies, Occupier and Investment Events, Portfolios, Market Boundaries) is noticeably faster and more reliable.
- Market boundary tiles are now simplified at the zoom level you’re looking at, so tiles arrive faster and the map stays responsive when you pan and zoom.
- Tiles you see immediately reflect boundary edits — saving in the boundary editor now invalidates the cache, no more waiting for a refresh or for a stale tile to expire.
- Empty tiles at the world view are no longer re-requested over and over.
- The map now re-fits to your results when you change a filter, sort, or page — previously the viewport could stay stuck on the old bounds.
- The boundary-type selector responds immediately instead of waiting the previous third-of-a-second debounce.
Company detail page now lists every record the company is involved in
Section titled “Company detail page now lists every record the company is involved in”Opening a company used to show only the company’s own attributes. The page now also lists every Scheme, Investment Event, Occupier Event, and Portfolio the company appears on, in four side-by-side columns. All four use the same card components that already render on the address detail page, so the layout, badges, dates, prices, sizes, and edit affordances match what you see elsewhere. Schemes show their name, building-type icon, description, owners and developers, external links, and tags — the same overview block that sits at the top of each scheme card on an address page. Each card also carries a small breadcrumb-style link to the parent address (or the portfolio detail page) for one-click navigation. Columns with more than three cards start collapsed (matching the existing rule on each scheme’s event panels) and gain an “Expand all / Collapse all” toggle; individual cards still expand and collapse on their own as before. The most recent fifty of each kind are shown; if there are more, a footer line states how many remain.
Deep-link to a specific scheme, occupier, or investment on an address page
Section titled “Deep-link to a specific scheme, occupier, or investment on an address page”The address-page URL fragment that highlights a single card now also
accepts the analyst-friendly numeric IDs introduced alongside the
/addresses/1 route shortcut. Use a one-letter type prefix:
/addresses/1#s5highlights the scheme withSchemeId5./addresses/1#o12highlights the occupier event withOccupierEventId12./addresses/1#i7highlights the investment event withInvestmentEventId7.
The original encoded form (e.g. #PDI--A==) still works.
Append ?edit=1 to the same URL to open that card’s edit panel
automatically — for example /addresses/1?edit=1#o12 lands on the page,
scrolls to occupier event 12, and opens its edit flyout. The flag is
also tolerated after the fragment (/addresses/1#o12?edit=1) for
convenience. The ?edit flag is single-shot: once consumed, it’s
stripped from the URL so a refresh or back-nav doesn’t reopen the
panel. If the fragment doesn’t match anything visible, ?edit is
silently dropped.
The card context menus (the three-dots button) now also include a Share action that opens a small dialog with the deep-link URL and copies it to your clipboard. Available on Address, Company, and Portfolio pages, and on Scheme / Occupier / Investment cards within an address page.
Country pickers now filter as you type
Section titled “Country pickers now filter as you type”The Country dropdowns on the Address, Company, and Portfolio market-sector forms now narrow as you type. Typing “un” leaves only United Kingdom and United States; typing “fra” leaves France. Match is case-insensitive, starts with. Arrow keys move the highlight, Enter picks the highlighted country, Escape closes without changing the selection. Mouse and click behaviour unchanged for users who don’t type.
The underlying SelectField form component now supports an opt-in
filterable mode — other dropdowns (Building Type, etc.) keep the
existing native behaviour for now and can be flipped over individually.
List-page Rent / Price columns now sort by the displayed currency
Section titled “List-page Rent / Price columns now sort by the displayed currency”Sorting the Rent column on the Occupiers list (or Price on Portfolios / Investment Events) used to always sort by the EUR field regardless of your currency preference, while the values shown were in whichever currency you’d picked. With non-EUR currencies the result was a list whose visible numbers looked out of order — sorting was correct in EUR, but each row’s FX rate is independent so EUR-descending doesn’t translate to GBP-descending. The sort now follows the displayed currency, so the column you’re looking at and the order it’s in match.
Switching currency mid-session also rewrites an existing sort silently, so you don’t need to click the column header again to make the order catch up.
Rent on occupier events now shows two decimal places
Section titled “Rent on occupier events now shows two decimal places”Rent on occupier events used to round to a whole number — a £50.49 rent and a £50.00 rent both showed as “£50”. Now displayed to two decimals (e.g. “£50.49 psf pa”, “£50.00 psf pa”) so the difference is visible at a glance. Applies in three places:
- The rent figure under each occupier on the Scheme detail panel.
- The Rent column on the Occupiers list page (
/occupiers). - The Rent column in Excel exports of the same list — both the cell display and the underlying value. Excel’s General format used to hide trailing zeros (50.00 → “50”); the export’s sqm→sqft unit conversion also introduced floating-point drift (a clean £31.50 arrived as 31.49998985 once you clicked into the cell). Rent cells now carry a “0.00” display format and the underlying value is rounded to two decimals so the cell shows the same number whether you’re glancing at it or piping the file into another tool.
Saves on busy records feel snappier
Section titled “Saves on busy records feel snappier”Edits to records with lots of dependants — e.g. an Address tied to many Schemes, Investment Events, Occupier Events and Companies — used to take several seconds to come back. Two changes landed together to fix this:
- Address edits no longer pre-load related data they don’t need. A single-field tweak (e.g. correcting an address line) on a busy multi-scheme address used to spend 6+ seconds materialising linked schemes, market boundaries, and spatial polygons before applying the patch. Those loads are now skipped — bringing a representative edit from ~8.8s to ~2.2s end-to-end.
- Behind-the-scenes list-view refreshes are batched. Saving any record kicks off a refresh of the denormalised list views. Those refreshes used to happen one affected row at a time; they now happen in a single batched round-trip per kind. A wide-fan-out save that previously made hundreds of sequential database calls now makes a handful.
No behaviour change beyond the responsiveness — the same list views are kept in sync.
Concurrent edits no longer silently overwrite each other
Section titled “Concurrent edits no longer silently overwrite each other”Two users editing the same record at the same time used to last-write-wins — whichever save landed second would silently overwrite the first, with no indication that anyone’s changes had been lost. Now, the second save is rejected and the user sees a toast: “This record was edited by another user since you loaded it. Reload the page to see the latest version.” No automatic merge — reload, see the latest, re-apply your changes.
Applies to Address, Scheme, Company, Development, Investment Event, Occupier Event, Portfolio, and Statistic edits — the entities people actually modify concurrently. Sub-collection edits (e.g. a Portfolio’s market sectors, a Scheme’s companies) are also covered.
Writes no longer fail on transient cascade errors
Section titled “Writes no longer fail on transient cascade errors”Saving a record kicks off a behind-the-scenes update of the denormalised list views (so the row reappears in Schemes, Addresses, Companies etc. with fresh fields). If one of those background updates hit a transient hiccup — e.g. a brief database blip — the entire save would appear to fail in the UI, even though the record itself was already written. Users would retry, the second attempt would usually succeed, and the audit log would end up with two write events for the same change.
The save is now considered successful as soon as the record is persisted. Background list-view updates that fail are recorded and self-heal via the existing nightly query-view rebuild, so the data is never lost — and you no longer see ghost “save failed” errors for work that actually went through.
Investment Events: Market filter now matches again
Section titled “Investment Events: Market filter now matches again”Picking a market on the Investment Events list (e.g. United Kingdom) silently returned no results after a recent filter rework. Fixed — the predicate now matches the picked code as a complete dotted segment anywhere within each entry, so country-level picks light up city-level boundary rows again.
Investment size for every ownership type, with smart scheme defaults
Section titled “Investment size for every ownership type, with smart scheme defaults”Investment-event size, size unit, and units fields used to appear only when the Ownership Type was Strata. They now appear for Whole, Share, and Strata alike — record size on any deal.
Two related conveniences:
- Auto-fill from the linked scheme. When you add an investment against a scheme that has a recorded size, the Size and Size Unit fields are pre-filled from the scheme’s total — using your preferred Net/Gross basis. The seed unit is sqm (the basis the scheme stores internally); switch unit or type a different value to override — your edit always wins. Auto-fill only runs when one scheme is linked, so multi-scheme investments stay blank for you to enter manually.
- Auto-create scheme from address. Adding an investment against an address that has no schemes yet now creates the scheme, a development, and a use behind the scenes (marked unverified so you can tell it apart from curated schemes). The development’s use size matches the investment, so the scheme totals are populated from day one. The new scheme uses the Building Type you pick on the investment form.
Once recorded, the investment size and the scheme size are independent — editing one never silently rewrites the other. To make that explicit, editing a development’s use sizes on a scheme that already has investments now shows a confirmation dialog: “You’re changing the scheme size. Existing investment sizes won’t be updated. Continue?”
Reporting → Formation navigation in Fabric (Analysts)
Section titled “Reporting → Formation navigation in Fabric (Analysts)”Correction to the previous release-notes entry on EncodedId columns.
Those columns are still in the OLTP database, but Fabric mirroring
doesn’t replicate computed columns even when marked PERSISTED, so
they don’t reach a Fabric-mirrored warehouse. The “no extra deploy”
framing in the original entry was wrong for the Fabric pipeline
specifically.
The cleanest way to take an analyst from a record they spot in reporting back to the matching page in Formation is:
- Add a DAX calculated column to the Fabric semantic model:
URL = "https://formation.pma.co.uk/schemes/" & FORMAT([SchemeId], "0") - Set its Data Category to
Web URL. - Excel pivot tables, Excel via Analyze in Excel, and Power BI visuals all render it as a clickable hyperlink.
It works because SchemeId (the integer PK) mirrors as an ordinary
column, and the API already accepts /schemes/{SchemeId} URLs
alongside the encoded form. No OLTP schema change needed; same shape
for the other four entities — swap schemes and SchemeId for the
matching slug + PK column.
The EncodedId columns on the OLTP tables stay where they are — they
remain useful for ad-hoc SQL queries against the OLTP database
directly (SELECT EncodedId FROM app.Scheme WHERE SchemeId = 42),
and for any non-Fabric replication pipeline that does carry computed
columns through.
Faster list-page search, with a “Deep search” toggle (default behaviour change)
Section titled “Faster list-page search, with a “Deep search” toggle (default behaviour change)”Searches on the list pages — Schemes, Addresses, Companies, Investment Events, Occupier Events, Portfolios — now run a fast full-text query by default and skip the slower pattern-matching pass that previously ran on every search. In our load tests against dev, this drops typical search latency from 5–25 seconds down to sub-second.
The pattern-matching pass occasionally found rows the fast index missed (mostly case variants and unusual single-character tokens), so we’ve kept it available as an opt-in. There’s now a Deep search switch on every list-page toolbar, next to the Verified toggle — flip it on to union the slower scan back in. Your choice is remembered across sessions.
Exports follow the same default. If you noticed a drop in row counts on an export, re-run it with Deep search switched on first; the export will pick up whatever the list view is currently showing.
EF Core slow-query telemetry (Ops / DBA)
Section titled “EF Core slow-query telemetry (Ops / DBA)”The API now records every EF Core DbCommand execution as a histogram
(formation.efcommand.duration, tagged with command type and success)
and attaches an ef.slow_command span event to the current Activity
whenever a command exceeds 500 ms (configurable via
Telemetry:SlowQueryThresholdMs). Both signals ship to Application
Insights via the existing Azure Monitor exporter, so when an endpoint
feels slow you can now answer “which SQL was the culprit” with a
KQL query rather than a guess. See
Observability for the
queries to copy/paste and the configuration knobs.
Behaviour change is purely additive — no app or DB schema changes, no impact on user-visible flows.
Portfolio market-sector breakdowns sum to exactly 100%, auto-rebalance on edit
Section titled “Portfolio market-sector breakdowns sum to exactly 100%, auto-rebalance on edit”Portfolios now always carry at least one market sector, and the shares across sectors always sum to exactly 100%. The old “up to 100%” rule meant a breakdown could sit at 80% forever without anyone noticing; the new rule makes the breakdown trustworthy.
What this changes in practice:
- Creating a portfolio now asks for at least one sector up front — there’s a new inline table on the create form with one row pre-filled at 100%. Add more sectors if the deal spans several building types; each row needs a Building Type at minimum.
- Editing a share on the detail page redistributes the delta proportionally across the other sectors. If a three-way split was 33.33% / 33.33% / 33.34%, changing the first to 40% now leaves the others at 30% / 30% automatically, rather than you having to type repeating decimals into each row by hand.
- Adding a sector scales the existing rows down proportionally and gives the new row an equal slice. Removing a sector scales the survivors up. The total always stays at exactly 100%.
- The last remaining sector can’t be deleted — the trash icon disables with a tooltip. Replace it with a different sector first if you need to change direction.
Building Type on the Portfolio itself (the single field separate from the sector breakdown) still works the same way for this release — the plan to derive it from the sectors ships in a later change.
Int PKs in URLs for Address, Scheme, Investment Event, Occupier Event, Portfolio (Analysts / DBAs)
Section titled “Int PKs in URLs for Address, Scheme, Investment Event, Occupier Event, Portfolio (Analysts / DBAs)”The five entity routes that carry an EncodedId column in the database
now accept their integer primary key directly in the URL too. So an
analyst looking at SchemeId = 42 in SQL can paste /schemes/42 into a
browser and land on the record — no decoder step needed. The same is
true inside OData filters: /Schemes?$filter=Id eq '42' matches the
same row that /Schemes?$filter=Id eq '<encoded>' would.
The regular encoded-form URLs keep working unchanged; this is an
additive power-user shortcut. The frontend still emits encoded URLs
everywhere it navigates, so normal user workflows are unaffected. Rule
of thumb: a path segment that’s entirely digits is treated as an int;
anything with letters or = padding flows through the encoded decoder
as before. Full reference:
Entity Identifiers → Int PKs work directly in URLs too.
EncodedId column on key entity tables (Analysts / DBAs)
Section titled “EncodedId column on key entity tables (Analysts / DBAs)”The app.Address, app.Scheme, app.InvestmentEvent, app.OccupierEvent
and app.Portfolio tables now carry an EncodedId column alongside the
integer primary key. It holds the same 8-character identifier the app
shows in URLs (e.g. SC1b2Cd==), so a bug ticket referencing /schemes/SC1b2Cd
can be traced to a row in SQL with a direct WHERE EncodedId = '…' query —
no decoder step required. Because the column is physically persisted, it
replicates to the data warehouse alongside everything else. Two helper
functions (app.EncodeEntityId, app.DecodeEntityId) cover every entity
type in case you need to translate an ID on a table that doesn’t have the
column yet. Full reference:
EncodedId columns on entity tables.
Market filter: pick between “self only” and “descendants only”
Section titled “Market filter: pick between “self only” and “descendants only””On the Schemes and Occupier Events list pages, the Market filter tree now shows a small mode chip next to each parent market you’ve ticked. The chip cycles through three modes when clicked:
- any (default) — the tick includes this market and everything under it. Same as before.
- self only — only rows whose Market is exactly this level (e.g. filter for Austria and get only country-level rows, not Vienna-level ones).
- descendants only — everything under this market, excluding rows at the market’s own level.
The chip only appears on parent markets that have children and that you’ve explicitly ticked. Leaf markets have no distinction to make and still look the same. The choice is saved in the URL, so filter links keep working when shared or reloaded.
The other list pages (Portfolios, Addresses, Investment Events) still use the simpler “self + descendants” behaviour; they’ll pick up the chip in a later release once their Market field shape supports the new modes.
Market Boundaries editor (Admin)
Section titled “Market Boundaries editor (Admin)”Admin-only. Admins can now create, edit, and delete market boundary polygons directly in the app — no more ArcGIS round-trip. From the Market Boundaries list, New Market Boundary opens a map with a blank canvas and metadata fields on the side. Opening any existing boundary drops admins straight into the full polygon editor — no separate Edit page or Edit button; the detail page itself is the editor for admins, with Save / Delete controls on the left and the map + drawing tools on the right.
- A vertex-count banner at the top of the map goes green / amber / red as the polygon grows, with a Simplify… button at amber and above that previews the reduced vertex count before committing.
- Polygons over 10,000 vertices can’t be saved — the banner turns red and the save button disables until the shape is simplified.
- Saving a boundary automatically re-syncs the address-to-boundary links for every address that entered or exited the polygon; the linked-address count on the detail panel refreshes straight after.
- Delete on the left panel prompts for confirmation and notes how many linked addresses will be re-synced.
- Non-admins continue to see the read-only detail panel.
- If another admin saves the same boundary while you’re editing it, your save is rejected rather than silently overwriting theirs — the page reloads and you’ll need to re-apply your changes.
Market Boundaries list + detail pages (Admin)
Section titled “Market Boundaries list + detail pages (Admin)”Admin-only. A new Admin group in the main sidebar now carries a Market Boundaries entry. The list view is searchable and filterable by name, code, sector and market, with a map projection showing each boundary’s centroid. Clicking through lands on a detail page with the boundary’s metadata, a preview map pinned to its centroid, and a badge showing how many addresses currently fall inside the polygon. The Edit button is visible on the detail page, but the in-app polygon editor itself lands in a later release.
Unlink a portfolio from a scheme
Section titled “Unlink a portfolio from a scheme”Linked portfolios on a scheme’s Investments panel now show an unlink icon in the top right of each card. Clicking it opens a confirmation dialog, and on confirm removes the link between the scheme and the portfolio without deleting the portfolio itself — you can link it back later if needed.
Search ranks near-match addresses higher
Section titled “Search ranks near-match addresses higher”List-page search (Addresses, Companies, Schemes, Portfolios, Investment
Events, Occupier Events) now boosts rows where every word of the query
appears as a word-prefix in the row — even if one word has a mid-word
typo. Searching 396-398 cit road now surfaces 396-398 City Road
on the first page instead of burying it behind rows that only share a
partial number or a phonetically similar word. True typo correction
(character-level edit distance) still isn’t handled — a missing space
or a wrong character at the start of a word will still miss — but
queries where every word has a valid prefix in the target now rank
strongly.
Scheme Statistics grid usability
Section titled “Scheme Statistics grid usability”- Delete quarter rows and statistic columns directly from the grid. Every row header and column header carries a red trash icon; a confirmation dialog runs first when the target has saved values.
- Occupancy Rate and Vacancy Rate are always shown as % regardless of the unit chosen on the compound — the rule is now enforced at render time, not just in the data model.
- Clearing one of your entered values in a compound statistic (e.g. Vacancy & Occupancy) also clears the auto-calculated siblings so the row doesn’t show stale figures derived from a value you removed. The row itself stays on the grid until you explicitly remove it with the row trash icon.
- The Add Statistic trigger sits above the table at the top-right so it stays visible no matter how wide the grid scrolls, and hides once every statistic type has been added to the scheme.
- A short “Tips” panel below the grid explains save-on-blur, the 2-of-5 compound rule, the % pin on rate components, paste-from-Excel, and what the trash icons do.
- The Add Statistic flyout clarifies which components the unit applies to when you pick a compound (it only covers the quantitative ones; rates are always %).
System Info panel
Section titled “System Info panel”- App Version and App Build Time now populate correctly on the deployed app — previously they appeared blank because of a build-arg name mismatch in the container image.
- Both the app and API Build Time values render in the format configured under Settings → Formatting, honouring both Date Format and Time Format preferences.
- A “View release notes” link next to the App Version opens this page in a new tab.
Scheme sector change with cascade confirmation
Section titled “Scheme sector change with cascade confirmation”Changing a scheme’s sector (Building Type) is allowed again even when child Occupier Events or Development Uses have a Building Use set. A confirmation dialog reports how many rows will be affected and — on confirm — clears the Building Use only on the rows that are incompatible with the new sector, leaving compatible ones untouched. You then re-pick the Building Use on each affected row.
Occupier Scheme picker on deployed environments
Section titled “Occupier Scheme picker on deployed environments”The same-address scheme picker on Occupier Events now loads correctly on the deployed dev environment. The underlying OData query was being truncated by the ingress layer, which made the picker fail to render.
Entity pickers now use the same search as the list pages
Section titled “Entity pickers now use the same search as the list pages”Linking a Portfolio, Company, Address, Scheme, Investment Event, or Occupier Event from a flyout now searches the same full-text-indexed endpoint used by the corresponding list page. Results match what you see when searching the list view for the same query.
Scheme Statistics grid
Section titled “Scheme Statistics grid”Schemes now carry an Excel-like Statistics grid with quarter rows, dynamic columns, duplicate detection, and currency conversion across Local / EUR / GBP / USD. Includes a compound Vacancy & Occupancy statistic where any two of the five components (Total Size, Occupancy, Vacancy, Occupancy Rate, Vacancy Rate) auto-derive the other three.
Multi-block duplicate detection
Section titled “Multi-block duplicate detection”Address, Company, and Scheme duplicate detection now scans multiple candidate blocks per entity, catching near-duplicates that a single-block scan would miss.
List-page search improvements
Section titled “List-page search improvements”- List-page search now ranks exact phrase matches above partial matches and is case-insensitive across the board.
- Portfolio search migrated to the same consolidated search stack as the other list pages.
- Results are sorted by a date tiebreak (newest first) so ties feel intuitive.
Occupier picker shows more context
Section titled “Occupier picker shows more context”The same-address scheme picker on Occupier Events now shows sector, development type, and year alongside the scheme name, so sibling schemes with identical names are easier to tell apart.
Size + currency display consistency
Section titled “Size + currency display consistency”- Measurement and currency display consolidated into a shared set of
components, so every
sqm/sqft/psf/psmlabel across the app reads the same and reacts to your preferences. - Detail cards now show sizes in full rather than truncating.
- Occupier sizes validate against the Local value only; size triplets self-heal when Local is missing but Net/Gross are present.
- Portfolio deal-summary totals row is now correct.
- Investment Event currency validation no longer shows a spurious “field required” error.
For the full commit history behind any release, see
main on GitHub.