Commit Graph

66 Commits

Author SHA1 Message Date
Tudor Sitaru 6d685b7e8a refactor(admissions): rename published_admission_number to places_offered
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 18s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 46s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Failing after 13s
Build and Push Docker Images / Trigger Portainer Update (push) Has been skipped
The staging model aliased EES's total_number_places_offered column as
published_admission_number, but PAN is the school's published capacity
(not exposed by EES at school level) — what we actually have is the
count of places offered in a given admissions round. The misnomer
propagated to the mart, SQLAlchemy model, API response, TS types, and
UI copy ("places per year", "(PAN)").

Rename end-to-end and fix the UI labels:
  - "29 places for 42 first-choice applications"
      → "29 places offered for 42 first-choice applications"
  - "Reception/Year 7 places per year"
      → "Reception/Year 7 places offered"
  - drop the misleading "(PAN)" suffix in the secondary view

Also add a comment in stg_ees_admissions clarifying this is the number
of places offered, not PAN. Requires dbt to rebuild fact_admissions
(marts are materialized as tables) before the backend can start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 09:45:43 +01:00
Tudor Sitaru 3bf2e8f262 feat(detail): replace SATs text tables with cascade bar charts, add admissions bar and history accordion
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 19s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 46s
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
Redesign the School Details page for better parent comprehension:
- New SatsChart component: horizontal cascade bars with ruler scale and
  national average marker (teal/coral palette matching site theme)
- Admissions section: visual progress bar showing 1st-preference demand
  vs available places, colour-coded by oversubscription status
- Historical data: collapse raw year-by-year table behind a disclosure
  element while keeping the performance line chart always visible
- EAL metric: add national average comparison via DeltaChip (backend now
  includes eal_pct in national averages endpoint)
- New formatWithSuppression utility for null/suppressed data handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:22:24 +01:00
Tudor Sitaru 8ce34b3ecc fix(list): read ofsted grade from fact_ofsted_inspection directly, fix dim_school schema lookup
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 18s
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 1m9s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
dim_school.sql was checking for int_ofsted_latest in target.schema (wrong schema)
due to the custom generate_schema_name macro using literal schema names. The
model lives in 'intermediate', so ofsted_grade/date/framework were always NULL
in dim_school, causing all list cards to show 'Not yet inspected'.

Fix 1: data_loader.py joins marts.fact_ofsted_inspection with DISTINCT ON to
get latest inspection per school — no pipeline re-run needed.

Fix 2: dim_school.sql uses schema='intermediate' so future dbt runs correctly
denormalise the Ofsted summary into dim_school.
2026-04-13 14:51:14 +01:00
Tudor Sitaru 6d02d366ce feat(api): expose ofsted_framework in school list response 2026-04-13 13:45:50 +01:00
Tudor Sitaru fe31be34a0 fix(secondary): expose GIAS total_pupils in school_info API response
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 22s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 48s
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 school_info object was missing total_pupils entirely, so the frontend
always fell back to the KS4 exam cohort from yearly_data. Now selects
s.total_pupils (GIAS NumberOfPupils — full school roll) as gias_total_pupils
in the main query and exposes it as total_pupils on school_info.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:15:31 +01:00
Tudor Sitaru dc66e22d4d feat: ingest official DfE KS2 national averages from EES data catalogue
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 19s
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 1m24s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Replaces computed means from our school dataset with the published DfE
national headline figures for the KS2 chart reference line.

- tap-uk-ees: new EESKs2NationalStream fetches the stable EES data-catalogue
  CSV (one row per year, England national total, AllSchools filter)
- dbt staging: stg_ees_ks2_national normalises columns, casts to float,
  filters to years >= 201617
- dbt mart: fact_ks2_national_averages — one row per year, official figures
- backend/models: Ks2NationalAverage SQLAlchemy model
- backend/app: /api/national-averages queries the mart for KS2 by_year;
  secondary by_year stays computed (no DfE KS4 national dataset yet)
- DAG: extract_ks2_national task added to school_data_annual_ees,
  runs in parallel with the main EES extract

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:40:33 +01:00
Tudor Sitaru 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 Sitaru 1e5c66d6ab fix(admissions): correct first_preference_offer_pct in dbt staging
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 18s
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 1m12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
The staging model was mapping EES column ``proportion_1stprefs_v_totaloffers``
straight onto ``first_preference_offer_pct``. That raw column is not a
percentage — it is a ratio of first-preference applications to total offers
(an oversubscription indicator, >1 means oversubscribed), so OLQH rendered
as "1%" when the true first-choice success rate is 27/42 = 64%.

