Update documentation files (DAT.md, DEX.md, WIKI.md) to version 1.4, incorporating changes related to Gitea synchronization, the `GITEA_TOKEN` secret, and the `scripts/push-gitea.sh` script. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: cae3c6dc-0372-4c09-9980-7184f80535a3 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/qCk7LE3 Replit-Helium-Checkpoint-Created: true
13 KiB
Document d'Architecture Technique — La Voix du Peuple
Version : 1.4
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.4 | Avril 2026 | Synchronisation Gitea sécurisée — GITEA_TOKEN + scripts/push-gitea.sh |
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 :
- Filtrées par un agent IA selon le droit international des droits humains
- Synthétisées automatiquement en un résumé clair par thème
- Affichées en temps réel à destination d'élus ou de décideurs
- 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_URLen ligne 10 desrc/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 viaFILTER_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 viaSYNTHESIS_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 :
MISTRAL_API_KEY→https://api.mistral.ai/v1OPENAI_API_KEY→ API OpenAI standardAI_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) |
GITEA_TOKEN |
✅ (pour push) | Token d'accès Gitea — utilisé par scripts/push-gitea.sh |
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 supportdeploy/voix-du-peuple-api.service— Unité systemd pour Gunicornartifacts/voix-du-peuple/vite.config.selfhost.ts— Build sans plugins Replitscripts/push-gitea.sh— Push sécurisé vers Gitea (compatible Git 2.50+, litGITEA_TOKEN)
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 10–1000 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