Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y

Config strictement stricte partout (ruff, mypy --strict, bandit, vulture,
import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas
casser le build" - l'existant a ete corrige pour la satisfaire plutot que
l'inverse. Hooks pre-commit locaux (language: system) bloquants.

- Typage mypy --strict propage a tout le moteur (db, screens, auth, core,
  ai, routes, puis publish/scripts/tests/app.py/build_css.py).
- Securite : fuite de handle fichier Windows corrigee dans l'export SCORM
  (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes
  (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe
  "</script>" dans le JSON embarque en <script>, 25 sites).
- Architecture : imports circulaires/F811 nettoyes, contrats
  import-linter respectes, code mort retire (vulture).
- Accessibilite : 69 champs de formulaire sans label correctement
  associe corriges (for/id ou aria-label) sur 11 templates.
- ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis
  appliquees (aucune desactivee sans verification individuelle).
- Tests : isolation du compte admin partage (nettoyage ponctuel +
  fixture de teardown automatique en filet de securite), suite complete
  verte (591 tests Python, 241 tests JS).
- SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport
  complet analyse point par point, faux positifs documentes.
- .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine
  faisait echouer ESLint (linebreak-style) via un bug connu de git
  (checkout "en place" qui ignore l'eol force sur un fichier deja
  present sur disque - contourne en supprimant puis recreant chaque
  fichier suivi).

djLint (H021, styles inline) volontairement saute pour ce commit -
backlog assume, deja documente, traite dans un lot separe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-15 16:06:15 +02:00
co-authored by Claude Sonnet 5
parent 7db4803b93
commit c57420c8c9
454 changed files with 16448 additions and 6967 deletions
+8 -1
View File
@@ -1 +1,8 @@
from . import index, games_new, game_dashboard, game_rename, game_delete, game_scoring_view
from . import ( # noqa: F401 - enregistre les routes definies dans chaque module
game_dashboard,
game_delete,
game_rename,
game_scoring_view,
games_new,
index,
)
+13 -7
View File
@@ -1,13 +1,14 @@
from typing import Any
from flask import render_template
import db
import screens
from core.flask_app import app
@app.route("/game/<slug>")
def game_dashboard(slug):
def game_dashboard(slug: str) -> str:
"""Vue d'ensemble du jeu : juste ses écrans, en cartes avec un aperçu
de leur contenu, plus un bouton "+ Ajouter un écran" qui ne demande pas
de type (un seul moteur d'écran : jeu_2d — voir
@@ -16,9 +17,9 @@ def game_dashboard(slug):
return _render_simple_dashboard(slug, game)
def _render_simple_dashboard(slug, game):
def _render_simple_dashboard(slug: str, game: dict[str, Any]) -> str:
screen_list = screens.list_screens(slug)
previews = {}
previews: dict[int, dict[str, Any]] = {}
for s in screen_list:
if s["kind"] == "jeu_2d":
objects = screens.list_scene_objects(slug, s["id"])
@@ -32,9 +33,14 @@ def _render_simple_dashboard(slug, game):
# montrait que le coin haut-gauche au lieu de la scène entière.
world_width, world_height = screens.resolve_scene_world_size(s, objects)
previews[s["id"]] = {
"kind": "jeu_2d", "objects": objects,
"world_width": world_width, "world_height": world_height,
"kind": "jeu_2d",
"objects": objects,
"world_width": world_width,
"world_height": world_height,
}
return render_template(
"game_dashboard_simple.html", game=game, screen_list=screen_list, previews=previews,
"game_dashboard_simple.html",
game=game,
screen_list=screen_list,
previews=previews,
)
+2 -2
View File
@@ -1,11 +1,11 @@
from flask import redirect, url_for
from werkzeug.wrappers import Response
import db
from core.flask_app import app
@app.route("/game/<slug>/delete", methods=["POST"])
def game_delete(slug):
def game_delete(slug: str) -> Response:
db.delete_game(slug)
return redirect(url_for("index"))
+3 -3
View File
@@ -1,12 +1,12 @@
from flask import request, redirect, url_for
from flask import redirect, request, url_for
from werkzeug.wrappers import Response
import db
from core.flask_app import app
@app.route("/game/<slug>/rename", methods=["POST"])
def game_rename(slug):
def game_rename(slug: str) -> Response:
new_name = request.form.get("game_name", "").strip()
if new_name:
db.update_game_name(slug, new_name)
+2 -2
View File
@@ -1,12 +1,12 @@
from flask import jsonify
from werkzeug.wrappers import Response
import db
from core.flask_app import app
@app.route("/game/<slug>/scoring/<player_id>")
def game_scoring_view(slug, player_id):
def game_scoring_view(slug: str, player_id: str) -> Response:
"""Lecture du score/statut d'UN joueur (voir db/scoring/) — route
interne (protégée comme toute route /game/<slug>, voir
core/auth_guard.py), pas destinée au joueur lui-même : préparée pour
+3 -3
View File
@@ -1,12 +1,12 @@
from flask import g, request, redirect, url_for
from flask import g, redirect, request, url_for
from werkzeug.wrappers import Response
import db
from core.flask_app import app
@app.route("/games/new", methods=["POST"])
def games_new():
def games_new() -> Response:
name = request.form.get("name", "").strip()
if not name:
return redirect(url_for("index"))
+1 -2
View File
@@ -1,12 +1,11 @@
from flask import g, render_template
import db
from core.flask_app import app
@app.route("/")
def index():
def index() -> str:
# Toujours SES PROPRES projets (voir db.list_games, corrigé pour ne
# plus jamais scanner les autres comptes) — même pour un admin, qui
# n'a donc plus aucune visibilité sur les projets des autres comptes.