Files
la-voix-du-peuple/docs/DAT.md
T
pironantoine 78eb58844e Add dark mode and accessibility features for improved user experience
Integrate a dark mode, an accessibility panel with options for dyslexia, high contrast, and text scaling, and enhance keyboard navigation. Update documentation to reflect these changes.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: bbd001b6-1b5f-4425-9310-55a9081dabf8
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/vOeFCU4
Replit-Helium-Checkpoint-Created: true
2026-04-04 11:45:46 +00:00

12 KiB
Raw Blame History

Document d'Architecture Technique — La Voix du Peuple

Version : 1.3
Date : Avril 2026
Statut : En production (Replit), prêt pour auto-hébergement


Historique des versions

Version Date Modifications
1.0 Avril 2026 Version initiale
1.1 Avril 2026 Ajout page Flyer QR, boutons Partager / PDF, qrcode.react
1.2 Avril 2026 Palette pétrol neutre, textes de posture (expression vs. vérité)
1.3 Avril 2026 Dark mode pétrol, panneau d'accessibilité (dyslexie, contraste, zoom)

1. Présentation générale

La Voix du Peuple est une plateforme civique permettant à des citoyens de soumettre des propositions politiques. Ces contributions sont :

  1. Filtrées par un agent IA selon le droit international des droits humains
  2. Synthétisées automatiquement en un résumé clair par thème
  3. Affichées en temps réel à destination d'élus ou de décideurs
  4. Exportables (PDF, partage natif) et diffusables via un flyer imprimable avec QR code

2. Architecture globale

┌─────────────────────────────────────────────────────────┐
│                     Navigateur client                   │
│              React + Vite (TypeScript)                  │
└────────────────────────┬────────────────────────────────┘
                         │ HTTP/REST (JSON)
                         ▼
┌─────────────────────────────────────────────────────────┐
│              Reverse Proxy — Nginx / HAProxy            │
│         (rate limiting, TLS, X-Forwarded-For)           │
└────────────────────────┬────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────┐
│              Backend API — Flask (Python)               │
│                      Port 8080                          │
│                                                         │
│  ┌──────────────┐   ┌─────────────┐  ┌──────────────┐  │
│  │  app.py      │   │ ai_agent.py │  │ database.py  │  │
│  │  (routes)    │──▶│ (filtre +   │  │ (PostgreSQL) │  │
│  │              │   │  synthèse)  │  │              │  │
│  └──────────────┘   └──────┬──────┘  └──────┬───────┘  │
└─────────────────────────────┼───────────────┼──────────┘
                              │               │
                              ▼               ▼
               ┌──────────────────┐   ┌──────────────┐
               │  Mistral AI API  │   │  PostgreSQL   │
               │ api.mistral.ai   │   │   (base DB)   │
               └──────────────────┘   └──────────────┘

3. Composants

3.1 Frontend — React + Vite

Élément Détail
Framework React 18 + TypeScript
Build Vite 7
Styles Tailwind CSS + shadcn/ui
Routing Wouter
État serveur TanStack Query
Client API @workspace/api-client-react (généré depuis OpenAPI)
QR code qrcode.react (page Flyer)
Police Bahnschrift (titres), Inter (corps)
Couleur principale Pétrol foncé hsl(185 42% 28%) — neutre, sans connotation partisane

Pages :

URL Description
/ Page principale : formulaire de soumission, fil des contributions, colonne de synthèse
/about À propos, fondements juridiques, posture de la démarche
/transparence Fonctionnement de l'IA, données collectées, limites, posture éditoriale
/flyer Flyer imprimable avec QR code configurable pour diffusion physique

Accessibilité :

Fonctionnalité Mécanisme
Dark mode Classes CSS .dark sur <html>, variables pétrol clair (hsl(185 55% 58%))
Police dyslexie Classe .dyslexia : Arial/Verdana, letter-spacing 0.06em, line-height 2
Contraste élevé Classe .high-contrast : fond blanc/noir pur, ratio WCAG AA+
Texte agrandi Classe .large-text : font-size 120% global
Navigation clavier skip-link "Aller au contenu principal" + anneau :focus-visible 3 px
ARIA aria-pressed sur les toggles, aria-label sur tous les boutons d'action
Persistance Préférences stockées dans localStorage, relues au chargement
Panneau Composant AccessibilityPanel — icône dans la barre de navigation
Fournisseur d'état AccessibilityProvider + useAccessibility hook (React Context)

Textes de posture (infusés dans plusieurs sections) :

  • Bandeau d'intro : "espace d'expression citoyenne, pas un sondage ni une vérité établie"
  • Pied de synthèse : note italique discrète rappelant l'ancrage dans l'expertise de l'auteur
  • Page À propos — section "Expression, pas vérité" : distinction expression/consensus, posture d'auteur
  • Page Fonctionnement — encart "Ce que cette plateforme n'est pas" : limites éditoriales et posture

Fonctionnalités de la colonne de synthèse :

  • Bouton Partager / Copier : compose un texte horodaté (texte + nombre de contributions + date) et l'envoie via l'API Web Share (mobile) ou le presse-papier (bureau) ; un toast confirme la copie
  • Bouton PDF : ouvre une fenêtre dédiée avec un rendu mise en page (tricolore, titre, métadonnées, texte, pied de page lavoixdupeuple.fr) et déclenche l'impression navigateur

Page Flyer (/flyer) :

  • QR code SVG haute résolution (niveau de correction H) pointant vers l'URL de destination
  • URL modifiable en temps réel via un champ texte, sans rechargement
  • URL par défaut définie par la constante DEFAULT_QR_URL en ligne 10 de src/pages/flyer.tsx
  • Passage d'une URL via paramètre GET : /flyer?url=https://monsite.fr
  • Barre de contrôle masquée à l'impression (@media print) — seul le flyer A4 est imprimé
  • Bouton "Imprimer / Exporter PDF" sur la page

