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+RepresentativeVotemodels (AN roll-call votes), idempotent import with per-scrutin integrity checks (8,434 scrutins / 1.27M votes verified against AN decompte totals) -
Scrutin.lawFK (plugin-gated) — 2,608 law-linked;isAmendmentVoteclassifier (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 byimport_elected_gouvfrom the ANmandature.placeHemicycle - Migration 0272 (law FK) is plugin-gated following the 0224/0255 pattern
- NEW
sync_rep_seatscommand: 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.lawin 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