feat(admissions): publish the latest cut-off only, holding history back
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m4s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 3m17s

Earlier years are to become a paid feature, so they stop being published.

The load-bearing part is that this is a change to the API, not only to the
page. /api/schools/{urn} is public and unauthenticated: leaving
admission_distance_history in the payload while declining to render it would
have handed the whole record to anyone who opened the network tab. It is
withheld at the source, and the page follows.

Nothing changes upstream. The tap, the plausibility band and
fact_admission_distance are untouched and still load every published year, so
restoring history for entitled callers is a change to one function in
data_loader rather than a re-collection.

What the reader now gets is the latest figure on the Admissions tile, and a
Distance section that answers the question the number alone cannot: whether
their own address falls inside it. Retitled to "How far away are you?", which
is what it now does — the previous title described a record that is no longer
there.

Removed with the history: the trend chart, the year-by-year table, the
per-year verdict strip, the trend summary and the coverage note, along with
their CSS. The section goes from 743px to 417px.

One consequence worth naming. A run of years used to soften a single close
call — a home just outside one year's cut-off was usually inside another. With
one year published, the "too close to call" band is the entire safety margin
between a parent and a place they do not have, so the verdict now names its
year, and the three outcomes are tinted apart rather than distinguished by
wording alone.

The existing stylesheet test earned its keep here: the three verdict classes
were referenced before they were written, and it caught them. Unstyled, a
"beyond the cut-off" result would have been indistinguishable from an "inside"
one — the exact failure the longhand class map was written to prevent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
This commit is contained in:
TudorandClaude Opus 5 committed 2026-08-20 18:44:57 +01:00
1 parent 50b599a09b
commit c9a1892bfb
20 files changed
+382 -1215

No files matched your search

@@ -1,50 +1,51 @@
/**
* DistanceSection — "How far the last place went".
* DistanceSection — "How far away are you?"
*
* Its own section rather than a third tab inside Admissions.
* The section exists to answer one question a parent cannot answer from a
* number alone: whether their own address falls inside it. The figure itself is
* already on the Admissions tile above; this turns it into something they can
* act on.
*
* It began as a tab, which broke the admissions card in a way worth recording:
* the two admissions views are stacked in one grid cell so switching them never
* shifts layout, which means the card is sized by its TALLEST view. A view
* carrying a chart, a table and a map came to 1402px against the tile grid's
* 316px, so the default view — the one nobody had clicked — rendered as four
* tiles adrift in ~1080px of blank card, and Admissions alone accounted for
* half the height of every section on the page.
*
* The structural point stands on its own though: every other topic here is a
* section with a nav entry, and "how close did we need to live, and would we
* have got in?" is a topic, not a variant reading of the intake figures. The
* headline number stays on the Admissions tile where the intake story is; the
* record behind it lives here.
* Scope note. This used to carry the full published record — chart, year table
* and a set of shrinking rings. Earlier years are now held back as a paid
* feature and no longer leave the API at all, so what remains is the latest
* year and the check against it. The pipeline is unchanged: every published
* year is still loaded into fact_admission_distance, so restoring history for
* entitled callers is a serving change rather than a re-collection.
*
* Server component; the map and postcode form carry their own client boundary.
*/
import type { School, SchoolAdmissions, SchoolAdmissionDistance } from '@/lib/types';
import type { School, SchoolAdmissionDistance } from '@/lib/types';
import { Section, sectionStyles as styles } from './sectionShared';
import { CutoffDistanceDetail } from './CutoffDistanceDetail';
import { cutoffYearRows, hasCutoffDetail } from './lastDistanceOffered';
import { CutoffMapPanel } from './CutoffMapPanel';
export function DistanceSection({
admissionDistanceHistory,
admissionsHistory,
admissionDistance,
schoolInfo,
}: {
admissionDistanceHistory: SchoolAdmissionDistance[];
admissionsHistory: SchoolAdmissions[];
admissionDistance: SchoolAdmissionDistance | null;
schoolInfo: School;
}) {
const rows = cutoffYearRows(admissionDistanceHistory, admissionsHistory);
if (!hasCutoffDetail(rows)) return null;
// Without a figure there is nothing to compare against, and without
// coordinates there is nothing to measure — CutoffMapPanel enforces the
// second, but the section must not render an empty card either way.
if (
admissionDistance?.distance_m == null
|| schoolInfo.latitude == null
|| schoolInfo.longitude == null
) {
return null;
}
return (
<Section id="distance">
<h2 className={styles.sectionTitle}>How far the last place went</h2>
<h2 className={styles.sectionTitle}>How far away are you?</h2>
<p className={styles.sectionSubtitle}>
The distance of the furthest home offered a place each year, and how your
own address compares.
Check your postcode against the furthest home offered a place in
September {admissionDistance.year}.
</p>
<CutoffDistanceDetail rows={rows} schoolInfo={schoolInfo} />
<CutoffMapPanel schoolInfo={schoolInfo} cutoff={admissionDistance} />
</Section>
);
}