mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
Écran Voix R5 (maquetté, jamais construit) implémenté sur décision du référent : open-source et local, pas d'API externe. - apps/ai : faster-whisper (CTranslate2, CPU, MIT) opt-in (AI_TRANSCRIPTION=off|locale|deterministe, défaut off) ; endpoint /internal/transcrire — l'audio ne survit JAMAIS à l'appel (fichier temporaire supprimé quoi qu'il arrive) ; indexer_bilans inclut désormais InterventionReport.note anonymisée (champ existant depuis R2, jamais eu d'écran jusqu'ici) ; 29 pytest. - Contrat (77 opérations) : POST /assistant/transcribe (multipart). - API : proxy multipart vers siop2-ai (WORK_ORDERS.edit — même droit que la saisie du bilan) ; 2 tests e2e (80 tests API au total). - Mobile : expo-audio + expo-file-system, bouton dicter/terminer sur l'écran de clôture, purge locale après transcription, « Joindre la description à l'OT » (corrige un bug latent : enfilerBilan ignorait silencieusement les mises à jour de note). - Docker : siop2-ai embarque le modèle Whisper au build (1,54→2,19 Go), construit et vérifié (transcription réelle en conteneur, non-root). - Vérifié réellement : transcription fidèle (voix de synthèse française) en direct, bout en bout via l'API, dans le conteneur Docker construit, et chaîne corpus complète (note → clôture → réindexation → recherche sémantique). - Base de dev locale réinitialisée avec accord explicite du référent (prisma migrate reset, bloqué par défaut pour un agent IA) après pollution par les tests manuels de la recette terrain précédente. Reste : test tactile sur iPhone physique (bouton dicter) — bloqué par une connexion USB qui ne s'est pas rétablie malgré câble/port/ redémarrage essayés à plusieurs reprises, reporté comme la recette Android. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
177 lines
10 KiB
Markdown
177 lines
10 KiB
Markdown
# Runbook — déploiement Dokploy
|
|
|
|
> **Rôle de ce document (playbook)** : la procédure REJOUABLE de mise en production
|
|
> sur Dokploy, écrite en R0.13 et répétée à chaque release (principe « déployer tôt »).
|
|
>
|
|
> **Instances** : démonstration/cours → `https://siop2.apps.enset.top` (Dokploy ENSET,
|
|
> projet créé le 15/07/2026) ; production client SPELEV → en attente des accès au
|
|
> serveur du partenaire (même procédure, profil d'environnement « production client »).
|
|
|
|
## 1. Topologie
|
|
|
|
Un projet Dokploy = un service « Compose » pointant sur ce dépôt,
|
|
fichier [`infra/docker-compose.dokploy.yml`](../../infra/docker-compose.dokploy.yml) :
|
|
|
|
```text
|
|
domaine (Traefik/Dokploy) ──▶ siop2-web :80 (nginx, statique)
|
|
│ /api/** (préfixe retiré)
|
|
▼
|
|
siop2-api :3000 (NestJS)
|
|
│── siop2-postgres :5432 (pgvector + PostGIS)
|
|
│── siop2-redis :6379
|
|
│── siop2-minio :9000
|
|
└── siop2-ai :8000 (FastAPI — R5, interne)
|
|
```
|
|
|
|
- **Convention `siop2-`** (leçon v1) : le réseau Dokploy est partagé entre projets ;
|
|
un service nommé `postgres` ou `api` collisionne. Tout est préfixé, sans exception.
|
|
- Seul `siop2-web` rejoint `dokploy-network` (réseau externe du reverse-proxy) :
|
|
l'API n'est **jamais** exposée directement, elle est servie via le proxy `/api` de nginx
|
|
— même topologie que le proxy Vite en dev, donc mêmes chemins partout.
|
|
- Les migrations Prisma s'appliquent **au démarrage du conteneur API**
|
|
(`prisma migrate deploy`, idempotent) : la base est toujours au niveau du code déployé.
|
|
|
|
## 2. Variables d'environnement (à saisir dans Dokploy, jamais dans le dépôt)
|
|
|
|
| Variable | Obligatoire | Rôle |
|
|
| --- | --- | --- |
|
|
| `POSTGRES_PASSWORD` | ✅ | mot de passe PostgreSQL (générer : `openssl rand -hex 24`) |
|
|
| `MINIO_ROOT_PASSWORD` | ✅ | secret MinIO |
|
|
| `JWT_SECRET` | ✅ | signature des jetons (générer : `openssl rand -hex 32`) |
|
|
| `POSTGRES_USER` / `POSTGRES_DB` | — | défaut `siop` / `siop` |
|
|
| `DEMO_MODE` | — | **absent en production client.** `true` uniquement sur l'instance de démonstration |
|
|
| `DEMO_MODE_I_KNOW` | — | double verrou ADR-002 : requis si `DEMO_MODE=true` en production, sinon l'API **refuse de démarrer** |
|
|
| `SEED_ON_START` | — | `true` sur l'instance de démonstration : rôles, matrice et comptes démo au boot (idempotent) |
|
|
| `SEED_DEMO_PASSWORD` | — | mot de passe commun des comptes démo (défaut `Demo!2026`) |
|
|
| `AI_SERVICE_TOKEN` | ✅ (R5) | secret partagé api ↔ service IA (générer : `openssl rand -hex 32`) — le service IA refuse tout appel sans lui |
|
|
| `AI_GENERATION` | — | `off` (défaut, mode extractif — la recette R5 passe ainsi) ou `api` (rédaction LLM, exige `AI_API_KEY`) |
|
|
| `AI_API_KEY` | — | clé API Anthropic — **uniquement** si `AI_GENERATION=api` ; le service refuse de démarrer si elle manque en mode api |
|
|
| `AI_MODEL` | — | défaut `claude-opus-4-8` |
|
|
| `AI_SEUIL_PERTINENCE` / `AI_SEUIL_SUGGESTION` / `AI_SEUIL_CONFIANCE_FORTE` | — | défauts 0,45 / 0,40 / 0,55 — **à calibrer sur le corpus client** (voir §7) |
|
|
| `AI_TRANSCRIPTION` | — | `off` (défaut) ou `locale` (dictée R5 D5, `faster-whisper` — le modèle est déjà dans l'image, l'activer ne demande pas de rebuild) |
|
|
| `AI_TRANSCRIPTION_MODEL` | — | défaut `small` — `tiny`/`base`/`small`/`medium`/`large-v3`, compromis qualité/vitesse CPU non calibré sur le darija (voir ADR-004 §5) |
|
|
|
|
Profils types :
|
|
|
|
- **Production client** : les 4 dernières variables **absentes**. Les routes
|
|
`/auth/demo-accounts` et `/auth/demo-login` n'existent pas (404, testé en CI).
|
|
- **Instance de démonstration** : `DEMO_MODE=true`, `DEMO_MODE_I_KNOW=true`,
|
|
`SEED_ON_START=true`.
|
|
|
|
## 3. Première mise en production — déploiement manuel depuis GitHub (checklist)
|
|
|
|
Dans le service **Compose** du projet Dokploy (déjà créé pour l'instance ENSET) :
|
|
|
|
1. **Onglet General — source GitHub** : Provider `GitHub` (application GitHub
|
|
Dokploy autorisée sur `siop-spelev/siop2`, dépôt privé), Branch `main`,
|
|
**Compose Path** `infra/docker-compose.dokploy.yml`.
|
|
2. **Onglet Environment** : coller les variables du §2. Pour l'instance de
|
|
démonstration ENSET :
|
|
|
|
```env
|
|
POSTGRES_PASSWORD=<openssl rand -hex 24>
|
|
MINIO_ROOT_PASSWORD=<openssl rand -hex 24>
|
|
JWT_SECRET=<openssl rand -hex 32>
|
|
AI_SERVICE_TOKEN=<openssl rand -hex 32>
|
|
DEMO_MODE=true
|
|
DEMO_MODE_I_KNOW=true
|
|
SEED_ON_START=true
|
|
```
|
|
|
|
3. **Deploy** (bouton). Dokploy clone le dépôt, construit les images (contexte =
|
|
racine, Dockerfiles `apps/api` et `apps/web` — ~5-10 min au premier build) puis
|
|
démarre les 5 services ; l'API attend PostgreSQL/Redis/MinIO sains et migre la base.
|
|
Suivi : onglet **Deployments** (logs de build) puis **Logs** par service.
|
|
4. **Onglet Domains** : Add Domain → Host `siop2.apps.enset.top`,
|
|
**Service Name `siop2-web`**, **Container Port `80`**, HTTPS activé
|
|
(certificat Let's Encrypt géré par Dokploy/Traefik).
|
|
5. Vérifications :
|
|
- `https://siop2.apps.enset.top/api/health` → `{"status":"ok", ...}` (3 services `up`) ;
|
|
- écran de connexion : 7 comptes démo, bascule de rôle < 3 s, bi-thème ;
|
|
- production client (quand elle existera) : `/api/auth/demo-accounts` → **404**.
|
|
6. Consigner la mise en production dans `docs/journal/journal.md` (date, commit, domaine).
|
|
|
|
## 4. Releases suivantes — déploiement continu par GitHub Actions
|
|
|
|
Le job `deploy` de `.github/workflows/ci.yml` appelle le **webhook Dokploy**
|
|
à chaque push sur `main`, **uniquement si les 5 jobs (lint, contrat, api, web, e2e)
|
|
sont verts** — c'est la CI qui garde la porte, pas l'inverse. Mise en place (une fois) :
|
|
|
|
1. Dokploy → service compose → onglet **Deployments** → copier la **Webhook URL**.
|
|
2. Dépôt GitHub → Settings → Secrets and variables → Actions →
|
|
**New repository secret** : nom `DOKPLOY_WEBHOOK_URL`, valeur = l'URL copiée.
|
|
(Ou en CLI : `gh secret set DOKPLOY_WEBHOOK_URL`.)
|
|
3. C'est tout : le prochain push vert sur `main` déclenche le build + redéploiement
|
|
côté Dokploy. Tant que le secret n'existe pas, le job `deploy` se termine en
|
|
« skip » explicite sans faire échouer le pipeline.
|
|
|
|
> **Ne pas activer** l'« Auto Deploy » natif de Dokploy (webhook GitHub direct) :
|
|
> il déploierait aussi les commits dont la CI est rouge.
|
|
|
|
Les migrations de la release s'appliquent au démarrage du conteneur API ; en cas
|
|
d'échec de migration, le conteneur s'arrête **sans** servir de trafic (l'ancienne
|
|
version reste visible côté web).
|
|
|
|
## 5. Le service IA en production (R5)
|
|
|
|
- `siop2-ai` **ne rejoint jamais** `dokploy-network` : il n'a pas de domaine, pas de
|
|
port publié — seul `siop2-api` le contacte, avec `AI_SERVICE_TOKEN`. S'il est
|
|
éteint, l'assistant répond « indisponible » (503 propre) et **tout le reste de
|
|
l'application fonctionne**.
|
|
- Le modèle d'embeddings (mpnet multilingue, ~1 Go) est **dans l'image** : premier
|
|
build long (téléchargement au build), démarrages rapides ensuite, aucun accès à
|
|
Hugging Face requis en production.
|
|
- **Après chaque déploiement qui change le modèle ou le découpage** (ex. migration
|
|
`r5_embeddings_mpnet`) : l'index est vide — cliquer **« Réindexer tout »** dans
|
|
Bibliothèque (ou `POST /assistant/reindex`) pour reconstruire le corpus.
|
|
- Mode génératif : ajouter `AI_GENERATION=api` + `AI_API_KEY` dans l'environnement
|
|
Dokploy puis redéployer le service `siop2-ai` seul. La clé ne transite jamais par
|
|
le dépôt ni par les journaux (`/healthz` n'expose que le mode).
|
|
- Dictée (D5) : ajouter `AI_TRANSCRIPTION=locale` dans l'environnement Dokploy puis
|
|
redéployer `siop2-ai` — le modèle Whisper est déjà dans l'image (§ci-dessus), pas
|
|
de rebuild ni de téléchargement au boot. L'audio n'est **jamais** stocké, ni par
|
|
`siop2-api` ni par `siop2-ai` : transcrit à la volée, effacé aussitôt (voir
|
|
ADR-004 §5). Point de vigilance non calibré : la qualité en darija (fréquent sur
|
|
le terrain, mélangé au français) — à mesurer sur de vrais enregistrements si la
|
|
dictée déçoit en recette.
|
|
|
|
## 6. Calibrage des seuils sur le corpus client
|
|
|
|
Les seuils par défaut ont été mesurés sur un banc synthétique (journal 17/07/2026).
|
|
Sur le vrai corpus SPELEV (notices réelles), rejouer une dizaine de questions métier
|
|
et quelques questions hors corpus via l'écran Assistant, puis ajuster :
|
|
|
|
- trop de refus → baisser `AI_SEUIL_PERTINENCE` par pas de 0,03 ;
|
|
- réponses « à côté » sur des questions hors corpus → le monter ;
|
|
- suggestions de bilan trop rares/trop bruyantes → jouer sur `AI_SEUIL_SUGGESTION`.
|
|
|
|
Chaque ajustement = variable d'environnement Dokploy + redéploiement de `siop2-ai`
|
|
(pas de rebuild, pas de réindexation).
|
|
|
|
## 7. Incidents & retours arrière
|
|
|
|
- **Rollback applicatif** : Dokploy → Deployments → redéployer le commit précédent.
|
|
Les migrations Prisma étant additives (convention projet : jamais de `DROP` sans
|
|
tâche dédiée validée), l'ancienne API fonctionne sur le schéma plus récent.
|
|
- **API en échec au boot** : `docker logs siop2-api`. Causes classiques :
|
|
`DEMO_MODE=true` sans `DEMO_MODE_I_KNOW` (verrou ADR-002 — c'est voulu),
|
|
`DATABASE_URL` erronée, migration échouée.
|
|
- **Sauvegardes** : volumes `pg-data` (base) et `minio-data` (fichiers).
|
|
Sauvegarde PostgreSQL planifiée côté Dokploy (onglet Backups) — à configurer
|
|
à la première mise en production réelle.
|
|
|
|
## 8. Répétition locale (sans Dokploy)
|
|
|
|
```bash
|
|
# depuis la racine — construit et lance les 5 services comme en production
|
|
docker network create dokploy-network 2>/dev/null || true
|
|
POSTGRES_PASSWORD=repetition MINIO_ROOT_PASSWORD=repetition-minio \
|
|
JWT_SECRET=$(openssl rand -hex 32) DEMO_MODE=true DEMO_MODE_I_KNOW=true SEED_ON_START=true \
|
|
docker compose -f infra/docker-compose.dokploy.yml up -d --build
|
|
# web : http://localhost via un port publié à ajouter ponctuellement, ou :
|
|
docker compose -f infra/docker-compose.dokploy.yml exec siop2-web wget -qO- http://localhost/api/health
|
|
```
|
|
|
|
C'est cette répétition (images construites, migrations au boot, parcours démo via
|
|
nginx) qui a validé R0.13 en local — voir le journal du 15/07/2026.
|