Commit Graph
13 Commits
Author SHA1 Message Date
TudorandClaude Opus 5 8ab0ac0a04 feat(design): adopt the Cohort identity — new palette, type, mark and dark theme
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m6s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 12s
PR Checks / Build Frontend (no push) (pull_request) Successful in 49s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 3m13s
Implements the direction agreed from the identity board: Route C ("Cohort")
with the paper ground from C1, the Schibsted Grotesk / Literata pairing from
C2, and the iris accent from C3. Dark theme is in scope from the start rather
than retrofitted.

The audit found three things wrong beyond taste:

* No brand asset set. og:image was absent entirely, so every link shared into
  a class WhatsApp group rendered as a bare grey card. apple-touch-icon pointed
  at an SVG, which iOS ignores, and the manifest shipped no PNGs, so Android
  installs had no icon. The header mark and the favicon had also drifted into
  two different logos.
* No colour discipline. --primary and --trend-down were the same coral, so the
  main CTA and "below average" shared a hue. 58 distinct hex values were spread
  across component CSS, and the chart palette was still Chart.js's stock demo
  colours.
* A dark theme that was declared but never built — themeColor announced a dark
  variant with no dark styling behind it.

What changed:

Colour now has exactly three jobs that never borrow each other's hues: brand
(iris) for interactive and identity, status (teal/amber) for above/below a
comparison point, and phase for categories. Teal/amber rather than green/red
keeps the above/below signal readable for every form of colour blindness.
Every chromatic literal in component CSS is now a token, and the JS-painted
surfaces (Chart.js, Leaflet) read the tokens through lib/theme so they follow
the theme instead of ignoring it.

The mark is the five-bar cohort spread — the same object as the distribution
strip inside a school row, built from opacity steps so it inverts cleanly.
components/Logo.tsx is the single source; the favicon, apple-icon and share
card all derive from its geometry.

globals.css drops 123 dead global classes left over from the vanilla-JS app
(only the btn family, .skip-link and .main were still referenced), along with
the noise overlay. It also gains prefers-reduced-motion support, which was
missing entirely, and a type scale so the 54 ad-hoc font sizes have somewhere
to converge.

Verified: tsc clean, 159 unit tests pass, production build succeeds and
prerenders /icon.svg, /apple-icon and /opengraph-image. Three e2e journeys
added for the asset set, the themeColor/background match, and the dark theme
actually repainting — all silent failures that nothing on the page reveals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:13:04 +01:00
TudorandClaude Opus 4.8 17bd4d5a5e fix(detail): gap-honest year axis on the detail-page trend charts (Batch D)
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m8s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 12s
PR Checks / Build Frontend (no push) (pull_request) Successful in 50s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 2m38s
Applies the compare screen's chart-truthfulness rules (spec §8.1) to the
school detail page's time-series charts.

PerformanceChart (Results Over Time, both phases):
- Fill every academic year between the first and last data point via the
  shared fillAcademicYears helper, so cancelled/unpublished years (2019/20,
  2020/21, and — for KS2 — 2021/22) render as real gaps instead of
  compressed time. Each series and the England overlay map onto this filled
  axis with null for missing years; spanGaps:false so school lines never
  bridge a gap.
- Replace the primary-only COVID note with a distinct, honest gap caption:
  KS2 names the cancelled tests plus the unpublished 2021/22 school-level
  year; KS4 names the unpublished 2019/20–2020/21 GCSE grading years.
- Mobile x-axis switches to autoSkip so the longer (gap-honest) axis stays
  readable; the broken line still marks a missing year even when its tick
  label is skipped.

AdmissionsTrendChart:
- Same gap-honest axis + spanGaps:false so a missing admissions year is a
  real gap, not compressed time.

Point 12 (definite canvas heights): desktop is already a definite 280px;
fix the secondary detail's mobile .chartContainer, which fixed the outer
box at 220px and double-constrained PerformanceChart's own 220px canvas +
chip strip (clipping the chips onto the plot) — now height:auto to match
the primary view.

SatsChart is out of scope (single-year per-subject CSS bars — no year axis,
no canvas height to constrain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
2026-07-20 12:19:37 +01:00
TudorandClaude Fable 5 22769b6295 feat(compare): readable comparison chart on mobile
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 9m36s
PR Checks / Backend Smoke (pull_request) Successful in 5s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m49s
The compare chart squashed clustered schools into a thin band (y pinned
0-100) under an in-chart title + per-school legend that ate ~40% of a
300px card, leaving converging lines indistinguishable on phones.

- Auto-fit the y-axis to the data on all viewports (computeYBounds in
  lib/utils: padded + min-span for percentages, symmetric around 0 for
  progress, fitted for scores; negative pct-named trend metrics are not
  zero-clamped).
- Distinct point style per school (circle/triangle/rect/rectRot/star)
  as secondary encoding for convergence and colour-blindness.
- Mobile: drop in-chart title/legend/axis titles; add a chip row (colour
  dot + name) that doubles as tap-to-focus — highlights one school's
  line and dims the rest. Chart card 300px -> 340px, nearly all plot.
- Fix a latent colour mismatch: datasets were built from Object.entries
  whose integer-like URN keys enumerate in ascending numeric order,
  desyncing line colours from card colours; the chart now receives the
  ordered school list.
- Union years across schools instead of taking the first school's.
- Extract PerformanceChart's matchMedia pattern into hooks/useIsMobile.

Unit tests for metricKind/computeYBounds; e2e journey covers the mobile
chips and focus toggle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:02:26 +01:00
Tudor SitaruandClaude Opus 4.7 87442788d4 fix(chart): name the metric in the primary trend summary
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 14s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 51s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
The "Peaked at X%" hint didn't say which metric it referenced, leaving
parents to guess. Both branches now lead with "Reading, Writing & Maths".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 15:16:31 +01:00
Tudor SitaruandClaude Opus 4.7 7e182e88b2 feat(analytics): typed Umami event taxonomy across the funnel
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 18s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 57s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Add lib/analytics.ts with a single typed track() wrapper. SSR-safe,
never throws, no-ops when Umami isn't loaded. Event names form a
fixed union so refactors stay safe.

14 events wired:

  Discovery (3)
    search_submitted          FilterBar submit + near_me path
    near_me_used              all geolocation outcomes
    empty_results             search returns 0 schools

  Engagement (5)
    school_viewed             SchoolDetail + Secondary on mount, with
                              urn / phase / local_authority / from
    section_nav_used          section-nav links on both detail views
    chart_metric_changed      mobile chart chip switch
    metric_compared_in_rankings rankings metric dropdown
    external_link_clicked     Ofsted / school website / DfE (declarative
                              data-umami-event attributes)

  Conversion (5)
    compare_school_added      search/rankings/detail/compare sources
    compare_school_removed    detail toggle and compare page
    compare_viewed            once per session when there's a selection
                              (school_count, phase_mix)
    compare_metric_changed    compare page metric dropdown
    compare_shared            native sheet vs clipboard distinguished

  Operational (1)
    api_error                 caught in handleResponse, includes
                              endpoint / status / route

Suggested Goals to configure in the Umami dashboard for the funnel
report: search_submitted → school_viewed → compare_school_added →
compare_viewed → compare_shared.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 22:04:22 +01:00
Tudor SitaruandClaude Opus 4.7 d569a2afda feat(chart): mobile-only single-metric chip selector for Results Over Time
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 15s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 53s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
The Results Over Time chart on the school detail page was overcrowded
on phones — 5-item legend wrapping over the plot area, a hidden right
y-axis still rendered for the (collapsed) progress series, fixed
0–100 percentage scale that flattened all variation, angled x-axis
labels eating vertical space.

At ≤640px the chart now renders one metric at a time, selected via a
chip strip above the plot. No more legend. No more dual y-axis. The
y-axis auto-tightens around the actual data range so variation is
visible (a school sitting in the 70–90% band now uses a 65–95 axis
instead of squashing onto a 0–100 line). A small subtitle above the
chips sets the subject context ("KS2 SATs · Reading, Writing & Maths"
or "GCSE results · Year 11") so chip labels can describe the *view*
rather than re-stating the subject.

Chip labels are spelled out in parent-facing language — no internal
shorthand like "RWM":

  Primary  (KS2):  At expected level (default)
                   Above expected level
                   Pupil progress         (shows 3 series + mini-legend)

  Secondary (KS4): Attainment 8 (default)
                   English & Maths grade 4+
                   Progress 8

Chips disable themselves (greyed, with a "No data for this school"
title) when the underlying series has no data points. Desktop
behaviour is unchanged — the full multi-series chart with dual y-axis
still renders >640px.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 12:49:19 +01:00
Tudor SitaruandClaude Sonnet 4.6 f05bbba613 perf: resolve all P1–P5 performance issues from code review
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 21s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 50s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
P1 (backend/data_loader.py): Add load_latest_school_data() which pre-computes
the one-row-per-school latest-year snapshot (groupby, prev-year trend merge)
once at startup instead of on every /api/schools request. get_schools route
now starts from the cached snapshot rather than rebuilding it.

S3 (backend/app.py): Wrap synchronous geocode_single_postcode() call in
asyncio.to_thread() so postcode lookups no longer block the uvicorn event
loop. Admin reload endpoint also uses to_thread for both cache primes.

P2 (nextjs-app/components/HomeView.tsx): Add mapParamsRef guard so switching
back to map view does not re-fetch 500 schools when search params haven't
changed. Reset ref on new searches so fresh data is always fetched.

P3 (nextjs-app/lib/chartSetup.ts): Extract Chart.js registration into a
shared side-effect module. ComparisonChart and PerformanceChart now import
it instead of each calling ChartJS.register() independently.

P4 (backend/database.py): Remove unnecessary db.commit() from the read-only
get_db_session() context manager — saves a DB round-trip on every request.

P5 (backend/database.py): Add pool_recycle=1800 to SQLAlchemy engine to
prevent stale TCP connections from accumulating in long-running processes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 22:45:46 +01:00
Tudor SitaruandClaude Sonnet 4.6 a3cfffa4d0 feat: national average reference line now tracks per year on history chart
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 24s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 51s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m52s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Previously the dashed reference line was a flat horizontal at the latest
year's national average across all historical data, implying the national
figure was constant. Now the backend returns per-year averages in `by_year`
and the chart maps each data year to its own national average, so the
reference line correctly reflects how the national picture changed over time
(including COVID recovery dip/recovery).

- backend: /api/national-averages now includes `by_year` list alongside
  existing `year`/`primary`/`secondary` latest-year snapshot
- types: NationalAverages extended with `by_year: NationalAveragesYear[]`
- PerformanceChart: accepts `nationalByYear` prop; builds per-year series
  aligned to school data years, falling back to scalar prop if absent
- SchoolDetailView + SecondarySchoolDetailView: pass `nationalAvg.by_year`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:55:14 +01:00
Tudor SitaruandClaude Opus 4.6 dfa8058efc feat(school-detail): page-wide improvements across 5 sections
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 49s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
History chart
- Strip the redundant school-name title (already the page heading)
- Default to 2 visible lines: Reading, Writing & Maths expected % (teal,
  bold) + Exceeding (gold, lighter); progress score lines hidden by
  default, togglable via legend
- Add dashed national average reference line for RWM (primary) or
  Attainment 8 (secondary) so the school's trajectory is always in
  context
- Add trend summary chip above the chart computed from the data
  ("↓ Peaked at 90% (2016/17), currently 70%")
- Add COVID footnote when 2019/20 and 2020/21 data is absent

Ofsted section
- Collapse the four identical "Outstanding / Outstanding / Outstanding /
  Outstanding" boxes into a single prose line when all sub-grades match
  the overall verdict; show individual cards only when grades differ

SATs sub-metrics
- DeltaChip vs national average on Expected level row for Reading,
  Writing and Maths (national averages already in the API response)

Admissions
- Fix label: "Year 3 places per year" → "Reception places per year" for
  primary schools

Pupils & Inclusion
- DeltaChip + national avg hint on Eligible for pupil premium and
  Pupils receiving SEN support (both keys present in /api/national-averages)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:13:13 +01:00
Tudor SitaruandClaude Opus 4.6 d5f6366c28 fix(years): format academic years as 2016/17 across all views, remove legacy frontend and data
Build and Push Docker Images / Build Backend (FastAPI) (push) Failing after 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 53s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Has been skipped
Apply formatAcademicYear to all year displays in ComparisonChart, ComparisonView,
PerformanceChart, and RankingsView. Remove old vanilla JS frontend and CSV data
directory — both superseded by the Next.js app and Meltano pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 14:58:13 +01:00
tudorandClaude Sonnet 4.6 1009d7c976 fix(chart): format year as 2022/23 instead of 202223 on performance chart
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 32s
Build and Push Docker Images / Build Frontend (Next.js) (push) Failing after 55s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 31s
Build and Push Docker Images / Trigger Portainer Update (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 20:17:04 +01:00
tudorandClaude Sonnet 4.6 5eff9af69c feat: add secondary school support with KS4 data and metric tooltips
Build and Push Docker Images / Build Frontend (Next.js) (push) Has been cancelled
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Has been cancelled
Build and Push Docker Images / Trigger Portainer Update (push) Has been cancelled
Build and Push Docker Images / Build Backend (FastAPI) (push) Has been cancelled
- Backend: replace INNER JOIN ks2 with UNION ALL (ks2 + ks4) so primary
  and secondary schools both appear in the main DataFrame
- Backend: add /api/national-averages endpoint computing means from live
  data, replacing the hardcoded NATIONAL_AVG constant on the frontend
- Backend: add phase filter param to /api/schools; return phases from
  /api/filters; fix hardcoded "phase": "Primary" in school detail endpoint
- Backend: add KS4 metric definitions (Attainment 8, Progress 8, EBacc,
  English & Maths pass rates) to METRIC_DEFINITIONS and RANKING_COLUMNS
- Frontend: SchoolDetailView is now phase-aware — secondary schools show
  a GCSE Results section (Att8, P8, E&M, EBacc) instead of SATs; phonics
  tab hidden for secondary; admissions says Year 7 instead of Year 3;
  history table shows KS4 columns; chart datasets switch for secondary
- Frontend: new MetricTooltip component (CSS-only ⓘ icon) backed by
  METRIC_EXPLANATIONS — added to RWM, GPS, SEN, EAL, IDACI, progress
  scores and all KS4 metrics throughout SchoolDetailView and SchoolCard
- Frontend: METRIC_EXPLANATIONS extended with KS4 terms (Attainment 8,
  Progress 8, EBacc) and previously missing terms (SEN, EHCP, EAL, IDACI)
- Frontend: SchoolCard expands "RWM" to "Reading, Writing & Maths" and
  shows Attainment 8 / English & Maths Grade 4+ for secondary schools
- Frontend: FilterBar adds Phase dropdown (Primary / Secondary / All-through)
- Frontend: HomeView hero copy updated; compact list shows phase-aware metric
- Global metadata updated to remove "primary only" framing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 14:59:40 +00:00
TudorandClaude Sonnet 4.5 ff7f5487e6 Complete Next.js migration with SSR and Docker deployment
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 1m26s
Build and Push Docker Images / Build Frontend (Next.js) (push) Failing after 1m48s
Build and Push Docker Images / Trigger Portainer Update (push) Has been skipped
- Migrate from vanilla JavaScript SPA to Next.js 16 with App Router
- Add server-side rendering for all pages (Home, Compare, Rankings)
- Create individual school pages with dynamic routing (/school/[urn])
- Implement Chart.js and Leaflet map integrations
- Add comprehensive SEO with sitemap, robots.txt, and JSON-LD
- Set up Docker multi-service architecture (PostgreSQL, FastAPI, Next.js)
- Update CI/CD pipeline to build both backend and frontend images
- Fix Dockerfile to include devDependencies for TypeScript compilation
- Add Jest testing configuration
- Implement performance optimizations (code splitting, caching)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-02 20:34:35 +00:00