Commit Graph
16 Commits
Author SHA1 Message Date
GGThed 9647003c3f fix(auth): garder l identite Discord du cote verifie 2026-08-16 23:36:30 -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
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
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 3882b6035f fix(ops): defauts surs a la copie, CDN epingles, actions epinglees
Quatre taches de la matrice du rapport, toutes sans dependance, qu aucune
liste de « ce qui reste » ne reprenait.

OPS-003 — app/.env.exemple disait « copiez ce fichier et remplissez les
valeurs pour la production », puis posait FLASK_DEBUG=true,
SESSION_COOKIE_SECURE=false et FORCE_HTTPS=false. Le debogueur Werkzeug
execute du code soumis par le navigateur : cette ligne transformait un
copier-coller en shell distant. Chaque valeur est desormais sure a la
copie, et le fichier refuse de demarrer tant que les deux secrets
obligatoires ne sont pas remplis plutot que de demarrer grand ouvert.

Renomme en .env.example : l orthographe francaise ne correspondait pas a
l exception !.env.example du .gitignore, donc le fichier n etait suivi que
par accident de l ordre des regles. Les deux points de la decision ouverte
du §8 tombent d un seul git mv.

OPS-002 — trusted_proxy='*' et HOST ne sont plus soudes dans wsgi.py. Les
defauts sont **inchanges**, deliberement : choisir sans connaitre la
topologie coupe la prod si nginx est ailleurs, ou casse la limitation de
debit pour tout le monde si on cesse de croire X-Forwarded-For alors que
c etait la seule source d adresses. Ce sont maintenant des variables, les
valeurs sures sont dans .env.example pour un nouveau deploiement, et
docs/deployment.md donne les quatre topologies avec la valeur de chacune.
wsgi.py avertit au demarrage tant que les deux defauts sont en place.

Le commentaire de HOST annoncait « bind to localhost by default » a cote
d un defaut a 0.0.0.0 : il decrivait l intention pendant que le code
faisait l inverse. Il dit maintenant ce qu il fait.

QUA-004 — Font Awesome et FullCalendar etaient charges sans empreinte,
depuis des hotes que la CSP autorise nommement. Qui controle ces CDN
controlait ce qui s execute sur chaque page. Empreintes posees, avec ce
que SRI promet et ce qu il ne promet pas ecrit a cote : ca fige le fichier,
ca ne prouve pas qu il etait honnete au moment du calcul.

**Le CSS de FullCalendar n existait pas.** La v6 embarque ses styles dans
le JS et ce fichier n est pas publie : le <link> repondait 404 a chaque
ouverture du calendrier depuis la montee de version. Une feuille de style
en echec est silencieuse dans le navigateur, c est ce qui l a fait durer.

CI-003 — actions epinglees sur un commit, version en commentaire, dans les
deux forges. Un tag est un pointeur mobile : deplacer v4 fait executer du
code arbitraire dans le job qui detient la cle SSH de production. Ce job
recoit aussi enfin un bloc permissions.

517 tests.
2026-08-11 14:42:23 -04:00
GGThed 39808dd04e ops: un deploiement qui refuse de partir casse, et qui se verifie
OPS-011, en partie. Ce que le workflow garantit maintenant :

- rien ne part d un arbre casse. La suite, ruff check et ruff format
  tournent sur le runner de deploiement avant tout envoi. Une CI verte sur
  GitHub ne prouve rien ici : le deploiement se declenche a la main, sur ce
  que la branche contient a cet instant ;
- seuls les fichiers nommes partent. La charge est une liste blanche —
  app/, wsgi.py, requirements.txt — et non l arbre de travail moins neuf
  exclusions. C est par cette porte que clear_db.py, la suite de tests et
  les definitions de CI se sont retrouves sur le noeud de production ;
- le deploiement est verifie. /health est interroge pendant deux minutes
  apres l envoi et le job echoue s il ne repond jamais « healthy ». Avant,
  un arbre a moitie televerse etait un deploiement vert.

