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.
8.5 KiB
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.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
-
/healthrépond-il ?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.200mais le site est inutilisable → le problème est dans nginx ou dans le DNS, pas dans l'application.
-
L'application est-elle démarrée ? Console Pterodactyl. Au démarrage elle imprime
Starting Waitress server on <hôte>:<port>. -
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 :
{"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_URLa-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 voyezModuleNotFoundError: 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.
-
/healthle rapporte :{"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.
-
Chercher les livraisons manquées :
grep 'not delivered' logs/app.logTrois 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.
-
Le bilan du lot quotidien :
grep 'Daily reminders' logs/app.log17 of 20 delivered, 3 failed— les trois sont nommées juste au-dessus. -
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.
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 queTRUSTED_PROXY=*(voirOPS-002dansdocs/deployment.md) : il est falsifiable à chaque requête.
Une page renvoie 500
- Récupérer la référence auprès de la personne, ou la dernière trace :
tail -50 logs/errors.log - La trace complète y est. Elle n'est jamais montrée à l'utilisateur.
- 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 :
curl -s https://<hôte>/auth/login | grep -o 'style.css?v=[0-9]*' - 404 sur
/static/…→ le cheminaliasdu bloclocation /static/denginx.confne pointe pas au bon endroit. - Une page est cassée et la console du navigateur parle de CSP → un bloc
<script>a été ajouté sansnonce="{{ 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
integrityne 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 :
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 |