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
+34 -38
View File
@@ -4,75 +4,71 @@
// d'un écran-modèle d'élément de jeu réutilisable, précalculé une fois à
// l'export) au lieu de screens/element_types/load_template_tree.py.
function forgeRenderRepeater(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
var style = forgeStyleString(el, parentFlexDirection, el.widget) + " overflow:auto;";
var attrsRaw = el.attributes || {};
var definitionId = attrsRaw._definition_id;
var definitionAttr = definitionId ? ' data-definition-id="' + forgeHtmlEscape(String(definitionId)) + '"' : "";
var templateTypeId = attrsRaw._template_type_id;
var template = forgeApplyCtx(el.content || "", ctx);
var children = (childrenMap && childrenMap[el.id]) || [];
const style = `${forgeStyleString(el, parentFlexDirection, el.widget)} overflow:auto;`;
const attrsRaw = el.attributes || {};
let definitionId = attrsRaw._definition_id;
const definitionAttr = definitionId ? ` data-definition-id="${forgeHtmlEscape(String(definitionId))}"` : '';
const templateTypeId = attrsRaw._template_type_id;
const template = forgeApplyCtx(el.content || '', ctx);
const children = (childrenMap && childrenMap[el.id]) || [];
if (!definitionId) {
return '<div id="' + forgeAutoId(el) + '" data-element-id="' + el.id + '" style="' + forgeHtmlEscape(style) +
'"><em>Répéteur : choisis un objet de données dans les propriétés.</em></div>';
return `<div id="${forgeAutoId(el)}" data-element-id="${el.id}" style="${forgeHtmlEscape(style)
}"><em>Répéteur : choisis un objet de données dans les propriétés.</em></div>`;
}
definitionId = String(definitionId);
var fieldsMetaForDef = gameData.fields_meta && gameData.fields_meta[definitionId];
const fieldsMetaForDef = gameData.fields_meta && gameData.fields_meta[definitionId];
if (!fieldsMetaForDef) {
return '<div id="' + forgeAutoId(el) + '" data-element-id="' + el.id + '" style="' + forgeHtmlEscape(style) +
'"><em>Objet introuvable — a-t-il été supprimé ?</em></div>';
return `<div id="${forgeAutoId(el)}" data-element-id="${el.id}" style="${forgeHtmlEscape(style)
}"><em>Objet introuvable — a-t-il été supprimé ?</em></div>`;
}
var rows = gameData.data[definitionId] || [];
let rows = gameData.data[definitionId] || [];
rows = forgeFilterRepeaterRows(gameData, definitionId, rows, attrsRaw);
var templateTree = null;
let templateTree = null;
if (templateTypeId) {
templateTree = gameData.element_type_templates && gameData.element_type_templates[String(templateTypeId)];
}
var items = rows.map(function (row) {
var rowCtx = Object.assign({}, ctx || {});
const items = rows.map((row) => {
const rowCtx = { ...ctx || {} };
// gameData.data est déjà indexé par le nom D'AFFICHAGE du champ (voir
// full_game_payload.py), jamais par sa colonne SQL — row[f.name] lit
// donc directement la bonne valeur, y compris pour un champ "relation"
// (voir filter-repeater-rows.js pour le détail de ce piège).
fieldsMetaForDef.forEach(function (f) {
var v = row[f.name];
rowCtx[f.name] = v === null || v === undefined ? "" : String(v);
fieldsMetaForDef.forEach((f) => {
const v = row[f.name];
rowCtx[f.name] = v === null || v === undefined ? '' : String(v);
});
var inner;
let inner;
if (templateTree) {
inner = templateTree.top.map(function (c) {
return forgeRenderElementHtml(c, gameData, templateTree.children_map, rowCtx);
}).join("");
inner = templateTree.top.map((c) => forgeRenderElementHtml(c, gameData, templateTree.children_map, rowCtx)).join('');
} else if (children.length) {
inner = children.map(function (c) {
return forgeRenderElementHtml(c, gameData, childrenMap, rowCtx);
}).join("");
inner = children.map((c) => forgeRenderElementHtml(c, gameData, childrenMap, rowCtx)).join('');
} else {
var line = template;
Object.keys(rowCtx).forEach(function (fname) {
if (fname === "_forge_play_mode") return;
line = line.split("{{" + fname + "}}").join(rowCtx[fname]);
let line = template;
Object.keys(rowCtx).forEach((fname) => {
if (fname === '_forge_play_mode') return;
line = line.split(`{{${fname}}}`).join(rowCtx[fname]);
});
inner = forgeHtmlEscape(line);
}
return '<div class="repeaterItem" data-row-id="' + row.id + '">' + inner + '</div>';
return `<div class="repeaterItem" data-row-id="${row.id}">${inner}</div>`;
});
var emptyMsg = attrsRaw._filtre_champ ? "Aucune ligne ne correspond au filtre réglé." : "Aucune donnée dans cet objet pour l'instant.";
var innerAll = items.length ? items.join("") : "<em>" + emptyMsg + "</em>";
return '<div id="' + forgeAutoId(el) + '" data-element-id="' + el.id + '"' + definitionAttr +
' style="' + forgeHtmlEscape(style) + '">' + innerAll + '</div>';
const emptyMsg = attrsRaw._filtre_champ ? 'Aucune ligne ne correspond au filtre réglé.' : "Aucune donnée dans cet objet pour l'instant.";
const innerAll = items.length ? items.join('') : `<em>${emptyMsg}</em>`;
return `<div id="${forgeAutoId(el)}" data-element-id="${el.id}"${definitionAttr
} style="${forgeHtmlEscape(style)}">${innerAll}</div>`;
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { forgeRenderRepeater };
var renderMod = require('./render-element.js');
["forgeStyleString", "forgeHtmlEscape", "forgeAutoId", "forgeApplyCtx", "forgeRenderElementHtml"].forEach(function (name) {
const renderMod = require('./render-element.js');
['forgeStyleString', 'forgeHtmlEscape', 'forgeAutoId', 'forgeApplyCtx', 'forgeRenderElementHtml'].forEach((name) => {
global[name] = global[name] || renderMod[name];
});
var filterMod = require('./filter-repeater-rows.js');
const filterMod = require('./filter-repeater-rows.js');
global.forgeFilterRepeaterRows = global.forgeFilterRepeaterRows || filterMod.forgeFilterRepeaterRows;
}