Ce qui n est pas garanti, et c est ecrit dans le fichier : la bascule n est
pas atomique. Le miroir se fait sur place, donc pendant le transfert la
production execute un melange de deux versions.

En cherchant a fermer ce point, un defaut a part entiere est apparu. Les
contrats etaient ranges a os.getcwd()/documents et leur chemin absolu
ecrit en base. La racine de stockage suivait donc le repertoire depuis
lequel le processus avait ete lance : redemarrer le serveur ailleurs
envoie les nouveaux contrats dans un nouvel arbre et rend les anciens
illisibles — la base continuant d affirmer qu ils sont la, la panne se
manifeste par un 500 au telechargement, pas par quelque chose
d actionnable.

app/storage.py fixe la racine et DOCUMENTS_ROOT la deplace. Les nouvelles
lignes gardent un chemin relatif, les anciennes gardent leur chemin absolu
et continuent de resoudre : aucune migration de donnees n est necessaire,
donc ce changement n attend pas Alembic.

C etait aussi le troisieme pre-requis de la bascule par repertoires de
version. Les deux autres sont hors d atteinte d ici — la commande de
demarrage Pterodactyl doit pointer sur current/, et les repertoires
partages doivent etre installes sur le noeud. Les deux sont decrits dans
docs/deployment.md, avec la procedure de retour arriere qui manquait.

511 tests.
2026-08-11 13:58:54 -04:00
GGThed d0a9e75fe6 perf: servir les statiques par nginx, et dire ce que le bot n a pas livre
PERF-006. Le bloc location /static/ etait commente : 59 Ko de CSS et de JS
passaient par Waitress a chaque page. L activer tel quel aurait ete une
regression : ces URL ne changent jamais, donc un cache de 30 jours sert une
feuille de style vieille d un mois apres chaque deploiement, sans moyen de
l invalider. url_for('static') estampille maintenant chaque URL du mtime du
fichier ; c est ce qui rend le immutable vrai et pas seulement rapide.

Deux pieges nginx consignes dans le fichier : un add_header dans un location
annule tous les add_header herites du server (nosniff disparaissait du
JavaScript), et un statique manquant doit renvoyer 404 plutot que retomber
sur Flask, sinon un deploiement casse se cache derriere une page qui marche.

PERF-005. Les objets utilisateur Discord sont mis en cache. A etre precis
sur le gain : un envoi coute deux appels reseau, resoudre puis envoyer, et
seul le premier est economise — un premier match a vingt joueurs fait
toujours vingt resolutions. Ce qui est gagne l est entre notifications, la
ou le bot ecrit aux memes personnes soir apres soir.

Chaque message dit desormais ce qu il est devenu, avec le destinataire et
la raison. Les trois echecs ne se ressemblent pas et ne se lisent plus
pareil : une boite fermee est definitive et ne se retente pas, une erreur
HTTP est passagere, un identifiant sans proprietaire est un compte a
corriger. Le lot quotidien annonce son propre deficit.

Piege trouve en ecrivant les tests : configure_logging met propagate=False
sur le logger 'app', et le handler de caplog est sur la racine. Les
assertions sur les journaux passaient seules et echouaient dans la suite
complete, ou une application avait deja ete construite — elles lisaient un
journal vide, pas un bot silencieux.

417 tests.
2026-08-11 11:56:48 -04:00
GGThedandClaude Opus 5 aebc28fb8a docs: une seule source d architecture, et un README qui dit vrai
DOC-002.

docs/architecture.html (746 l.) et docs/architecture-v3.html (798 l.)
coexistaient, en HTML versionne, sans rien qui indique laquelle faisait foi
ni ou etait passee la v2. docs/architecture.md reprend la v3 -- la plus
complete : 24 classes au lieu de 19, plus la structure des paquets -- en
Markdown avec les diagrammes en Mermaid. Lisible en revue, comparable en
diff, rendu directement par Gitea comme par GitHub.

