Commit Graph
14 Commits
Author SHA1 Message Date
GGThed 9647003c3f fix(auth): garder l identite Discord du cote verifie 2026-08-16 23:36:30 -04:00
GGThed 437b229c82 Merge immortal/main into audit/securite-maintenabilite-standards 2026-08-12 14:58:28 -04:00
GGThedandClaude Opus 5 bda23dbb67 fix(ops): la sauvegarde des contrats archivait le mauvais repertoire
OBS-006. Trois racines etaient baties sur os.getcwd() : le magasin de
documents, les journaux et les sauvegardes. La vague G a corrige la
premiere, parce qu'elle bloquait aussi OPS-011, et a laisse les deux autres.
C'est la lecon deja consignee deux fois : un motif fautif corrige dans une
seule couche reste dans les autres.

Le plus serieux n'est pas le motif, c'est l'ecart qu'il a ouvert.
backup.py gardait sa propre constante DOCUMENTS_DIR sur os.getcwd(), donc
il ignorait DOCUMENTS_ROOT -- la variable que la vague G a introduite et que
docs/deployment.md dit maintenant de regler pour sortir les televersements
des repertoires de version. Des qu'un exploitant suit cette consigne, le
script archive un repertoire ou l'application n'a jamais rien ecrit. Et
comme il repond a un repertoire absent par une ligne d'information et un
code de sortie 0, une tache planifiee qui surveille le code de sortie voit
vert indefiniment.

Autrement dit : plus l'exploitant suivait correctement la documentation de
deploiement, plus surement ses sauvegardes de contrats etaient vides.

Les trois racines viennent desormais d'app/storage.py, resolues a l'appel et
non a l'import, et la sauvegarde imprime la source qu'elle a utilisee. Le
message d'absence nomme le chemin ou elle a cherche : "No documents
directory found" se lisait comme "il n'y a pas de documents" plutot que
comme "je regarde au mauvais endroit".

Le test qui porte est celui qui ouvre l'archive : un zip vide est un fichier
de taille non nulle, donc verifier qu'un fichier a ete produit ne prouvait
rien. Verifie par mutation.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-11 18:34:52 -04:00
GGThedandClaude Opus 5 66f2838402 chore(lint): interdire d avaler une exception sans laisser de trace
Regle BLE de ruff activee. Ce qu'elle enforce n'est pas "ne jamais attraper
large" : elle se satisfait d'un logger.exception. C'est exactement la
discipline visee — une frontiere peut tout avaler, a condition de laisser de
quoi distinguer un defaut d'une panne. Les cinq noqa que j'avais prepares
d'avance etaient donc inertes ; la raison reste en commentaire simple.

Ce que la regle a trouve, une fois activee :

app.py, demarrage du bot — les deux facons d'echouer, jeton invalide et
import casse, se lisaient a l'identique sur une seule ligne et aucune
n'etait diagnosticable. Passe en error avec exc_info : un club qui ne
recoit plus aucun rappel a perdu une fonctionnalite, et warning mettait ca
a cote des avis de depreciation.

services/notifications.py — le bloc webhook attrapait large autour d'un
requests.post. RequestException couvre toutes les facons dont un appel HTTP
echoue ; le reste est un defaut. La branche DM et la branche webhook etaient
en plus imbriquees dans un seul try alors qu'elles s'excluent.

logging_config.py et les deux scripts CLI gardent leur largeur, avec la
raison sur la ligne. Le filtre de journalisation est le cas ou la trace que
BLE001 reclame est precisement ce qu'il ne faut pas produire : journaliser
depuis un filtre rentre dans le meme filtre.

RUF100 (noqa inutile) n'est volontairement pas active : il ferait remonter
des directives preexistantes sans rapport avec ce chantier.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-11 16:29:13 -04:00
GGThed 7b9eee4805 feat(db): mesurer la derive du schema, au lieu de la supposer
DB-001. create_all() cree les tables manquantes et ne fait jamais d ALTER.
Une colonne ajoutee a un modele il y a six mois est donc absente de toute
base qui possedait deja la table, et rien ne le dit : l application demarre,
et la premiere requete qui touche cette colonne echoue a l execution. C est
la raison d etre de migrations/add_tryout_coaches.py, ecrit a la main pour
rattraper un cas. Personne ne sait combien il y en a d autres.

