Add technical and operational guides for the platform
Add ARCHITECTURE.md and EXPLOITATION.md files detailing the system's technical design and operational procedures. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 9fd0e274-629e-4dae-8960-adf4c556797f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/VnHW0bR Replit-Helium-Checkpoint-Created: true
This commit is contained in:
+335
@@ -0,0 +1,335 @@
|
|||||||
|
# Architecture Technique — La Voix du Peuple
|
||||||
|
|
||||||
|
**Version :** 1.0
|
||||||
|
**Date :** Avril 2026
|
||||||
|
**Statut :** Production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vue d'ensemble
|
||||||
|
|
||||||
|
La Voix du Peuple est une plateforme démocratique citoyenne permettant la soumission d'idées politiques, leur filtrage automatique par un agent IA ancré dans le droit international des droits humains, et leur synthèse en un texte collectif vivant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture générale
|
||||||
|
|
||||||
|
```
|
||||||
|
RÉSEAU EXTERNE (Internet)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ HAProxy │
|
||||||
|
│ (réseau séparé) │
|
||||||
|
│ - Terminaison TLS │
|
||||||
|
│ - Load balancing │
|
||||||
|
│ - ACL / filtrage IP │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ HTTP (port 80, réseau interne)
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Nginx │
|
||||||
|
│ (reverse proxy) │
|
||||||
|
│ - Routage /api/* │
|
||||||
|
│ - Serve SPA React │
|
||||||
|
│ - Cache statiques │
|
||||||
|
└────────┬────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┴──────────────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌───────────────────────┐ ┌───────────────────────┐
|
||||||
|
│ Gunicorn + Flask │ │ Fichiers statiques │
|
||||||
|
│ (127.0.0.1:8000) │ │ React SPA (dist/) │
|
||||||
|
│ - API REST /api/* │ │ (servis par Nginx) │
|
||||||
|
│ - Filtrage IA │ └───────────────────────┘
|
||||||
|
│ - Rate limiting │
|
||||||
|
└──────────┬────────────┘
|
||||||
|
│
|
||||||
|
┌─────────┴──────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌──────────┐ ┌───────────────┐
|
||||||
|
│PostgreSQL│ │ OpenAI API │
|
||||||
|
│(local) │ │ (externe) │
|
||||||
|
│- ideas │ │ gpt-4o-mini │
|
||||||
|
│- synthesis│ │ gpt-4o │
|
||||||
|
└──────────┘ └───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Composants
|
||||||
|
|
||||||
|
### 3.1 HAProxy (réseau séparé)
|
||||||
|
|
||||||
|
| Propriété | Valeur |
|
||||||
|
|-----------|--------|
|
||||||
|
| Rôle | Point d'entrée unique, terminaison TLS, load balancing |
|
||||||
|
| Réseau | DMZ / réseau séparé dédié |
|
||||||
|
| Protocoles entrants | HTTPS/443, HTTP/80 (redirect) |
|
||||||
|
| Protocoles sortants | HTTP/80 vers Nginx (réseau interne) |
|
||||||
|
| TLS | Terminaison SSL/TLS sur HAProxy, communication interne en HTTP clair |
|
||||||
|
|
||||||
|
Configuration recommandée dans HAProxy :
|
||||||
|
```
|
||||||
|
frontend voix_du_peuple_https
|
||||||
|
bind *:443 ssl crt /etc/ssl/certs/voix-du-peuple.pem
|
||||||
|
mode http
|
||||||
|
option forwardfor
|
||||||
|
http-request set-header X-Forwarded-Proto https
|
||||||
|
default_backend voix_du_peuple_backend
|
||||||
|
|
||||||
|
frontend voix_du_peuple_http
|
||||||
|
bind *:80
|
||||||
|
mode http
|
||||||
|
redirect scheme https code 301 if !{ ssl_fc }
|
||||||
|
|
||||||
|
backend voix_du_peuple_backend
|
||||||
|
mode http
|
||||||
|
balance roundrobin
|
||||||
|
option httpchk GET /api/healthz
|
||||||
|
http-check expect status 200
|
||||||
|
server app01 <IP_NGINX>:80 check inter 10s
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Nginx
|
||||||
|
|
||||||
|
| Propriété | Valeur |
|
||||||
|
|-----------|--------|
|
||||||
|
| Rôle | Reverse proxy applicatif, service des fichiers statiques |
|
||||||
|
| Écoute | 0.0.0.0:80 (réseau interne uniquement) |
|
||||||
|
| Vers Flask | 127.0.0.1:8000 pour toute requête `/api/*` |
|
||||||
|
| Vers SPA | `dist/public/` pour tout le reste (`try_files`) |
|
||||||
|
|
||||||
|
Responsabilités :
|
||||||
|
- **Routage** : `/api/*` → Gunicorn, `/*` → SPA React
|
||||||
|
- **Cache** : assets JS/CSS/images avec `Cache-Control: immutable`
|
||||||
|
- **Headers de sécurité** : X-Frame-Options, X-Content-Type-Options, Referrer-Policy
|
||||||
|
- **Logging** : access.log et error.log structurés
|
||||||
|
|
||||||
|
> **Note HAProxy :** Nginx doit faire confiance à l'en-tête `X-Forwarded-For` positionné par HAProxy pour que Flask voie la vraie IP cliente (rate limiting par IP).
|
||||||
|
|
||||||
|
Configuration Nginx requise :
|
||||||
|
```nginx
|
||||||
|
set_real_ip_from <IP_HAPROXY>;
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
real_ip_recursive on;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Gunicorn + Flask (Backend API)
|
||||||
|
|
||||||
|
| Propriété | Valeur |
|
||||||
|
|-----------|--------|
|
||||||
|
| Serveur WSGI | Gunicorn 23+ |
|
||||||
|
| Workers | 4 (synchrones, ajustable) |
|
||||||
|
| Bind | 127.0.0.1:8000 (non exposé directement) |
|
||||||
|
| Framework | Flask 3.1+ |
|
||||||
|
| Python | 3.11+ |
|
||||||
|
|
||||||
|
**Routes exposées :**
|
||||||
|
|
||||||
|
| Méthode | Route | Description | Rate limit |
|
||||||
|
|---------|-------|-------------|------------|
|
||||||
|
| GET | `/api/healthz` | Santé du service | Aucun |
|
||||||
|
| GET | `/api/ideas` | Liste des idées acceptées | 120/min |
|
||||||
|
| POST | `/api/ideas` | Soumettre une idée | 5/min, 20/h par IP |
|
||||||
|
| GET | `/api/ideas/stats` | Statistiques | 120/min |
|
||||||
|
| GET | `/api/synthesis` | Texte synthétisé collectif | 120/min |
|
||||||
|
|
||||||
|
**Sécurité applicative :**
|
||||||
|
- Rate limiting par IP réelle (via `flask-limiter`)
|
||||||
|
- Assainissement XSS : `bleach.clean()` sur toutes les entrées
|
||||||
|
- Requêtes SQL paramétrées (`psycopg2`) — pas de concaténation de chaînes
|
||||||
|
- En-têtes HTTP de sécurité sur toutes les réponses
|
||||||
|
- Aucun secret exposé dans les messages d'erreur
|
||||||
|
|
||||||
|
### 3.4 Agent IA
|
||||||
|
|
||||||
|
Deux agents distincts, chacun configuré avec son propre modèle :
|
||||||
|
|
||||||
|
#### Agent de filtrage
|
||||||
|
|
||||||
|
| Propriété | Valeur |
|
||||||
|
|-----------|--------|
|
||||||
|
| Modèle par défaut | `gpt-4o-mini` |
|
||||||
|
| Variable d'env | `OPENAI_FILTER_MODEL` |
|
||||||
|
| Tokens max | 300 |
|
||||||
|
| Format de sortie | JSON strict (`response_format: json_object`) |
|
||||||
|
| Déclenchement | À chaque soumission d'idée |
|
||||||
|
| Temps de réponse | ~3-6 secondes |
|
||||||
|
|
||||||
|
Logique de décision :
|
||||||
|
1. Appel OpenAI avec le prompt légal complet
|
||||||
|
2. Parse JSON `{"accepted": bool, "reason"?: str, "legal_basis"?: str}`
|
||||||
|
3. Si l'API retourne une erreur de filtre de contenu → rejet automatique avec citation DUDH/PIDCP/CEDH
|
||||||
|
4. Résultat persisté en base avec l'idée
|
||||||
|
|
||||||
|
#### Agent de synthèse
|
||||||
|
|
||||||
|
| Propriété | Valeur |
|
||||||
|
|-----------|--------|
|
||||||
|
| Modèle par défaut | `gpt-4o` |
|
||||||
|
| Variable d'env | `OPENAI_SYNTHESIS_MODEL` |
|
||||||
|
| Tokens max | 1200 |
|
||||||
|
| Déclenchement | En arrière-plan après chaque acceptation |
|
||||||
|
| Temps de réponse | ~8-15 secondes |
|
||||||
|
|
||||||
|
La synthèse est non-bloquante : l'API répond immédiatement à l'utilisateur, la synthèse s'effectue dans un thread daemon.
|
||||||
|
|
||||||
|
#### Base légale du filtre
|
||||||
|
|
||||||
|
Le prompt de filtrage intègre textuellement les articles pertinents de :
|
||||||
|
- Déclaration universelle des droits de l'homme (DUDH, ONU 1948)
|
||||||
|
- Pacte international relatif aux droits civils et politiques (PIDCP, ONU 1966)
|
||||||
|
- Convention européenne des droits de l'homme (CEDH, 1950)
|
||||||
|
- Charte des droits fondamentaux de l'UE (2000/2009)
|
||||||
|
- Convention pour la prévention du génocide (ONU 1948)
|
||||||
|
- Statut de Rome / CPI (1998)
|
||||||
|
- Convention sur la discrimination raciale (CERD, ONU 1965)
|
||||||
|
|
||||||
|
### 3.5 PostgreSQL
|
||||||
|
|
||||||
|
| Propriété | Valeur |
|
||||||
|
|-----------|--------|
|
||||||
|
| Version | 15+ |
|
||||||
|
| Accès | Localhost uniquement (127.0.0.1) |
|
||||||
|
| Connexion Flask | Via `DATABASE_URL` (psycopg2-binary) |
|
||||||
|
| Authentification | md5 / scram-sha-256 |
|
||||||
|
|
||||||
|
**Schéma de la base de données :**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE ideas (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
author VARCHAR(100),
|
||||||
|
accepted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
rejection_reason TEXT,
|
||||||
|
legal_basis TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE synthesis (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
idea_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 Frontend React
|
||||||
|
|
||||||
|
| Propriété | Valeur |
|
||||||
|
|-----------|--------|
|
||||||
|
| Framework | React 18 + Vite 6 |
|
||||||
|
| Styles | Tailwind CSS v4 + shadcn/ui + Radix UI |
|
||||||
|
| Routing | Wouter (léger, côté client) |
|
||||||
|
| Requêtes API | TanStack React Query |
|
||||||
|
| Build | Sortie statique dans `dist/public/` |
|
||||||
|
|
||||||
|
**Communication avec le backend :**
|
||||||
|
- Hooks générés depuis la spécification OpenAPI (`lib/api-spec/openapi.yaml`)
|
||||||
|
- Codegen via Orval → `lib/api-client-react/src/generated/`
|
||||||
|
- Toutes les requêtes passent par `/api/` (même origine, pas de CORS cross-origin en production)
|
||||||
|
- Rafraîchissement automatique de la synthèse toutes les 15 secondes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Flux de données
|
||||||
|
|
||||||
|
### 4.1 Soumission d'une idée
|
||||||
|
|
||||||
|
```
|
||||||
|
Citoyen
|
||||||
|
│
|
||||||
|
│ POST /api/ideas {"content": "...", "author": "..."}
|
||||||
|
▼
|
||||||
|
HAProxy ──[TLS]──► Nginx ──► Gunicorn/Flask
|
||||||
|
│
|
||||||
|
├─ Validation entrées (longueur, XSS)
|
||||||
|
├─ Rate limiting (5/min par IP)
|
||||||
|
│
|
||||||
|
├─ Agent filtrage ──► OpenAI API
|
||||||
|
│ │
|
||||||
|
│ JSON {"accepted": bool, ...}
|
||||||
|
│
|
||||||
|
├─ INSERT INTO ideas (...)
|
||||||
|
│
|
||||||
|
├─ Si accepté :
|
||||||
|
│ Thread daemon ──► Agent synthèse ──► OpenAI
|
||||||
|
│ │
|
||||||
|
│ UPDATE synthesis
|
||||||
|
│
|
||||||
|
◄── HTTP 201 {"accepted": bool, "reason": "...", "idea": {...}} ──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Lecture de la synthèse
|
||||||
|
|
||||||
|
```
|
||||||
|
Citoyen (polling 15s)
|
||||||
|
│
|
||||||
|
│ GET /api/synthesis
|
||||||
|
▼
|
||||||
|
HAProxy ──► Nginx ──► Flask ──► SELECT FROM synthesis ──► {"text": "...", "ideaCount": N}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sécurité
|
||||||
|
|
||||||
|
### 5.1 Couches de défense
|
||||||
|
|
||||||
|
| Couche | Mécanisme |
|
||||||
|
|--------|-----------|
|
||||||
|
| Réseau | HAProxy sur réseau séparé, Nginx non exposé directement |
|
||||||
|
| Transport | TLS 1.2/1.3 terminé sur HAProxy |
|
||||||
|
| Applicative Flask | Rate limiting, validation, assainissement XSS, headers sécurité |
|
||||||
|
| IA (double filtre) | Filtre de contenu Azure/OpenAI + filtre légal interne |
|
||||||
|
| Base de données | Requêtes paramétrées, accès localhost uniquement |
|
||||||
|
| Système | Utilisateur dédié non-root, systemd sandboxing |
|
||||||
|
|
||||||
|
### 5.2 Périmètre des ports réseau
|
||||||
|
|
||||||
|
| Port | Interface | Exposé à | Rôle |
|
||||||
|
|------|-----------|----------|------|
|
||||||
|
| 443 | HAProxy | Internet | HTTPS public |
|
||||||
|
| 80 | HAProxy | Internet | Redirect HTTPS |
|
||||||
|
| 80 | Nginx | Réseau interne | HTTP depuis HAProxy |
|
||||||
|
| 8000 | Gunicorn | 127.0.0.1 | API Flask |
|
||||||
|
| 5432 | PostgreSQL | 127.0.0.1 | Base de données |
|
||||||
|
|
||||||
|
### 5.3 En-têtes HTTP de sécurité (Flask)
|
||||||
|
|
||||||
|
```
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-Frame-Options: DENY
|
||||||
|
X-XSS-Protection: 1; mode=block
|
||||||
|
Referrer-Policy: strict-origin-when-cross-origin
|
||||||
|
Content-Security-Policy: default-src 'self'; script-src 'none'; object-src 'none'
|
||||||
|
Cache-Control: no-store
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Décisions d'architecture
|
||||||
|
|
||||||
|
| Décision | Choix | Justification |
|
||||||
|
|----------|-------|---------------|
|
||||||
|
| ORM vs SQL direct | psycopg2 direct | Transparence, simplicité, facilité d'audit |
|
||||||
|
| Sync vs Async Flask | Synchrone (Gunicorn) | Complexité réduite, suffisant pour la charge attendue |
|
||||||
|
| Synthèse bloquante vs thread | Thread daemon | Latence utilisateur réduite, synthèse non-critique au retour |
|
||||||
|
| TLS sur HAProxy vs Nginx | HAProxy | TLS centralisé sur le composant réseau dédié à cet usage |
|
||||||
|
| SPA vs SSR | SPA statique | Déploiement simple, aucune dépendance Node en production |
|
||||||
|
| Rate limiting en mémoire vs Redis | Mémoire (`memory://`) | Installation simple ; Redis optionnel pour cluster multi-instances |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Scalabilité
|
||||||
|
|
||||||
|
L'architecture actuelle est dimensionnée pour une instance unique. En cas de montée en charge :
|
||||||
|
|
||||||
|
- **Gunicorn workers** : augmenter via `--workers` (règle : `2 × CPU + 1`)
|
||||||
|
- **Multi-instances** : ajouter des backends dans HAProxy + basculer le rate limiting sur Redis (`RATELIMIT_STORAGE_URL=redis://...`)
|
||||||
|
- **Base de données** : ajouter des index sur `ideas.accepted` et `ideas.created_at`
|
||||||
|
- **Cache synthèse** : la synthèse est unique en base, naturellement partagée entre workers
|
||||||
+496
@@ -0,0 +1,496 @@
|
|||||||
|
# Manuel d'Exploitation — La Voix du Peuple
|
||||||
|
|
||||||
|
**Version :** 1.0
|
||||||
|
**Date :** Avril 2026
|
||||||
|
**Public :** Administrateurs système / DevOps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Services et composants
|
||||||
|
|
||||||
|
| Service | Technologie | Géré par |
|
||||||
|
|---------|-------------|----------|
|
||||||
|
| Équilibreur frontal | HAProxy | Infrastructure réseau séparée |
|
||||||
|
| Reverse proxy | Nginx | systemd / DNF |
|
||||||
|
| API backend | Gunicorn + Flask | systemd (`voix-du-peuple-api`) |
|
||||||
|
| Base de données | PostgreSQL 15 | systemd (`postgresql-15`) |
|
||||||
|
| Frontend | Fichiers statiques | Nginx (pas de service dédié) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Commandes de gestion courantes
|
||||||
|
|
||||||
|
### 2.1 API Flask (Gunicorn)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Statut
|
||||||
|
sudo systemctl status voix-du-peuple-api
|
||||||
|
|
||||||
|
# Démarrer / arrêter / redémarrer
|
||||||
|
sudo systemctl start voix-du-peuple-api
|
||||||
|
sudo systemctl stop voix-du-peuple-api
|
||||||
|
sudo systemctl restart voix-du-peuple-api
|
||||||
|
|
||||||
|
# Rechargement gracieux (sans coupure de connexions)
|
||||||
|
sudo systemctl reload voix-du-peuple-api
|
||||||
|
|
||||||
|
# Activer/désactiver le démarrage automatique
|
||||||
|
sudo systemctl enable voix-du-peuple-api
|
||||||
|
sudo systemctl disable voix-du-peuple-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl status nginx
|
||||||
|
sudo systemctl reload nginx # Recharge la config sans coupure
|
||||||
|
sudo systemctl restart nginx # Redémarrage complet
|
||||||
|
|
||||||
|
# Tester la configuration avant reload
|
||||||
|
sudo nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl status postgresql-15
|
||||||
|
sudo systemctl restart postgresql-15
|
||||||
|
|
||||||
|
# Connexion console
|
||||||
|
sudo -u postgres psql -d voixdupeuple
|
||||||
|
|
||||||
|
# Sauvegarde manuelle
|
||||||
|
sudo -u postgres pg_dump voixdupeuple > /backup/voixdupeuple_$(date +%Y%m%d_%H%M).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 HAProxy (réseau séparé)
|
||||||
|
|
||||||
|
HAProxy est géré sur son propre hôte/réseau. Référez-vous à la documentation de votre équipe réseau.
|
||||||
|
|
||||||
|
Vérification de la connectivité depuis l'hôte applicatif :
|
||||||
|
```bash
|
||||||
|
# Vérifier que Nginx répond bien sur le réseau interne
|
||||||
|
curl -v http://<IP_NGINX>/api/healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Vérifications de santé
|
||||||
|
|
||||||
|
### 3.1 Endpoint de santé applicatif
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis l'hôte (via Nginx)
|
||||||
|
curl http://localhost/api/healthz
|
||||||
|
# Réponse attendue : {"status":"ok"}
|
||||||
|
|
||||||
|
# Directement sur Gunicorn (bypass Nginx)
|
||||||
|
curl http://127.0.0.1:8000/api/healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Vérification complète de la chaîne
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. PostgreSQL
|
||||||
|
sudo -u postgres psql -c "SELECT 1" voixdupeuple
|
||||||
|
|
||||||
|
# 2. Gunicorn
|
||||||
|
curl -s http://127.0.0.1:8000/api/healthz | grep ok
|
||||||
|
|
||||||
|
# 3. Nginx
|
||||||
|
curl -s http://localhost/api/healthz | grep ok
|
||||||
|
|
||||||
|
# 4. Stats de la plateforme
|
||||||
|
curl -s http://localhost/api/ideas/stats
|
||||||
|
# Réponse : {"total": N, "accepted": N, "rejected": N}
|
||||||
|
|
||||||
|
# 5. Depuis HAProxy (test end-to-end TLS)
|
||||||
|
curl -s https://voix-du-peuple.example.com/api/healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Script de vérification rapide
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# /opt/voix-du-peuple/scripts/healthcheck.sh
|
||||||
|
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
echo -n "$1 ... "
|
||||||
|
if eval "$2" &>/dev/null; then
|
||||||
|
echo "OK"
|
||||||
|
else
|
||||||
|
echo "ERREUR"
|
||||||
|
ERRORS=$((ERRORS+1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check "PostgreSQL" "sudo -u postgres psql -c 'SELECT 1' voixdupeuple"
|
||||||
|
check "Gunicorn" "curl -sf http://127.0.0.1:8000/api/healthz"
|
||||||
|
check "Nginx" "curl -sf http://localhost/api/healthz"
|
||||||
|
check "Systemd API" "systemctl is-active --quiet voix-du-peuple-api"
|
||||||
|
|
||||||
|
[ $ERRORS -eq 0 ] && echo "Tout OK" || echo "$ERRORS service(s) en erreur"
|
||||||
|
exit $ERRORS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Logs
|
||||||
|
|
||||||
|
### 4.1 Localisation des logs
|
||||||
|
|
||||||
|
| Source | Fichier / Commande |
|
||||||
|
|--------|--------------------|
|
||||||
|
| API Flask (applicatif) | `/var/log/voix-du-peuple/api-error.log` |
|
||||||
|
| API Flask (accès HTTP) | `/var/log/voix-du-peuple/api-access.log` |
|
||||||
|
| Journald (systemd) | `journalctl -u voix-du-peuple-api` |
|
||||||
|
| Nginx accès | `/var/log/nginx/access.log` |
|
||||||
|
| Nginx erreurs | `/var/log/nginx/error.log` |
|
||||||
|
| PostgreSQL | `/var/lib/pgsql/15/data/log/` |
|
||||||
|
| HAProxy | Voir hôte HAProxy dédié |
|
||||||
|
|
||||||
|
### 4.2 Consultation en temps réel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Logs applicatifs Flask
|
||||||
|
tail -f /var/log/voix-du-peuple/api-error.log
|
||||||
|
|
||||||
|
# Logs systemd en temps réel
|
||||||
|
journalctl -u voix-du-peuple-api -f
|
||||||
|
|
||||||
|
# Logs Nginx
|
||||||
|
tail -f /var/log/nginx/error.log
|
||||||
|
tail -f /var/log/nginx/access.log
|
||||||
|
|
||||||
|
# Combiner plusieurs sources
|
||||||
|
journalctl -u voix-du-peuple-api -u nginx -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Recherche dans les logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Erreurs OpenAI des dernières 24h
|
||||||
|
grep -i "openai\|erreur\|error" /var/log/voix-du-peuple/api-error.log | tail -50
|
||||||
|
|
||||||
|
# Requêtes POST /api/ideas
|
||||||
|
grep "POST /api/ideas" /var/log/voix-du-peuple/api-access.log | tail -20
|
||||||
|
|
||||||
|
# IP ayant déclenché le rate limiting (code 429)
|
||||||
|
grep " 429 " /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Rotation des logs
|
||||||
|
|
||||||
|
Créer `/etc/logrotate.d/voix-du-peuple` :
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/log/voix-du-peuple/*.log {
|
||||||
|
daily
|
||||||
|
rotate 30
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
postrotate
|
||||||
|
systemctl reload voix-du-peuple-api > /dev/null 2>&1 || true
|
||||||
|
endscript
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sauvegardes
|
||||||
|
|
||||||
|
### 5.1 Base de données
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sauvegarde manuelle
|
||||||
|
sudo -u postgres pg_dump voixdupeuple \
|
||||||
|
| gzip > /backup/voixdupeuple_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||||
|
|
||||||
|
# Restauration
|
||||||
|
gunzip -c /backup/voixdupeuple_20260401_120000.sql.gz \
|
||||||
|
| sudo -u postgres psql voixdupeuple
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Sauvegarde automatique (cron)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo crontab -e -u postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
```cron
|
||||||
|
# Sauvegarde quotidienne à 3h00
|
||||||
|
0 3 * * * pg_dump voixdupeuple | gzip > /backup/voixdupeuple_$(date +\%Y\%m\%d).sql.gz
|
||||||
|
|
||||||
|
# Nettoyage des sauvegardes de plus de 30 jours
|
||||||
|
0 4 * * * find /backup -name "voixdupeuple_*.sql.gz" -mtime +30 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Mise à jour applicative
|
||||||
|
|
||||||
|
### 6.1 Mise à jour du code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u voixdupeuple bash << 'EOF'
|
||||||
|
cd /opt/voix-du-peuple
|
||||||
|
git fetch origin
|
||||||
|
git log HEAD..origin/main --oneline # Voir ce qui va changer
|
||||||
|
git pull origin main
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Mise à jour des dépendances Python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u voixdupeuple bash << 'EOF'
|
||||||
|
cd /opt/voix-du-peuple
|
||||||
|
.venv/bin/pip install -r artifacts/flask-api/requirements.txt
|
||||||
|
EOF
|
||||||
|
sudo systemctl restart voix-du-peuple-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Rebuild du frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u voixdupeuple bash << 'EOF'
|
||||||
|
cd /opt/voix-du-peuple
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
cd artifacts/voix-du-peuple
|
||||||
|
pnpm exec vite build --config vite.config.selfhost.ts
|
||||||
|
EOF
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 Procédure de mise à jour complète
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u voixdupeuple bash << 'EOF'
|
||||||
|
cd /opt/voix-du-peuple
|
||||||
|
git pull origin main
|
||||||
|
.venv/bin/pip install -r artifacts/flask-api/requirements.txt
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
cd artifacts/voix-du-peuple
|
||||||
|
pnpm exec vite build --config vite.config.selfhost.ts
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo systemctl restart voix-du-peuple-api
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
# Vérification
|
||||||
|
sleep 3
|
||||||
|
curl -s http://localhost/api/healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. HAProxy — intégration réseau séparé
|
||||||
|
|
||||||
|
### 7.1 Vérification de la connectivité inter-réseaux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis l'hôte HAProxy : vérifier que Nginx est joignable
|
||||||
|
curl -v http://<IP_NGINX_INTERNE>:80/api/healthz
|
||||||
|
|
||||||
|
# Depuis l'hôte applicatif : vérifier que les X-Forwarded-For arrivent bien
|
||||||
|
curl -s http://localhost/api/ideas/stats
|
||||||
|
journalctl -u voix-du-peuple-api -n 20 | grep "127.0.0.1\|X-Forwarded"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 IP réelle dans Flask (rate limiting)
|
||||||
|
|
||||||
|
Flask utilise l'IP réelle du client via l'en-tête `X-Forwarded-For` positionné par HAProxy. Nginx doit être configuré pour transmettre cet en-tête et déclarer l'IP de HAProxy comme source de confiance.
|
||||||
|
|
||||||
|
Dans `/etc/nginx/conf.d/voix-du-peuple.conf` :
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Remplacez par l'IP réelle de votre HAProxy
|
||||||
|
set_real_ip_from 192.168.10.5;
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
real_ip_recursive on;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Vérification du rate limiting par IP réelle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simuler une requête comme si elle venait d'une IP externe via HAProxy
|
||||||
|
curl -H "X-Forwarded-For: 1.2.3.4" http://127.0.0.1:8000/api/healthz
|
||||||
|
|
||||||
|
# Vérifier dans les logs Flask que l'IP 1.2.3.4 est bien loggée
|
||||||
|
journalctl -u voix-du-peuple-api -n 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 Check HAProxy depuis la plateforme
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier les en-têtes qui arrivent réellement sur Gunicorn
|
||||||
|
python3 -c "
|
||||||
|
import urllib.request
|
||||||
|
req = urllib.request.Request('http://127.0.0.1:8000/api/healthz')
|
||||||
|
r = urllib.request.urlopen(req)
|
||||||
|
print(dict(r.headers))
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Gestion des incidents
|
||||||
|
|
||||||
|
### 8.1 L'API ne répond plus (5xx)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Vérifier le service
|
||||||
|
sudo systemctl status voix-du-peuple-api
|
||||||
|
|
||||||
|
# 2. Lire les logs récents
|
||||||
|
journalctl -u voix-du-peuple-api -n 50 --no-pager
|
||||||
|
|
||||||
|
# 3. Redémarrer
|
||||||
|
sudo systemctl restart voix-du-peuple-api
|
||||||
|
|
||||||
|
# 4. Si l'erreur persiste, vérifier PostgreSQL
|
||||||
|
sudo systemctl status postgresql-15
|
||||||
|
curl -s http://127.0.0.1:8000/api/healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Erreurs OpenAI (filtrage/synthèse dégradé)
|
||||||
|
|
||||||
|
Les erreurs OpenAI n'interrompent pas le service :
|
||||||
|
- Si le filtrage échoue → l'idée est **rejetée par défaut** (fail-safe)
|
||||||
|
- Si la synthèse échoue → le texte précédent est conservé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier les erreurs OpenAI
|
||||||
|
grep -i "openai\|openrouter\|api_key" /var/log/voix-du-peuple/api-error.log | tail -20
|
||||||
|
|
||||||
|
# Tester la clé API manuellement
|
||||||
|
source /opt/voix-du-peuple/.env
|
||||||
|
curl https://api.openai.com/v1/models \
|
||||||
|
-H "Authorization: Bearer $OPENAI_API_KEY" | python3 -m json.tool | head -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 Base de données inaccessible
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier PostgreSQL
|
||||||
|
sudo systemctl status postgresql-15
|
||||||
|
sudo -u postgres psql -c "SELECT version();"
|
||||||
|
|
||||||
|
# Connexion directe avec l'utilisateur applicatif
|
||||||
|
source /opt/voix-du-peuple/.env
|
||||||
|
psql "$DATABASE_URL" -c "SELECT COUNT(*) FROM ideas;"
|
||||||
|
|
||||||
|
# Redémarrer PostgreSQL en dernier recours
|
||||||
|
sudo systemctl restart postgresql-15
|
||||||
|
sudo systemctl restart voix-du-peuple-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 Attaque ou abus (rate limit dépassé)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Identifier les IPs agressives
|
||||||
|
grep " 429 " /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
|
||||||
|
|
||||||
|
# Bloquer une IP via firewalld
|
||||||
|
sudo firewall-cmd --add-rich-rule='rule family="ipv4" source address="1.2.3.4" reject' --permanent
|
||||||
|
sudo firewall-cmd --reload
|
||||||
|
|
||||||
|
# Ou déléguer le blocage à HAProxy (recommandé — ACL HAProxy)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.5 Disque plein
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Localiser ce qui prend de la place
|
||||||
|
df -h
|
||||||
|
du -sh /var/log/voix-du-peuple/*
|
||||||
|
du -sh /backup/*
|
||||||
|
|
||||||
|
# Nettoyer les anciens logs (si logrotate non configuré)
|
||||||
|
find /var/log/voix-du-peuple -name "*.log.*" -mtime +14 -delete
|
||||||
|
|
||||||
|
# Nettoyer les anciennes sauvegardes
|
||||||
|
find /backup -name "voixdupeuple_*.sql.gz" -mtime +30 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Supervision (recommandations)
|
||||||
|
|
||||||
|
### 9.1 Métriques à surveiller
|
||||||
|
|
||||||
|
| Métrique | Seuil d'alerte | Commande |
|
||||||
|
|----------|----------------|----------|
|
||||||
|
| CPU Gunicorn | > 80% sustained | `top -p $(pgrep -d, gunicorn)` |
|
||||||
|
| Mémoire | > 80% | `free -m` |
|
||||||
|
| Espace disque | > 80% | `df -h` |
|
||||||
|
| Temps de réponse `/api/ideas` | > 10s | `time curl http://localhost/api/ideas` |
|
||||||
|
| Taux d'erreur 5xx | > 1% | `grep " 5[0-9][0-9] " /var/log/nginx/access.log` |
|
||||||
|
| PostgreSQL connexions | > 80 | `SELECT count(*) FROM pg_stat_activity;` |
|
||||||
|
|
||||||
|
### 9.2 Intégration avec des outils de supervision
|
||||||
|
|
||||||
|
**Prometheus + Alertmanager** (optionnel) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installer flask-prometheus-metrics si vous souhaitez exposer des métriques
|
||||||
|
.venv/bin/pip install prometheus-flask-exporter
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nagios / Zabbix** — check HTTP simple :
|
||||||
|
```
|
||||||
|
check_http -H localhost -u /api/healthz -e "200 OK" -s '{"status":"ok"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Opérations PostgreSQL courantes
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Nombre d'idées par statut
|
||||||
|
SELECT accepted, COUNT(*) FROM ideas GROUP BY accepted;
|
||||||
|
|
||||||
|
-- Dernières idées soumises
|
||||||
|
SELECT id, LEFT(content, 60), author, accepted, created_at
|
||||||
|
FROM ideas ORDER BY created_at DESC LIMIT 10;
|
||||||
|
|
||||||
|
-- Idées rejetées avec motif
|
||||||
|
SELECT LEFT(content, 60), rejection_reason, legal_basis
|
||||||
|
FROM ideas WHERE accepted = FALSE ORDER BY created_at DESC LIMIT 10;
|
||||||
|
|
||||||
|
-- Synthèse actuelle
|
||||||
|
SELECT idea_count, updated_at, LEFT(text, 200) FROM synthesis;
|
||||||
|
|
||||||
|
-- Taille des tables
|
||||||
|
SELECT relname, pg_size_pretty(pg_total_relation_size(relid))
|
||||||
|
FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC;
|
||||||
|
|
||||||
|
-- Connexions actives
|
||||||
|
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Référence des fichiers de configuration
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---------|------|
|
||||||
|
| `/opt/voix-du-peuple/.env` | Variables d'environnement (secrets) |
|
||||||
|
| `/etc/systemd/system/voix-du-peuple-api.service` | Service systemd Gunicorn |
|
||||||
|
| `/etc/nginx/conf.d/voix-du-peuple.conf` | Configuration Nginx |
|
||||||
|
| `/var/lib/pgsql/15/data/pg_hba.conf` | Authentification PostgreSQL |
|
||||||
|
| `/var/lib/pgsql/15/data/postgresql.conf` | Configuration PostgreSQL |
|
||||||
|
| `/etc/logrotate.d/voix-du-peuple` | Rotation des logs |
|
||||||
|
| `/opt/voix-du-peuple/artifacts/flask-api/ai_agent.py` | Configuration agents IA |
|
||||||
Reference in New Issue
Block a user