Variables d'environnement frontend :

  • BASE_URL — Préfixe de chemin (injecté par Vite)
  • PORT — Port du serveur de développement (assigné par Replit)

3.2 Backend — Flask

Élément Détail
Framework Flask 3 (Python 3.11+)
Serveur WSGI Gunicorn (production)
CORS flask-cors
Rate limiting flask-limiter (5 req/min par IP sur POST /api/ideas)
ORM psycopg2-binary (requêtes SQL directes)

Endpoints :

Méthode Route Description
GET /api/ideas Liste des contributions acceptées
POST /api/ideas Soumet une nouvelle contribution
GET /api/ideas/stats Statistiques (acceptées/refusées)
GET /api/synthesis Texte de synthèse actuel
GET /health Health check

Réponses : JSON camelCase, statuts HTTP standard.


3.3 Agent IA

Deux appels distincts à l'API Mistral (compatible OpenAI SDK) :

Filtre de modération

  • Modèle : mistral-small-latest (configurable via FILTER_MODEL)
  • Entrée : Texte brut de la contribution
  • Sortie : JSON {"accepted": bool, "reason"?: string, "legal_basis"?: string}
  • Référentiel : DUDH, PIDCP, CEDH, Charte UE, CERD, Statut de Rome
  • Max tokens : 300

Synthèse collective

  • Modèle : mistral-large-latest (configurable via SYNTHESIS_MODEL)
  • Entrée : Liste de toutes les contributions acceptées
  • Sortie : Texte libre structuré par thèmes, destiné aux élus
  • Max tokens : 1200
  • Déclencheur : À chaque nouvelle contribution acceptée (asynchrone)

Priorité de configuration du client IA :

  1. MISTRAL_API_KEYhttps://api.mistral.ai/v1
  2. OPENAI_API_KEY → API OpenAI standard
  3. AI_INTEGRATIONS_OPENAI_* → Proxy Replit (intégration native)

3.4 Base de données — PostgreSQL

Table ideas :

Colonne Type Description
id SERIAL PK Identifiant
content TEXT Texte de la contribution
author VARCHAR(100) Pseudonyme (nullable)
accepted BOOLEAN Résultat du filtre
rejection_reason TEXT Motif de refus (nullable)
legal_basis TEXT Base légale du refus (nullable)
created_at TIMESTAMPTZ Horodatage de soumission

Table synthesis :

Colonne Type Description
id SERIAL PK (toujours 1 — ligne unique)
text TEXT Dernier texte de synthèse
idea_count INTEGER Nombre de contributions intégrées
updated_at TIMESTAMPTZ Dernière mise à jour

4. Variables d'environnement

Variable Obligatoire Description
DATABASE_URL URL PostgreSQL complète
MISTRAL_API_KEY (ou OpenAI) Clé API Mistral
SESSION_SECRET Secret Flask (sessions)
FILTER_MODEL Modèle de filtrage (défaut : mistral-small-latest)
SYNTHESIS_MODEL Modèle de synthèse (défaut : mistral-large-latest)
OPENAI_API_KEY Alternative à Mistral
MISTRAL_BASE_URL URL custom Mistral (défaut : https://api.mistral.ai/v1)

5. Infrastructure de déploiement (auto-hébergement)

Internet ──▶ HAProxy (TLS, load balancing)
               └──▶ Nginx (reverse proxy, rate limiting IP)
                      └──▶ Gunicorn × N workers (Flask)
                      └──▶ Fichiers statiques Vite (build)
                                    │
                             PostgreSQL (local ou RDS)

Fichiers fournis :

  • deploy/nginx.conf — Configuration Nginx avec HAProxy support
  • deploy/voix-du-peuple-api.service — Unité systemd pour Gunicorn
  • artifacts/voix-du-peuple/vite.config.selfhost.ts — Build sans plugins Replit

6. Sécurité

Mesure Implémentation
Rate limiting 5 POST/min par IP (flask-limiter)
Sanitisation bleach sur le contenu avant stockage
CORS Origines configurées explicitement
SQL injection Requêtes paramétrées (psycopg2)
Secrets Variables d'environnement uniquement, jamais dans le code
Données personnelles Aucune IP stockée, pseudonyme facultatif
Export PDF Généré côté client uniquement, aucune donnée transmise au serveur

7. Flux de traitement d'une contribution

POST /api/ideas
  │
  ├─ Validation (longueur 101000 chars)
  ├─ Sanitisation (bleach)
  ├─ INSERT en base (statut pending)
  │
  ├─ Appel Mistral (filtre)
  │     ├─ accepted=true  → UPDATE idea, déclenche synthèse async
  │     └─ accepted=false → UPDATE idea avec motif
  │
  ├─ [async] Appel Mistral (synthèse) sur toutes contributions acceptées
  │     └─ UPSERT table synthesis
  │
  └─ Réponse JSON 201

8. Flux d'export / partage (côté client)

Clic "Partager / Copier"
  │
  ├─ Composition du texte horodaté (date locale, nb contributions, texte synthèse)
  ├─ navigator.share disponible ? → Partage natif (mobile)
  └─ Sinon → navigator.clipboard.writeText → toast de confirmation

Clic "PDF"
  │
  ├─ Génération HTML en mémoire (tricolore, titre, méta, texte, pied de page)
  ├─ window.open("", "_blank") → document.write(html)
  └─ window.print() → dialogue d'impression / export PDF navigateur

Page /flyer — Clic "Imprimer / Exporter PDF"
  │
  ├─ CSS @media print masque .no-print (barre de contrôle, navbar)
  └─ window.print() → impression du flyer A4 seul