Ajoute au document ce que les diagrammes ne montrent pas et qu il faut
savoir avant de les lire : la double modelisation coach/equipe, l absence
de migrations, et le bot dans le meme processus que le web.

README
Il annoncait « Authorization Checks: Proper ownership validation on all
sensitive operations » a une epoque ou trois IDOR etaient ouverts, et
« Rate Limiting » sans mentionner que trusted_proxy='*' la rend
contournable. Il ne disait pas non plus comment installer, lancer ni
tester le projet.

Reecrit en francais, avec deux sections distinctes : ce qui est en place et
verifie par des tests, et **ce qui ne l est pas** -- migrations absentes,
secrets non revoques, proxy de confiance, identite Discord non prouvee.
Une liste de securite qui ne mentionne que les bonnes nouvelles est pire
qu absente : on s y fie.

Instructions d installation, de lancement et de verification ajoutees, avec
les deux seuls points d entree (ARCH-007).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 18:25:20 -04:00
GGThedandClaude Opus 5 2f40290f00 fix(deps): nommer le pilote PostgreSQL, sinon rien ne demarre
QUA-001, volet dialecte.

`postgresql://` ne veut pas dire "le pilote installe" : SQLAlchemy y lit
psycopg2 et importe ce module a la creation du moteur. requirements.txt
epingle psycopg 3 (`psycopg[binary]`) et pas psycopg2. Une installation
propre demarree sur cette URL leve donc

  ModuleNotFoundError: No module named 'psycopg2'

avant la premiere requete. Verifie dans le .venv du depot, et c est
exactement la forme que Render distribue -- celle que docs/deployment.md et
docs/database-restore.md donnaient en exemple.

normalise_database_url() nomme le pilote quand l URL n en nomme pas.
`postgres://` (alias hérite, abandonne par SQLAlchemy en 1.4) est traite de
meme. Une URL qui nomme deja son pilote est laissee telle quelle, y compris
`postgresql+psycopg2://` : un environnement qui a psycopg2 garde le choix.

La normalisation a lieu apres l application de la configuration passee en
argument, pour couvrir aussi les appels de test. backup.py n avait pas
besoin d etre touche : il retirait deja le suffixe +pilote.

Documentation alignee sur les trois fichiers qui donnaient l exemple, dont
docs/deployment.md qui proposait sqlite:/// pour DATABASE_URL alors que
create_app refuse de demarrer sans PostgreSQL.

Reste de QUA-001, dit franchement
  - les trois paquets parasites (dotenv, login, discord) ne sont plus dans
    requirements.txt : deja retires. psycopg est deja epingle.
  - la consolidation vers des groupes de dependances n est PAS faite. Le
    deploiement est un miroir de fichiers lftp sans etape de construction ;
    les groupes PEP 735 demandent pip >= 25.1 sur une machine dont on ne
    peut pas verifier la version d ici. A revoir avec OPS-011.

14 tests, dont trois qui prouvent que l echec est reel et non theorique.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 15:50:35 -04:00
GGThedandClaude Opus 5 37f70c89e3 feat(i18n): traduire les messages flash et de validation
198 appels flash dans les sept modules de routes, plus les 23 messages de
validation de app/validators.py. Le catalogue compte desormais 631 chaines,
aucune non traduite.

validators.py utilise lazy_gettext : les champs de schema sont construits a
l'import, donc avant qu'une requete existe. Un gettext ordinaire s'y
resoudrait une seule fois, dans la langue active au demarrage.

Un bug introduit par la conversion, puis corrige
  Le convertisseur automatique ne voyait que le premier litteral d'un appel
  flash, ce qui a casse deux chaines concatenees sur plusieurs lignes dans
  users.py -- le resultat n'etait meme pas du Python valide. Ma premiere
  verification ne l'a pas vu : elle enchainait py_compile sur head, or head
  reussit toujours, donc le "OK" s'affichait quoi qu'il arrive. Les deux
  appels sont reecrits et la verification refaite correctement.

