Commit Graph
977 Commits
Author SHA1 Message Date
TudorandClaude Opus 5.5 93f17211b1 fix(pipeline): read the latest Ofsted visit from the dates
int_ofsted_latest treated any report card as the latest visit, relying on report cards postdating legacy inspections. That holds today (0 of 2,451 report-card schools in the 31 Aug 2026 MI) but is now not assumed: the latest visit is the newest of the three dates. A report card still leaves no legacy grade in force, which is the spec's rule rather than an inference. New unit test newer_legacy_visit_after_a_report_card failed RED on the old logic. Review on #183.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:39:24 +01:00
TudorandClaude Opus 5.5 dfa4928641 feat(pipeline): add fact_ofsted_latest, one current Ofsted status per school
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m18s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 20s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m31s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 50s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 32s
The backend will read this instead of picking the latest row of fact_ofsted_inspection itself (twice, with arbitrary ties). Schema tests and assert_ofsted_current_grade_consistent pin its invariants. Built by the monthly Ofsted DAG (int_ofsted_latest+), not the daily one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:24:41 +01:00
TudorandClaude Opus 5.5 e6babe21f5 feat(pipeline): work out each school's current Ofsted status once
int_ofsted_latest now picks each school's latest visit (report card, graded or ungraded inspection) and the overall grade still in force, dated by the inspection that awarded or confirmed it. A 'School remains Good' from an older ungraded visit is no longer carried past a newer inspection that gave no grade (audit C1). Duplicate monthly rows resolve to the newest visit, so a newer report card always wins. dim_school's columns are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:23:38 +01:00
TudorandClaude Opus 5.5 358705bf39 feat(pipeline): keep Ofsted's graded, ungraded and report-card dates apart
int_ofsted_latest needs to know which inspection came last. Report-card-only schools are no longer dropped (123 schools, audit H3); inspection_date stays for current readers and now falls back to the report-card date, so fact_ofsted_inspection's not_null test still holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:22:49 +01:00
TudorandClaude Opus 5.5 3be902e98f docs(plan): implementation plan for the current Ofsted status
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:20:49 +01:00
TudorandClaude Opus 5.5 12c52244ee docs(spec): one current Ofsted status per school
Design for audit findings C1 and M1. A grade carried forward from an older
ungraded visit is shown under a newer inspection's date ("Good · 2025" for
Rabbsfarm, whose 2025 inspection gave no grade), and "Inspected" dates show the
last graded inspection rather than the latest visit. The rule moves into
int_ofsted_latest and a new fact_ofsted_latest mart, shipped as a pipeline PR
and then a backend/UI PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:00:35 +01:00
tudor e1373fb6df Merge pull request 'fix(school): drop the nearby section's lede, which repeated its heading' (#180) from fix/nearby-drop-lede 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 1m31s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 2m8s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 4s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 45s
Reviewed-on: #180
2026-10-04 09:13:48 +00:00
TudorandClaude Opus 5.5 ccfa44389e fix(school): drop the nearby section's lede, which repeated its heading
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m19s
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 1m32s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m18s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 14s
"Other schools nearby" was followed by "Other primary schools near
<school>.", which says the same thing again. The heading now stands alone.

