Skip to content

feat(votes): scrutins recap on law detail + voting history on rep detail

Scrutins votes — full feature (law + rep pages, real AN seating)

Closes the vote-recap feature: AN roll-call votes end-to-end, from the AN open data to interactive hemicycles on the site.

Backend (risefor-lobbying !39 (merged) — merge together with this MR)

  • Scrutin + RepresentativeVote models (AN roll-call votes), idempotent import with per-scrutin integrity checks (8,434 scrutins / 1.27M votes verified against AN decompte totals)
  • Scrutin.law FK (plugin-gated) — 2,608 law-linked; isAmendmentVote classifier (apostrophe/typo-tolerant, calibrated on all 8,434 titles: 7,219 amendment / 1,215 law-level)
  • Endpoints: /scrutins/{id}/votes/, /scrutins/{id}/party-tallies/ (per-party tallies + seatDirectory with occupant identity/party + per-group voter seats), /representatives/{id}/vote-history/ (server-side join, 200/page)
  • Representative.placeHemicycle + migration 0274; saved by import_elected_gouv from the AN mandature.placeHemicycle
  • Migration 0272 (law FK) is plugin-gated following the 0224/0255 pattern
  • NEW sync_rep_seats command: derives each deputy's seat from votes numPlace (≥70% confidence, dry-run default)

Law detail — "Les votes sur cette loi à l'Assemblée nationale"

  • Three-block structure: Votes sur la loi (Ensemble/motions, open) · Votes par article (collapsed) · Votes sur les amendements et sous-amendements (collapsed)
  • "Dernier vote sur la loi" banner — outcome of the last Ensemble vote
  • Chips carry real labels (l'article 7, l'amendement n° 846 (art. 3)…) calibrated 100% on all titles; reading context (1ʳᵉ lecture / CMP…)
  • Real AN hemicycle — official seat plan (582 seats), voters at their true numPlace, hollow chairs = real empty ones
  • Filters: click party rows and legend positions to select (multi-select, same-date tie-break on banner); filtered-out votes render hollow, never vanish
  • Seat-click popover: photo, name, group, position chip, "Voir la fiche" inner-nav — works on empty chairs too (seatDirectory identifies occupants)

Representative detail — "Historique de votes"

  • Same 3-block split, all collapsed by default, cross-block text search
  • Lifetime position chips (server aggregate)
  • Gated on the law-tracker plugin (features.law in orbit config — same source the menu reads)

Also in this MR

  • Law-detail heading architecture: H2 sections / H3 subsections (SEO)
  • Initiateurs resolved to real Representatives (normalized-name match against the law's linked reps)
  • "Où en est-on ?" banner: considers acte nodes AND events (laws whose last acte has no events were skipping their true last step), shows the organe body
  • Person-card link → inner navigation to the fiche (house pattern)
  • Law lifecycle: law-current-stage + timeline work continues in law-tracker #6 (closed); rep insights queued in #8 (closed)

Dependencies

  • risefor-lobbying !39 (merged) (models, endpoints, migrations 0269–0274) — merge with this MR
  • law-tracker !6 (merged) (scrutins download + upload-votes-data) — merge with this MR
  • Update-data chains verified: update-data.py --type suivi-loi → laws + scrutins + votes import; --type deputee → deputies + seats

Verified

  • 347/347 voters seat-mapped on test scrutins; group mean-x order matches the real AN layout (LFI far left → RN far right)
  • Absent counters == amber chairs by construction (SOC 25=25, DEM 11=11, Horizons 15=15)
  • tsc: no new errors (baseline intact); audit pass done: plugin-gated migration 0272, no origin-reconstruction navigation, group styles unified, no dead code/console.logs in the new components
Edited by Scaffold Digital Fleet

Merge request reports