Option 1 from the header proposal (round 2): one fact line, then flags only for what applies. Search rows print the same words.
Builds on #175 and #176. Merge those first. This branch contains both (merged, not cherry-picked), so once they land, this diff shrinks to the redesign alone. The three merge cleanly in any order.
Header
Fact line, fixed order: phase pill (search-row colours) · Ages 11–18 · type in the search filter's words ("State school", "Independent school", "Special school (SEND)"…) · 1,478 pupils. A type in no group (secure units, online providers) prints the register's own name.
Flags, only when true, fixed order. Outlined = who can apply or what it costs: Fee-paying, Selective (exact match only), Girls'/Boys' school, the religious character as recorded. Filled = what the school offers: Nursery class, Sixth form. No hue: they are facts, not verdicts, and the design rules keep colour for brand, status and phase.
Address: the council follows the postcode unless the address already names it.
Details: headteacher · website · phone · academy trust (hidden when it has the school's own name) · capacity. County, constituency and religious character left the header (place pages keep county and constituency).
Proposed to close: the strip is unchanged, as decided.
Search rows
Both rows take their type label and tags from lib/schoolFacts: "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. The Selective tag no longer borrows the status colour.
API
type_group on list, place and detail payloads, computed with the filter's own type_group_for. nursery_provision joins SCHOOL_COLUMNS so rows can flag it.
Mobile (MOBILE.md)
No local server, so I injected the new markup and CSS into Tiffin's staging page, with the longest religious character as a worst case, and measured right edges at 360 / 390 / 430: nothing past the 16px gutter, no page overflow. The long faith flag wraps inside its chip. That pass caught a wrapped fact line opening with "· 1,478 pupils"; fixed by clipping the separator strip (last commit). Flags aren't interactive, so no new tap targets.
Tests
Jest: 71 suites, 623 tests (new schoolFacts.test.ts, rewritten schoolDetailHeader.test.tsx, row tests for the shared flags). tsc --noEmit clean.
Backend: 303 tests (new test_type_group_payload.py).
E2E: a selective school is flagged Selective on its page and in its search row; the girls' school journey looks for "Nursery class"; the identity journey checks constituency and county have left the header. All three fail against current staging, as they should.
Option 1 from the header proposal ([round 2](https://claude.ai/artifact/FTVpkEFLmJXVfCGKzqWQrd)): one fact line, then flags only for what applies. Search rows print the same words.
**Builds on #175 and #176. Merge those first.** This branch contains both (merged, not cherry-picked), so once they land, this diff shrinks to the redesign alone. The three merge cleanly in any order.
## Header
- **Fact line**, fixed order: phase pill (search-row colours) · Ages 11–18 · type in the search filter's words ("State school", "Independent school", "Special school (SEND)"…) · 1,478 pupils. A type in no group (secure units, online providers) prints the register's own name.
- **Flags**, only when true, fixed order. Outlined = who can apply or what it costs: Fee-paying, Selective (exact match only), Girls'/Boys' school, the religious character as recorded. Filled = what the school offers: Nursery class, Sixth form. No hue: they are facts, not verdicts, and the design rules keep colour for brand, status and phase.
- **Address**: the council follows the postcode unless the address already names it.
- **Details**: headteacher · website · phone · academy trust (hidden when it has the school's own name) · capacity. County, constituency and religious character left the header (place pages keep county and constituency).
- **Proposed to close**: the strip is unchanged, as decided.
## Search rows
Both rows take their type label and tags from `lib/schoolFacts`: "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. The Selective tag no longer borrows the status colour.
## API
`type_group` on list, place and detail payloads, computed with the filter's own `type_group_for`. `nursery_provision` joins `SCHOOL_COLUMNS` so rows can flag it.
## Mobile (MOBILE.md)
No local server, so I injected the new markup and CSS into Tiffin's staging page, with the longest religious character as a worst case, and measured right edges at **360 / 390 / 430**: nothing past the 16px gutter, no page overflow. The long faith flag wraps inside its chip. That pass caught a wrapped fact line opening with "· 1,478 pupils"; fixed by clipping the separator strip (last commit). Flags aren't interactive, so no new tap targets.
## Tests
- Jest: 71 suites, 623 tests (new `schoolFacts.test.ts`, rewritten `schoolDetailHeader.test.tsx`, row tests for the shared flags). `tsc --noEmit` clean.
- Backend: 303 tests (new `test_type_group_payload.py`).
- E2E: a selective school is flagged Selective on its page and in its search row; the girls' school journey looks for "Nursery class"; the identity journey checks constituency and county have left the header. All three fail against current staging, as they should.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
The PR makes search and place payloads carry the whole-school pupil count (from gias_total_pupils) and a type_group, and adds nursery_provision to the search rows. On the frontend it redesigns the school header and search rows around a shared schoolFacts helper. It also fixes the Selective and faith tag bugs and updates the e2e tests to use page_size. The change looks healthy: the tests cover the new behaviour and I found no breaking, data-leaking or data-corrupting problems.
🟡 Minor
nextjs-app/components/school/SchoolDetailShell.tsx: The fact line uses the fact text as the React key. If two facts ever have identical text, such as an age range and a type label, the keys collide. The risk is low, but using the index or a slot name as the key would be safer.
backend/app.py: _with_whole_school_pupils sets total_pupils to None when gias_total_pupils is absent from the data. Before this change, a mart that had not been rebuilt still showed the results cohort. During that window every card will show no pupil count, which is an intentional trade-off but worth knowing about at deploy time.
## 🤖 AI Code Review (Claude Code)
The PR makes search and place payloads carry the whole-school pupil count (from gias_total_pupils) and a type_group, and adds nursery_provision to the search rows. On the frontend it redesigns the school header and search rows around a shared schoolFacts helper. It also fixes the Selective and faith tag bugs and updates the e2e tests to use page_size. The change looks healthy: the tests cover the new behaviour and I found no breaking, data-leaking or data-corrupting problems.
### 🟡 Minor
- **nextjs-app/components/school/SchoolDetailShell.tsx**: The fact line uses the fact text as the React key. If two facts ever have identical text, such as an age range and a type label, the keys collide. The risk is low, but using the index or a slot name as the key would be safer.
- **backend/app.py**: _with_whole_school_pupils sets total_pupils to None when gias_total_pupils is absent from the data. Before this change, a mart that had not been rebuilt still showed the results cohort. During that window every card will show no pupil count, which is an intentional trade-off but worth knowing about at deploy time.
Adds a search-filter type group (state, independent and so on) to the list, school-detail and place payloads. Adds nursery_provision to SCHOOL_COLUMNS. Moves the header and both search rows onto a shared schoolFacts helper that prints one fact line plus flags only for what applies. The change is well covered by backend, unit and e2e tests. I found no correctness, security or deploy problems in the diff.
✅ No issues found.
## 🤖 AI Code Review (Claude Code)
Adds a search-filter type group (state, independent and so on) to the list, school-detail and place payloads. Adds nursery_provision to SCHOOL_COLUMNS. Moves the header and both search rows onto a shared schoolFacts helper that prints one fact line plus flags only for what applies. The change is well covered by backend, unit and e2e tests. I found no correctness, security or deploy problems in the diff.
✅ No issues found.
tudor
merged commit 392ec29fdb into main2026-10-02 22:32:47 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Option 1 from the header proposal (round 2): one fact line, then flags only for what applies. Search rows print the same words.
Builds on #175 and #176. Merge those first. This branch contains both (merged, not cherry-picked), so once they land, this diff shrinks to the redesign alone. The three merge cleanly in any order.
Header
Search rows
Both rows take their type label and tags from
lib/schoolFacts: "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. The Selective tag no longer borrows the status colour.API
type_groupon list, place and detail payloads, computed with the filter's owntype_group_for.nursery_provisionjoinsSCHOOL_COLUMNSso rows can flag it.Mobile (MOBILE.md)
No local server, so I injected the new markup and CSS into Tiffin's staging page, with the longest religious character as a worst case, and measured right edges at 360 / 390 / 430: nothing past the 16px gutter, no page overflow. The long faith flag wraps inside its chip. That pass caught a wrapped fact line opening with "· 1,478 pupils"; fixed by clipping the separator strip (last commit). Flags aren't interactive, so no new tap targets.
Tests
schoolFacts.test.ts, rewrittenschoolDetailHeader.test.tsx, row tests for the shared flags).tsc --noEmitclean.test_type_group_payload.py).🤖 Generated with Claude Code
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>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>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>🤖 AI Code Review (Claude Code)
The PR makes search and place payloads carry the whole-school pupil count (from gias_total_pupils) and a type_group, and adds nursery_provision to the search rows. On the frontend it redesigns the school header and search rows around a shared schoolFacts helper. It also fixes the Selective and faith tag bugs and updates the e2e tests to use page_size. The change looks healthy: the tests cover the new behaviour and I found no breaking, data-leaking or data-corrupting problems.
🟡 Minor
🤖 AI Code Review (Claude Code)
Adds a search-filter type group (state, independent and so on) to the list, school-detail and place payloads. Adds nursery_provision to SCHOOL_COLUMNS. Moves the header and both search rows onto a shared schoolFacts helper that prints one fact line plus flags only for what applies. The change is well covered by backend, unit and e2e tests. I found no correctness, security or deploy problems in the diff.
✅ No issues found.