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:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
+67
-62
@@ -7,7 +7,7 @@
|
||||
|
||||
// Propriété CSS réelle + éventuel suffixe (unité) pour chaque propriété
|
||||
// pilotable par une action "Modifier un élément".
|
||||
var ELEMENT_PROPERTY_CSS = {
|
||||
const ELEMENT_PROPERTY_CSS = {
|
||||
bg_color: { css: 'backgroundColor', suffix: '' },
|
||||
border_color: { css: 'borderColor', suffix: '' },
|
||||
width: { css: 'width', suffix: '%' },
|
||||
@@ -97,7 +97,7 @@ function applyElementProperty(targetEl, property, value) {
|
||||
// toujours au même endroit.
|
||||
const relSpec = ELEMENT_PROPERTY_CSS[property === 'pos_x_relatif' ? 'pos_x' : 'pos_y'];
|
||||
const delta = parseFloat(value);
|
||||
if (isNaN(delta)) return;
|
||||
if (Number.isNaN(delta)) return;
|
||||
const current = parseFloat(targetEl.style[relSpec.css]) || 0;
|
||||
targetEl.style[relSpec.css] = (current + delta) + relSpec.suffix;
|
||||
return;
|
||||
@@ -113,7 +113,7 @@ function applyElementProperty(targetEl, property, value) {
|
||||
// 1re valeur si l'écran est rechargé.
|
||||
if (typeof value === 'string' && value.indexOf('||') !== -1) {
|
||||
const parts = value.split('||');
|
||||
const key = 'toggle' + property;
|
||||
const key = `toggle${property}`;
|
||||
const atSecond = targetEl.dataset[key] === '1';
|
||||
const next = atSecond ? parts[0] : parts[1];
|
||||
targetEl.style[spec.css] = next + spec.suffix;
|
||||
@@ -137,7 +137,7 @@ function applyElementProperty(targetEl, property, value) {
|
||||
// pour un objet de scène), la valeur n'est pas bornée plutôt que de
|
||||
// risquer un blocage à 0.
|
||||
function clampSceneObjectPosition(targetEl, css, rawValue) {
|
||||
const screenData = screensData.find(function(s){ return s.id === window.currentScreenId; });
|
||||
const screenData = screensData.find((s) => s.id === window.currentScreenId);
|
||||
if (!screenData || screenData.scene_width == null) return rawValue;
|
||||
const size = parseFloat(targetEl.style[css === 'left' ? 'width' : 'height']) || 0;
|
||||
const boundWidth = screenData.world_width != null ? screenData.world_width : screenData.scene_width;
|
||||
@@ -156,15 +156,15 @@ function applyObjectProperty(targetEl, property, value) {
|
||||
if (property === 'pos_x_relatif' || property === 'pos_y_relatif') {
|
||||
const css = property === 'pos_x_relatif' ? 'left' : 'top';
|
||||
const delta = parseFloat(value);
|
||||
if (isNaN(delta)) return;
|
||||
if (Number.isNaN(delta)) return;
|
||||
const current = parseFloat(targetEl.style[css]) || 0;
|
||||
targetEl.style[css] = clampSceneObjectPosition(targetEl, css, current + delta) + 'px';
|
||||
targetEl.style[css] = `${clampSceneObjectPosition(targetEl, css, current + delta)}px`;
|
||||
return;
|
||||
}
|
||||
if (property === 'pos_x' || property === 'pos_y') {
|
||||
if (value === undefined || value === null) return;
|
||||
const css = property === 'pos_x' ? 'left' : 'top';
|
||||
targetEl.style[css] = clampSceneObjectPosition(targetEl, css, parseFloat(value) || 0) + 'px';
|
||||
targetEl.style[css] = `${clampSceneObjectPosition(targetEl, css, parseFloat(value) || 0)}px`;
|
||||
return;
|
||||
}
|
||||
applyElementProperty(targetEl, property, value);
|
||||
@@ -175,14 +175,14 @@ function runActionNode(node) {
|
||||
const idx = screenIndexById(window.currentScreenId);
|
||||
if (idx > -1 && idx < screensData.length - 1) showScreen(screensData[idx + 1].id);
|
||||
return Promise.resolve();
|
||||
} else if (node.action_type === 'ecran_precedent') {
|
||||
} if (node.action_type === 'ecran_precedent') {
|
||||
const idx = screenIndexById(window.currentScreenId);
|
||||
if (idx > 0) showScreen(screensData[idx - 1].id);
|
||||
return Promise.resolve();
|
||||
} else if (node.action_type === 'aller_a' && node.target_screen_id) {
|
||||
} if (node.action_type === 'aller_a' && node.target_screen_id) {
|
||||
showScreen(node.target_screen_id);
|
||||
return Promise.resolve();
|
||||
} else if (node.action_type === 'ouvrir_ligne' && node.target_screen_id) {
|
||||
} if (node.action_type === 'ouvrir_ligne' && node.target_screen_id) {
|
||||
// Mémorise QUELLE ligne a été cliquée juste avant d'exécuter cette
|
||||
// action (voir bindClicks dans triggers.js) puis affiche l'écran de
|
||||
// détail avec ses placeholders "champ" résolus contre cette ligne
|
||||
@@ -205,27 +205,27 @@ function runActionNode(node) {
|
||||
bindHoverTexts();
|
||||
bindHoverTriggers();
|
||||
return Promise.resolve();
|
||||
} else if (node.action_type === 'modifier_element' && node.target_element_id) {
|
||||
const targetEl = document.querySelector('[data-element-id="' + node.target_element_id + '"]');
|
||||
} if (node.action_type === 'modifier_element' && node.target_element_id) {
|
||||
const targetEl = document.querySelector(`[data-element-id="${node.target_element_id}"]`);
|
||||
if (targetEl) applyElementProperty(targetEl, node.element_property, node.element_value);
|
||||
return Promise.resolve();
|
||||
} else if (node.action_type === 'modifier_objet_scene' && (node.target_element_id || node.target_object_id)) {
|
||||
} if (node.action_type === 'modifier_objet_scene' && (node.target_element_id || node.target_object_id)) {
|
||||
// Fondations multi-éditeurs — éditeur de scène 2D : équivalent de
|
||||
// "modifier_element" ci-dessus, mais en pixels (voir
|
||||
// applyObjectProperty() plus bas) plutôt qu'en %. data-element-id
|
||||
// est posé sur CHAQUE objet de scène en plus de data-object-id (voir
|
||||
// render_scene_object.py), donc le même sélecteur suffit.
|
||||
const targetEl = document.querySelector('[data-element-id="' + (node.target_element_id || node.target_object_id) + '"]');
|
||||
const targetEl = document.querySelector(`[data-element-id="${node.target_element_id || node.target_object_id}"]`);
|
||||
if (targetEl) applyObjectProperty(targetEl, node.element_property, node.element_value);
|
||||
return Promise.resolve();
|
||||
} else if (node.action_type === 'declencher_evenement' && node.target_custom_event_id) {
|
||||
} if (node.action_type === 'declencher_evenement' && node.target_custom_event_id) {
|
||||
// Notification pure (voir screens/custom_events/) : ne fait que
|
||||
// signaler l'événement, jamais choisir un élément — c'est à
|
||||
// l'écouteur (déclencheur → condition → action) de décider quoi
|
||||
// faire, avec ses propres réglages habituels.
|
||||
window.dispatchGameEvent(node.target_custom_event_id);
|
||||
return Promise.resolve();
|
||||
} else if (node.action_type === 'activer_onglet' && node.target_element_id) {
|
||||
} if (node.action_type === 'activer_onglet' && node.target_element_id) {
|
||||
// 2.3 — panneau à onglets / visibilité mutuellement exclusive : montre
|
||||
// l'élément visé et masque tous ses FRÈRES DIRECTS (même parent_id,
|
||||
// y compris "aucun parent" = les autres éléments de premier niveau de
|
||||
@@ -234,31 +234,30 @@ function runActionNode(node) {
|
||||
// (imbriqués compris, voir list_elements côté serveur), donc ça
|
||||
// fonctionne aussi bien pour des onglets imbriqués dans un même
|
||||
// conteneur que pour des "vues" posées directement sur l'écran.
|
||||
const screenData = screensData.find(function(s){ return s.id === window.currentScreenId; });
|
||||
const screenData = screensData.find((s) => s.id === window.currentScreenId);
|
||||
const allElements = screenData ? screenData.elements : [];
|
||||
const target = allElements.find(function(e){ return e.id === node.target_element_id; });
|
||||
const target = allElements.find((e) => e.id === node.target_element_id);
|
||||
if (target) {
|
||||
const parentKey = target.parent_id || null;
|
||||
allElements
|
||||
.filter(function(e){ return (e.parent_id || null) === parentKey; })
|
||||
.forEach(function(e) {
|
||||
const el = document.querySelector('[data-element-id="' + e.id + '"]');
|
||||
.filter((e) => (e.parent_id || null) === parentKey)
|
||||
.forEach((e) => {
|
||||
const el = document.querySelector(`[data-element-id="${e.id}"]`);
|
||||
if (!el) return;
|
||||
if (e.id === target.id) { el.style.display = ''; el.style.visibility = ''; }
|
||||
else { el.style.display = 'none'; }
|
||||
if (e.id === target.id) { el.style.display = ''; el.style.visibility = ''; } else { el.style.display = 'none'; }
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
} else if (node.action_type === 'attendre') {
|
||||
} if (node.action_type === 'attendre') {
|
||||
// 3.2 (Confort) — séquences temporisées : suspend la suite du graphe
|
||||
// pendant N secondes (node.data_value) avant de continuer sur le(s)
|
||||
// fil(s) de sortie — permet par ex. un mail qui "arrive" tout seul
|
||||
// quelques secondes après l'affichage de l'écran, sans action du
|
||||
// joueur (à combiner avec le déclencheur "À l'affichage de l'écran").
|
||||
const seconds = parseFloat(node.data_value);
|
||||
const ms = isNaN(seconds) ? 0 : Math.max(0, seconds) * 1000;
|
||||
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
||||
} else if (node.action_type === 'modifier_donnee') {
|
||||
const ms = Number.isNaN(seconds) ? 0 : Math.max(0, seconds) * 1000;
|
||||
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
||||
} if (node.action_type === 'modifier_donnee') {
|
||||
// Export Web/SCORM (voir static/js/play/offline/apply-actions.js) :
|
||||
// aucun serveur disponible dans le paquet exporté — la même mutation
|
||||
// s'applique directement sur gameData, en mémoire, dans le navigateur.
|
||||
@@ -272,21 +271,22 @@ function runActionNode(node) {
|
||||
// LAST_INSERTED_ROW_ID (voir flow_node_run_data.py) — sans effet
|
||||
// sinon, donc toujours transmis sans condition ici.
|
||||
return fetch(runDataUrl, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ clicked_row_id: window.lastClickedRowId, last_inserted_row_id: window.lastInsertedRowId })
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ clicked_row_id: window.lastClickedRowId, last_inserted_row_id: window.lastInsertedRowId }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then((r) => r.json())
|
||||
.then(() => refreshRuntimeData());
|
||||
} else if (node.action_type === 'modifier_variable') {
|
||||
} if (node.action_type === 'modifier_variable') {
|
||||
if (window.FORGE_OFFLINE) {
|
||||
forgeApplyVariableActionOffline(gameData, node);
|
||||
return refreshRuntimeData();
|
||||
}
|
||||
const runVarUrl = FORGE_PLAY_URLS.flowNodeRunVariable.replace('999999', node.id);
|
||||
return fetch(runVarUrl, {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
return fetch(runVarUrl, { method: 'POST' })
|
||||
.then((r) => r.json())
|
||||
.then(() => refreshRuntimeData());
|
||||
} else if (node.action_type === 'modifier_score') {
|
||||
} if (node.action_type === 'modifier_score') {
|
||||
// Score/progression (voir db/scoring/, screens/data_actions/
|
||||
// apply_score_action.py) : même patron que "Modifier une variable
|
||||
// globale" ci-dessus, calculé et écrit côté serveur EN LIGNE, ou
|
||||
@@ -296,19 +296,19 @@ function runActionNode(node) {
|
||||
return refreshRuntimeData();
|
||||
}
|
||||
const runScoreUrl = FORGE_PLAY_URLS.flowNodeRunScore.replace('999999', node.id);
|
||||
return fetch(runScoreUrl, {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
return fetch(runScoreUrl, { method: 'POST' })
|
||||
.then((r) => r.json())
|
||||
.then(() => refreshRuntimeData());
|
||||
} else if (node.action_type === 'definir_statut_partie') {
|
||||
} if (node.action_type === 'definir_statut_partie') {
|
||||
if (window.FORGE_OFFLINE) {
|
||||
forgeApplyStatusActionOffline(gameData, node);
|
||||
return refreshRuntimeData();
|
||||
}
|
||||
const runStatusUrl = FORGE_PLAY_URLS.flowNodeRunStatus.replace('999999', node.id);
|
||||
return fetch(runStatusUrl, {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
return fetch(runStatusUrl, { method: 'POST' })
|
||||
.then((r) => r.json())
|
||||
.then(() => refreshRuntimeData());
|
||||
} else if (node.action_type === 'ajouter_ligne') {
|
||||
} if (node.action_type === 'ajouter_ligne') {
|
||||
// Phase 4 — crée une ligne VIDE (côté serveur en ligne, en mémoire
|
||||
// hors ligne) ; son id devient window.lastInsertedRowId pour que les
|
||||
// nœuds "Modifier une donnée" enchaînés (ciblant LAST_INSERTED_ROW_ID,
|
||||
@@ -320,21 +320,21 @@ function runActionNode(node) {
|
||||
}
|
||||
const runAddRowUrl = FORGE_PLAY_URLS.flowNodeRunAddRow.replace('999999', node.id);
|
||||
return fetch(runAddRowUrl, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(function(json) {
|
||||
.then((r) => r.json())
|
||||
.then((json) => {
|
||||
window.lastInsertedRowId = json.row_id;
|
||||
return refreshRuntimeData();
|
||||
});
|
||||
} else if (node.action_type === 'jouer_son') {
|
||||
} if (node.action_type === 'jouer_son') {
|
||||
// Phase 6 — effet sonore ponctuel : non bouclé, indépendant de la
|
||||
// musique de fond de l'écran (runScreenBackgroundMusic() dans
|
||||
// screens.js, qui elle boucle et s'arrête au changement d'écran).
|
||||
// Fire-and-forget : ne bloque jamais la suite du graphe.
|
||||
if (node.data_value) new Audio(node.data_value).play().catch(function() {});
|
||||
if (node.data_value) new Audio(node.data_value).play().catch(() => {});
|
||||
return Promise.resolve();
|
||||
} else if (node.action_type === 'jouer_animation_sprite' && (node.target_element_id || node.target_object_id)) {
|
||||
} if (node.action_type === 'jouer_animation_sprite' && (node.target_element_id || node.target_object_id)) {
|
||||
const spriteTargetId = node.target_element_id || node.target_object_id;
|
||||
const targetEl = document.querySelector('[data-element-id="' + spriteTargetId + '"]');
|
||||
const targetEl = document.querySelector(`[data-element-id="${spriteTargetId}"]`);
|
||||
if (targetEl) {
|
||||
let spriteData = {};
|
||||
try { spriteData = JSON.parse(node.data_value || '{}'); } catch (e) { /* data_value invalide : rien à jouer */ }
|
||||
@@ -359,7 +359,7 @@ function runActionNode(node) {
|
||||
// ci-dessus (événementiel) ET un clip de la Timeline d'animation de kind
|
||||
// "sprite" (déclaratif, voir applyAnimationClip() dans screens.js) — les
|
||||
// deux partagent ce même moteur, aucune logique dupliquée.
|
||||
var activeSpriteAnimations = new Map();
|
||||
const activeSpriteAnimations = new Map();
|
||||
|
||||
// Phase 8 — résout le NOM d'une animation (ex. "walk") en liste de frames à
|
||||
// partir du personnage ACTUELLEMENT assigné à cet élément
|
||||
@@ -373,7 +373,7 @@ var activeSpriteAnimations = new Map();
|
||||
// `spriteData.frames` (ancien format, Phase 7) reste géré en repli pour
|
||||
// les nœuds/clips déjà créés avant ce changement.
|
||||
function resolveSpriteFrames(elementId, spriteData) {
|
||||
var byAnimation = (gameData.personnage_animations || {})[elementId];
|
||||
const byAnimation = (gameData.personnage_animations || {})[elementId];
|
||||
if (byAnimation && spriteData.animation && byAnimation[spriteData.animation]) {
|
||||
return { frames: byAnimation[spriteData.animation], fps: spriteData.fps, loop: spriteData.loop };
|
||||
}
|
||||
@@ -404,11 +404,11 @@ function resolveSpriteFrames(elementId, spriteData) {
|
||||
// aussitôt posée si on ne la protégeait pas explicitement — un setTimeout
|
||||
// dédié la maintient "en cours" un court instant avant de la libérer.
|
||||
function runSpriteAnimation(targetEl, spriteData) {
|
||||
var frames = spriteData.frames || [];
|
||||
var fps = spriteData.fps > 0 ? spriteData.fps : 8;
|
||||
var loop = !!spriteData.loop;
|
||||
var signature = frames.join('|') + '@' + fps + (loop ? '+loop' : '');
|
||||
var active = activeSpriteAnimations.get(targetEl);
|
||||
const frames = spriteData.frames || [];
|
||||
const fps = spriteData.fps > 0 ? spriteData.fps : 8;
|
||||
const loop = !!spriteData.loop;
|
||||
const signature = `${frames.join('|')}@${fps}${loop ? '+loop' : ''}`;
|
||||
const active = activeSpriteAnimations.get(targetEl);
|
||||
if (active && active.signature === signature) return;
|
||||
if (active && !active.loop) return;
|
||||
if (active) {
|
||||
@@ -416,16 +416,16 @@ function runSpriteAnimation(targetEl, spriteData) {
|
||||
activeSpriteAnimations.delete(targetEl);
|
||||
}
|
||||
if (!frames.length) return;
|
||||
var frameIndex = 0;
|
||||
let frameIndex = 0;
|
||||
targetEl.src = frames[0];
|
||||
if (frames.length === 1) {
|
||||
if (loop) return;
|
||||
var holdMs = Math.max(1000 / fps, 150);
|
||||
var timeoutId = setTimeout(function() { activeSpriteAnimations.delete(targetEl); }, holdMs);
|
||||
activeSpriteAnimations.set(targetEl, { intervalId: timeoutId, signature: signature, loop: loop });
|
||||
const holdMs = Math.max(1000 / fps, 150);
|
||||
const timeoutId = setTimeout(() => { activeSpriteAnimations.delete(targetEl); }, holdMs);
|
||||
activeSpriteAnimations.set(targetEl, { intervalId: timeoutId, signature, loop });
|
||||
return;
|
||||
}
|
||||
var intervalId = setInterval(function() {
|
||||
const intervalId = setInterval(() => {
|
||||
frameIndex++;
|
||||
if (frameIndex >= frames.length) {
|
||||
if (!loop) { clearInterval(intervalId); activeSpriteAnimations.delete(targetEl); return; }
|
||||
@@ -433,7 +433,7 @@ function runSpriteAnimation(targetEl, spriteData) {
|
||||
}
|
||||
targetEl.src = frames[frameIndex];
|
||||
}, 1000 / fps);
|
||||
activeSpriteAnimations.set(targetEl, { intervalId: intervalId, signature: signature, loop: loop });
|
||||
activeSpriteAnimations.set(targetEl, { intervalId, signature, loop });
|
||||
}
|
||||
|
||||
// Appelée depuis showScreen() (screens.js) à chaque changement d'écran —
|
||||
@@ -441,14 +441,19 @@ function runSpriteAnimation(targetEl, spriteData) {
|
||||
// directement depuis screens.js, même patron que runScreenTimerTriggers()
|
||||
// dans triggers.js (l'appelant ne connaît jamais la structure interne).
|
||||
function stopAllSpriteAnimations() {
|
||||
activeSpriteAnimations.forEach(function(active) { clearInterval(active.intervalId); });
|
||||
activeSpriteAnimations.forEach((active) => { clearInterval(active.intervalId); });
|
||||
activeSpriteAnimations.clear();
|
||||
}
|
||||
|
||||
// static/js/play/__tests__/ (node:test) — même patron que conditions.js.
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
resolveSpriteFrames, runSpriteAnimation, stopAllSpriteAnimations, activeSpriteAnimations,
|
||||
applyObjectProperty, clampSceneObjectPosition, runActionNode,
|
||||
resolveSpriteFrames,
|
||||
runSpriteAnimation,
|
||||
stopAllSpriteAnimations,
|
||||
activeSpriteAnimations,
|
||||
applyObjectProperty,
|
||||
clampSceneObjectPosition,
|
||||
runActionNode,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user