Un bug plus interessant, revele par le test de fumee
  La langue choisie ne survivait pas a la connexion. login() et logout()
  appellent tous deux session.clear() -- l'un contre la fixation de session,
  l'autre pour terminer la session -- et le choix de langue partait avec le
  reste. Concretement : quelqu'un qui lisait la page de connexion en anglais
  se retrouvait en francais des qu'il se connectait.

  La langue est une preference d'affichage, pas un etat appartenant au
  compte. Les deux endroits la reportent maintenant explicitement, a cote du
  jeton CSRF. Quatre tests couvrent le cas, dont un qui verifie que corriger
  une cle preservee n'a pas fait tomber l'autre.

Detail de nommage : le convertisseur avait genere %(value)s pour une
expression conditionnelle, ce qui n'aide pas un traducteur. Renomme en
%(player)s.

Les 14 traductions ecrites avec une apostrophe droite sont normalisees en
apostrophe typographique. Sans consequence en HTML, ou &#39; s'affiche
correctement -- mais les blocs <script> ne decodent pas les entites, et
autant que le catalogue soit homogene.

200 tests.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 22:12:27 -04:00
GGThedandClaude Opus 5 80bc1413f1 feat(i18n): traduire l'ensemble des gabarits
Le site s'affiche desormais integralement en francais. 483 chaines, aucune
non traduite, dans les deux catalogues.

Couvert : navigation et mise en page partagee, connexion, les cinq pages
d'erreur, et les 29 gabarits de pages.

Methode
  Marquage semi-automatique, conservateur par construction : seuls des
  motifs sans ambiguite sont balises -- contenu de balises de texte,
  attributs placeholder/title/aria-label, texte suivant une icone, blocs
  title et page_title -- et tout contenu comportant du Jinja ou du balisage
  imbrique est laisse de cote. Deux passes, la seconde pour td, li, h6,
  strong, em et caption.

  97 entrees etaient marquees fuzzy par pybabel update, c'est-a-dire
  devinees par similarite. Une entree fuzzy est **ignoree a l'execution** :
  elles ont donc ete traitees comme non traduites, et le drapeau retire une
  fois la traduction ecrite.

Deux pieges rencontres, tous deux documentes dans docs/translations.md

  Une entite HTML n'est pas du texte. &times;, utilise comme libelle de
  bouton de fermeture, a ete balise par la passe automatique. Jinja
  l'echappait alors en &amp;times; et le bouton aurait affiche le texte
  litteral &times; au lieu de la croix. Corrige dans cinq gabarits.

  Huit chaines subsistent dans des blocs <script>. Elles fonctionnent mais
  restent fragiles : Jinja echappe & < > " ' dans un bloc script, et ces
  entites n'y sont pas decodees -- une traduction contenant une apostrophe
  droite arriverait dans la chaine JavaScript sous la forme &#39;. Le
  francais retenu utilise des apostrophes typographiques, non echappees,
  donc l'existant est sur. Toute nouvelle chaine a cet endroit devra passer
  par un attribut data- ou un bloc <script type="application/json">.

