Files
team-tryouts/docs/incident-runbook.md
T
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

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

  1. /health ré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.
    • 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 :

{"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 :

    {"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 :

    grep 'not delivered' logs/app.log
    

    Trois causes, qui ne se traitent pas pareil :

    • their direct messages are closeddé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 :

    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.

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 :
    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 :
    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 :

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