Windows-GPU-UNLOCK
RTX3090 UEFI 포트, 한국어 무리셋 윈도우 부팅 가이드, 검증된 소스와 벤치마크 제공
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기MonÉlu is a civic transparency platform that makes the voting record of every deputy in the French Assemblée Nationale fully accessible
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Every vote. Every deputy. In plain French.
MonÉlu is a civic transparency platform that makes the voting record of every deputy in the French Assemblée Nationale fully accessible — in plain language, in real time. Built for journalists, researchers, and engaged citizens who shouldn't need to dig through government ZIP exports to understand how their representatives vote.
Site: https://mon-elu.vercel.app · API: https://monelu-production.up.railway.app · API docs: /docs · Machine-readable spec: /openapi.json
| Phase | Status | What it covers |
|---|---|---|
| Phase 1 — Data platform | Live | Full ingestion pipeline, REST API, deputy profiles, vote records, scorecards |
| Phase 2 — Intelligence layer | Live | Semantic search over the legislative corpus (RAG, pgvector, Groq LLM) |
| Phase 3 — Automated refresh | Live | GitHub Actions daily cron — ingest + dbt run + RAG re-index |
| Phase 4 — dbt transformation layer | Live | Staging → intermediate → marts; lineage docs on GitHub Pages |
| Phase 5 — AWS infrastructure | Deferred | Terraform IaC archived to archive/infra-aws/ — modeled an architecture never built; Railway + Supabase cover current load |
| Phase 6 — CI/CD | Live | PR gates (ruff, pytest, dbt test bot), deploy workflow |
Assemblée Nationale Open Data (ZIP exports)
→ scripts/ingest_*.py fetch · parse · upsert with exponential-backoff retry
→ Supabase PostgreSQL deputies · votes · vote_positions · document_chunks
→ dbt (GitHub Actions) staging → intermediate → analytics_marts
→ api/routers/ direct psycopg2, RealDictCursor, parameterized SQL
→ Railway (FastAPI) JSON API · POST /search (RAG) · POST /verify (fact-check)
→ Next.js (frontend/, Vercel) Landing page · deputy / vote / group / department / theme
pages · chat · fact-check · vote-matching quiz
The API tier is fully stateless. All state lives in Supabase (managed Postgres with pgvector). Railway restarts on failure; /health returns live DB counts on every check. Errors are reported to Sentry when SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN are set - the integration no-ops when they are not (see docs/monitoring.md).
| Doc | What's in it |
|---|---|
docs/decisions.md | The full decisions log (ADR-001 to ADR-029, ADR-015 unused) - read before changing anything structural |
CLAUDE.md | Working guide for AI assistants on this repo |
docs/monitoring.md | Sentry setup, health checks, weekly feedback triage query |
docs/branch_protection.md | Required checks and merge rules |
docs/quiz-curation.md | How the quiz question set is curated and refreshed |
docs/pr-attention-score.md | PR review prioritisation heuristic |
Full interactive reference at /docs; the raw spec agents and generated clients should read is at /openapi.json.
Every route carries a summary and a description written for that reader - what it returns, in what units, and the domain caveat that applies (MON-260).
Each operationId is the handler's name in camelCase (listDeputies, getVote), so generated clients get readable method names; renaming a handler is therefore an API change (ADR-041 §5).
Successful public GET responses carry Cache-Control: public, max-age=300, so a CDN or a client cache may keep them for five minutes; /account/*, /keys/*, /health, writes and errors never do.
Rate limits are per endpoint (column rpm) - see Rate Limiting.
| Method | Endpoint | rpm | Description |
|---|---|---|---|
| GET | / | - | Redirects to the Next.js frontend |
| GET | /health | - | API status, live record counts, last ingestion, dbt mart row counts, RAG staging-table orphan check, cached Groq key and model probe |
| GET | /deputies | 30 | List deputies (search, department, party, active filters) |
| GET | /deputies/stats | 30 | Aggregate counts by party, department, mandate status |
| GET | /deputies/{id} | 30 | Deputy profile |
| GET | /deputies/{id}/votes | 30 | A deputy's voting record |
| GET | /deputies/{id}/scorecard | 10 | Presence rate, vote breakdown by position |
| GET | /deputies/{id}/alignment | 10 | Alignment with the deputy's own parliamentary group |
| GET | /deputies/{id}/dissident-votes | 10 | Votes where the deputy broke with their group |
| GET | /deputies/{id}/diverging-votes | 10 | Votes where the deputy diverged from the chamber majority |
| GET | /votes | 30 | List votes (result filter) |
| GET | /votes/latest | 30 | Last 10 votes |
| GET | /votes/{id} | 30 | Vote detail + all individual positions, and a dossier block linking the bill page (#369) |
| GET | /lois | 30 | Bills with at least one scrutin, newest scrutin first, status/theme filters (#369, ADR-035) |
| GET | /lois/{dossier_uid} | 30 | One bill's acte parcours with headline scrutins attached and amendment counts per acte (#369) |
| Method | Endpoint | rpm | Description |
|---|---|---|---|
| GET | /deputies/scorecards | 10 | Every deputy's scorecard in one response (dense table view) |
| GET | /deputies/scorecard.csv | 10 | Every deputy's scorecard as CSV |
| GET | /deputies/{id}/votes.csv | 10 | One deputy's full voting record as CSV |
| GET | /votes/{id}/positions.csv | 10 | Every deputy's position on one vote as CSV |
| Method | Endpoint | rpm | Description |
|---|---|---|---|
| POST | /search/ | 10 | Natural-language query over the legislative corpus (Phase 2) |
| POST | /search/share | 30 | Store a chat answer as an immutable snapshot (no LLM call, ADR-024) |
| GET | /search/share/{id} | 300 | Stored chat answer - backs the /chat/s/<id> share URL |
| POST | /verify/ | 10 | Fact-check a claim about a deputy's votes - structured verdict + citations |
| GET | /verify/{id} | 300 | Stored verdict snapshot (no LLM call) - backs /verifier/v/<id> |
| Method | Endpoint | rpm | Description |
|---|---|---|---|
| GET | /quiz/questions | 30 | Curated question set (versioned repo file) + live vote tallies |
| GET | /quiz/weekly | 30 | The scrutin of the week, picked automatically |
| POST | /quiz/match | 10 | Stateless agreement computation - nothing persisted or logged |
| POST | /quiz/share | 10 | Store an immutable result snapshot (recomputed server-side) |
| GET | /quiz/share/{id} | 300 | Stored result snapshot - backs the /quiz/s/<id> share URL |
| Method | Endpoint | rpm | Description |
|---|---|---|---|
| POST | /feedback/chat | 10 | Thumbs up/down on a chat answer (MON-70) |
| POST | /feedback/report | 10 | Report an error on a data page (MON-101) |
| POST | /install-events | 20 | Add one to today's total for a web app install-flow event (#598) |
| GET | /keys/usage | 10 | Per-endpoint, per-day usage for the calling API key |
| Method | Endpoint | rpm | Description |
|---|---|---|---|
| GET | /app/config | 300 | Minimum supported app version, remote feature switches (chat, verify), data horizon and caveat texts; overridable with APP_MIN_IOS_VERSION, APP_FEATURE_CHAT, APP_FEATURE_VERIFY |
Requires Authorization: Bearer <Supabase access token>, forwarded by the Next.js server or sent by the iOS app from its Keychain (ADR-041 §6) - the API verifies the signature itself and never trusts an asserted user id.
There are no sign-up, sign-in or password endpoints: Supabase Auth owns that flow.
The rpm column reads - because these routes carry no per-IP limit: every verified request counts against a 120 req/min allowance per account, keyed on the token's subject, and a rejected token counts against nothing.
| Method | Endpoint | rpm | Description |
|---|---|---|---|
| GET | /account/me | - | The caller's own profile, read through the restricted role under RLS (#413) |
| POST | /account/me | - | Create the caller's MonÉlu profile on first sign-in; idempotent (201 new, 200 existing) (#414) |
| PATCH | /account/me | - | Update display name, language, department and circonscription (#414) |
| DELETE | /account/me | - | Delete the account and, by cascade, every follow, bookmark and preference - irreversible (#414) |
| GET | /account/export | - | Every stored row about the caller as a JSON download (RGPD export, #414) |
| GET | /account/follows/deputies | - | Followed deputies, with their public identity fields (#414) |
| PUT | /account/follows/deputies/{id} | - | Follow a deputy; unknown id is 422 (#414) |
| DELETE | /account/follows/deputies/{id} | - | Unfollow a deputy; idempotent (#414) |
| GET | /account/follows/themes | - | Followed themes (#414) |
| PUT | /account/follows/themes/{slug} | - | Follow one of the ten themes; unknown slug is 422 (#414) |
| DELETE | /account/follows/themes/{slug} | - | Unfollow a theme; idempotent (#414) |
| GET | /account/bookmarks | - | Bookmarked votes, with title, date and result (#414) |
| PUT | /account/bookmarks/{vote_id} | - | Bookmark a vote; unknown id is 422 (#414) |
| DELETE | /account/bookmarks/{vote_id} | - | Remove a bookmark; idempotent (#414) |
| GET | /account/preferences | - | Stored notification preferences - nothing is ever sent (#414) |
Implemented with slowapi. Anonymous requests are keyed by remote IP; requests carrying a valid API key are keyed by key id and get the endpoint's base limit multiplied by that key's rate_limit_multiplier (api/limiter.py). There is no global default limit - every endpoint declares its own, listed in the rpm column above.
| Tier | Typical limit |
|---|---|
| Read endpoints | 30 req / min |
| Expensive reads (scorecards, CSV exports) | 10 req / min |
LLM-backed (POST /search/, POST /verify/) | 10 req / min |
| Stored-snapshot reads (share URLs) | 300 req / min |
Account routes (/account/*) | 120 req / min per verified account, not per IP |
API keys are issued manually (api_keys table, sha256-hashed) and sent in the X-API-Key header; usage is counted per key, per endpoint, per day in api_key_usage.
On limit exceeded: HTTP 429 · {"error": "Too Many Requests", "detail": "..."} · Retry-After + X-RateLimit-* headers.
Core: FastAPI · PostgreSQL 15 + pgvector (Supabase) · Python 3.11 · Railway · slowapi
Frontend: Next.js 15 · React 18 · Tailwind CSS · Framer Motion · SWR · Vercel
Monitoring: Sentry (API + frontend), opt-in via SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN
Phase 2 — RAG: OpenAI text-embedding-3-small · Groq openai/gpt-oss-120b · tiktoken · MLflow
Phase 4 — Transform: dbt 1.12 · dbt_utils · analytics_staging + analytics_marts schemas
Code quality: ruff (lint + format) · pre-commit
Assemblée Nationale Open Data — data.assemblee-nationale.fr
Static ZIP exports only — no REST API exists at the source (see ADR-010).
| Dataset | File |
|---|---|
| Deputies + organes (active, 17th legislature) | AMO10_deputes_actifs_mandats_actifs_organes.json.zip |
| Votes (scrutins, since 2025-07-01 in prod) | Scrutins.json.zip |
git clone <repo> && cd MonElu
cp .env.example .env # set DATABASE_URL, OPENAI_API_KEY, GROQ_API_KEY
python3 -m venv venv
venv/bin/pip install -r requirements.txt
make start # start local Postgres + pgAdmin
make migrate # apply schema (001_init.sql)
make ingest # deputies → votes → positions
make fix-deputies
make api # → http://localhost:8000/docs
For the frontend:
cd frontend && npm install
cp .env.example .env.local # point NEXT_PUBLIC_API_URL at http://localhost:8000
npm run dev # → http://localhost:3000
make start docker compose up -d (Postgres + pgAdmin)
make stop docker compose down
make migrate apply 001_init.sql to DATABASE_URL
make ingest full local ingestion (deputies → votes → positions)
make ingest-prod production ingestion (last 3 months)
make fix-deputies resolve party names + expand department codes
make api uvicorn api.main:app --reload
make psql psql into the running Postgres container
make check-db table sizes, row counts, pgvector status
make rag-index full rebuild: re-embed all chunks via the staging swap (~$0.006)
make rag-notable build the notable-deputy chunks (ADR-017)
make rag-laws build the law-summary chunks
make rag-stats chunk counts by type
make rag-clear truncate document_chunks
make rag-test run 3 sample queries end-to-end
make rag-test-sql check which sample questions the SQL router claims
make rag-eval MLflow evaluation (router + retrieval suites)
make mlflow-ui MLflow dashboard at http://localhost:5001
make dbt-run run all dbt models (staging → intermediate → marts)
make dbt-test run all dbt tests
make dbt-docs generate + serve docs at http://localhost:8082
make dbt-lineage open lineage graph in browser
make dbt-clean remove compiled dbt artifacts
make frontend-dev next dev → http://localhost:3000
make frontend-build next build
make frontend-start next start (serves the production build)
A full dbt project (transform/) sits between raw ingestion and the FastAPI layer.
| Layer | Schema | Materialisation | Models |
|---|---|---|---|
| Staging | analytics_staging | View | stg_deputies, stg_votes, stg_vote_positions |
| Intermediate | analytics_intermediate | Table (overrides the layer default) | int_party_vote_majority |
| Marts | analytics_marts | Table | mart_deputy_scorecard, mart_vote_summary, mart_party_alignment |
| Endpoint | Source |
|---|---|
GET /deputies/{id}/scorecard | analytics_marts.mart_deputy_scorecard |
GET /deputies/{id}/alignment · /dissident-votes | analytics_marts.mart_party_alignment |
GET /votes · GET /votes/latest | analytics_marts.mart_vote_summary |
GET /votes/{id} | analytics_marts.mart_vote_summary + raw vote_positions |
GET /groups | raw deputies |
GET /groups/{slug} | mart_deputy_scorecard + mart_party_alignment + raw vote_positions |
GET /departments/{code} | Raw tables, plus the marts for the highlight rates on a dedicated connection - falls back to raw-only when the marts are absent |
GET /themes/{slug} | Raw tables only - no mart dependency |
| Landing page latest votes | analytics_marts.mart_vote_summary |
dbt runs automatically on every merge to master via deploy.yml, and after every ingestion in ingest_prod.yml.
Lineage docs are published to GitHub Pages on push to master touching transform/.
Required GitHub secrets: DBT_HOST · DBT_PORT · DBT_USER · DBT_PASSWORD · DBT_DBNAME
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml | Every PR to master | ruff lint + pytest (unit and integration) + frontend lint / test / build + dbt compile + dbt test (posts results as PR comment) |
deploy.yml | Merge to master | dbt run + test against prod Supabase |
ingest_prod.yml | Daily 06:00 UTC, weekdays | Ingest votes + deputies + rebuild RAG index |
summarize_backfill.yml | Daily 07:00 UTC | Backfill plain-French vote summaries (runs after the Groq TPD reset) |
dbt_docs.yml | Push to master touching transform/ | Deploy lineage docs to GitHub Pages |
All PRs must pass CI before merge — see docs/branch_protection.md.
rag/
├── pipeline/
│ ├── chunker.py Five strategies: vote · deputy · party · global_stats ·
│ │ notable_deputy; chunk_all() emits the whole corpus
│ ├── chunk_notable_deputies.py Index builder for the notable-deputy chunks (ADR-017,
│ │ `make rag-notable`) - has its own already_indexed guard
│ ├── chunk_law_summaries.py plain-French summaries of the main texts
│ ├── embedder.py Batched OpenAI embedding (100 chunks/batch) → document_chunks
│ └── index_manager.py CLI: build / stats / clear
├── chain/
│ ├── retriever.py pgvector exact cosine similarity + chunk_type / deputy / result filters
│ ├── hybrid_retriever.py BM25 + vector blend - not on the live path, kept for eval (ADR-013)
│ ├── sql_router.py ranking-intent questions answered by SQL, not retrieval
│ ├── llm_router.py routes a question to SQL vs RAG; detect_claim() for the verify nudge
│ ├── prompts.py TTL-cached French civic assistant system prompt + RAG template
│ ├── rag_chain.py ask() - retrieve → format → Groq LLM
│ └── verify.py verify_claim() - structured fact-check verdict (ADR-022)
└── experiments/
└── mlflow_eval.py 11 golden Q&A pairs: router suite (SQL ground truth) + retrieval suite
Index stats (2026-07, refresh with make rag-stats): ~5,900 chunks - 5,105 vote · 645 deputy · 102 notable_deputy · 20 law_summary · 12 party · 1 global_stats. ~$0.006 to embed the full corpus · $0 per query beyond the question embedding.
The IVFFlat index was dropped (migration 003) - at this corpus size an exact cosine scan is faster and gives perfect recall. The notable-deputy retrieval pin was removed with it (MON-76): an MLflow pin-on/pin-off run showed the exact scan already ranks the deputy's own chunk first. See ADR-008 in docs/decisions.md.
The AWS Terraform stack moved to archive/infra-aws/ — it modeled an
Airflow+Spark architecture that was never built, with no compute for the
actual FastAPI app. See ADR-021. networking, s3, and rds modules are
kept as reference for a future AWS migration; ec2 does not survive that
design.
deputies| Column | Type | Notes |
|---|---|---|
deputy_id | TEXT PK | AN uid, e.g. PA1592 |
full_name | TEXT | |
first_name / last_name | TEXT | |
party | TEXT | Full GP name, e.g. Rassemblement National |
party_short | TEXT | organeRef, e.g. PO845401 |
circonscription / department | TEXT | Full name, e.g. Yvelines |
mandate_start / mandate_end | DATE | mandate_end is null if active |
photo_url | TEXT | Official portrait from assemblee-nationale.fr |
votes| Column | Type | Notes |
|---|---|---|
vote_id | TEXT PK | e.g. VTANR5L17V1234 |
voted_at | TIMESTAMPTZ | |
vote_title | TEXT | Full legislative title |
vote_type | TEXT | e.g. SPO |
result | TEXT | adopté or rejeté |
votes_for / votes_against / abstentions / total_voters | INTEGER | |
dossier_id | TEXT | Linked dossier, if any - only populated by the AN since 2026-03 (ADR-035) |
scrutin_kind | TEXT | What was voted on: ensemble / motion / amendement / article / autre, classified from objet.libelle at ingestion (ADR-035 §4) |
vote_positions| Column | Type | Notes |
|---|---|---|
vote_id | TEXT FK → votes | Composite PK with deputy_id |
deputy_id | TEXT FK → deputies | Composite PK with vote_id |
position | VARCHAR(15) | pour / contre / abstention / nonVotant |
document_chunks (Phase 2)| Column | Type | Notes |
|---|---|---|
id | BIGSERIAL PK | |
content | TEXT | French prose chunk |
metadata | JSONB | chunk_type, vote_id or deputy_id, etc. |
embedding | vector(1536) | OpenAI text-embedding-3-small |
| Table | Purpose |
|---|---|
api_keys | Manually issued public API keys - sha256 hash, label, rate_limit_multiplier |
api_key_usage | Per-key, per-endpoint, per-day request counters |
verifications | Immutable fact-check verdict snapshots behind /verifier/v/<id> (ADR-022) |
chat_shares | Immutable chat answer snapshots behind /chat/s/<id> (ADR-024) |
quiz_shares | Server-recomputed quiz result snapshots behind /quiz/s/<id> (ADR-025); result JSONB optionally carries the sharer's answers, and the themes derived from them, when they opt in (ADR-028) |
feedback | type-discriminated feedback sink (chat thumbs / data-page reports) with a JSONB payload |
install_event_counts | Daily (day, event, platform) -> count totals for the web app install flow, no visitor data (#598) |
agenda_items | Séance publique ODJ points, one denormalized row per point (ADR-030) |
dossiers | One row per L17 dossier législatif - title, procédure, derived status + raw status_label, and has_scrutins, which decides whether a bill gets a page (ADR-035) |
dossier_actes | The flattened acte parcours, PRIMARY KEY (dossier_uid, acte_uid) - 43 acte uids belong to two dossiers each (ADR-035 §3) |
data/migrations/ is applied by scripts/migrate.py, which keeps a schema_migrations ledger so each file runs exactly once. All statements are IF NOT EXISTS-guarded and safe to re-run.
| File | What it adds |
|---|---|
001_init.sql | Core schema: deputies, votes, vote_positions, document_chunks |
002_vote_summaries.sql | votes.summary_plain + votes.theme |
003_schema_cleanup.sql | Drops dead indexes, including IVFFlat (see the RAG section) |
004_api_keys.sql | api_keys + api_key_usage |
005_feedback.sql · 005_verifications.sql | feedback, verifications |
006_drop_position_id.sql | Removes the redundant surrogate key on vote_positions |
007_chat_shares.sql · 008_quiz_shares.sql | Share snapshot tables |
009_agenda.sql | agenda_items (ADR-030) |
010_rls_backfill.sql | ENABLE ROW LEVEL SECURITY on the seven tables 004-009 missed (MON-248) |
011_changed_at.sql | changed_at on the four ingested tables - the cache-invalidation signal, distinct from ingested_at (GH #353) |
012_dossiers.sql | dossiers, dossier_actes, votes.scrutin_kind (MON-243, ADR-035) |
014_install_events.sql | install_event_counts, daily install-flow totals (#598) |
api/)| Module | Purpose |
|---|---|
main.py | App entry point - CORS, rate limiting, Sentry, exception handlers, API-key usage middleware, health check |
auth.py | API key resolution from the X-API-Key header |
account_auth.py | Supabase access-token verification (JWKS, ES256/RS256) and the require_account dependency (ADR-040, #413) |
db.py | psycopg2 connection helpers - the owner pool for public routes, and the restricted monelu_app_user pool + account_transaction() for account routes |
limiter.py | Shared slowapi Limiter + tiered_limit() (per-key multipliers) |
csv_export.py | Streaming CSV response helper (MON-97) |
routers/deputies.py | Deputy list, profile, scorecard, alignment, dissident/diverging votes, CSV exports |
routers/votes.py | Vote list, latest, detail, positions CSV |
routers/departments.py | GET /departments/{code} - department page data (MON-107) |
routers/groups.py | GET /groups - the groups and their seat counts; GET /groups/{slug} - parliamentary group page data (ADR-026) |
routers/themes.py | GET /themes/{slug} - theme hub pages (MON-106) |
routers/search.py | POST /search/ - RAG query, plus chat share snapshots (ADR-024) |
routers/verify.py | POST /verify/ + GET /verify/{id} - fact-check verdicts (ADR-022) |
routers/quiz.py | /quiz/* - questions, weekly scrutin, stateless matching, share snapshots (ADR-025) |
routers/feedback.py | Chat thumbs and data-page error reports (MON-70, MON-101) |
routers/install_events.py | POST /install-events - daily install-flow totals for the ADR-041 Android trigger (#598) |
frontend/)Next.js 15 (App Router) + Tailwind + Framer Motion, deployed on Vercel separately from the FastAPI backend. Dark mode is supported (ADR-027).
| Route | Purpose |
|---|---|
/ | Landing page - cinematic scroll experience, live stats, hero search |
/deputes · /deputes/[id] · /deputes/tableau · /deputes/comparer | Deputy directory, profile, dense sortable table, side-by-side comparison |
/votes · /votes/[id] | Vote list and vote detail with per-deputy positions |
/groupes/[slug] · /departements/[code] · /themes/[slug] | Group, department, and theme hub pages |
/agenda | Ordre du jour of the séance publique, grouped by sitting day (MON-213); renders an explicit empty state during recess |
/chat · /chat/s/[id] | RAG chat (the fact-check UI lives here, ADR-023) and shared answer snapshots |
/verifier · /verifier/v/[id] | Redirect to the chat · shared fact-check verdicts |
/quiz · /quiz/s/[id] | Vote-matching quiz and shared results |
/mon-depute | Personal deputy view |
/embed/votes/[id] | Embeddable vote widget |
/partager | Web Share Target endpoint (MON-115) - normalises an OS share payload into a claim and redirects to /chat?mode=verify |
/donnees · /developpeurs · /methodologie | Open data, API usage guide, methodology |
/api/oembed | oEmbed provider (MON-266) - resolves a /votes/<id> URL to the rich payload that makes Notion, Slack, Substack, Ghost, WordPress and Discourse unfurl the pasted link into the embed widget |
/api/revalidate | Internal ISR revalidation webhook, guarded by a shared secret - not a public route |
/a-propos · /accessibilite · /mentions-legales · /confidentialite · /licence-donnees | Institutional pages |
| Module | Purpose |
|---|---|
src/lib/api.ts | Typed API client for all backend endpoints |
src/lib/seo.ts · src/app/sitemap.ts | Canonical site URL, metadata helpers, generated sitemap |
src/lib/agenda.ts | Paris-timezone window arithmetic and the summary/objet fallback shared by /agenda and the homepage teaser - summary_plain is NULL until MON-211 ships, so the official wording is the lead whenever there is no one-liner |
src/lib/an.ts | Official assemblee-nationale.fr URLs built from stored ids - the deputy profile link behind Person.sameAs and the dossier link shared by vote cards and Event.about (MON-267) |
src/lib/faq.ts | Q&A copy for /methodologie and /a-propos, rendered as the visible text and published as FAQPage JSON-LD (MON-268) - edit the answers here, not in the pages; __tests__/app/faq-jsonld.test.tsx fails if the two diverge |
src/lib/oembed.ts | The oEmbed contract in one place (MON-266): which URLs the provider answers for, the discovery-link href every embeddable page must emit, and the iframe markup - all derived from SITE_URL |
src/lib/exports.ts | The three published CSV exports, described once - /donnees renders the cards from this array and buildDataCatalogJsonLd() marks the same entries up as DataCatalog/Dataset JSON-LD (MON-262); __tests__/app/dataset-jsonld.test.tsx fails if the page and the markup diverge |
src/components/home/ | Landing-page scenes, live pulse panel, trust strip |
src/components/HeroSearch.tsx · GlobalSearch.tsx | Search entry points wired to the API |
scripts/)| Script | Purpose |
|---|---|
ingest_deputies.py | Downloads AMO10 ZIP, upserts deputy profiles |
ingest_votes.py | Downloads Scrutins ZIP, upserts votes (--since flag) |
ingest_positions.py | Extracts individual deputy positions from Scrutins ZIP |
ingest_organes.py | Parses Organes.json for parliamentary group membership |
ingest_agenda.py | Downloads the Agenda ZIP, upserts séance publique ODJ points (--since flag, MON-210) |
ingest_dossiers.py | Downloads the Dossiers_Legislatifs ZIP, upserts every L17 dossier and its flattened acte parcours (MON-243, ADR-035) |
run_ingestion_prod.py | Orchestrates the full pipeline with timing summary |
update_party.py | Resolves GP party names and expands department codes |
backfill_party_labels.py | Enforces the 12 canonical group labels behind /groups/{slug} |
generate_vote_summaries.py | LLM-generated plain-French vote summaries (summarize_backfill.yml) |
create_dbt_profile.py | Writes transform/profiles.yml from DBT_* env vars (CI + deploy) |
migrate.py | Applies pending migrations against the ledger - also the Railway start hook |
check_db_size.py | Prints table sizes and DB storage usage |
purge_test_fixtures.py | Removes test fixture rows from a database |
All scripts use exponential-backoff retry (5 attempts, 2 s base) and upsert via ON CONFLICT ... DO UPDATE.
allow_credentials=False, allow_methods=["GET", "POST"] - POST required for /search. An empty CORS_ORIGINS blocks all cross-origin requests and logs a startup warninglimit capped at 200, offset at 100,000 on all list endpoints/account/* verifies the Supabase access token itself (ES256/RS256 against the project JWKS - signature, expiry, audience, issuer) and never trusts a forwarded user id. Account data is read through a second pool connecting as the restricted monelu_app_user role, inside a transaction that opens with SET LOCAL app.user_id, so RLS applies; public routes keep the owner connection. Tokens and claims are never logged or returned.env is gitignoredpip install pre-commit ruff
pre-commit install # runs automatically on every git commit
| Hook | What it enforces |
|---|---|
trailing-whitespace | No stray spaces at line ends |
end-of-file-fixer | Files end with a newline |
check-yaml / check-json | Syntax errors in config files |
check-merge-conflict | No committed <<<<<< markers |
check-added-large-files | Blocks files over 500 KB |
debug-statements | Blocks breakpoint() / pdb.set_trace() |
ruff | Lint + auto-fix (imports, bugbear patterns, isort) |
ruff-format | Black-compatible formatting |
Lint config: ruff.toml — line length 100, T201 (print) allowed in scripts/ and rag/.
pytest tests/ -m "not integration" # unit tests - no database required
pytest tests/integration -m integration
cd frontend && npm test # Jest + Testing Library
Both suites run on every PR via ci.yml and block merge on failure.
tests/unit/test_readme_endpoints.py checks the API Endpoints tables above against app.openapi() and the live slowapi limits: adding, removing, or re-limiting an endpoint without updating this file fails CI. Edit the table, don't loosen the test.
The same file asserts every route in the spec has a non-empty summary and a description of real length, so a new endpoint cannot ship undocumented (MON-260). FastAPI reads summary off the @router decorator and description off the handler's docstring.
nonVotant ≠ abstention — present in chamber but did not vote; counts toward presence_rate (a nonVotant deputy was present) but is excluded from the pour/contre/abstention percentages (see ADR-019 in docs/decisions.md)rejeté outnumbers adopté — the 17th legislature has no stable majorityOrganes.json GP mandats; 575/577 deputies covered (2 have no active parliamentary group — expected edge case)"78" → "Yvelines") for all 96 metropolitan + DOM departments2025-07-01 (Supabase free tier); run --since 2024-07-07 locally for the full legislatureAll unhandled exceptions return a consistent envelope — never a traceback:
{"error": "Internal server error", "status": 500}
Full stack traces are written to the server log (logging.error) and never exposed to clients.
| GET | /lois/{dossier_uid}/amendements | 30 | A bill's amendment and article scrutins on demand, optional acte_uid filter (#369) |
| GET | /departments/{code} | 30 | Department page data - deputies, aggregates, split votes (MON-107) |
| GET | /groups | 30 | Every parliamentary group with its current seat count, largest first |
| GET | /groups/{slug} | 30 | Parliamentary group page data - members, dissidence, divided votes (ADR-026) |
| GET | /themes/{slug} | 30 | Theme hub - per-theme stats, party positioning, vote list (MON-106) |
| GET | /agenda | 30 | Upcoming séance publique items grouped by day, from/to window (MON-212, ADR-030) |
| GET | /feed | 30 | The iPhone app's Accueil feed: cards for a followed deputy_id and themes, tab pour_vous or assemblee, one ISO week (#562, ADR-042) |
| GET | /feed/week | 30 | The recap behind the app's « Votre semaine » card: the week's scrutin counts and the followed deputy's positions, nonVotant kept apart (#563, ADR-042) |
| PATCH | /account/preferences | - | Store notification preferences - nothing is ever sent (#414) |
| DELETE | /account/preferences | - | Reset notification preferences to defaults (#414) |
routers/lois.py | /lois/* - bill parcours, headline scrutins bound to AN séance actes, amendment counts (ADR-035) |
routers/keys.py | GET /keys/usage - usage accounting for the calling key |
routers/account.py | /account/* - the signed-in caller's profile, follows, bookmarks, stored notification preferences, RGPD export and deletion, all under RLS (ADR-040) |
quiz_data.py | Curated, versioned quiz question set - updated quarterly by PR (ADR-025) |
departments_data.py · groups_data.py · themes_data.py | Canonical code/slug ↔ label maps, mirrored in frontend/src/lib/ |
schemas.py | Pydantic response models (all fields Optional to match DB NULLs) |