app/supporting_scripts/schema_report.py compare le catalogue d une base
vivante aux modeles : tables, colonnes, types, nullabilite, contraintes
d unicite, cles etrangeres, index. En **lecture seule** — il ouvre une
connexion, lit, imprime, sort. Aucun DDL, aucun DML.

Les constats sont classes par ce qu ils coutent, pas par ce qu ils sont :

- BLOCKING : les modeles l attendent, la base ne l a pas. C est la derive ;
- RISK : la base l a, aucun modele ne le decrit. Inoffensif tant que rien
  ne bouge — et **un alembic --autogenerate proposera de le supprimer**,
  avec ses donnees. C est la classe qu on lit en entier ;
- DIFFERENCE : types, nullabilite, contraintes. Chacune demande un humain.

Les types sont compares apres compilation vers le meme dialecte : opposer
String(200) a VARCHAR(200) en chaines aurait signale chaque colonne comme
differente, et un rapport qui crie partout ne se lit plus.

--check-seed-accounts repond a la question de SEC-003 a laquelle le depot
ne peut pas repondre : le compte admin/password seme par clear_db.py
existe-t-il encore, et son mot de passe est-il toujours celui-la.

13 tests le pilotent contre des bases SQLite fabriquees pour diverger d une
facon connue. Le cas qui compte le plus est la base propre : un rapport qui
crie sur une base saine ne sera pas lu, et un rapport qui dit « aucun
ecart » sur une base derivee est pire que pas de rapport — c est un feu
vert pour laisser autogenerate ecrire la difference en DROP.

docs/database-schema.md donne la suite, etape par etape, avec le piege de
DB-002 en toutes lettres : la migration initiale doit decrire la base telle
qu elle est, pas telle que les modeles la decrivent. Generer depuis les
modeles puis estampiller revient a declarer que la derive n existe pas.

Alembic n est pas ajoute aux dependances : rien ne l utilise encore, et une
dependance que rien n utilise est exactement ce que l audit reprochait
ailleurs. Le document dit a quelle etape l ajouter.

530 tests.
2026-08-11 15:00:18 -04:00
GGThed 47ff544848 chore(lint): trier les imports, sauf la facade des modeles
Active la regle isort (I) de ruff. 45 fichiers reordonnes, aucun
changement de comportement : la suite passe avant comme apres.

app/models/__init__.py en est exclu. Ses imports sont ranges en onze
couches commentees qui decrivent le graphe de dependances ; trier par
ordre alphabetique laisse chaque titre au-dessus d un import qu il ne
decrit pas, et ce fichier n a qu un role, etre lu.

Commit isole, comme le formatage : un diff de brassage ne doit pas
servir de couverture a un changement de comportement.
2026-08-11 11:40:41 -04:00
cedrick2711 d007900f6c régler problème de dispos des coachs 2026-08-10 17:20:07 -04:00
GGThedandClaude Opus 5 8e3865f557 chore(lint): elargir les regles ruff et rendre le format bloquant en CI
QUA-002, seconde moitie. Le depot etant formate, l elargissement porte sur
des defauts et non sur du brassage.

Ajoute a la selection : B (bugbear), C4, RET, SIM, UP. Le lot entier n a
produit que 24 signalements sur 76 fichiers -- le code etait plus propre
que l audit ne le craignait. Neuf corriges automatiquement, quinze a la
main.

SIM108 est ignore : forcer un ternaire se lit moins bien que le if/else
qu il remplace, au seul endroit ou il se declenche.

isort (I) n est PAS active. Il reordonnerait les imports de 48 fichiers,
soit une seconde passe de pur brassage juste apres le commit de formatage.
A faire, mais seul.

Deux vrais defauts trouves par les nouvelles regles
  - team_matches.edit_match faisait `except ValueError: pass` sur l heure de
    debut et l heure de fin, trois lignes sous un champ date qui, lui,
    signale et redirige. Une heure mal saisie etait donc acceptee par le
    formulaire, jetee, l ancienne valeur conservee -- et la page annoncait
    la reussite. Meme traitement que la date desormais.
  - backup.py levait BackupError depuis deux blocs `except` sans `from`,
    ce qui perdait la cause d origine dans la trace.

