0cc4f52816b12b36798d664ff28592dad7542c6e
929
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0cc4f52816 |
fix(search): offer every filter option, not only those in the results
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m17s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m22s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 15s
School type, gender and admissions took their options from the result set, which the filter had already narrowed: choose "Girls" and only "Girls" was offered, so switching to "Boys" meant clearing first. They now offer the full lists from /api/filters, as phase already did. Local authority stays scoped to the results, so a postcode search offers the councils nearby rather than all 153. Whether gender, sixth form and admissions show was also decided by the results (any secondary school in them). It is now decided by the phase alone: hidden for Primary, Nursery and Middle deemed primary, shown otherwise. Choosing one of those phases clears the three filters, which would otherwise stay applied with no control showing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
eb13ab0b5e |
Merge pull request 'feat(search): filter on phones through one sheet' (#168) from feat/mobile-filter-sheet into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 44s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m25s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 2m14s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 4s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m4s
Reviewed-on: #168 |
||
|
|
fa49164143 |
fix(search): keep the filter sheet usable while a change lands
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 32s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m14s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 20s
The sheet's controls were disabled while a filter change navigated, and a control disabled under focus drops it to <body>, out of the dialog. They now stay enabled, with aria-busy on the sheet instead. The disabling had also been covering a race: updateURL built from useSearchParams, which only catches up once a navigation lands, so a second change made before then undid the first. It now builds on the URL the navigation in flight is heading to. The sheet also closes if the screen widens past phone width while it is open, so its selects and the desktop row's are never both showing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cf3c773f86 |
feat(search): filter on phones through one sheet
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 35s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m15s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 22s
On phones the results toolbar's filters were a sideways-scrolling row led by "More filters", so phase showed only in part and school type not at all, and the panel "More filters" opened held neither of them. Phones now get a single Filters button beside the folded search summary, counting every applied filter. It opens a bottom sheet with every filter: distance as five segments, then phase, school type, local authority and the secondary-only filters. Changes apply at once, as on desktop, so the footer's "Show N schools" only closes the sheet. Applied filters show as removable chips on a second line, which appears only when something is applied. Desktop and tablet are unchanged. Modal gains dialog semantics, a pinned footer and focus handling, and moves above the pinned toolbar, the floating List/Map button and the comparison toast, which its old z-index sat beneath. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4b54c25943 |
Merge pull request 'test(e2e): scroll a long list, and wait out the popup crossfade' (#167) from fix/e2e-after-half-mile-default into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 19s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m24s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 27s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 3m3s
Reviewed-on: #167 |
||
|
|
e7645d1ba5 |
test(e2e): scroll a long list, and wait out the popup crossfade
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m17s
PR Checks / Backend Smoke (pull_request) Successful in 11s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m31s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 12s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 13s
The pinned-toolbar journeys scrolled the results of a typed B1 1BB search. At the new half-mile default that is two schools, so the page ended before the toolbar had anything to stay pinned over (desktop scrollY stopped at 229; on phones the scroll reached the footer). They now open the search at a mile, which is what they were measuring. Leaflet crossfades popups for 200ms, so for that window there are two .sc-popup elements, and a strict locator fails at once rather than retrying. Wait for one popup before reading it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
29b5f85952 |
Merge pull request 'fix(map): draw the map card's View button like the list's' (#166) from fix/map-popup-view-button into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 0s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 20s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m23s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 29s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 4m4s
Reviewed-on: #166 |
||
|
|
c077c27720 |
fix(map): draw the map card's View button like the list's
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m20s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 12s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 14s
leaflet.css colours every link in the map `.leaflet-container a` (#0078A8, specificity 0,1,1), which beat .btn-tertiary (0,1,0), so the card's View button came out in Leaflet's link blue in both themes. Pull it back onto --text-secondary next to the other Leaflet overrides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e2fc7a8f15 |
Merge pull request 'fix(search): default to half a mile, and offer a quarter mile' (#165) from fix/default-radius-half-mile into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 21s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m24s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 26s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 4m4s
Reviewed-on: #165 |
||
|
|
355a5a841c |
feat(search): offer a quarter-mile distance
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 17s
Adds "Within 0.25 miles" to the Distance control; 0.5 stays the default. The radius shown in the results heading and drawn on the map was rounded to one decimal place, which would have turned 0.25 into 0.3. It now keeps two, and the list heading says "1 mile" rather than "1.0 miles". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
96deab7d58 |
fix(search): start postcode searches at half a mile
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 21s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 16s
New postcode searches, the near-me button and URLs without a radius now use 0.5 miles. A postcode URL with no radius used to show "1 mile" in the Distance control while the API applied its own 5-mile default; the page and the map fetch now send the same default the control displays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e9886361d2 |
Merge pull request 'fix(search): set the toolbar's line count by width, not by results' (#164) from fix/toolbar-one-line into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 21s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m25s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 28s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m3s
Reviewed-on: #164 |
||
|
|
8ebe461435 |
fix(search): set the toolbar's line count by width, not by results
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 18s
The results toolbar wrapped wherever it ran out of room, and the List/Map switch only appears when there are results, so the same search took two lines with results and one without. From 1340px the controls never wrap away from the search, which takes what they leave (at least 12rem); phase and type chips cap at 11rem to fit. Between 641px and 1339px the controls always take a full line of their own. The switch now sits in FilterBar's row via a viewSwitch slot, so that line runs the full width instead of stopping short of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2002529137 |
Merge pull request 'fix(api): filter by every GIAS phase, not just the grouped ones' (#163) from fix/phase-filter-exact-match into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 22s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m25s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 29s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m3s
Reviewed-on: #163 |
||
|
|
bd7c8593d9 |
fix(api): filter by every GIAS phase, not just the grouped ones
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 20s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m20s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 16s
/api/schools only recognised primary, secondary and all-through. Any other phase the search page offers (nursery, 16 plus, middle deemed primary/secondary) fell through to no filter, so "Nursery" returned the whole result set, mostly primaries. Ungrouped phases now match exactly, and an unknown phase returns nothing rather than everything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0c414680fd |
Merge pull request 'fix(search): offer every phase while a phase filter is applied' (#162) from fix/phase-filter-global into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 20s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m25s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 29s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m0s
Reviewed-on: #162 |
||
|
|
e211e1376d |
fix(search): offer every phase while a phase filter is applied
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m20s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 15s
The phase select read its options from the result-scoped filters, which the backend computes after applying the phase filter. With secondary chosen only secondary and all-through were offered, so switching to primary meant going back to "Any phase" first. Read the global phase list instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
74418ca6b9 |
Merge pull request 'fix(search): keep the map list's count and sort on one line' (#161) from fix/map-pane-header into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 21s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m26s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 28s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m1s
Reviewed-on: #161 |
||
|
|
5df8c93420 |
fix(search): keep the map list's count and sort on one line
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m21s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 18s
Beside the map, "19 schools within 1.0 miles of SW196AR" (359px) and the sort menu (281px, as wide as its longest option) could not share the pane's 427px, so the sort dropped to a second line. In the map view the heading now reads "19 schools within 1 mile": the postcode is already in the search box just above, and the radius reads as the toolbar's does. The sort menu is capped at 11rem, truncating a long choice, and the header no longer wraps; on a narrow pane the count truncates rather than pushing the sort down. List view keeps the full heading. Tried on staging by injecting the change: 243px + 176px, one line at 1440. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ebf9c12446 |
Merge pull request 'feat(search): show the list view's rows beside the map' (#160) from feat/map-list-rows into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 20s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m24s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 28s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m1s
Reviewed-on: #160 |
||
|
|
ca4ddd2b12 |
fix(search): address review on the shared result rows
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 21s
- The rows' narrow layout now switches at a 608px list, not 600px. Below 769px the page pads 1rem each side, so a 640px screen gives a 608px list: exactly the old max-width: 640px media query, where 600px left 633-640px screens on the wide layout. - rowContainerGuard.test.ts fails if anything other than HomeView renders SchoolRow or SecondarySchoolRow, or if one of HomeView's row lists loses its `results` container. Outside one the rows silently keep their wide layout on phones. (Checked: HomeView is the only importer today.) - Picking a pin from the list beside the map now works from the keyboard: each row carries a "Show <school> on the map" button, visually hidden until focused, with aria-pressed for the selected school. The row itself cannot be the button, since it holds links and buttons of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dff3e210ab |
feat(search): show the list view's rows beside the map
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 17s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m20s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 20s
The list beside the map used its own cut-down card, so it showed less than the list view (no phase, age range, denomination, trend, local authority) and looked different. It now renders the list view's SchoolRow / SecondarySchoolRow, through one renderRow shared by the list view, the map pane and the phone's bottom sheet, which also gains Remove from compare. CompactSchoolItem and its styles are gone. The rows switched to their stacked layout on a max-width: 640px media query, which never fires in a ~430px pane on a desktop screen. It is now a container query on the list the row sits in (`results`, set on the list view, the map pane and the bottom sheet). 600px of list is a 632px screen less padding, so phones lay out as before. Clicking a row, outside its links and buttons, still picks its pin, and the selected row is outlined. The pane widens to 360–460px for the taller rows. The bottom sheet's close button was a 30px target; it is now 44px with the same 30px circle (MOBILE.md), and the phone journey checks it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
37bbda1da1 |
Merge pull request 'fix(search): keep the phone results toolbar inside the screen' (#159) from fix/phone-toolbar-width into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 19s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m26s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 27s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 3m5s
Reviewed-on: #159 |
||
|
|
a37da15008 |
fix(search): keep the phone results toolbar inside the screen
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 15s
The search summary ("SW196AR · within 1 mile Edit") ran about 40px off
the right edge on every phone, cutting "Edit" in half, and the filter row
bled past the screen with it (reported on an iPhone 17, 402px).
The phone toolbar is a column flex container, but it kept flex-wrap:
wrap from the desktop rule. In a wrapping flex container each line is as
wide as its widest item's content, not the container, so the summary's
~410px of text stretched the line past a 370px toolbar. nowrap makes
stretch mean the container's width. Verified on staging by injecting the
rule: nothing in the toolbar passes the right edge at 360 or 402px.
The E2E overflow check could not see this: .main clips on x, so
scrollWidth stayed equal to the viewport. The phone journey now measures
the toolbar's own right edges, and runs at 402px as well. It also waits
for the bottom sheet's slide-up to finish before measuring, and measures
the Map/List button again after the sheet pushes it up; both assertions
read a mid-animation or stale box before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
cff3854e63 |
Merge pull request 'feat(search): align the map view with the mockup and open postcode searches on it' (#158) from feat/results-map-view into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 20s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m25s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 14s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 26s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m1s
Reviewed-on: #158 |
||
|
|
1bb3e0360f |
feat(search): open postcode searches on the map on phones too
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 21s
Trial the map as the default for every postcode search, not only on desktop (DEFAULT_LOCATION_VIEW in HomeView; results_view_changed shows how often readers switch away). The view is now derived: the reader's choice once made, else the default for the kind of search. That follows a hero search through client-side navigation without syncing state. With nothing left varying by device, the user-agent default goes (lib/device.ts, the headers() read in page.tsx). That settles two review points: the HTML no longer needs Vary: User-Agent, and there is no narrow-window fallback left to flash the map before the list. Review fix in LeafletMapInner: the card reopened only when the selection or the school list changed, so a rebuild caused by the centre, reference point or radius closed it for good while the page still had the school selected. The selection effect now follows a pins-rebuilt counter. SchoolMap memoises its fallback centre, which was a fresh array on every render and would have rebuilt every pin each time. Phones hide the map view's list pane, so it no longer builds up to 500 hidden cards there (decided after mount, so hydration still matches). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4e4b30e812 |
feat(search): align the map view with the mockup and open it on desktop
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m20s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 26s
The map view now follows option B of the results-controls mockups: - The list sits in a pane on the left, under the result count and sort, and the map fills the rest of the screen below the pinned toolbar. The toolbar's height is measured, so the split ends 1rem above the bottom of the screen however the controls wrap. - Pins are brand-teal dots and the selected one is coral. The search location is an ink dot, the search radius a dashed circle with its distance, and the view fits that circle. Tiles are muted and the zoom sits under the fullscreen button. - A school picked on the map or in the list opens a card on the map (View, + Compare, following the basket), and its list card is ringed and scrolled into view. Phones keep the bottom sheet. - The list cards show the full name, Ofsted and school type, the headline figure and pupils. The map cards now follow the list rows: no England benchmark for special schools, PRUs and AP, and no placeholder all-zero RWM (Greenmead showed "0% RWM -62 pts vs national"). That rule moves to a shared listRwmValue helper. A postcode search opens on the map for desktop browsers, chosen on the server from the user agent so the list never paints first. The client falls back to the list below 1024px, and follows the default through client-side navigation from the hero until the reader picks a view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0e177ca2ec |
Merge pull request 'feat(search): pin the results toolbar and float the map button on phones' (#157) from feat/results-toolbar into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 43s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m26s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 2m11s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 4s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 43s
Reviewed-on: #157 |
||
|
|
367a07c15d |
fix(search): address review on the results toolbar
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 33s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m17s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 21s
- Refold the phone search, and resync its text, when the query changes without a submit (back/forward, "Use my location"). The summary and the unfolded input could otherwise describe two different searches. - Cap the "More filters" panel at every width, not only on phones, so an open panel scrolls inside the pinned toolbar on a short screen. - Drop the phone filter row's right-edge fade once it is scrolled to the end or does not overflow (the MOBILE.md pattern), and give focus rings room inside the scroll clip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
983a581555 |
feat(search): pin the results toolbar and float the map button on phones
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 33s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m16s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 26s
The List/Map switch was a small grey control beside the results heading, and the filters were plain dropdowns labelled "All Phases" and "Advanced". Both scrolled away with the first result. Search, filters and the List/Map switch now share one card pinned under the header. Distance, phase and school type are pill controls in the row; "Advanced" becomes "More filters" and counts only what it hides. The switch is filled brand teal and says which view is on (aria-pressed). On phones the search folds to a one-line summary once made, the filter pills scroll sideways, and a floating Map/List button sits above the tab bar in place of the toolbar switch. The selected pin's card now stacks under that button instead of covering the tab bar. Switching view from far down the list scrolls back to the top of the results, and each switch is tracked as results_view_changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b34feb8e98 |
Merge pull request 'feat(compare): five per phase, not five overall' (#156) from feat/compare-limit-per-phase into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 21s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m25s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 14s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 29s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 2m57s
Reviewed-on: #156 |
||
|
|
cc99865bd4 |
style(api): drop the em dash from the sitemap url docstring
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m21s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 19s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
587cfe3f0b |
fix(compare): give every basket entry a real phase
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 17s
PR Checks / Build Frontend (no push) (pull_request) Canceled after 1m6s
PR Checks / Build Pipeline (no push) (pull_request) Canceled after 0s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 0s
Review of the per-phase limit found entries reaching the basket with no phase, and a phase-less entry holds a slot in both groups: - Nearby-school cards added without one. The API now returns each peer's own phase (its pool is a phase group, so an all-through school can sit beside a primary); the button passes it through, and an older API simply leaves the conservative both-groups count in place. - Baskets saved before this change were never migrated. The compare page now backfills missing phases from the data it already fetches, and never overwrites a phase the basket has. - "16 plus" counted against both groups; it is secondary, as the API's PHASE_GROUPS files it. Also rewraps the HomeView doc comment the previous commit left awkward. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0a4c051ee5 |
feat(compare): limit the basket to five per phase, not five overall
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 22s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m17s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 12s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 20s
A parent choosing a primary and a secondary school at once hit the old cap of five total. The basket now holds up to five primary and five secondary schools (ten in all), matching the compare page's phase tabs. Schools that could land in either tab (all-through, special schools with phase "Not applicable", unknown phase) count against both groups, so no tab ever exceeds the five-slot chart palette and point styles. - lib/compareLogic: compareGroups, fullGroupFor, fitToGroupLimits - search modal disables only the full group and says which one - rankings rows carry the phase of the tab they are ranked under - shared ?urns= links are trimmed per group - copy: compare metadata, homepage value prop, How it works card now name primary and secondary schools (also better for search intent) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
343b40c645 |
Merge pull request 'copy(web): replace em dashes in public copy' (#155) from copy/remove-em-dashes into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 20s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m25s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 28s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 2m51s
Reviewed-on: #155 |
||
|
|
d1688ac150 |
copy(web): replace em dashes in public copy
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m20s
Rewrites every visitor-facing string that used an em dash, choosing the punctuation for what the dash was doing: a colon before a list or explanation, a comma for an aside, a full stop between two thoughts, parentheses for an aside mid-sentence. Covers page titles and meta descriptions, the home and admissions guide copy, school page headings and notes, the compare page, metric labels and tooltips. Two rewrites also fix the sentence around them: the closure banner no longer repeats "proposed for closure", and the cut-off caveat's list of priorities now parses. A lone dash marking a missing value in a table cell stays: it is a data convention, not prose. A Jest guard walks the source with the TypeScript parser and fails on any other em dash in a string or JSX text node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
271ffe92d4 |
Merge pull request 'fix(web): lift the mobile jump sheet above the bottom tab bar' (#154) from fix/jump-sheet-under-bottom-bar into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 21s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m23s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 14s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 28s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 3m0s
Reviewed-on: #154 |
||
|
|
0571d1c0ff |
fix(web): stop the sheet-open rule stealing .sectionNav's layout
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m17s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m18s
Review catch, and a bad one: the previous commit anchored its insertion on `padding: 0.5rem 0.75rem;` and closed .sectionNav there. Everything that followed in the rule — margin-bottom, box-shadow, display: flex, align-items, gap — was orphaned into .sectionNavSheetOpen, which is only applied while the mobile jump sheet is open. So the sticky nav lost its flex layout, spacing and shadow in the closed state, which is virtually every page view on every school detail page. A site-wide regression introduced by a fix for one mobile menu. Redone by anchoring on the complete rule, closing brace included, so nothing can be orphaned. .sectionNav is now byte-identical to main and the diff is purely additive; .sectionNavSheetOpen carries the z-index and nothing else. The staging experiment that validated this fix set nav.style.zIndex = '1100' with every other declaration intact, so it was always testing this version rather than the broken one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
180d6e9b3e |
fix(web): lift the jump sheet above the bottom tab bar
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 17s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 37s
On mobile the last item in "Jump to section" was painted over by the fixed bottom tab bar and could not be tapped. Reported as nearby schools missing from the menu; it was there, underneath the bar. The sticky nav sets `position: sticky` with `z-index: 10`, which makes it a stacking context. The sheet's own `z-index: 1600` therefore orders it only inside that context — against the tab bar (z-index 1000) the nav's 10 is what counts, so the bar wins. Verified on staging: every menu item returns itself from elementFromPoint except the last, which returns the tab bar. Latent rather than new. With five sections the list stopped just above the bar; "Nearby schools" made six, and the sixth is the first to reach it. Any section added later would have done the same. Lifted only while the sheet is open, and only to 1100 — above the bar, below the comparison toast (2000), the fullscreen map (5000) and the info popover (9999). The backdrop rises with it, so tapping over the bar now dismisses the sheet instead of navigating away. The journey asks what a thumb asks: for each item, whether it is the topmost element at its own centre. A bounding-box check cannot see this — the item is in the viewport and the right size, just underneath something. Confirmed to fail against current staging, naming "Nearby schools", before the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
80f405123e |
Merge pull request 'test(e2e): wait for the carousel's smooth scroll to settle before measuring it' (#153) from fix/nearby-journey-waits-for-smooth-scroll into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 19s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m25s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 28s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 2m53s
Reviewed-on: #153 |
||
|
|
077aca6008 |
test(e2e): wait for the carousel's smooth scroll to settle before measuring it
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 32s
Second failure of the same assertion, and the first fix addressed a real but different problem. This is the one that was actually producing the number. The arrows scroll with `behavior: 'smooth'`. The test polled for "has it moved at all" — satisfied 50ms in, at 13px of a 1300px journey — then recorded the offset, clicked, and recorded again. The row was still travelling throughout, so the delta it measured was the tail of the arrow's animation, not the effect of the selection. Hence a deterministic 659, roughly half of the 1317 this school's row scrolls. Measured against staging rather than reasoned about: the animation runs about 700ms, and the samples are in the helper's comment. settledScrollLeft waits for two identical readings before trusting one. The app was never at fault — driving staging by hand, the offset holds at 1317 across the selection, exactly as intended. Verified against staging both ways: main's version of this test fails there, this version passes, along with all three mobile widths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9dba5ff1ff |
Merge pull request 'test(e2e): fix the nearby-schools journey clicking a card it scrolled past' (#152) from fix/nearby-scroll-journey-clicks-offscreen-card into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 20s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m23s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 25s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 2m49s
Reviewed-on: #152 |
||
|
|
f530a912bc |
test(e2e): stop the nearby-schools journey clicking a card it scrolled past
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 21s
The assertion that adding to compare does not reset the carousel failed on staging: 537 → 2. The app was not at fault. Playwright scrolls a target into view before clicking, and the test clicked the FIRST card's button after paging the row to the end — so Playwright scrolled the container back to the start, and the assertion measured that. Reproduced on a static page with no React on it: a snap scroller at 615, Playwright clicks the off-screen first card, scrollLeft becomes 2. Scroll-snap was ruled out first — mandatory, proximity and no-snap all behave identically when the button mutates in place. Now clicks the last card's button, which is visible at the end of the travel, and allows a few pixels for snap and sub-pixel adjustment while still failing on a reset to the start. The property was never actually under test before; it is now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
029fe8d8a6 |
Merge pull request 'fix: order nearby schools by distance, not by how alike they are' (#151) from fix/nearby-schools-order-by-distance into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 21s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m24s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 30s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 2m51s
Reviewed-on: #151 |
||
|
|
cd1c5d1e1a |
docs: revise the spec to the design that survived staging
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m15s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 57s
The spec described the tier system as the design of record. It is gone, so the document was describing something the code deliberately does not do. The revision note and the "why not, having built it the other way first" passage are kept rather than overwritten. The mistake is the instructive part: treating a preference as a constraint inverted the ranking, and the stopping rule added to prevent weak distant matches is what guaranteed six Catholic schools and no community school down the road. A spec that quietly presents the second design as the plan teaches nobody why the first one failed. The mockup link is annotated as one revision behind rather than silently left to look current. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cd6a45bf7d |
refactor: rename similar → nearby, so the code says what the section does
The section ranks on distance and is headed "Other schools nearby", but every identifier still called it "similar" — the exact drift that leaves a later reader trusting a name over the behaviour. Mechanical: files, the module, the payload key, the type, the components, the prop. No behaviour change; the suites are unchanged in count and still green. Free to do now because #150 has not merged, so the payload key rename needs no lockstep deploy. Uses of "similar" that are ordinary English — progress measures compared to similar pupils, and unrelated comments — are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5c0ccc693d |
fix: order nearby schools by distance, not by how alike they are
Reported from staging: a Catholic primary showed six Catholic primaries, none of them close enough to be a real option, and omitted the community school down the road. Three causes, compounding. Ranking put tier before distance, so a faith match at 2.9 miles outranked a community school at 0.3. The ENOUGH=3 stopping rule — added so a cap of six would not drag in weak distant matches — filled the row from the best tier before it ever widened, which is what made every card Catholic. And a 3-mile tier-1 radius is sane for a secondary and most of a city for a primary, whose catchments are routinely under a mile. The premise was backwards. For a parent, distance is a constraint and intake is a preference; a school beyond a primary catchment is not a weaker option, it is not an option. So distance now decides the order and nothing else does. The hard filters are untouched — they were always where the defensibility lived. Similarity survives as chips on the card: reported, so a reader applies their own weighting, rather than ranked, so we apply ours for them. Reach is capped per phase (primary 2, secondary 6, post-16 10) as a sanity bound, not a target: ordering already handles density, so the cap only decides what happens where an area is sparse. A primary with nothing inside two miles now renders no section, which is the honest answer. Deleted: the tier system, the stopping rule, the tier-dependent lede, the `tier` field, the tier-3 fallback chip and its style. select_similar also stops taking is_secondary — it reads the phase from the subject's own row, so no caller can hand it one that disagrees with the data. The heading is now "Other schools nearby". The hard filters still guarantee a comparable set, but nothing ranks on likeness, so the heading no longer says it does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
151cf4bc80 |
Merge pull request 'feat: similar schools nearby on the detail page' (#150) from feat/similar-schools-nearby into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 1s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 44s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 1m25s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 2m14s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 6s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 42s
Reviewed-on: #150 |
||
|
|
83dc5ae5dc |
docs: correct the metric rule for "16 plus"
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m11s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 31s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m15s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m11s
The spec said the metric follows the template the page renders. That is no longer true for GIAS phase 6: a sixth-form college renders the primary template but is matched, correctly, against secondaries. The rule is phase group membership, decided once in is_secondary_phase — and the section's lede noun comes from the school's phase rather than its template for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2175dccb7c |
fix(api): treat "16 plus" as secondary, the way PHASE_GROUPS already does
PR Checks / Frontend Typecheck + Tests (pull_request) Canceled after 9s
PR Checks / Backend Smoke (pull_request) Canceled after 0s
PR Checks / Build Backend (no push) (pull_request) Canceled after 0s
PR Checks / Build Frontend (no push) (pull_request) Canceled after 0s
PR Checks / Build Pipeline (no push) (pull_request) Canceled after 0s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 0s
GIAS phase 6 is "16 plus", and PHASE_GROUPS deliberately files it in the secondary group. The payload helper decided the same question with `"secondary" in phase_text`, which that value does not satisfy — so a sixth-form college was handed the primary bucket and offered infant schools as its peers, with the KS2 metric key to label them. No crash; just a page confidently showing the wrong schools. The decision now lives in similar_schools.is_secondary_phase, beside the PHASE_GROUPS bucket it selects from, so the two cannot drift again. A test pins them together. The same binary assumption had a second output. computeSchoolFlags tests for the substring too, so a 16-plus school renders the primary template, and the composer was labelling the section from the template: "Other primary schools near <sixth form college>" above a row of secondaries. The section now derives its noun from the school's own phase, which also removes the duplicated wording from both composers. A 16-plus school's candidates span the whole secondary group, so no single noun fits and it gets the honest general one. Reported in review on #150. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |