Files
williamandClaude Sonnet 5 ccf836f2c5
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
Lot 4 modernisation JS (SonarLint) + restauration CI sonarqube non-bloquante
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>
2026-09-18 16:38:29 +02:00

139 lines
6.3 KiB
JavaScript

// Export Web/SCORM — port de screens/rendering/render_jauge.py (jauge liée
// à une donnée : remplissage % + dégradé de couleur recalculés depuis un
// champ numérique d'un objet). Lit gameData.data/fields_meta directement
// au lieu d'une requête SQL (voir _read_field_value côté serveur).
function forgeHexToRgb(hexColor) {
let hex = (hexColor || '#5b8cff').replace(/^#/, '');
if (hex.length !== 6) hex = '5b8cff';
return [
Number.parseInt(hex.substr(0, 2), 16),
Number.parseInt(hex.substr(2, 2), 16),
Number.parseInt(hex.substr(4, 2), 16),
];
}
// Extrait de forgeInterpolateColor (lot 4 "modernisation JS", S7721) :
// fonction pure (aucune fermeture sur lo/hi/mixed/ratio), pas besoin
// d'être redéfinie à chaque appel.
function forgeByteToHex(n) {
return n.toString(16).padStart(2, '0');
}
function forgeInterpolateColor(lowHex, highHex, ratio) {
const lo = forgeHexToRgb(lowHex); const
hi = forgeHexToRgb(highHex);
const mixed = lo.map((c, i) => Math.round(c + (hi[i] - c) * ratio));
return `#${mixed.map(forgeByteToHex).join('')}`;
}
// Port de _read_field_value — repli sur la ligne la PLUS RÉCENTE si
// row_id est absent/introuvable (comportement historique, un objet à une
// seule ligne n'est jamais impacté).
function forgeReadFieldValue(gameData, definitionId, rowId, fieldName) {
if (!definitionId || !fieldName) return null;
definitionId = String(definitionId);
const rows = gameData.data?.[definitionId];
if (!rows?.length) return null;
let row = null;
if (rowId) row = rows.find((r) => String(r.id) === String(rowId));
if (!row) row = rows[0];
return row[fieldName];
}
// Label superposé qui affiche la valeur BRUTE du champ (indépendant du
// pourcentage/couleur) — extrait de forgeRenderJauge (lot 4 "modernisation
// JS", S3776).
function forgeJaugeValueLabelHtml(afficherValeur, rawValue) {
if (!afficherValeur) return '';
return '<div style="position:absolute; inset:0; display:flex; align-items:center; '
+ 'justify-content:center; font-family:system-ui,sans-serif; font-size:12px; '
+ `color:#e8eaf0; text-shadow:0 1px 2px rgba(0,0,0,.6);">${
forgeHtmlEscape(rawValue !== null && rawValue !== undefined ? String(rawValue) : '—')}</div>`;
}
// Label du NOM (ex. "Alice"), lu depuis un second champ optionnel — extrait
// de forgeRenderJauge.
function forgeJaugeNameLabelHtml(gameData, definitionId, rowId, fieldNameNom, nomPosition, nomAlignement, nomStyle) {
if (!fieldNameNom) return '';
const nomValue = forgeReadFieldValue(gameData, definitionId, rowId, fieldNameNom);
if (nomValue === null || nomValue === undefined || nomValue === '') return '';
const textAlign = (nomPosition === 'dessus' && nomAlignement === 'gauche') ? 'left' : 'center';
const {
nomPolice, nomTaille, nomGras, nomItalique,
} = nomStyle;
return `<div style="flex:0 0 auto; font-family:${forgeHtmlEscape(nomPolice)}; `
+ `font-size:${forgeHtmlEscape(String(nomTaille))}px; `
+ `font-weight:${nomGras ? '700' : '400'}; `
+ `font-style:${nomItalique ? 'italic' : 'normal'}; `
+ `color:#e8eaf0; text-align:${textAlign};">${forgeHtmlEscape(String(nomValue))}</div>`;
}
function forgeRenderJauge(el, meta, gameData, childrenMap, ctx, parentFlexDirection) {
const attrs = el.attributes || {};
const nomPosition = attrs._nom_position || 'dessus';
const nomAlignement = attrs._nom_alignement || 'centre';
const nomPolice = attrs._nom_police || 'inherit';
const nomTaille = attrs._nom_taille || '12';
const nomGras = attrs._nom_gras === '1';
const nomItalique = attrs._nom_italique === '1';
const flexDir = nomPosition === 'cote' ? 'row' : 'column';
const alignItems = nomPosition === 'cote' ? 'center' : 'stretch';
const style = `${forgeStyleString(el, parentFlexDirection, el.widget)} display:flex; flex-direction:${flexDir}; align-items:${alignItems}; justify-content:center; gap:4px;`;
const definitionId = attrs._definition_id;
const rowId = attrs._row_id;
const fieldName = (attrs._champ || '').trim();
const fieldNameNom = (attrs._champ_nom || '').trim();
let minV = Number.parseFloat(attrs._min); if (Number.isNaN(minV)) minV = 0;
let maxV = Number.parseFloat(attrs._max); if (Number.isNaN(maxV)) maxV = 100;
const couleurBasse = attrs._couleur_basse || '#e05252';
const couleurHaute = attrs._couleur_haute || '#4caf50';
const afficherValeur = attrs._afficher_valeur === '1';
const rawValue = fieldName ? forgeReadFieldValue(gameData, definitionId, rowId, fieldName) : null;
let value = Number.parseFloat(rawValue);
if (Number.isNaN(value)) value = minV;
const span = (maxV - minV) || 1;
const ratio = Math.max(0, Math.min(1, (value - minV) / span));
const percent = Math.round(ratio * 1000) / 10;
const fillColor = forgeInterpolateColor(couleurBasse, couleurHaute, ratio);
const labelHtml = forgeJaugeValueLabelHtml(afficherValeur, rawValue);
const nomStyle = {
nomPolice, nomTaille, nomGras, nomItalique,
};
const nameHtml = forgeJaugeNameLabelHtml(
gameData,
definitionId,
rowId,
fieldNameNom,
nomPosition,
nomAlignement,
nomStyle,
);
const extraClasses = Object.keys(attrs)
.filter((k) => k.startsWith('_class_') && attrs[k])
.map((k) => attrs[k]).join(' ');
const barClass = forgeHtmlEscape((`progress jaugeBar ${extraClasses}`).trim());
const bar = `<progress class="${barClass}" value="${percent}" max="100" `
+ `style="--bulma-progress-value-background-color:${fillColor}; --bulma-progress-bar-background-color:#1c2130; `
+ `margin:0; border-radius:6px; transition:background-color .3s ease;">${percent}%</progress>`;
const barWrap = '<div style="position:relative; display:flex; align-items:center; '
+ `flex:1 1 auto; min-height:14px;">${bar}${labelHtml}</div>`;
return `<div id="${forgeAutoId(el)}" data-element-id="${el.id}" style="${forgeHtmlEscape(style)}">${
nameHtml}${barWrap}</div>`;
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
forgeRenderJauge, forgeHexToRgb, forgeInterpolateColor, forgeReadFieldValue,
};
const renderMod = require('./render-element.js');
global.forgeStyleString = global.forgeStyleString || renderMod.forgeStyleString;
global.forgeHtmlEscape = global.forgeHtmlEscape || renderMod.forgeHtmlEscape;
global.forgeAutoId = global.forgeAutoId || renderMod.forgeAutoId;
}