Ainsi que : un `return` explicite dans force_https, `%`-formatage remplace
dans log_auth_event (operations de chaine avant journalisation, pas des
gabarits de logger -- la redaction n est pas affectee), une compréhension
inutile, un `set(...)` en compréhension d ensemble, `open(..., 'r')`, une
variable de boucle inutilisee, et `contextlib.suppress` dans conftest.

CI : `ruff format --check` remplace le commentaire qui expliquait pourquoi
il etait absent.

263 tests passent. Les deux nouveaux messages sont traduits ; attention,
pybabel les avait apparies en `fuzzy` avec des entrees « date » existantes,
et une entree fuzzy est ignoree a l execution.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 15:59:40 -04:00
GGThedandClaude Opus 5 7cec18c139 style: formater le depot avec ruff format
QUA-002, premiere moitie. **Ce commit ne fait que reformater** : aucun
changement de comportement, aucune ligne de logique touchee. 72 fichiers,
4 restaient deja conformes. Il est isole exprès, pour que `git log -p` sur
les commits voisins reste lisible.

`quote-style = "preserve"` etait deja pose dans pyproject.toml, ce qui
evite le brassage guillemets simples / doubles : le diff porte sur les
retours a la ligne, l indentation des appels longs et les virgules
finales, pas sur le style de chaine.

Verification : 263 tests passent avant et apres, ruff check propre.

L activation en CI arrive dans le commit suivant, separement, pour que ce
diff-ci ne contienne rien d autre.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 15:53:10 -04:00
GGThedandClaude Opus 5 7bf428f9a0 fix(ops): sauvegarder reellement la base PostgreSQL
DATA-002 / OPS-001. backup.py ciblait SQLite : import sqlite3, DATABASE_PATH
par defaut instance/team_tryouts.db, et l'API de sauvegarde sqlite3. La
production tourne sur PostgreSQL, donc le fichier n'existait pas. Le script
affichait "[WARNING] Database not found... Skipping database backup" -- puis,
main() ne suivant que le resultat de la verification, **sortait avec le code
0**. Toute tache planifiee surveillant le code de sortie voyait vert alors
qu'aucune sauvegarde n'avait jamais ete produite.

Il n'existait donc aucune sauvegarde applicative de la base.

