50bc1f5ce9
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
293 lines
13 KiB
Markdown
293 lines
13 KiB
Markdown
# 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 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
|
||
```
|