Files
team-tryouts/docs/incident-runbook.md
GGThed 7a1dab21cd docs: les deux documents d exploitation qui manquaient
DOC-001 demandait quatre documents courts. Deux existaient — installation
(deployment.md) et restauration (database-restore.md). Les deux autres
n existaient pas.

docs/roles-and-permissions.md decrit le comportement **implemente**, pas
celui qu on souhaiterait, et note les endroits ou les deux divergent : un
gerant gere toutes les equipes mais pas toutes les selections, asymetrie
presente depuis toujours et ecrite nulle part ailleurs. Le piege du double
rattachement coach/equipe y est en toutes lettres, avec le motif a ne
jamais reintroduire.

Le document dit aussi qui a raison en cas de desaccord : les tests. Une
documentation d autorisation qui se contredit avec le code est pire que
pas de documentation, parce qu on la croit.

docs/incident-runbook.md part du symptome, pas du composant. Le reflexe
d ouverture est la reference affichee sur la page d erreur — c est ce que
l identifiant de requete rend possible, et sans un endroit qui le dise, la
fonctionnalite ne sert a personne.

Trois affirmations ont ete verifiees contre le code avant d etre ecrites,
et deux etaient fausses : le corps de /health en echec dit
database="error" et non "disconnected", et les evenements de verrouillage
s appellent login.failure et account.throttled. La liste complete des
quatorze evenements de auth.log est maintenant dans le manuel.

README : index des documents, et deux points d etat corriges — TRUSTED_PROXY
est desormais une variable, et l absence de validation des inscriptions par
le staff est nommee comme decision en attente.
2026-08-11 15:40:30 -04:00

205 lines
8.5 KiB
Markdown