Reecriture
  pg_dump en --format=custom : compresse, et pg_restore permet une
  restauration selective, ce qu'un dump SQL a plat ne permet pas.
  parse_database_url accepte les suffixes de dialecte SQLAlchemy
  (postgresql+psycopg://) que pg_dump ne comprend pas, et refuse
  explicitement une URL SQLite -- le cas exact qui passait en silence.

  Le mot de passe ne figure jamais dans la ligne de commande : il serait
  visible de tout processus capable de lister argv. Il passe par PGPASSWORD.
  Il est egalement absent des messages affiches, qui atterrissent dans les
  journaux du planificateur.

  verify_backup lit l'archive avec pg_restore --list et exige au moins une
  table : une archive illisible ne se restaure pas, et une archive sans
  table signifie que le dump a vise la mauvaise cible. Les deux sont des
  echecs silencieux qu'il vaut mieux attraper maintenant que pendant un
  incident.

  Le code de sortie vaut 0 uniquement si le dump a ete produit ET verifie.

L'archive des documents est conservee : les contrats signes n'existent que
sur disque, la base ne stocke que des chemins. Restaurer l'une sans l'autre
laisse des lignes pointant vers des fichiers absents.

docs/database-restore.md
  Procedure de restauration testable sur une base jetable, requetes de
  controle, demarrage de l'application sur la copie restauree, plan de
  reprise par scenario. ENABLE_DISCORD_BOT=false y est signale comme non
  optionnel : sans lui, l'exercice demarre un vrai bot et envoie de vraies
  notifications a de vraies personnes, a partir de donnees restaurees.

  Les points ouverts sont listes tels quels : aucune copie hors site, pas de
  chiffrement au repos, aucune planification, et l'exercice de restauration
  n'a jamais ete effectue.

17 tests sur ce qui est verifiable sans serveur PostgreSQL : analyse de
l'URL, construction de la commande, non-fuite du mot de passe, et surtout
codes de sortie -- le silence ne vaut plus succes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 20:06:51 -04:00
GGThedandClaude Opus 5 32f193c008 style: f-strings sans placeholder signalees par ruff (F541)
Aucun changement de comportement. Les chaines concernees ne contenaient
aucune substitution.

A noter pour plus tard : run_https.py conserve une banniere en caracteres
semi-graphiques, du meme type que celle qui faisait planter security_scan.py
sur une console Windows en cp1252. Le script n'etant lance qu'en
developpement et de facon explicite, le point est signale sans etre corrige
ici.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 19:47:45 -04:00
GGThedandClaude Opus 5 5ecea55f55 fix(ci): rendre la chaine d'integration reellement verifiante
Les quatre jobs existaient ; aucun ne verifiait ce qu'il annoncait.

security-audit
  `pip-audit --require-hashes --no-deps || pip-audit`. L'etape
  d'installation ne posait que pip-audit, et aucune des deux formes ne
  nommait le fichier d'exigences : le repli auditait l'environnement du
  runner, qui ne contenait que pip-audit lui-meme. Le job passait au vert
  sans avoir examine une seule dependance de l'application. Remplace par
  `pip-audit -r requirements.txt`.

security-scan
  Appelait `python security_scan.py`, alors que le fichier se trouve dans
  app/supporting_scripts/. En echec a chaque execution depuis le
  deplacement du fichier. Trois autres defauts sont apparus en le faisant
  tourner :
    - la CI passe --skip-http, un argument que l'argparse du script
      n'acceptait pas : sortie en erreur 2 meme avec le bon chemin.
    - check_dependencies lisait data['dependencies'] comme la liste des
      vulnerabilites. Ce tableau liste en realite TOUTES les dependances,
      chacune portant un champ vulns vide si le paquet est sain. Les ~45
      paquets installes etaient donc signales vulnerables a chaque
      execution. Le filtrage se fait desormais sur vulns non vide.
    - check_flask_config interceptait son exception et renvoyait quand
      meme all_ok : ne pas reussir a charger l'application comptait comme
      un controle reussi. La section la plus importante du rapport n'avait
      jamais tourne. Elle renvoie desormais False, et l'import fonctionne
      grace a l'ajout de la racine du projet dans sys.path.
    - la banniere en caracteres semi-graphiques faisait planter le script
      sur une console Windows en cp1252, la plateforme meme du projet.
      Passee en ASCII.

lint
  Ruff n'avait aucun fichier de configuration : le job tournait sur le jeu
  de regles par defaut. La configuration vit maintenant dans pyproject.toml.
  `ruff format --check` est retire pour l'instant : la base n'ayant jamais
  ete formatee, il echouerait sur 62 fichiers sur 64 pour des raisons
  etrangeres a la correction. Reformatage puis application : QUA-002.

test
  Un `echo` protege par continue-on-error : le job annoncait un succes
  sans rien executer. Il lance desormais pytest avec couverture, et bloque.

permissions: contents: read au niveau du workflow, aucune etape n'ecrivant
dans le depot.

Deploiement Gitea
  actions/checkout@v7 n'existe pas (derniere majeure : v5) : le workflow
  echouait des sa premiere etape. Ramene a v4.
  Le miroir lftp poussait l'integralite de l'arbre de travail, dont
  clear_db.py -- un script qui vide toutes les tables et recree
  admin/password -- vers le noeud de production. Liste d'exclusions ajoutee.
  --delete reste volontairement absent : les contrats televerses, les
  journaux et le .env du serveur vivent sous la racine de deploiement et
  sont absents du depot ; les supprimer detruirait des donnees.

Le workflow de deploiement n'a pas pu etre execute depuis ici : la
syntaxe lftp reste a valider lors du prochain deploiement manuel.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 19:47:24 -04:00
cedrick2711 7bb80229db added a clear_db to start fresh with only an admin 2026-08-04 13:09:19 -04:00
cedrick2711 0dd4ecdb4c Erreur de frappe dans un des dossiers 2026-08-04 12:57:18 -04:00