Vocabulaire retenu -- a valider avec le club
  tryout -> selection · manager -> gerant · coach -> coach (conserve, terme
  d'usage en e-sport) · scout -> recruteur · email -> courriel · scrim ->
  scrim. Les noms de jeux et les postes (Support, Duelist, AWPer) restent
  en anglais : ce sont les termes employes par les joueurs.
  Ces choix vivent dans un seul catalogue, donc chacun se change en un
  endroit.

Reste a faire : les messages flash hors routes/auth.py, et les messages de
validation de app/validators.py, qui necessitent lazy_gettext puisque les
champs de schema sont construits a l'import.

Verifie : 13 pages parcourues dans les deux langues, aucune ne laisse de
marqueur Jinja non evalue ni d'entite doublement echappee. 193 tests.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 21:43:29 -04:00
GGThedandClaude Opus 5 a92600c305 feat(i18n): francais comme langue principale, anglais accessible
Le site s'affiche desormais en francais par defaut, avec un selecteur de
langue permettant de basculer vers l'anglais.

Choix de conception : les chaines sources restent en anglais
  Elles servent d'identifiants gettext, et le francais est fourni par
  catalogue avec BABEL_DEFAULT_LOCALE = 'fr'. Le code reste ainsi dans une
  seule langue -- la meme que ses commentaires et docstrings -- tandis que
  ce qu'un membre voit est du francais.

  Consequence qui rend la migration praticable : une chaine non encore
  traduite retombe en anglais, pas sur un identifiant brut. Les gabarits
  peuvent donc etre migres un par un sans jamais laisser le site a moitie
  casse.

Selection de la langue (app/i18n.py)
  1. choix explicite via le selecteur, garde en session
  2. sinon en-tete Accept-Language du navigateur, restreint a fr et en
  3. sinon francais
  Un choix explicite prime toujours, y compris sur un navigateur anglophone.

Selecteur
  Extrait en partiel et inclus dans les deux branches de la mise en page :
  barre laterale une fois connecte, ET page d'authentification. Quelqu'un
  qui ne lit pas la langue courante doit pouvoir en changer AVANT de se
  connecter -- le laisser derriere l'authentification aurait ete un defaut
  d'accessibilite. Chaque langue est ecrite dans sa propre langue.

  La route /lang/<locale> valide le Referer avant de rediriger : sans ce
  controle, elle constituait une redirection ouverte.

Migre dans cette passe
  navigation complete, page de connexion, les cinq pages d'erreur, et
  l'integralite des messages flash de routes/auth.py. 64 chaines, dont
  aucune non traduite.

Verification
  25 tests, dont deux garde-fous d'integrite : un catalogue .mo manquant
  ou une entree non traduite font echouer la suite. Sans cela, une
  compilation oubliee servirait de l'anglais partout, en silence et sans
  rien dans les journaux.

Un test existant a du etre corrige, et c'est instructif
  test_login_failure_message_does_not_reveal_account_existence cherchait la
  sous-chaine anglaise 'attempt(s) remaining'. La page etant desormais en
  francais, elle etait absente des deux cotes, l'assertion passait, et le
  mode strict a signale le faux succes. Le test comparait donc l'anglais,
  pas le comportement. Il compare desormais les messages flash rendus,
  quelle que soit la langue. La faille SEC-AUTH-006 reste ouverte, et le
  test la documente toujours.

Les catalogues .po ET .mo sont versionnes : le deploiement est un simple
miroir de fichiers, sans etape de compilation. messages.pot, regenerable,
ne l'est pas.

docs/translations.md documente le processus, les deux pieges (concatenation
de phrases, traduction a l'import), et l'etat de la migration. A noter pour
la suite : les chaines dans les blocs <script> ne peuvent pas etre balisees
telles quelles, il faudra les passer par des attributs data- -- ce qui
rejoint le chantier de sortie de unsafe-inline (OPS-010).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 20:31:25 -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
cedrick2711 b80c5b7aa2 UML avec la derniere version du projet 2026-07-29 14:46:49 -04:00
cedrick2711 90ac3eb804 Uml et fonctionnel dans un document .html 2026-07-28 18:41:05 -04:00
cedrick2711 666673fa8f demander IA de faire tous les modifications pour que le webapp soit pret au déploiement.
Force connection HTTPS, proxy-inversé, WSGI de production, reset cookie de conncection à chaque reconnection, limite sur les mdp, fichiers et One on One par minute, verification d'injection de SQL dans les champs d'entrées. renommage des fichiers lors du téléchargement, fichier de backup quotidien pour la bd et j'ai oublié quelque chose :(
2026-07-25 18:47:27 -04:00