The frontend display code is not at fault and is not patched here —
data-quality issues must be fixed at the source.

- stg_ees_admissions: compute ``first_preference_offer_pct`` as
  ``100 * number_1st_preference_offers / times_put_as_1st_preference`` —
  of families who listed this school first, the % that received an offer
  (0–100). Guard against divide-by-zero.
- stg_ees_admissions: expose the legitimate EES ratio as the new column
  ``oversubscription_ratio`` (1st-preference applications per place) for
  future use, clearly named.
- fact_admissions, FactAdmissions model, data_loader: propagate the new
  ``oversubscription_ratio`` column.
- SchoolAdmissions type: document both columns inline.
- buildSchoolSummary: reword the oversubscription clause so it reads
  sensibly across the whole 0–100 range (no more "just 64%").
- Hero chip subtitle: clearer phrasing "X% of first-choice applicants
  offered a place".

Requires a dbt run of stg_ees_admissions and fact_admissions on deploy
so the new column materialises.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 11:29:40 +01:00
Tudor Sitaru a562f408d2 refactor: expand RWM to "Reading, Writing & Maths" in user-facing text
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 24s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 52s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m51s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Expand the abbreviation in metric names (backend schemas), the home page
sort dropdown, README/QA docs, and pipeline comments. Short_name fields
and the compact row/map-card labels remain abbreviated for space.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 15:53:52 +01:00
Tudor Sitaru 17617137ea fix(data-info): drop NaN years before converting to int
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m12s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 21:41:11 +01:00
tudor f48faa1803 showing schools with no KS2 results
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 44s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m9s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
2026-03-30 18:14:43 +01:00
tudor 6e5249aa1e refactor(phase): merge KS2+KS4 into fact_performance, fix all phase inconsistencies
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 50s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m12s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m24s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
Root cause: the UNION ALL query in data_loader.py produced two rows per
all-through school per year (one KS2, one KS4), with drop_duplicates()
silently discarding the KS4 row. Fixes:

- New dbt mart `fact_performance`: FULL OUTER JOIN of fact_ks2_performance
  and fact_ks4_performance on (urn, year). One row per school per year.
  All-through schools have both KS2 and KS4 columns populated.
- data_loader.py: replace 175-line UNION ALL with a simple JOIN to
  fact_performance. No more duplicate rows or drop_duplicates needed.
- sync_typesense.py: single LATERAL JOIN to fact_performance instead of
  two separate KS2/KS4 joins.
- app.py: remove drop_duplicates (no longer needed); add PHASE_GROUPS
  constant so all-through/middle schools appear in primary and secondary
  filter results (were previously invisible to both); scope result_filters
  gender/admissions_policies to secondary schools only.
- HomeView.tsx: isSecondaryView is now majority-based (not "any secondary")
  and isMixedView shows both sort option sets for mixed result sets.
- school/[slug]/page.tsx: all-through schools route to SchoolDetailView
  (renders both SATs + GCSE sections) instead of SecondarySchoolDetailView
  (KS4-only). Dedicated SEO metadata for all-through schools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 14:07:30 +01:00
tudor d6a45b8e12 feat(map): fetch all schools for map view, add reference pin, cap radius at 5mi
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 45s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m6s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 31s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
- Remove 10-mile radius option; cap backend radius max at 5 miles
- Raise backend page_size max to 500 so map can fetch all schools in one call
- HomeView: when map view is active, fetch all schools within radius
  (page_size=500) instead of showing only the paginated first page;
  falls back to initial SSR schools while loading
- SchoolMap/LeafletMapInner: accept referencePoint prop and render a
  distinctive coral circle pin at the search postcode location

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 09:13:14 +01:00
tudor daf24e4739 fix(search): fix load-more silently failing due to missing page_size param
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m12s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 31s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Load-more requests read URL params (postcode, radius, etc.) but page_size
is never in the URL — it's hardcoded in page.tsx. Without it the backend
received page_size=None, hit a TypeError on (page-1)*None, returned 500,
and the silent catch left the user stuck on page 1.

In a dense area (e.g. Wimbledon SW19) 50 schools fit within ~1.8 miles,
so page 1 never shows anything beyond that regardless of selected radius.

Fix:
- Backend: give page_size a safe default of 25 instead of None
- Frontend: explicitly pass initialSchools.page_size in load-more params

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 09:08:15 +01:00
tudor b7bff7bf6b feat(seo): static sitemap generation job via Airflow
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 45s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m5s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m29s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
- Backend builds sitemap.xml from school data at startup (in-memory)
- POST /api/admin/regenerate-sitemap refreshes it after data updates
- New Airflow DAG (sitemap_generate) runs Sundays 05:00 and calls the endpoint
- Next.js proxies /sitemap.xml to the backend; removes the slow dynamic sitemap.ts
- docker-compose passes BACKEND_URL + ADMIN_API_KEY to Airflow env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 15:15:41 +01:00
tudor 1d22877aec feat(ux): 8 UX improvements — simpler home, advanced filters, phase tabs, 4-line rows
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 48s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m13s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
1. Simpler home page: only search box on landing, no filter dropdowns
2. Advanced filters: hidden behind toggle on results page, auto-open if active
3. Per-school phase rendering: each row renders based on its own data
4. Taller 4-line rows with context line (type, age range, denomination, gender)
5. Result-scoped filters: dropdown values reflect current search results
6. Fix blank filter values: exclude empty strings and "Not applicable"
7. Rankings: Primary/Secondary phase tabs with phase-specific metrics
8. Compare: Primary/Secondary tabs with school counts and phase metrics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 08:57:06 +01:00
tudor e8175561d5 updates for secondary schools
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 46s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m15s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
2026-03-28 22:36:00 +00:00
tudor 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
tudor 55749bdfaf debug(backend): log safe_query exceptions and rollback on failure
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 45s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m5s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 31s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 22:00:27 +00:00
tudor 4b02ab3d8a feat: wire Typesense search into backend, fix sync performance data bug
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 1m1s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m7s
Build and Push Docker Images / Build Integrator (push) Successful in 55s
Build and Push Docker Images / Build Kestra Init (push) Successful in 31s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m25s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
sync_typesense.py:
- Fix query string replacement: was matching 'ST_X(l.geom) as lng' but
  QUERY_BASE uses 'l.longitude as lng' — KS2/KS4 lateral joins were
  silently dropped on every sync run

backend:
- Add typesense_url/typesense_api_key settings to config.py
- Add search_schools_typesense() to data_loader.py — queries Typesense
  'schools' alias, returns URNs in relevance order with typo tolerance;
  falls back to empty list if Typesense is unavailable
- /api/schools: replace pandas str.contains with Typesense search;
  results are filtered from the DataFrame and returned in relevance order;
  graceful fallback to substring match if Typesense is down

requirements.txt: add typesense==0.21.0, numpy==1.26.4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 13:23:32 +00:00
tudor ca351e9d73 feat: migrate backend to marts schema, update EES tap for verified datasets
Pipeline:
- EES tap: split KS4 into performance + info streams, fix admissions filename
  (SchoolLevel keyword match), fix census filename (yearly suffix), remove
  phonics (no school-level data on EES), change endswith → in for matching
- stg_ees_ks4: rewrite to filter long-format data and extract Attainment 8,
  Progress 8, EBacc, English/Maths metrics; join KS4 info for context
- stg_ees_admissions: map real CSV columns (total_number_places_offered, etc.)
- stg_ees_census: update source reference, stub with TODO for data columns
- Remove stg_ees_phonics, fact_phonics (no school-level EES data)
- Add ees_ks4_performance + ees_ks4_info sources, remove ees_ks4 + ees_phonics
- Update int_ks4_with_lineage + fact_ks4_performance with new KS4 columns
- Annual EES DAG: remove stg_ees_phonics+ from selector

Backend:
- models.py: replace all models to point at marts.* tables with schema='marts'
  (DimSchool, DimLocation, KS2Performance, FactOfstedInspection, etc.)
- data_loader.py: rewrite load_school_data_as_dataframe() using raw SQL joining
  dim_school + dim_location + fact_ks2_performance; update get_supplementary_data()
