Files
la-voix-du-peuple/docs/DAT.md
T
pironantoine f72a64f23e Update documentation with new features and improved usability
Update several documentation files (.toml, .md) to reflect new features such as a QR code flyer page, share/PDF export buttons, and dependency updates, including version increments to v1.1.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 88b9da52-be77-4c86-854b-f097a8c7ae86
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/Z3YUti7
Replit-Helium-Checkpoint-Created: true
2026-04-04 10:27:58 +00:00

267 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Document d'Architecture Technique — La Voix du Peuple
**Version** : 1.1
**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. 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) |
**Pages** :
| URL | Description |
|-----|-------------|
| `/` | Page principale : formulaire de soumission, fil des contributions, colonne de synthèse |
| `/about` | À propos et fondements juridiques |
| `/transparence` | Fonctionnement de l'IA, données collectées, limites |
| `/flyer` | Flyer imprimable avec QR code configurable pour diffusion physique |
**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_KEY``https://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
```