Files
la-voix-du-peuple/docs/DAT.md
T
pironantoine 50bc1f5ce9 Update documentation and push code to Gitea repository
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
2026-04-04 14:09:32 +00:00

293 lines
13 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.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 :
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_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) |
| `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 support
- `deploy/voix-du-peuple-api.service` — Unité systemd pour Gunicorn
- `artifacts/voix-du-peuple/vite.config.selfhost.ts` — Build sans plugins Replit
- `scripts/push-gitea.sh` — Push sécurisé vers Gitea (compatible Git 2.50+, lit `GITEA_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 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
```