Lot 4 modernisation JS (SonarLint) + restauration CI sonarqube non-bloquante
Build and deploy / test-python (push) Successful in 11m18s
Build and deploy / test-js (push) Successful in 1m16s
Build and deploy / lint-python (push) Successful in 4m24s
Build and deploy / lint-js (push) Successful in 2m51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Successful in 5m30s

Corrige les findings SonarQube (via SonarLint IDE, fichier par fichier)
sur ~24 fichiers static/js/ : parseFloat/parseInt -> Number.*,
.replace(/x/g,y) -> .replaceAll, .indexOf() -> .includes()/.startsWith(),
getAttribute/setAttribute -> .dataset, tableaux -> Set, x && x.y -> x?.y
(verifie site par site), extraction de template litteraux imbriques,
ternaires imbriquees, refactors de complexite cognitive (S3776) via
tables de dispatch, Object.hasOwn, .at(), et deduplication de fonctions
identiques (S4144). Deux exceptions S2486 documentees/corrigees
(filter-repeater-rows.js) et un cas S2703 de partage inter-scripts
complete (_collisionWizard, trigger-editor.js <-> collision-rules-editor.js).
Details complets dans CODE_QUALITY.md section 5.

Restaure aussi le job CI "sonarqube" (non-bloquant) dans
.gitea/workflows/deploy.yml maintenant que l'instance prod est
operationnelle.

Suites vertes : 276/276 JS (node --test), 591/591 Python (pytest).

