Apply a11y classes (dark, dyslexia, high-contrast, large-text) from
localStorage synchronously before any paint, eliminating the visible
flash on initial load. Also fix lang="en" → lang="fr".
The useEffect in AccessibilityProvider continues to handle subsequent
toggles — the inline script only covers the initial page load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- home.tsx: add SynthesisText component — detects "Sur X : / Concernant X :"
thematic prefixes and renders them in serif/teal, replaces raw whitespace-pre-line
- home.tsx: redesign client-side PDF — site color palette (#F9F7F1, #1b5f6a),
Georgia serif typography, thematic prefix highlighting, tricolor stripe
- app.py: redesign consultation print export — same visual identity, server-side
paragraph parsing with theme span injection before HTML-escaping
- transparence.tsx: rewrite priority explanation to be explicit about what
"critique/haute/moyenne" means in practice (injection vs monitoring, why)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 11 articles resolved manually via Légifrance web search and validated
against the PISTE API (getArticle endpoint). The search endpoint returned
HTTP 500 consistently; IDs were extracted from canonical Légifrance URLs
and cross-checked against the API for num/état correctness.
Articles covered: CP-225-1, L1881-24, CP-222-33, CP-222-33-2-2,
CP-421-2-5, CP-211-1, L1881-24bis, CP-226-4-1, CP-212-1, CP-223-13,
L1881-29. Baseline texts stored for drift detection via --check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a two-layer approach to ensure legal texts are accurate and current:
Option A — runtime injection (legifrance_client.py):
- LegifranceClient: OAuth2 via PISTE (oauth.piste.gouv.fr), fetches consolidated
article text via api.piste.gouv.fr/dila/legifrance/lf-engine-app
- Redis cache with 24h TTL (in-memory fallback if no Redis)
- build_filter_prompt() injects live texts into the prompt with explicit priority
over static descriptions — model uses official Légifrance text, not training memory
- Graceful fallback to static prompt if API is unavailable — filtering never blocks
Option B — weekly sync script (scripts/check_legal_refs.py):
- Reads legal_refs.yaml (11 tracked articles, priority critical/high/medium)
- Fetches current text from Légifrance, diffs against stored baseline
- --init: resolves LEGIARTI IDs on first run
- --check: report only; --update: saves new texts to YAML
- Exit code 2 when changes detected (CI/n8n-compatible)
Supporting changes:
- legal_refs.yaml: tracked articles with code LEGITEXT IDs, priorities, rationale
- requirements.txt: add pyyaml>=6.0.2 and requests>=2.32.0
- .env.example: document PISTE_CLIENT_ID, PISTE_CLIENT_SECRET, LEGIFRANCE_CACHE_TTL
- transparence.tsx: document the Légifrance injection for public transparency
- Fixed portal URL: piste.gouv.fr (not piste.api.gouv.fr — verified June 2026)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prompts (legal_framework.py + prompts.ts):
- Add source-grounding rule: model must use only the provided legal texts
- Fix section 8 heading: "Livre préliminaire" doesn't exist in French CP —
correct to "Livres II et IV" with proper structural references
- Fix Art. 211-2 description: was "incitation directe et publique" (wrong) —
corrected to "participation à un groupement formé en vue de préparer"
- Fix Loi 1881 Art. 24: remove misleading terrorism note — apologie du
terrorisme was transferred to CP Art. 421-2-5 by law n° 2014-1353
- Fix CRITÈRES DE REJET: remove phantom reference to Loi 1881 Art. 24 al. 6
for terrorism (no longer in force for this offense since 2014)
transparence.tsx:
- Fix step 1: IP hash IS stored (30 days, separate table, anti-abuse only)
- Fix step 2: "(GPT)" → "(Mistral)" — wrong AI provider
- Fix data table: IP row was "Non conservée" — corrected to hash SHA-256
- Fix récapitulatif: "Données personnelles: Aucune" → accurate with IP hash
- Fix sources list: "Livre préliminaire" → correct Livre II/IV references
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace all OpenAI/GPT references with Mistral AI (mistral-small-latest,
mistral-large-latest) and correct env var names (FILTER_MODEL, SYNTHESIS_MODEL)
- Expand DB schema: add missing columns on ideas/synthesis, add tables
consultations, consents, ip_abuse with full column list
- Expand API routes table from 5 to 33 routes (admin, consultations,
contributions, check-law, consent, exports, ip-blacklist)
- Add third AI agent: law-check (LAW_CHECK_PROMPT / POST /api/check-law)
- Expand legal basis from 7 to 16 sources
- Update Vite version: 6 → 7
- Add public pages routing table
- Add autoclose consultation flow diagram (section 4.3)
- Fix security section: replace Azure/OpenAI with Mistral AI
- Update date (April → June 2026), status (Production → Recette)
- Fix DEPLOIEMENT.md diagram: OpenAI API → Mistral AI API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace all homegit.gyozamancave.fr links with internal page links
or plain text (no external repo exposure)
- Replace piron.antoine@gmail.com with contact@example.com placeholder
- Add prompts verbatim section to /transparence page (filter, synthesis,
law-check prompts) via new src/lib/prompts.ts module
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nav: "Fonctionnement" and "Flyer QR" restored on mobile via a ···
overflow dropdown (click-outside closes); other links always visible
Law check: new POST /api/check-law endpoint (5/min, 20/h) calls
mistral-small with a new LAW_CHECK_PROMPT to detect if a proposal is
already covered by French or EU law. Informational only, non-blocking.
Frontend: "Vérifier le cadre légal existant" button below textarea in
home.tsx; result displayed with Scale icon; resets on content change.
Favicon: replaced placeholder red square with a petrol rounded square
containing a serif "V" (for Voix) in warm cream — matches brand palette
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Persistence: PostgreSQL table ip_abuse (ip_hash SHA-256, rejection_count,
timestamps, last_idea_id, blacklisted_at, expires_at). No raw IP stored.
Logic:
- First illegal contribution: recorded, tolerated (benefit of the doubt)
- Second illegal contribution: 30-day block (IP_BLACKLIST_DAYS, configurable)
- Counter is cumulative — valid contributions do not reset it
- Block check fires before all other validations in both submit routes
Backend:
- database.py: check_ip_blacklist, record_ip_rejection, get_ip_blacklist,
remove_ip_blacklist; ip_abuse table created in init_db()
- app.py: _get_ip_hash() helper; blacklist check + record in submit_idea()
and submit_consultation_idea(); admin routes GET/DELETE /api/admin/ip-blacklist
Admin panel: new "Blacklist" tab showing active entries with hash, rejection
count, trigger idea id, dates; "Lever" button for manual removal
Docs:
- SECURITE_ANTI_ABUS.md: section 8 describing the mechanism, RGPD basis,
limits, and IP_BLACKLIST_DAYS env var
- privacy-policy.tsx: IP row updated to reflect hash storage + legal basis
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Require exact counts ("une contribution", "X sur Y") — forbid vague
"des citoyens" or "plusieurs" without numerical basis
- Prohibit paraphrasing that softens or amplifies a concrete demand
- Require explicit mention of contradictions (no artificial consensus)
- Catch-all sentence for contributions that fit no identified theme,
rather than silently dropping them
- Grouping by theme conditional on ≥2 contributions covering it
- Cap raised to 5 themes to avoid forced merging on heterogeneous sets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Navbar: hide Fonctionnement and Flyer QR on mobile (hidden sm:inline),
preventing overflow on small screens
- Home: add mobile tab bar (Proposer / Synthèse) replacing the broken
two-column stacked layout that was constrained to h-[calc(100vh-5rem)]
- Grid: md:h-[calc(100vh-9rem)] (desktop only); columns show/hide via
mobileTab state
- Countdown: visible in synthesis tab label on mobile, in header on md+
- Textarea: 90px min-height on mobile, 120px on sm+
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show a live countdown (15→0s) in the synthesis header indicating when the
next auto-refresh will occur. Resets on every successful synthesis fetch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Export ApiError from api-client-react package
- home.tsx onError: extract error.data.message from ApiError and display it
(cooldown, rate limit, consent_required, validation messages are now visible)
- Differentiate alert title: "Contribution non retenue" for AI rejection
(has a reason), "Envoi impossible" for technical errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend (app.py / database.py / ai_agent.py):
- [Critique] Autoclose loop: add pg_try_advisory_lock so only one Gunicorn
worker runs the check per 60s cycle; add random startup jitter
- [Critique] admin_delete_idea: pass consultation_id to
_update_synthesis_background so the right synthesis is regenerated
- [Majeur] admin_login: return HMAC-signed session token instead of raw
ADMIN_SECRET; require_admin verifies the signature (TTL 8h)
- [Majeur] bulk_delete: replace str.isdigit() (Unicode-unsafe) with
try/except int() to prevent crash on Unicode digit characters
- [Majeur] create_consultation: force UTC timezone on naive datetime from
fromisoformat() to prevent TypeError when comparing with UTC-aware now()
- [Majeur] ai_agent.py: fix 'raw' in dir() -> 'raw' in locals() so the
JSON parse error log actually shows the raw response
- [Mineur] export print: use datetime.now(UTC) instead of datetime.now()
Frontend (React):
- [Majeur] consultation.tsx: show startsAt (not endsAt) for upcoming
consultations; add startsAt variable
- [Majeur] consultations-list.tsx: same fix for the list view
- [Mineur] home.tsx: guard new Date(idea.createdAt) against null
- [Mineur] admin.tsx: check HTTP status in exportCsv XHR before creating
download link; show error toast on non-200
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- home.tsx: extract postConsent() helper; make onSubmit async and await it
before doActualSubmit (returning users); await it in handleConsentConfirm
before doActualSubmit (new users, was fire-and-forget)
- consultation.tsx: same fix on returning-user path in onSubmit
Without this, returning users (consent_v1 in localStorage) never called
/api/consent so the _ct cookie was never set, causing all submissions to
return 403 consent_required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Require signed _ct cookie (set by POST /api/consent) before accepting any
submission. Direct API calls without a prior consent flow are rejected 403.
- Add _sign_consent_token / _verify_consent_token (HMAC-SHA256, 24h TTL).
- Lower default CONTRIBUTION_COOLDOWN_SECONDS from 3600 to 240 (4 min).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
init_db() was only called inside if __name__ == "__main__", which Gunicorn
never executes. Tables were never created, causing every request to fail.
Move the call to module level so it runs on import, before the autoclose
thread starts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@replit/vite-plugin-cartographer and @replit/vite-plugin-runtime-error-modal
were removed from the workspace catalog during de-Replit-isation but remained
in mockup-sandbox/package.json, breaking pnpm install. Lockfile regenerated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CONTRIBUTING.md : section "Proposer une contribution" réécrite avec
tableaux Markdown, blocs de code inline (convention branches, commits,
template PR), règles backend/frontend détaillées
- README.md : mention financement déplacée juste après la présentation,
avant la section Fonctionnalités ; section Soutenir le projet supprimée
en fin de fichier
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- README réécrit à partir du code : fonctionnalités réelles, stack,
installation (dev local, build, prod), variables d'environnement
documentées avec leur effet, références aux docs existants
- CONTRIBUTING.md créé : signalement de bug, procédure de PR,
mention que GitHub est un miroir public du dépôt Gitea source
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Venv Python : création automatique si absent (détecte python3.11,
python3, python >= 3.9 ; erreur guidée si introuvable)
- Dépendances pip : pip install -r requirements.txt à chaque lancement
(no-op si rien n'a changé, garantit la cohérence après un pull)
- pnpm : installation automatique via corepack (si dispo) ou npm
- Node.js : non installable sans root — message d'erreur avec commandes
exactes pour Rocky/RHEL, Debian/Ubuntu et nvm
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nouveau script scripts/dev-local.sh : lance Flask (port 8080) et le
serveur Vite dev (port 5173) en parallèle avec nettoyage propre
(trap EXIT/INT/TERM). VITE_API_URL est injecté pour pointer vers
Flask local, accessible depuis le client via tunnel SSH.
- docs/INSTALL_ROCKY.md : nouvelle section 16 avec schéma ASCII,
procédure pas-à-pas et commande tunnel SSH exacte.
- docs/INSTALL_ROCKY.md : corrige les deux références cassées à
vite.config.selfhost.ts (sections 9 et 15.1) supprimé lors de la
dé-Replit-isation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- .env.example : variables à jour (Mistral, SECRET_KEY, ADMIN_SECRET, Redis,
hCaptcha, anti-abus) — l'ancienne version référençait encore OpenAI uniquement
- Nouveau set-domain.sh : supprime la référence à vite.config.selfhost.ts supprimé
- Nouvelle page /consultations : index public des consultations actives/clôturées,
toggle "afficher les clôturées", lien dans le footer
- App.tsx : route /consultations + lien footer Consultations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P3 — RGPD :
- Table `consents` + `POST /api/consent` (art. 7.1 — preuve du consentement)
- Dialogue de consentement explicite avant la première contribution (art. 9.2.a)
- Pages `/mentions-legales` et `/politique-confidentialite`
- `docs/RGPD.md` — registre des traitements, bases légales, sous-traitants
- `getVisitorId()` exporté depuis l'API client React
P4 — Transparence éditoriale :
- Page `/contributions-brutes` avec pagination et export JSON/CSV
- `GET /api/contributions`, `GET /api/contributions/export/{json,csv}`
- `GET /api/stats/public` — stats publiques sans données de rejet
- Label de transparence IA sur la colonne de synthèse
- Compteurs (acceptées / soumises) dans le bandeau d'intro
- `docs/PROMPTS_IA.md` — prompts intégraux publiés + analyse des biais
- Pied de page avec liens légaux et transparence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>