nearbyNoun() and the phase and schoolName props existed only to build that
line, so they go with it. Its bottom margin was the only gap between the
heading and the cards, so the header row carries that gap now, and centres
the heading against the carousel arrows now that it is a single line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:10:36 +01:00
tudor 423b27140c Merge pull request 'fix(school): send the school page its admissions policy, and read it exactly' (#179) from fix/school-page-selective-flag 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) Successful in 3m22s
Reviewed-on: #179
2026-10-02 22:56:36 +00:00
tudor 1c62e8247d Merge pull request 'fix(search): stop replaying a failed LA-averages request forever' (#178) from fix/la-average-cached-failure 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 1m27s
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) Failing after 3m47s
Reviewed-on: #178
2026-10-02 22:44:57 +00:00
TudorandClaude Opus 5.5 da5d63593f test(e2e): a no-faith comprehensive makes no Selective or faith claim
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
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 16s
The Admissions section of a non-selective secondary with no religious
character must say neither "Selective:" nor "Faith priority:". Run against
staging before the fix, it fails on "Faith priority" (Burntwood's "(None)").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:43:43 +01:00
TudorandClaude Opus 5.5 ef4a2ccccb fix(school): send the school page its admissions policy, and read it exactly
The header's Selective flag read school_info.admissions_policy, which the
detail endpoint never sent, so no school page could flag Selective while
its search row did (staging E2E: The Grammar School at Leeds). The detail
payload now carries it, and a contract test checks it carries every field
the header's flags read.

Sending it would have switched on two older copies of the tag logic #176
fixed in the rows. The Admissions section and the cut-off note both tested
includes('selective'), so every non-selective secondary would have read
"entry is by selective examination". The section also counted "None" as a
faith: Burntwood reads "a faith-based admissions priority (None)" today.
All of them now share isSelective() and hasReligiousCharacter(), which also
treats "Not applicable" as no faith, as the place table already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:43:43 +01:00
tudor 392ec29fdb Merge pull request 'feat(school): facts and flags in the school header, shared with search rows' (#177) from feat/header-facts-and-flags 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 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 3m38s
Reviewed-on: #177
2026-10-02 22:32:47 +00:00
Tudor 03e44cf154 Merge branch 'main' into feat/header-facts-and-flags
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
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 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 17s
2026-10-02 23:28:33 +01:00
TudorandClaude Opus 5.5 59ea8a4bdd fix(search): stop replaying a failed LA-averages request forever
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 18s
The search page fetched LA averages with cache: 'force-cache', which serves
any stored response, however old, without asking the server. One failed
request (a staging deploy restart; the July proxy outage) was stored and
replayed on every later visit, and the error was swallowed, so the
"vs LA avg" delta silently vanished from every secondary row in that
browser. A Playwright profile still held a 500 dated 5 July.

The default cache mode honours the API's Cache-Control (five minutes), so
a good answer is still reused and an error never is. Browsers holding a
stored failure recover on their next visit.

A journey now checks that a mainstream secondary's row shows the
comparison: nothing did, which is how it could go missing unnoticed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:20:14 +01:00
tudor c931d1078c Merge pull request 'fix(search): tag only what the register records, and count the whole school' (#176) from fix/search-row-facts into main
Stage (build -> staging -> E2E gate) / prepare (push) Successful in 0s
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 29s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 3m11s
Reviewed-on: #176
2026-10-02 22:05:56 +00:00
tudor 807133c305 Merge pull request 'fix(school): show Nursery only for nursery classes, and say Girls' school' (#175) from fix/header-nursery-and-gender 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 27s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 3m8s
Reviewed-on: #175
2026-10-02 22:04:29 +00:00
TudorandClaude Opus 5.5 41d3f3b971 fix(school): never open a wrapped fact line with a separator
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) Successful in 20s
At phone widths the fact line wraps, and each dot travelled with the value
after it, so the second line read "· 1,478 pupils". The line now starts
1.125rem left of the column and clips that strip: a value that starts a
line sits flush with the title and its dot is hidden. Measured on staging
at 360, 390 and 430 px: no element past the gutter, no page overflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:01:24 +01:00
TudorandClaude Opus 5.5 c013265cb4 test(e2e): journeys for the header's facts and flags
A selective school is flagged Selective on its page and in its search row,
and its type reads in the search filter's words. The girls' school journey
looks for "Nursery class", the flag's new label, and the identity journey
checks that constituency and county have left the header. Run against
staging before the change, all three fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:59:45 +01:00
TudorandClaude Opus 5.5 ad9d3b67f7 feat(search): print the school page's type and flags in search rows
Both rows now take their type label and tags from lib/schoolFacts, so a
parent reads the same words in the list and on the page: "State school"
instead of "Academy converter", "Girls' school" instead of "Girls", the
religious character instead of "Faith priority", and a Nursery class flag
on primaries. Who-can-apply tags are outlined with an inset ring, which
keeps them the size of their neighbours; the Selective tag no longer
borrows the status colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:59:45 +01:00
TudorandClaude Opus 5.5 ca47d08186 feat(school): state the header's facts in one line, and flag what applies
The row of grey chips under the name mixed the council, a GIAS governance
term, the age range and provision, with no labels. Parents could not tell
what "Wandsworth" or "Nursery" meant, and selective, faith and fees were
missing or buried in the details.

The header now prints one fact line (phase pill · ages · type · pupils),
then flags only for what applies, in a fixed order: who can apply or what
it costs (Fee-paying, Selective, Girls'/Boys' school, the religious
character as recorded), then what the school offers (Nursery class, Sixth
form). Flags carry no hue: they are facts, not verdicts. The council joins
the address unless the address already names it. The details keep
headteacher, website, phone, academy trust (hidden when it has the
school's own name) and capacity; county, constituency and religious
character leave the header. The proposed-to-close strip is unchanged.

lib/schoolFacts holds the type label and the flags so the search rows can
print the same words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:59:45 +01:00
TudorandClaude Opus 5.5 62a6bfaf0c feat(api): name each school's type group, and send search its nursery provision
The school page and the search rows now print a school's type in the search
filter's terms ("State school", "Independent school") instead of GIAS's 34
establishment types. The list, place and detail payloads carry type_group,
computed with the filter's own type_group_for, so the two never disagree; a
type in no group stays null and the page prints the register's name.

Search rows flag a nursery class, so nursery_provision joins SCHOOL_COLUMNS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:59:45 +01:00
Tudor e344298440 Merge branch 'fix/search-row-facts' into feat/header-facts-and-flags 2026-10-02 22:51:35 +01:00
TudorandClaude Opus 5.5 8020191832 test(e2e): journeys for row tags and pupil counts, and ask for page_size
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 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
Three journeys pin the fixes: a non-selective secondary's row has no
Selective tag, a school recorded with no religious character has no faith
tag, and the list and the school page agree on a secondary's pupil count.
Run against staging before the fix, all three fail.

The list API reads page_size. Nine journeys asked for per_page, which it
ignores, so each got the default 25 rows whatever it asked for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:51:31 +01:00
TudorandClaude Opus 5.5 5f9caad7f4 fix: count pupils as the whole school, not the GCSE year group
fact_performance's total_pupils is the cohort a year's results were
measured on. For a secondary that is Year 11 alone, and the list API sent
it as the card's "pupils": Burntwood showed 245 in search and 1,462 on its
page. The list and place payloads now carry the register's whole-school
count, and nothing when the register has none. Map popups and the compare
basket read the same field.

The header and the wellbeing section fell back to the same results figure
when the census had no record. They now fall back to the register count.
The shell took yearlyData only for that fallback, so the prop is gone and
the results array no longer ships to the client for the chrome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:51:31 +01:00
TudorandClaude Opus 5.5 e65c93b68e fix(search): tag only what the register records
The secondary row tested the admissions policy with includes('selective'),
which "Non-selective" passes, so every comprehensive (Burntwood, Graveney)
was tagged Selective. It now needs an exact "Selective".

Both rows excluded only "Does not apply" from the religious character, so a
school recorded as "None" got "Faith priority" (Putney High) or a bare
"None" chip (Abacus Belsize Primary). hasReligiousCharacter() treats both of
the register's no-faith values as no faith.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:50:44 +01:00
TudorandClaude Opus 5.5 e3f21a5bc7 fix(school): address review on the header chip fixes
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
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 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 15s
- E2E: list with page_size (per_page was ignored), fail clearly if the
  gender filter is ignored, and require nursery_provision on the detail
  payload so the Nursery assertion cannot pass vacuously.
- singleSexLabel ignores case, as hasNurseryClasses does.
- Header test: type withSchool with Partial<School>, and match the
  single-sex labels exactly instead of any span ending in "school".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:16:49 +01:00
TudorandClaude Opus 5.5 dea435a906 fix(school): show Nursery only for nursery classes, and say Girls' school
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
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 16s
GIAS NurseryProvision is text ("Has Nursery Classes", "No Nursery
Classes", "Not applicable"), but the header and the place table tested it
for truthiness. Every school with a value got a Nursery chip or a "Yes",
including secondaries aged 11-18. hasNurseryClasses() matches the one
value that means a nursery, and the type now says the field is a string.

The single-sex chip appended 's to the plural GIAS gender, giving
"Girls's school". singleSexLabel() gives "Girls' school" / "Boys' school".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:02:32 +01:00
tudor dd5b48e612 Merge pull request 'feat(search): phases in the order a child meets them' (#174) from feat/phase-order-child-path 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 15s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 31s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 3m10s
Reviewed-on: #174
2026-10-02 18:07:08 +00:00
TudorandClaude Opus 5.5 a88139a539 feat(search): phases in the order a child meets them
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
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 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 16s
The phase filter listed GIAS phases alphabetically, so "16 plus" and
"All-through" came before Nursery. /api/filters (and the result-scoped
list) now order them Nursery, Primary, Middle deemed primary, Middle
deemed secondary, Secondary, 16 plus, then All-through, which spans the
whole path. A phase GIAS adds later follows the known ones, A-Z.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:56:14 +01:00
tudor 9b765125ad Merge pull request 'fix(search): desktop Clear all keeps the search' (#173) from fix/desktop-clear-keeps-search 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 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 3m8s
Reviewed-on: #173
2026-10-02 15:50:22 +00:00
tudor ccf0892a0e Merge pull request 'fix(modal): give the close button a 44px touch target' (#172) from fix/modal-close-tap-target 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 27s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 3m7s
Reviewed-on: #172
2026-10-02 15:50:14 +00:00
TudorandClaude Opus 5.5 0450f8ecd6 fix(search): desktop Clear all keeps the search
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
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) Successful in 17s
The desktop toolbar's Clear went to the bare home URL, dropping the
search along with the filters, so it landed on the home page instead of
the unfiltered results. It now clears every filter and keeps the search,
postcode and distance, as the phone's Clear all already did. It is
offered only once a filter is applied, since with only a search there is
nothing for it to clear, and is labelled Clear all to match the phone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:43:41 +01:00
TudorandClaude Opus 5.5 27d83f9bd0 fix(modal): give the close button a 44px touch target
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m13s
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 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 12s
The close button was 40px square: a 24px icon in 8px of padding. MOBILE.md
sets 44px as the minimum, and the phone filter sheet journey, which
checks every target in the sheet, has failed on it in the staging E2E
gate since the sheet shipped. Measured on staging with the rule
injected: the button is 44x44, nothing in the sheet is under 44, and
the page does not overflow. Every Modal gets the same button, the
compare page's school search included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:40:45 +01:00
tudor 19c574edb0 Merge pull request 'feat(search): one state school group, not academy and council-run' (#171) from feat/single-state-school-group 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 14s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 25s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m2s
Reviewed-on: #171
2026-10-02 15:33:13 +00:00
TudorandClaude Opus 5.5 e78ec14e2e feat(search): one state school group, not academy and council-run
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 1m21s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 20s
The School type filter split state schools into "academy or free
school" and "council-run". The two were near-halves of one pool (11,186
and 9,316 schools), so choosing one rarely narrowed anything, and the
split did not follow the difference a parent feels most, admissions:
voluntary aided and foundation schools set their own, as academies do.
Faith, which voluntary aided mostly meant, has its own filter.

They are now one group, "State school (free)", leaving five. The old
keys academy and council resolve to state, so a link made with them
keeps working instead of falling through to the raw-label path and
returning nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:51:35 +01:00
tudor fb3ef7d2b9 Merge pull request 'feat(search): school type groups and a faith filter' (#170) from feat/school-type-groups-faith 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 25s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m5s
Reviewed-on: #170
2026-10-02 14:05:43 +00:00
TudorandClaude Opus 5.5 49ac96b487 refactor(search): drop the applied-filter row above the 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 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 21s
The row repeated the search, local authority and school type above the
results list, and nothing else: no phase, distance, gender, sixth form,
admissions or faith. The filter bar already shows every one of them, in
its selects and More filters count on desktop and as removable chips on
phones, so the row was a partial second copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:27:53 +01:00
TudorandClaude Opus 5.5 214c80663e docs(spec): describe the API filters as they are built
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 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 26s
The /api/schools section still described filtering on stored
school_type_group and faith_groups columns, which the architecture
correction had already dropped: the filters look up the name columns at
request time. Also trims a trailing blank line from two test files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:25:20 +01:00
TudorandClaude Opus 5.5 84caee9f72 fix(search): show applied type and faith by label, in any case
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 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 22s
The results list's chip printed the school type group's key ("post16")
rather than its label. A URL value that no option spelled the same way
(an old link's raw GIAS type, or a key in another case) left the select
reading "Any school type" while the list stayed filtered, and choosing
"Any" changed nothing. Values now match options ignoring case, as the
API does, and an unlisted one is shown as an option of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:00:54 +01:00
TudorandClaude Opus 5.5 c992d7f3b9 test(e2e): school type groups and the faith filter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:55:32 +01:00
TudorandClaude Opus 5.5 17bfb4a3f7 feat(search): school type groups and a faith filter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:54:04 +01:00
TudorandClaude Opus 5.5 15b8923e60 feat(api): filter schools by type group and by faith
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:53:14 +01:00
TudorandClaude Opus 5.5 354244f755 feat(api): group GIAS school types and religions for parents
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:52:43 +01:00
TudorandClaude Opus 5.5 3e2fa4a419 docs(plan): school type groups and a faith filter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:44:57 +01:00
TudorandClaude Opus 5.5 50546ecf22 docs(spec): school type groups and a faith filter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:39:07 +01:00
tudor 99d62ef748 Merge pull request 'fix(search): offer every filter option, not only those in the results' (#169) from fix/filter-options-not-from-results 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 31s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 3m7s
Reviewed-on: #169
2026-10-02 09:34:02 +00:00
TudorandClaude Opus 5.5 68452681f8 fix(search): match primary phases by their words, not exact labels
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 1m20s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 12s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 18s
The secondary-only filters were hidden for an exact set of lowercased
labels, so a change of case, hyphen or spacing in a GIAS phase label
would have left them showing. Phases are now matched on their words, as
compareGroups already reads "primary".

The E2E journey no longer counts school types: it asserts that choosing
one leaves the same list on offer, whatever the data holds. A test now
pins that a filter with no options is left out, not shown with only its
"any" option.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:28:34 +01:00
TudorandClaude Opus 5.5 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>
2026-10-02 10:22:39 +01:00
tudor 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
2026-10-02 08:51:17 +00:00