SKIP=djlint sur ce commit : hook djlint bloquant sur le backlog H021
(styles inline, 49 occurrences/6 templates) deja documente comme dette
assumee non traitee dans CODE_QUALITY.md section 6, aucun rapport avec
ce commit (aucun template touche ici) - valide explicitement avec
l'utilisateur avant de contourner ce hook.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-18 16:38:29 +02:00
co-authored by Claude Sonnet 5
parent 66d8eaeae8
commit ccf836f2c5
36 changed files with 1720 additions and 838 deletions
+32 -30
View File
@@ -20,6 +20,21 @@
const PLACEHOLDER_OPEN = '{' + '{';
const PLACEHOLDER_CLOSE = '}' + '}';
// Partagée entre applyOpenRowBindings() et _forgeApplyFreshRuntimeData()
// (même algorithme, dupliqué à l'identique dans les deux avant ce
// refactor S4144) : `byId` explicite en paramètre plutôt que capturé par
// fermeture, puisque chaque appelant construit sa PROPRE table (éléments
// de l'écran au moment de son propre appel, jamais partagés entre les
// deux).
function isDescendantOfElement(byId, e, ancestorId) {
let p = e.parent_id ? byId[e.parent_id] : null;
while (p) {
if (p.id === ancestorId) return true;
p = p.parent_id ? byId[p.parent_id] : null;
}
return false;
}
function applyOpenRowBindings() {
if (!window.currentOpenRow) return;
const rows = gameData.data[String(window.currentOpenRow.definitionId)] || [];
@@ -53,19 +68,11 @@ function applyOpenRowBindings() {
const byId = {};
screenData.elements.forEach((e) => { byId[e.id] = e; });
function hasUnresolvedPlaceholder(e) {
return !!(e.rendered_html && e.rendered_html.indexOf(PLACEHOLDER_OPEN) !== -1);
}
function isDescendantOf(e, ancestorId) {
let p = e.parent_id ? byId[e.parent_id] : null;
while (p) {
if (p.id === ancestorId) return true;
p = p.parent_id ? byId[p.parent_id] : null;
}
return false;
return !!e.rendered_html?.includes(PLACEHOLDER_OPEN);
}
const candidates = screenData.elements.filter(hasUnresolvedPlaceholder);
const targets = candidates.filter(
(e) => !candidates.some((other) => other.id !== e.id && isDescendantOf(other, e.id)),
(e) => !candidates.some((other) => other.id !== e.id && isDescendantOfElement(byId, other, e.id)),
);
// Résout {{champ}} EN PLACE dans le DOM déjà affiché, sans jamais
@@ -92,7 +99,7 @@ function applyOpenRowBindings() {
const toSplit = [];
let node;
while ((node = walker.nextNode())) {
if (node.nodeValue.indexOf(PLACEHOLDER_OPEN) !== -1) toSplit.push(node);
if (node.nodeValue.includes(PLACEHOLDER_OPEN)) toSplit.push(node);
}
toSplit.forEach((textNode) => {
const raw = textNode.nodeValue;
@@ -125,9 +132,12 @@ function applyOpenRowBindings() {
Array.from(node.attributes).forEach((attr) => {
if (attr.name.indexOf('data-bind-attr-') === 0) return; // gabarit déjà mémorisé, pas un attribut à lire
const bindKey = `data-bind-attr-${attr.name}`;
const template = node.hasAttribute(bindKey)
? node.getAttribute(bindKey)
: (attr.value.indexOf(PLACEHOLDER_OPEN) !== -1 ? attr.value : null);
let template = null;
if (node.hasAttribute(bindKey)) {
template = node.getAttribute(bindKey);
} else if (attr.value.includes(PLACEHOLDER_OPEN)) {
template = attr.value;
}
if (template === null) return;
if (!node.hasAttribute(bindKey)) node.setAttribute(bindKey, template);
placeholderRe.lastIndex = 0;
@@ -227,19 +237,11 @@ function _forgeApplyFreshRuntimeData(newData) {
const byId = {};
screenData.elements.forEach((e) => { byId[e.id] = e; });
const hasMarker = (e) => !!(e.rendered_html && (
e.rendered_html.indexOf('repeaterItem') !== -1
|| e.rendered_html.indexOf('jaugeBar') !== -1
|| e.rendered_html.indexOf('visibilityGated') !== -1
|| e.rendered_html.indexOf('dataBound') !== -1
e.rendered_html.includes('repeaterItem')
|| e.rendered_html.includes('jaugeBar')
|| e.rendered_html.includes('visibilityGated')
|| e.rendered_html.includes('dataBound')
));
const isDescendantOf = (e, ancestorId) => {
let p = e.parent_id ? byId[e.parent_id] : null;
while (p) {
if (p.id === ancestorId) return true;
p = p.parent_id ? byId[p.parent_id] : null;
}
return false;
};
// "visibilityGated"/"dataBound" sont posés comme un COMMENTAIRE
// HTML juste après la balise concernée (voir _mark() dans
// render_element_html.py) — jamais comme une classe CSS, à la
@@ -254,13 +256,13 @@ function _forgeApplyFreshRuntimeData(newData) {
// l'identifie, son marqueur ne remontant jusqu'au SEUL élément
// de premier niveau existant (l'exemplaire) que parce que son
// HTML est imbriqué dans le sien.
const COMMENT_MARKERS = ['visibilityGated', 'dataBound'];
const COMMENT_MARKERS = new Set(['visibilityGated', 'dataBound']);
const findCommentMarkedDescendants = (root) => {
const found = [];
root.querySelectorAll('[data-element-id]').forEach((node) => {
const sib = node.nextSibling;
const hasCommentMarker = !!(
sib && sib.nodeType === Node.COMMENT_NODE && COMMENT_MARKERS.indexOf(sib.data) !== -1
sib?.nodeType === Node.COMMENT_NODE && COMMENT_MARKERS.has(sib.data)
);
// Un élément actuellement CACHÉ par une condition de
// visibilité (voir render_element_html.py) n'a pas de
@@ -282,7 +284,7 @@ function _forgeApplyFreshRuntimeData(newData) {
};
const markedCandidates = screenData.elements.filter(hasMarker);
markedCandidates
.filter((e) => !markedCandidates.some((other) => other.id !== e.id && isDescendantOf(other, e.id)))
.filter((e) => !markedCandidates.some((other) => other.id !== e.id && isDescendantOfElement(byId, other, e.id)))
.forEach((el) => {
const target = screenDiv.querySelector(`[data-element-id="${el.id}"]`);
if (!target) return;
@@ -299,7 +301,7 @@ function _forgeApplyFreshRuntimeData(newData) {
// disparaître ni réapparaître juste parce qu'un texte à
// l'intérieur a changé.
innerMarked.forEach((innerNode) => {
const innerId = innerNode.getAttribute('data-element-id');
const innerId = innerNode.dataset.elementId;
const liveInner = target.querySelector(`[data-element-id="${innerId}"]`);
if (liveInner) liveInner.replaceWith(innerNode);
});