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
+9 -8
View File
@@ -39,7 +39,7 @@ const COLLISION_TRIGGER_LABELS = {
// data_operations.py::DATA_OPERATIONS) — le champ valeur de l'étape
// "Modifier une variable" (renderCollisionWizardVariableStep) se masque
// pour celles-ci.
const COLLISION_NO_VALUE_OPERATIONS = ['definir_bool_vrai', 'definir_bool_faux', 'basculer_bool'];
const COLLISION_NO_VALUE_OPERATIONS = new Set(['definir_bool_vrai', 'definir_bool_faux', 'basculer_bool']);
// obj.thumbnail_html (screens.render_scene_object) porte un style inline
// PENSÉ POUR LE CANEVAS (position:absolute + left/top en dur, voir
@@ -242,7 +242,7 @@ function renderCollisionWizardAttendreStep(onChoose) {
function collisionWizardAttendreNext() {
const raw = document.getElementById('attendreWizSecondes').value;
const secondes = String(Math.max(0, parseFloat(raw) || 0));
const secondes = String(Math.max(0, Number.parseFloat(raw) || 0));
const onChoose = _collisionWizard.onAttendreChosen;
_collisionWizard.onAttendreChosen = null;
onChoose(secondes);
@@ -265,7 +265,7 @@ function renderCollisionWizardObjectTargetStep(onChoose) {
if (!objects.length) { grid.innerHTML = '<p class="hint">Aucun objet éligible sur ce jeu.</p>'; return; }
grid.innerHTML = objects.map((o, i) => `<button type="button" class="collisionWizardChoice questTriggerObjectChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardObjectTargetChosen(${o.id})">`
+ `<div class="questTriggerObjectThumb">${collisionRuleThumbHtml(o)}</div>`
+ `<span>${(o.name || '').replace(/</g, '&lt;')}</span>`
+ `<span>${(o.name || '').replaceAll('<', '&lt;')}</span>`
+ '</button>').join('');
_collisionWizard.onObjectTargetChosen = onChoose;
});
@@ -314,7 +314,7 @@ function renderCollisionWizardAssetStep(onChoose, kind) {
collisionWizardBodyEl().innerHTML = `<p class="hint mb-2">Aucun fichier ${label} dans "Mes assets" pour l'instant — importe-en un depuis le panneau de gauche (section "Mes assets"), puis recommence.</p>`;
return;
}
const options = assets.map((a) => `<option value="${a.url}">${(a.name || '').replace(/</g, '&lt;')}</option>`).join('');
const options = assets.map((a) => `<option value="${a.url}">${(a.name || '').replaceAll('<', '&lt;')}</option>`).join('');
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Quel fichier (voir "Mes assets") ?</p>'
+ `<select id="collisionWizardAssetSelect" style="width:100%; margin-bottom:10px;">${options}</select>`
+ '<button type="button" class="button primary" onclick="collisionWizardAssetNext()">Continuer</button>';
@@ -364,7 +364,7 @@ function collisionWizardIndicationNext() {
const action = {
type: 'indication', id: collisionGenerateActionId(), object_id: _collisionWizard.pendingIndicationObjectId, texte,
};
if (dureeRaw) action.duree_ms = parseInt(dureeRaw, 10);
if (dureeRaw) action.duree_ms = Number.parseInt(dureeRaw, 10);
_collisionWizard.pendingIndicationObjectId = null;
const onChoose = _collisionWizard.onIndicationChosen;
_collisionWizard.onIndicationChosen = null;
@@ -418,7 +418,7 @@ function renderCollisionWizardVariableStep(onChoose) {
function collisionWizardVariableOperationChanged() {
const operation = document.getElementById('varWizOperation').value;
const valueInput = document.getElementById('varWizValue');
valueInput.style.display = COLLISION_NO_VALUE_OPERATIONS.indexOf(operation) === -1 ? '' : 'none';
valueInput.style.display = COLLISION_NO_VALUE_OPERATIONS.has(operation) ? 'none' : '';
}
function collisionWizardVariableNext() {
@@ -429,7 +429,7 @@ function collisionWizardVariableNext() {
target_variable: document.getElementById('varWizVariable').value,
data_operation: operation,
};
if (COLLISION_NO_VALUE_OPERATIONS.indexOf(operation) === -1) {
if (!COLLISION_NO_VALUE_OPERATIONS.has(operation)) {
action.data_value = document.getElementById('varWizValue').value;
}
const onChoose = _collisionWizard.onVariableChosen;
@@ -456,9 +456,10 @@ function collisionWizardVariableNext() {
// dialogue, "attendre" disparaissait silencieusement.
function renderCollisionWizardChainStep(leafAction, onDone, headAction) {
headAction = headAction || leafAction;
const markCompletedChecked = leafAction.mark_completed ? ' checked' : '';
const completionHtml = leafAction.type === 'dialogue'
? '<label style="display:flex; align-items:center; gap:6px; font-size:13px; margin-bottom:10px;">'
+ `<input type="checkbox" id="condWizMarkCompleted"${leafAction.mark_completed ? ' checked' : ''}> Marquer ce dialogue comme terminé une fois joué`
+ `<input type="checkbox" id="condWizMarkCompleted"${markCompletedChecked}> Marquer ce dialogue comme terminé une fois joué`
+ '</label>'
: '';
const choiceButtons = COLLISION_CHAIN_CHOICES.map((choice, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${(i + 1) * 40}ms" onclick="collisionWizardChainDone('${choice}')"><span>${COLLISION_ACTION_LABELS[choice]}</span></button>`).join('');
+1 -1
View File
@@ -52,7 +52,7 @@ function createGlobalVariable(event) {
fetch(`/game/${GAME_SLUG}/variables/new`, {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body,
})
.then((r) => (r.ok ? r.json() : Promise.reject()))
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then((json) => {
if (!json.row_html) { window.location.reload(); return; }
const tbody = document.getElementById('globalVariablesBody');
+56 -56
View File
@@ -38,7 +38,7 @@ function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId, x, y) {
// posé en base à ce stade (voir screens/scenes/add_scene_object.py),
// seul un rechargement peut alors l'afficher.
const contentType = r.headers.get('content-type') || '';
if (!r.ok || contentType.indexOf('application/json') === -1) {
if (!r.ok || !contentType.includes('application/json')) {
window.location.reload();
return null;
}
@@ -124,7 +124,7 @@ function onCanvasDrop(ev) {
// screens/scenes/add_scene_object.py) — absent pour un son/une vidéo,
// qui n'ont rien à "poser".
function uploadUserAsset(inputEl, sceneKind) {
const file = inputEl.files && inputEl.files[0];
const file = inputEl.files?.[0];
if (!file) return;
const body = new FormData();
body.append('file', file);
@@ -193,14 +193,14 @@ function deleteSceneObject(id) {
})
.then((r) => {
const contentType = r.headers.get('content-type') || '';
if (!r.ok || contentType.indexOf('application/json') === -1) {
if (!r.ok || !contentType.includes('application/json')) {
window.location.reload();
return null;
}
return r.json();
})
.then((json) => {
if (!json || !json.ok) return;
if (!json?.ok) return;
const el = document.getElementById(`el-${id}`);
if (el) el.remove();
if (typeof _stopPersonnageIdlePreview === 'function') _stopPersonnageIdlePreview(id);
@@ -227,8 +227,8 @@ function deleteSceneObject(id) {
function applyCameraSize() {
const wEl = document.getElementById('scene-camera-w'); const
hEl = document.getElementById('scene-camera-h');
const width = parseInt(wEl.value, 10) || 1; const
height = parseInt(hEl.value, 10) || 1;
const width = Number.parseInt(wEl.value, 10) || 1; const
height = Number.parseInt(hEl.value, 10) || 1;
fetch(`/game/${GAME_SLUG}/screens/${SCREEN_ID}/scene-size`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -325,10 +325,10 @@ function onSceneObjectMouseDown(e, id) {
startY = e.clientY;
let lastClientX = e.clientX; let
lastClientY = e.clientY;
let curLeft = parseFloat(el.style.left) || 0; let
curTop = parseFloat(el.style.top) || 0;
const width = parseFloat(el.style.width) || 0; const
height = parseFloat(el.style.height) || 0;
let curLeft = Number.parseFloat(el.style.left) || 0; let
curTop = Number.parseFloat(el.style.top) || 0;
const width = Number.parseFloat(el.style.width) || 0; const
height = Number.parseFloat(el.style.height) || 0;
let moved = false;
// Les panneaux flottants "🧩 Objets"/"⚙️ Propriétés" (voir static/style.css
// .floatPanel, z-index:60) restent DÉLIBÉRÉMENT en position:fixed
@@ -405,10 +405,10 @@ function onSceneObjectResizeMouseDown(e, id) {
const el = document.getElementById(`el-${id}`);
let lastClientX = e.clientX; let
lastClientY = e.clientY;
let curWidth = parseFloat(el.style.width) || 0; let
curHeight = parseFloat(el.style.height) || 0;
const left = parseFloat(el.style.left) || 0; const
top = parseFloat(el.style.top) || 0;
let curWidth = Number.parseFloat(el.style.width) || 0; let
curHeight = Number.parseFloat(el.style.height) || 0;
const left = Number.parseFloat(el.style.left) || 0; const
top = Number.parseFloat(el.style.top) || 0;
// Voir la même note dans onSceneObjectMouseDown ci-dessus : au-dessus
// des panneaux flottants le temps du geste, pour rester visible même
// en redimensionnant sous l'un d'eux.
@@ -459,10 +459,10 @@ function initScenePosFields() {
if (!xEl || !wEl || !CURRENT_SELECTED_ID) return;
function apply() {
const canvasEl = document.getElementById(`el-${CURRENT_SELECTED_ID}`);
const x = parseFloat(xEl.value) || 0; const
y = parseFloat(yEl.value) || 0;
const w = parseFloat(wEl.value) || 1; const
h = parseFloat(hEl.value) || 1;
const x = Number.parseFloat(xEl.value) || 0; const
y = Number.parseFloat(yEl.value) || 0;
const w = Number.parseFloat(wEl.value) || 1; const
h = Number.parseFloat(hEl.value) || 1;
if (canvasEl) {
canvasEl.style.left = `${x}px`; canvasEl.style.top = `${y}px`;
canvasEl.style.width = `${w}px`; canvasEl.style.height = `${h}px`;
@@ -483,7 +483,7 @@ initScenePosFields();
function swapSceneObjectCharacter(forgeCharacter) {
const body = document.getElementById('personnagePanelBody');
if (!body) return;
const objectId = parseInt(body.dataset.elementId, 10);
const objectId = Number.parseInt(body.dataset.elementId, 10);
document.querySelectorAll('#personnageGallerySwap .iconGalleryTile').forEach((tile) => {
tile.classList.toggle('selected', tile.getAttribute('onclick') === `swapSceneObjectCharacter('${forgeCharacter}')`);
});
@@ -508,7 +508,7 @@ function swapSceneObjectCharacter(forgeCharacter) {
function saveSceneObjectRole(role) {
const body = document.getElementById('personnagePanelBody');
if (!body) return;
const objectId = parseInt(body.dataset.elementId, 10);
const objectId = Number.parseInt(body.dataset.elementId, 10);
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/role`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -527,19 +527,19 @@ function saveSceneObjectRole(role) {
function saveSceneObjectCollision() {
const body = document.getElementById('collisionPanelBody');
if (!body) return;
const objectId = parseInt(body.dataset.elementId, 10);
const objectId = Number.parseInt(body.dataset.elementId, 10);
const enabled = document.getElementById('collision-enabled').checked;
const shape = document.getElementById('collision-shape').value;
const width = parseFloat(document.getElementById('collision-width').value) || 0;
const height = parseFloat(document.getElementById('collision-height').value) || 0;
const offsetX = parseFloat(document.getElementById('collision-offset-x').value) || 0;
const offsetY = parseFloat(document.getElementById('collision-offset-y').value) || 0;
const width = Number.parseFloat(document.getElementById('collision-width').value) || 0;
const height = Number.parseFloat(document.getElementById('collision-height').value) || 0;
const offsetX = Number.parseFloat(document.getElementById('collision-offset-x').value) || 0;
const offsetY = Number.parseFloat(document.getElementById('collision-offset-y').value) || 0;
const overlay = document.getElementById('collisionBoxOverlay');
const objEl = document.getElementById(`el-${objectId}`);
if (overlay && objEl) {
const objWidth = parseFloat(objEl.style.width) || 0; const
objHeight = parseFloat(objEl.style.height) || 0;
const objWidth = Number.parseFloat(objEl.style.width) || 0; const
objHeight = Number.parseFloat(objEl.style.height) || 0;
overlay.style.width = `${width}px`;
overlay.style.height = `${height}px`;
overlay.style.left = `${(objWidth - width) / 2 + offsetX}px`;
@@ -579,14 +579,14 @@ function onCollisionBoxMouseDown(e, id) {
e.preventDefault();
const overlay = document.getElementById('collisionBoxOverlay');
const objEl = document.getElementById(`el-${id}`);
const objWidth = parseFloat(objEl.style.width) || 0; const
objHeight = parseFloat(objEl.style.height) || 0;
const objWidth = Number.parseFloat(objEl.style.width) || 0; const
objHeight = Number.parseFloat(objEl.style.height) || 0;
const startX = e.clientX; const
startY = e.clientY;
const startLeft = parseFloat(overlay.style.left) || 0; const
startTop = parseFloat(overlay.style.top) || 0;
const boxWidth = parseFloat(overlay.style.width) || 0; const
boxHeight = parseFloat(overlay.style.height) || 0;
const startLeft = Number.parseFloat(overlay.style.left) || 0; const
startTop = Number.parseFloat(overlay.style.top) || 0;
const boxWidth = Number.parseFloat(overlay.style.width) || 0; const
boxHeight = Number.parseFloat(overlay.style.height) || 0;
function onMove(ev) {
overlay.style.left = `${startLeft + (ev.clientX - startX)}px`;
@@ -595,8 +595,8 @@ function onCollisionBoxMouseDown(e, id) {
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
const offsetX = (parseFloat(overlay.style.left) || 0) - (objWidth - boxWidth) / 2;
const offsetY = (parseFloat(overlay.style.top) || 0) - (objHeight - boxHeight) / 2;
const offsetX = (Number.parseFloat(overlay.style.left) || 0) - (objWidth - boxWidth) / 2;
const offsetY = (Number.parseFloat(overlay.style.top) || 0) - (objHeight - boxHeight) / 2;
updateCollisionFields(boxWidth, boxHeight, offsetX, offsetY);
saveSceneObjectCollision();
}
@@ -614,14 +614,14 @@ function onCollisionBoxResizeMouseDown(e, id) {
e.preventDefault();
const overlay = document.getElementById('collisionBoxOverlay');
const objEl = document.getElementById(`el-${id}`);
const objWidth = parseFloat(objEl.style.width) || 0; const
objHeight = parseFloat(objEl.style.height) || 0;
const objWidth = Number.parseFloat(objEl.style.width) || 0; const
objHeight = Number.parseFloat(objEl.style.height) || 0;
const startX = e.clientX; const
startY = e.clientY;
const startWidth = parseFloat(overlay.style.width) || 0; const
startHeight = parseFloat(overlay.style.height) || 0;
const left = parseFloat(overlay.style.left) || 0; const
top = parseFloat(overlay.style.top) || 0;
const startWidth = Number.parseFloat(overlay.style.width) || 0; const
startHeight = Number.parseFloat(overlay.style.height) || 0;
const left = Number.parseFloat(overlay.style.left) || 0; const
top = Number.parseFloat(overlay.style.top) || 0;
function onMove(ev) {
overlay.style.width = `${Math.max(4, startWidth + (ev.clientX - startX))}px`;
@@ -630,8 +630,8 @@ function onCollisionBoxResizeMouseDown(e, id) {
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
const newWidth = parseFloat(overlay.style.width); const
newHeight = parseFloat(overlay.style.height);
const newWidth = Number.parseFloat(overlay.style.width); const
newHeight = Number.parseFloat(overlay.style.height);
const offsetX = left - (objWidth - newWidth) / 2;
const offsetY = top - (objHeight - newHeight) / 2;
updateCollisionFields(newWidth, newHeight, offsetX, offsetY);
@@ -650,12 +650,12 @@ function onCollisionBoxResizeMouseDown(e, id) {
function saveSceneObjectCommands() {
const body = document.getElementById('personnageCommandsBody');
if (!body) return;
const objectId = parseInt(body.dataset.elementId, 10);
const objectId = Number.parseInt(body.dataset.elementId, 10);
const extra = [];
document.querySelectorAll('#personnageCommandsExtra .personnageCommandExtraRow').forEach((row) => {
const toucheEl = row.querySelector('.personnageKeyCapture');
const animEl = row.querySelector('.cmdExtraAnim');
if (toucheEl && toucheEl.value && animEl && animEl.value) {
if (toucheEl?.value && animEl?.value) {
extra.push({ touche: toucheEl.value, animation: animEl.value });
}
});
@@ -713,7 +713,7 @@ function personnageCommandExtraRowEl(objectId, touche, animation) {
row.className = 'field is-grouped personnageCommandExtraRow';
const animNames = Object.keys(ELEMENT_ANIMATIONS_MAP[objectId] || {});
const options = animNames.map((name) => `<option value="${name}"${name === animation ? ' selected' : ''}>${name}</option>`).join('');
row.innerHTML = `<div class="control"><input class="input is-small personnageKeyCapture" readonly placeholder="Touche..." value="${(touche || '').replace(/"/g, '&quot;')}"></div>`
row.innerHTML = `<div class="control"><input class="input is-small personnageKeyCapture" readonly placeholder="Touche..." value="${(touche || '').replaceAll('"', '&quot;')}"></div>`
+ `<div class="control"><div class="select is-small"><select class="cmdExtraAnim">${options}</select></div></div>`
+ '<div class="control"><button type="button" class="button is-small" onclick="this.closest(\'.personnageCommandExtraRow\').remove(); saveSceneObjectCommands();">×</button></div>';
const keyInput = row.querySelector('.personnageKeyCapture');
@@ -727,7 +727,7 @@ function addPersonnageCommandExtraRow() {
const body = document.getElementById('personnageCommandsBody');
const container = document.getElementById('personnageCommandsExtra');
if (!body || !container) return;
const objectId = parseInt(body.dataset.elementId, 10);
const objectId = Number.parseInt(body.dataset.elementId, 10);
container.appendChild(personnageCommandExtraRowEl(objectId, '', ''));
}
@@ -767,7 +767,7 @@ function refreshSceneObjectNames() {
function forgeHexToRgb(hex) {
hex = String(hex || '').replace('#', '');
if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');
const num = parseInt(hex, 16);
const num = Number.parseInt(hex, 16);
if (Number.isNaN(num) || hex.length !== 6) return [0, 0, 0];
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
}
@@ -820,7 +820,7 @@ function saveDialogueBoxStyle(objectId) {
};
checkDialogueBoxContrast(style);
const el = document.getElementById(`el-${objectId}`);
const widget = el && el.querySelector('.dialogueBoxWidget');
const widget = el?.querySelector('.dialogueBoxWidget');
if (widget) {
widget.style.fontFamily = style.font_family;
widget.style.fontSize = `${style.font_size}px`;
@@ -847,10 +847,10 @@ function saveDialogueBoxStyle(objectId) {
function _selectQuizBoxTemplate(objectId, key, gridId) {
const grid = document.getElementById(gridId);
if (grid) {
grid.setAttribute('data-selected-template', key);
grid.dataset.selectedTemplate = key;
const tiles = grid.querySelectorAll('.quizTemplateTile');
for (let i = 0; i < tiles.length; i++) {
tiles[i].classList.toggle('is-selected', tiles[i].getAttribute('data-template') === key);
for (const tile of tiles) {
tile.classList.toggle('is-selected', tile.dataset.template === key);
}
}
saveQuizBoxConfig(objectId);
@@ -887,10 +887,10 @@ function saveQuizBoxConfig(objectId) {
const grid = document.getElementById(gridId);
if (!grid) return meta ? meta.getAttribute(metaAttr) : null;
const selectedTile = grid.querySelector('.quizTemplateTile.is-selected');
return grid.getAttribute('data-selected-template') || (selectedTile && selectedTile.getAttribute('data-template'));
return grid.dataset.selectedTemplate || selectedTile?.dataset.template;
}
const fullscreenEl = document.getElementById('quiz-fullscreen');
const fullscreenChanged = fullscreenEl.checked !== (fullscreenEl.getAttribute('data-was-checked') === '1');
const fullscreenChanged = fullscreenEl.checked !== (fullscreenEl.dataset.wasChecked === '1');
const config = {
fullscreen: fullscreenEl.checked,
timer_mode: document.getElementById('quiz-timer-mode').value,
@@ -907,7 +907,7 @@ function saveQuizBoxConfig(objectId) {
.then((data) => {
if (fullscreenChanged) { window.location.reload(); return; }
const el = document.getElementById(`el-${objectId}`);
const widget = el && el.querySelector('.quizBoxWidget');
const widget = el?.querySelector('.quizBoxWidget');
if (widget && data.rendered_html) widget.outerHTML = data.rendered_html;
});
}
@@ -915,7 +915,7 @@ function saveQuizBoxConfig(objectId) {
function initPersonnageCommandsPanel() {
const body = document.getElementById('personnageCommandsBody');
if (!body) return;
const objectId = parseInt(body.dataset.elementId, 10);
const objectId = Number.parseInt(body.dataset.elementId, 10);
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir'].forEach((id) => {
bindPersonnageKeyCapture(document.getElementById(id));
});