# Manuel d'incident
À lire quand quelque chose ne va pas. Chaque section : le symptôme, où
regarder, quoi faire.
> **Le réflexe préalable** — demander la **référence** affichée sur la page
> d'erreur (`Référence à indiquer si vous signalez ce problème`). C'est
> l'identifiant de requête ; il apparaît dans chaque ligne de journal produite
> par cette requête, entre crochets.
>
> ```bash
> grep '\[a1b2c3d4e5f60718\]' logs/*.log
> ```
## Où sont les journaux
| Fichier | Contenu |
|---|---|
| `logs/app.log` | Tout, à partir de `LOG_LEVEL` (défaut `INFO`) |
| `logs/errors.log` | `ERROR` et au-dessus, avec les traces |
| `logs/auth.log` | Connexions, échecs, verrous, changements de rôle, suppressions de compte, refus d'inscription |
| Console Pterodactyl | Les mêmes lignes que `app.log` — le handler console est actif en production, délibérément |
| `C:\nginx\logs\error.log` | Ce qui n'a jamais atteint l'application |
Format : `[date] NIVEAU [module:ligne] [id-de-requête] message`. Un `-` à la
place de l'identifiant signifie « hors requête » : démarrage, bot Discord,
planificateur.
### Les événements de `auth.log`
Tout est en `clé=valeur`, dans l'ordre, donc greppable sans dépendance JSON.
| Événement | Quand |
|---|---|
| `login.success` | Connexion réussie |
| `login.failure` | Mot de passe faux sur un compte existant |
| `login.failure.unknown_user` | Identifiant inconnu |
| `login.rejected.deactivated` | Bons identifiants, compte désactivé |
| `account.throttled` | Délai d'attente déclenché après des échecs répétés |
| `logout` | Déconnexion |
| `account.registered` | Inscription réussie |
| `account.registration_refused` | Inscription filtrée — `reason=honeypot`, `too-fast` ou `no-form-issued` |
| `account.created_by_admin` | Compte créé depuis l'administration |
| `account.updated` | Fiche modifiée |
| `account.role_changed` | Changement de rôle, avec l'ancien et le nouveau |
| `account.password_changed` | Changement par la personne elle-même |
| `account.password_reset_by_admin` | Réinitialisation par un président |
| `account.deleted` | Suppression, avec le nombre de fichiers de contrat retirés |
Le champ `ip=` vient de `request.remote_addr`, donc de `X-Forwarded-For`.
Tant que `TRUSTED_PROXY=*`, **c'est une indication et pas une preuve**.
## Le site ne répond pas
1. **`/health` répond-il ?**
```bash
curl -i https://<hôte>/health
```
- Pas de réponse du tout → nginx est tombé, ou l'application n'écoute plus.
Vérifier la console Pterodactyl.
- `503` → l'application tourne mais la base ne répond pas. Voir plus bas.
- `200` mais le site est inutilisable → le problème est dans nginx ou dans
le DNS, pas dans l'application.
2. **L'application est-elle démarrée ?** Console Pterodactyl. Au démarrage elle
imprime `Starting Waitress server on <hôte>:<port>`.
3. **Un déploiement vient-il d'avoir lieu ?** C'est la cause la plus fréquente.
Voir « Revenir en arrière » dans `docs/deployment.md`.
## `/health` renvoie 503
La base ne répond pas. Le corps de la réponse le dit :
```json
{"status": "unhealthy", "database": "error"}
```
Le corps ne contient **jamais** l'erreur du pilote : elle porte l'hôte, le nom
de la base et l'utilisateur de la chaîne de connexion, et `/health` n'est pas
authentifié. La trace complète est dans `logs/errors.log`.
- La base est hébergée sur Render : vérifier son état côté Render en premier.
- `DATABASE_URL` a-t-elle changé ? Un mot de passe tourné et pas reporté
produit exactement ça.
- **Piège connu** : `DATABASE_URL=postgresql://…` seul ne démarre pas —
SQLAlchemy y cherche psycopg **2**, le projet épingle psycopg **3**.
`normalise_database_url()` nomme le pilote, donc les deux formes marchent ;
si vous voyez `ModuleNotFoundError: psycopg2`, c'est que le code qui
normalise n'a pas été déployé.
## Plus aucune notification Discord
Le bot tourne dans un **fil du même processus** que le site. Quand il meurt,
les pages continuent d'être servies et toutes les notifications s'arrêtent.
C'est précisément ce qui est resté invisible longtemps.
1. `/health` le rapporte :
```json
{"discord_bot": {"running": false, ...}}
```
Rapporté et **non fatal** : un club sans rappels Discord est dégradé, pas
hors service, et un 503 le sortirait du répartiteur de charge pour ça.
2. Chercher les livraisons manquées :
```bash
grep 'not delivered' logs/app.log
```
Trois causes, qui ne se traitent pas pareil :
- `their direct messages are closed` → **définitif**. La personne doit
autoriser les messages privés des membres du serveur. Réessayer ne sert à
rien.
- `does not exist` → l'identifiant Discord sur le compte est faux ou le
compte a été supprimé. À corriger dans la fiche de la personne.
- autre chose → passager, côté Discord.
3. Le bilan du lot quotidien :
```bash
grep 'Daily reminders' logs/app.log
```
`17 of 20 delivered, 3 failed` — les trois sont nommées juste au-dessus.
4. Redémarrer le processus relance le bot. Les réactions en attente survivent
au redémarrage (`discord_pending.json`), sauf si ce fichier a été mis de
côté pour corruption — auquel cas le journal le dit et donne le nom du
fichier de quarantaine.
## Quelqu'un est verrouillé dehors
Cinq échecs consécutifs déclenchent un délai qui double ensuite. Le compte
n'est pas bloqué définitivement.
```bash
grep -E 'login\.failure|account\.throttled' logs/auth.log | grep '<identifiant>'
```
- **Le verrou se lève tout seul.** Le délai est dans le message.
- Un président peut réinitialiser le mot de passe depuis la fiche du compte,
ce qui remet le compteur à zéro.
- **Beaucoup de verrous sur des comptes différents en même temps** → quelqu'un
essaie des mots de passe. Le champ `ip=` est une indication et **pas une
preuve** tant que `TRUSTED_PROXY=*` (voir `OPS-002` dans
`docs/deployment.md`) : il est falsifiable à chaque requête.
## Une page renvoie 500
1. Récupérer la référence auprès de la personne, ou la dernière trace :
```bash
tail -50 logs/errors.log
```
2. La trace complète y est. Elle n'est jamais montrée à l'utilisateur.
3. Si c'est arrivé juste après un déploiement, revenir en arrière d'abord et
diagnostiquer ensuite.
## Le style ou le JavaScript ne se chargent plus
Les statiques sont servis par nginx avec un cache de 30 jours, ce qui n'est
sûr que parce que leurs URL portent une estampille (`?v=<mtime>`).
- **Après un déploiement, la page a l'ancien style** → l'estampille n'a pas
changé, donc le fichier n'a pas été déployé. Vérifier :
```bash
curl -s https://<hôte>/auth/login | grep -o 'style.css?v=[0-9]*'
```
- **404 sur `/static/…`** → le chemin `alias` du bloc `location /static/` de
`nginx.conf` ne pointe pas au bon endroit.
- **Une page est cassée et la console du navigateur parle de CSP** → un bloc
`<script>` a été ajouté sans `nonce="{{ csp_nonce }}"`. Sans nonce il ne
s'exécute pas, et **rien dans les journaux ne le signale**.
- **Une ressource de CDN ne charge plus** → une empreinte `integrity` ne
correspond plus. Le navigateur refuse le fichier en silence côté serveur.
Cas normal : quelqu'un a monté la version sans recalculer l'empreinte.
## Les données semblent fausses
**Une colonne existe dans le modèle et pas dans la base.** `create_all()` ne
fait jamais d'`ALTER`, donc c'est possible et ce n'est pas rare :
```bash
python app/supporting_scripts/schema_report.py --url "$DATABASE_URL"
```
Lecture seule. Le rapport dit exactement ce qui manque. Voir
`docs/database-schema.md`.
## Restaurer
Procédure complète dans `docs/database-restore.md`. Deux règles :
- restaurer **d'abord sur une copie**, jamais directement par-dessus la
production ;
- une sauvegarde qu'on n'a jamais restaurée est une hypothèse, pas une
sauvegarde.
## À faire une fois, et pas encore fait
Ces points sont ouverts et connus. Ils ne sont pas des incidents, ils en
causeront.
| Point | Pourquoi c'est urgent |
|---|---|
| Révoquer `SECRET_KEY` et le jeton du bot Discord | Les deux sont dans l'historique git des deux dépôts, et dans le HEAD du miroir GitHub. Avec la `SECRET_KEY`, on forge une session valide pour n'importe quel compte |
| Vérifier le compte `admin`/`password` | `python app/supporting_scripts/schema_report.py --check-seed-accounts` |
| Régler `HOST` et `TRUSTED_PROXY` | Tant que c'est `*`, la limite de débit et le journal d'authentification sont falsifiables |