- database.py: remove migration machinery, keep only connection setup
- app.py: remove check_and_migrate_if_needed, remove /api/admin/reimport-ks2
  endpoints (pipeline handles all imports)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 09:29:27 +00:00
tudor b850e8639c fix(migration): bump to schema v5 to re-trigger ALTER TABLE for RC columns
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 49s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m6s
Build and Push Docker Images / Build Integrator (push) Successful in 58s
Build and Push Docker Images / Build Kestra Init (push) Successful in 33s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
The v4 migration already ran before _apply_schema_alterations() was added,
so the new ofsted_inspections columns were never created. Bump to v5 so the
next backend restart re-runs the migration and applies the ALTER TABLE statements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:41:15 +00:00
tudor 5838f70ea4 fix(migration): ALTER TABLE to add new columns on existing supplementary tables
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 48s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m12s
Build and Push Docker Images / Build Integrator (push) Successful in 57s
Build and Push Docker Images / Build Kestra Init (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
create_all() only creates missing tables; it won't modify tables that already
exist from an older schema version. Add _apply_schema_alterations() which runs
idempotent ADD COLUMN IF NOT EXISTS statements after every migration so
supplementary tables (like ofsted_inspections) gain new columns without
dropping their existing data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:30:09 +00:00
tudor 1c49a135c4 feat(ofsted): add Report Card system support alongside legacy OEIF grades
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m11s
Build and Push Docker Images / Build Integrator (push) Successful in 58s
Build and Push Docker Images / Build Kestra Init (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Ofsted replaced single overall grades with Report Cards from Nov 2025.
Both systems are retained during the transition period.

- DB: new framework + 9 RC columns on ofsted_inspections (schema v4)
- Integrator: auto-detect OEIF vs Report Card from CSV column headers;
  parse 5-level RC grades and safeguarding met/not-met
- API: expose all new fields in the ofsted response dict
- Frontend: branch on framework='ReportCard' to show safeguarding badge
  + 8-category grid; fall back to legacy OEIF layout otherwise;
  always show inspection date in both layouts
- CSS: rcGrade1–5 and safeguardingMet/NotMet classes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:03:04 +00:00
tudor d1d994c1a2 fix(startup): stop re-migrating on every container restart
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 48s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m13s
Build and Push Docker Images / Build Integrator (push) Successful in 58s
Build and Push Docker Images / Build Kestra Init (push) Successful in 35s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Two issues caused the backend to drop and reimport school data on restart:

1. schema_version table was in the drop list inside run_full_migration(),
   so after any migration the breadcrumb was destroyed and the next
   restart would see no version → re-trigger migration
2. Schema version was set after migration, so a crash mid-migration
   left no version → infinite re-migration loop

Fix: remove schema_version from the drop list, and set the version
before running migration so crashes don't cause loops.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 10:57:01 +00:00
tudor b68063c9b9 fix(admissions): switch to EES content API + correct publication slug and columns
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 50s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m12s
Build and Push Docker Images / Build Integrator (push) Successful in 57s
Build and Push Docker Images / Build Kestra Init (push) Successful in 33s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
The EES statistics API only exposes ~13 publications; admissions data is not
among them. Switch to the EES content API (content.explore-education-statistics.
service.gov.uk) which covers all publications.

- ees.py: add get_content_release_id() and download_release_zip_csv() that
  fetch the release ZIP and extract a named CSV member from it
- admissions.py: use corrected slug (primary-and-secondary-school-applications-
  and-offers), correct column names from actual CSV (school_urn,
  total_number_places_offered, times_put_as_1st_preference, etc.), derive
  first_preference_offers_pct from offer/application ratio, filter to primary
  schools only, keep most recent year per URN

Also includes SchoolDetailView UX redesign: parent-first section ordering,
plain-English labels, national average benchmarks, progress score colour
coding, expanded header, quick summary strip, and CSS consolidation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:06:36 +00:00
tudor 00dca39fbd fix(migration): preserve geocoding across reimports
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m4s
Build and Push Docker Images / Build Integrator (push) Successful in 56s
Build and Push Docker Images / Build Kestra Init (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Before dropping tables, save all existing lat/lon coordinates keyed by URN.
After reimport, merge cached coordinates with any newly geocoded ones so
schools that already have coordinates skip the postcodes.io API call.
This makes repeated reimports fast and avoids re-geocoding ~15k schools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 22:09:51 +00:00
tudor 0e5b71d4a0 fix(ks2): make reimport async with polling to avoid HTTP timeout
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m12s
Build and Push Docker Images / Build Integrator (push) Successful in 58s
Build and Push Docker Images / Build Kestra Init (push) Successful in 31s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
The geocoding pass over ~15k schools takes longer than any reasonable
HTTP timeout. New approach:
- POST /api/admin/reimport-ks2 starts migration in background thread,
  returns {"status":"started"} immediately
- GET /api/admin/reimport-ks2/status returns {running, done}
- ks2.py polls status every 30s (max 2h) before returning
- Kestra flow timeout bumped to PT2H

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:08:06 +00:00
tudor 68b15400b0 feat(ks2): enable geocoding during reimport
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m3s
Build and Push Docker Images / Build Integrator (push) Successful in 56s
Build and Push Docker Images / Build Kestra Init (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Add geocode query param to /api/admin/reimport-ks2 (defaults true).
ks2.py passes ?geocode=true so postcodes are resolved to lat/lng in
the same migration pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:57:11 +00:00
tudor f1fb847164 feat(integrator): add KS2 re-import via Kestra and backend admin endpoint
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m13s
Build and Push Docker Images / Build Integrator (push) Successful in 40s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
- backend: POST /api/admin/reimport-ks2 runs full CSV migration in a thread
- backend/docker-compose: ADMIN_API_KEY env var (default: changeme) so the
  key is stable across restarts and the integrator can call the endpoint
- integrator: sources/ks2.py triggers the backend endpoint (900s timeout)
- integrator: flows/ks2.yml Kestra flow (manual trigger, no schedule)

To re-ingest after a DB wipe: trigger the ks2-reimport flow from the
Kestra UI at http://localhost:8080.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 12:25:29 +00:00
tudor dd49ef28b2 feat(data): integrate 9 UK government data sources via Kestra
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Trigger Portainer Update (push) Has been cancelled
Build and Push Docker Images / Build Frontend (Next.js) (push) Has been cancelled
Adds a full data integration pipeline for enriching school profiles with
supplementary data from Ofsted, GIAS, EES, IDACI, and FBIT.

Backend:
- Bump SCHEMA_VERSION to 3; add 8 new DB tables (ofsted_inspections,
  ofsted_parent_view, school_census, admissions, sen_detail, phonics,
  school_deprivation, school_finance) plus GIAS columns on schools
- Expose all supplementary data via GET /api/schools/{urn}
- Enrich school list responses with ofsted_grade + ofsted_date

Integrator (new service):
- FastAPI HTTP microservice; Kestra calls POST /run/{source}
- 9 source modules: ofsted, gias, parent_view, census, admissions,
  sen_detail, phonics, idaci, finance
- 9 Kestra flow YAMLs with scheduled triggers and 3× retry

Frontend:
- SchoolRow: colour-coded Ofsted badge (Outstanding/Good/RI/Inadequate)
- SchoolDetailView: 7 new sections — Ofsted sub-judgements, Parent View
  survey bars, Admissions, Pupils & Inclusion / SEN, Phonics, Deprivation
  Context, Finances
- types.ts: 8 new interfaces + extended School/SchoolDetailsResponse

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:44:04 +00:00
Tudor Sitaru 8c60614023 Fix CSP to allow Umami analytics and remove stale GA directives
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 58s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m48s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 15:43:51 +00:00
Tudor 5e296b6e5c Fix backend API to return location_info instead of search_location
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 57s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m17s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
The frontend expects location_info with coordinates array, but backend was
returning search_location with lat/lng keys. This fix enables the map toggle
to appear for location-based searches.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:38:56 +00:00
Tudor f4919db3b9 Add automatic schema versioning with startup migration
Build and Push Docker Image / build-and-push (push) Successful in 57s
On startup, the app now checks if the database schema version matches
the code. If there's a mismatch or no version exists, it automatically
runs a full data migration before starting.

- Add backend/version.py with SCHEMA_VERSION constant
- Add backend/migration.py with extracted migration logic
- Add SchemaVersion model to track DB version
- Add version check functions to database.py
- Update app.py lifespan to use check_and_migrate_if_needed()
- Simplify migrate_csv_to_db.py to use shared logic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:23:02 +00:00
Tudor 352eeec2db Add pupil absence data to school details modal
Build and Push Docker Image / build-and-push (push) Successful in 58s
Display test absence percentages (reading, maths, GPS, writing, science)
in a new section in the school modal. Requires database re-import.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 09:58:11 +00:00
Tudor e0e3bb788e Add list/map view toggle for location search results
Build and Push Docker Image / build-and-push (push) Successful in 56s
When searching by location, users can now toggle between list view
(school cards grid) and a split map view showing:
- Interactive map on left with all school markers
- Scrollable school list on right
- Blue marker for search location, default markers for schools
- Clicking a marker highlights and scrolls to the corresponding card

Mobile responsive with stacked layout on smaller screens.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 11:09:35 +00:00
Tudor 75677f4252 Add contact form to footer and simplify footer content
Build and Push Docker Image / build-and-push (push) Successful in 59s
Replace footer note with a contact form that emails contact@schoolcompare.co.uk
via FormSubmit.co. Keep only the data source attribution. Update CSP to allow
form submissions to FormSubmit.co and add responsive styling for the form.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 20:01:47 +00:00
Tudor 1a9341eaf4 Simplify school types and persist selected schools
Build and Push Docker Image / build-and-push (push) Successful in 1m1s
- Add runtime normalization of cryptic school type codes to user-friendly names
  (e.g., AC/ACC/ACCS -> "Academy", CY/CYS -> "Community")
- Update SCHOOL_TYPE_MAP in schemas.py with consolidated mappings
- Add normalize_school_type() and get_school_type_codes_for_filter() helpers
- Persist selected schools in localStorage across page refreshes
- Move "Add to Compare" button from modal footer to header

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 15:55:23 +00:00
Tudor 9cd36a0b15 Add Google Analytics 4 with cookie consent integration
Build and Push Docker Image / build-and-push (push) Successful in 1m2s
- Add GA4 measurement ID to config (default: G-J0PCVT14NY)
- Add /api/config endpoint to expose GA ID to frontend
- Update cookie consent with Analytics category (opt-in)
- Load GA4 only after user consents to analytics cookies
- Update CSP to allow Google Analytics domains

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 15:19:05 +00:00
Tudor 6597ee40fb bug fixing
Build and Push Docker Image / build-and-push (push) Successful in 58s
2026-01-10 11:40:02 +00:00
Tudor bb58d607c2 bug fix
Build and Push Docker Image / build-and-push (push) Successful in 56s
2026-01-10 11:32:40 +00:00
Tudor e1383b3432 Fix postcode search ValueError when calculating distances
Build and Push Docker Image / build-and-push (push) Successful in 59s
Use direct bracket indexing instead of .get() for pandas Series
row access in calc_distance function to ensure scalar values
are returned for pd.isna() checks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 11:25:21 +00:00
Tudor 79cf16d6b3 Add higher standard display and trend indicators to school cards
Build and Push Docker Image / build-and-push (push) Successful in 57s
- Display RWM Higher % alongside RWM Expected % on school cards
- Add trend indicators (up/down/stable arrows) showing year-over-year change
- Backend calculates previous year's RWM for trend comparison
- Trend appears on cards and in school detail modal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 14:36:01 +00:00
Tudor e3fc031ecf addings details and map to modal
Build and Push Docker Image / build-and-push (push) Successful in 59s
2026-01-09 11:52:13 +00:00
Tudor 8458d638ec bug fix
Build and Push Docker Image / build-and-push (push) Successful in 55s
2026-01-09 00:10:40 +00:00
Tudor 51836852e4 bug fix
Build and Push Docker Image / build-and-push (push) Successful in 59s
2026-01-09 00:07:51 +00:00
Tudor 116be294a3 bug fix
Build and Push Docker Image / build-and-push (push) Successful in 57s
2026-01-08 23:55:36 +00:00
Tudor 4b91eb403a bug fix
Build and Push Docker Image / build-and-push (push) Successful in 57s
2026-01-08 23:53:30 +00:00
Tudor b7943e1042 implementing map on school card; adding more school details
Build and Push Docker Image / build-and-push (push) Successful in 59s
2026-01-08 23:20:42 +00:00
Tudor 1d19c88e49 bug fix
Build and Push Docker Image / build-and-push (push) Successful in 56s
2026-01-08 16:18:11 +00:00