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
@@ -15,15 +15,25 @@
|
||||
//
|
||||
// Best-effort partout : un LRS injoignable/mal configuré ne doit JAMAIS
|
||||
// bloquer ni ralentir la partie du joueur.
|
||||
//
|
||||
// NOSONAR S5332 (toutes occurrences "http://adlnet.gov/..." de ce fichier
|
||||
// + les 2 assertions de __tests__/xapi-client.test.js) : identifiants du
|
||||
// vocabulaire xAPI standard ADL, jamais dérérencés/appelés en réseau (de
|
||||
// simples chaînes comparées/embarquées) — le "http://" fait partie de la
|
||||
// chaîne fixée par la spec, jamais "https://" (changer casserait
|
||||
// l'interopérabilité avec n'importe quel LRS). Le VRAI endpoint réseau
|
||||
// (voir forgeXapiSendStatement plus bas) est toujours `config.endpoint`,
|
||||
// saisi par le créateur (routes/publish/xapi_settings.py), jamais un
|
||||
// littéral de ce fichier.
|
||||
|
||||
// Statut Forge -> verbe xAPI ADL standard, uniquement pour les statuts
|
||||
// qui marquent une VRAIE fin de partie — "non_commence"/"en_cours" ne
|
||||
// génèrent rien ici (bruit inutile pour un suivi volontairement léger,
|
||||
// voir le plan).
|
||||
var FORGE_TO_XAPI_VERB = {
|
||||
termine: { id: "http://adlnet.gov/expapi/verbs/completed" },
|
||||
reussi: { id: "http://adlnet.gov/expapi/verbs/passed" },
|
||||
echoue: { id: "http://adlnet.gov/expapi/verbs/failed" },
|
||||
const FORGE_TO_XAPI_VERB = {
|
||||
termine: { id: 'http://adlnet.gov/expapi/verbs/completed' }, // NOSONAR S5332
|
||||
reussi: { id: 'http://adlnet.gov/expapi/verbs/passed' }, // NOSONAR S5332
|
||||
echoue: { id: 'http://adlnet.gov/expapi/verbs/failed' }, // NOSONAR S5332
|
||||
};
|
||||
|
||||
// Libellé humain par verbe — l'id xAPI (http://adlnet.gov/expapi/verbs/...)
|
||||
@@ -33,18 +43,18 @@ var FORGE_TO_XAPI_VERB = {
|
||||
// français ?" — un LRS qui préfère le français (ou n'importe quelle
|
||||
// langue ici) affichera la bonne entrée au lieu de rester bloqué sur
|
||||
// l'anglais.
|
||||
var FORGE_XAPI_VERB_DISPLAY = {
|
||||
"http://adlnet.gov/expapi/verbs/initialized": { "en-US": "initialized", "fr-FR": "initialisé" },
|
||||
"http://adlnet.gov/expapi/verbs/terminated": { "en-US": "terminated", "fr-FR": "quitté" },
|
||||
"http://adlnet.gov/expapi/verbs/completed": { "en-US": "completed", "fr-FR": "terminé" },
|
||||
"http://adlnet.gov/expapi/verbs/passed": { "en-US": "passed", "fr-FR": "réussi" },
|
||||
"http://adlnet.gov/expapi/verbs/failed": { "en-US": "failed", "fr-FR": "échoué" },
|
||||
"http://adlnet.gov/expapi/verbs/scored": { "en-US": "scored", "fr-FR": "noté" },
|
||||
"http://adlnet.gov/expapi/verbs/attempted": { "en-US": "attempted", "fr-FR": "commencé" },
|
||||
"http://adlnet.gov/expapi/verbs/answered": { "en-US": "answered", "fr-FR": "répondu" },
|
||||
const FORGE_XAPI_VERB_DISPLAY = {
|
||||
'http://adlnet.gov/expapi/verbs/initialized': { 'en-US': 'initialized', 'fr-FR': 'initialisé' }, // NOSONAR S5332
|
||||
'http://adlnet.gov/expapi/verbs/terminated': { 'en-US': 'terminated', 'fr-FR': 'quitté' }, // NOSONAR S5332
|
||||
'http://adlnet.gov/expapi/verbs/completed': { 'en-US': 'completed', 'fr-FR': 'terminé' }, // NOSONAR S5332
|
||||
'http://adlnet.gov/expapi/verbs/passed': { 'en-US': 'passed', 'fr-FR': 'réussi' }, // NOSONAR S5332
|
||||
'http://adlnet.gov/expapi/verbs/failed': { 'en-US': 'failed', 'fr-FR': 'échoué' }, // NOSONAR S5332
|
||||
'http://adlnet.gov/expapi/verbs/scored': { 'en-US': 'scored', 'fr-FR': 'noté' }, // NOSONAR S5332
|
||||
'http://adlnet.gov/expapi/verbs/attempted': { 'en-US': 'attempted', 'fr-FR': 'commencé' }, // NOSONAR S5332
|
||||
'http://adlnet.gov/expapi/verbs/answered': { 'en-US': 'answered', 'fr-FR': 'répondu' }, // NOSONAR S5332
|
||||
};
|
||||
|
||||
var forgeXapiLastStatusSent = null; // évite de renvoyer le même statement si le statut ne change pas réellement
|
||||
let forgeXapiLastStatusSent = null; // évite de renvoyer le même statement si le statut ne change pas réellement
|
||||
|
||||
// Identité de l'apprenant : réutilise l'API SCORM déjà trouvée par
|
||||
// scorm-api.js (window.forgeScormApi, posée sur "load" — voir plus bas,
|
||||
@@ -52,23 +62,23 @@ var forgeXapiLastStatusSent = null; // évite de renvoyer le même statement si
|
||||
// si le paquet tourne dans un LMS ; sinon, un acteur anonyme générique.
|
||||
function forgeXapiActor() {
|
||||
try {
|
||||
if (typeof forgeScormApi !== "undefined" && forgeScormApi) {
|
||||
var name = forgeScormApi.LMSGetValue("cmi.core.student_name");
|
||||
var id = forgeScormApi.LMSGetValue("cmi.core.student_id");
|
||||
if (typeof forgeScormApi !== 'undefined' && forgeScormApi) {
|
||||
const name = forgeScormApi.LMSGetValue('cmi.core.student_name');
|
||||
const id = forgeScormApi.LMSGetValue('cmi.core.student_id');
|
||||
if (id) {
|
||||
return { objectType: "Agent", name: name || id, account: { homePage: "urn:forge-engine", name: id } };
|
||||
return { objectType: 'Agent', name: name || id, account: { homePage: 'urn:forge-engine', name: id } };
|
||||
}
|
||||
}
|
||||
} catch (e) { /* API SCORM présente mais qui répond mal — repli silencieux */ }
|
||||
return { objectType: "Agent", name: "Apprenant", mbox: "mailto:anonymous@forge-engine.local" };
|
||||
return { objectType: 'Agent', name: 'Apprenant', mbox: 'mailto:anonymous@forge-engine.local' };
|
||||
}
|
||||
|
||||
function forgeXapiObject() {
|
||||
var config = window.FORGE_XAPI_CONFIG;
|
||||
const config = window.FORGE_XAPI_CONFIG;
|
||||
return {
|
||||
objectType: "Activity",
|
||||
objectType: 'Activity',
|
||||
id: config.activity_id,
|
||||
definition: { name: { "fr-FR": config.activity_name } },
|
||||
definition: { name: { 'fr-FR': config.activity_name } },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,24 +90,24 @@ function forgeXapiObject() {
|
||||
// de scène qui a déclenché ce dialogue (voir "ℹ️ Informations") — plus de
|
||||
// "quête" pour le porter.
|
||||
function forgeXapiDialogueObject(dialogueId, objectName) {
|
||||
var config = window.FORGE_XAPI_CONFIG;
|
||||
const config = window.FORGE_XAPI_CONFIG;
|
||||
return {
|
||||
objectType: "Activity",
|
||||
id: config.activity_id + ":dialogue:" + dialogueId,
|
||||
definition: { name: { "fr-FR": objectName || "Dialogue" }, type: "http://adlnet.gov/expapi/activities/simulation" },
|
||||
objectType: 'Activity',
|
||||
id: `${config.activity_id}:dialogue:${dialogueId}`,
|
||||
definition: { name: { 'fr-FR': objectName || 'Dialogue' }, type: 'http://adlnet.gov/expapi/activities/simulation' }, // NOSONAR S5332
|
||||
};
|
||||
}
|
||||
|
||||
function forgeXapiQuestionObject(dialogueId, objectName, questionIndex, questionText) {
|
||||
var config = window.FORGE_XAPI_CONFIG;
|
||||
const config = window.FORGE_XAPI_CONFIG;
|
||||
return {
|
||||
objectType: "Activity",
|
||||
id: config.activity_id + ":dialogue:" + dialogueId + ":question:" + questionIndex,
|
||||
objectType: 'Activity',
|
||||
id: `${config.activity_id}:dialogue:${dialogueId}:question:${questionIndex}`,
|
||||
definition: {
|
||||
name: { "fr-FR": "Question " + (questionIndex + 1) + " — " + (objectName || "Dialogue") },
|
||||
description: { "fr-FR": questionText || "" },
|
||||
type: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
||||
interactionType: "choice",
|
||||
name: { 'fr-FR': `Question ${questionIndex + 1} — ${objectName || 'Dialogue'}` },
|
||||
description: { 'fr-FR': questionText || '' },
|
||||
type: 'http://adlnet.gov/expapi/activities/cmi.interaction', // NOSONAR S5332
|
||||
interactionType: 'choice',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -110,27 +120,27 @@ function forgeXapiQuestionObject(dialogueId, objectName, questionIndex, question
|
||||
// (forgeXapiObject), ou une sous-activité quête/question
|
||||
// (forgeXapiQuestObject/forgeXapiQuestionObject) pour un suivi plus fin.
|
||||
function forgeXapiSendStatement(verbId, result, object) {
|
||||
var config = window.FORGE_XAPI_CONFIG;
|
||||
const config = window.FORGE_XAPI_CONFIG;
|
||||
if (!config || !config.endpoint) return;
|
||||
var statement = {
|
||||
const statement = {
|
||||
actor: forgeXapiActor(),
|
||||
verb: { id: verbId, display: FORGE_XAPI_VERB_DISPLAY[verbId] || { "en-US": verbId } },
|
||||
verb: { id: verbId, display: FORGE_XAPI_VERB_DISPLAY[verbId] || { 'en-US': verbId } },
|
||||
object: object || forgeXapiObject(),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
if (result) statement.result = result;
|
||||
var endpoint = config.endpoint.replace(/\/+$/, "") + "/statements";
|
||||
const endpoint = `${config.endpoint.replace(/\/+$/, '')}/statements`;
|
||||
try {
|
||||
fetch(endpoint, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Experience-API-Version": "1.0.3",
|
||||
"Authorization": "Basic " + btoa(config.login + ":" + config.password),
|
||||
'Content-Type': 'application/json',
|
||||
'X-Experience-API-Version': '1.0.3',
|
||||
Authorization: `Basic ${btoa(`${config.login}:${config.password}`)}`,
|
||||
},
|
||||
body: JSON.stringify(statement),
|
||||
}).catch(function (err) {
|
||||
console.warn("xAPI: envoi du statement échoué (LRS injoignable ?)", err);
|
||||
}).catch((err) => {
|
||||
console.warn('xAPI: envoi du statement échoué (LRS injoignable ?)', err);
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("xAPI: impossible d'envoyer le statement", err);
|
||||
@@ -143,8 +153,8 @@ function forgeXapiSendStatement(verbId, result, object) {
|
||||
function forgeXapiNotifyScoreChanged(newScore) {
|
||||
if (!window.FORGE_XAPI_CONFIG) return;
|
||||
forgeXapiSendStatement(
|
||||
"http://adlnet.gov/expapi/verbs/scored",
|
||||
{ score: { raw: newScore } }
|
||||
'http://adlnet.gov/expapi/verbs/scored', // NOSONAR S5332
|
||||
{ score: { raw: newScore } },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,44 +166,51 @@ function forgeXapiNotifyScoreChanged(newScore) {
|
||||
function forgeXapiNotifyDialogueCompleted(dialogueId, objectName) {
|
||||
if (!window.FORGE_XAPI_CONFIG) return;
|
||||
forgeXapiSendStatement(
|
||||
"http://adlnet.gov/expapi/verbs/completed",
|
||||
{ completion: true }, forgeXapiDialogueObject(dialogueId, objectName)
|
||||
'http://adlnet.gov/expapi/verbs/completed', // NOSONAR S5332
|
||||
{ completion: true },
|
||||
forgeXapiDialogueObject(dialogueId, objectName),
|
||||
);
|
||||
}
|
||||
|
||||
function forgeXapiNotifyQuestionAnswered(dialogueId, objectName, questionIndex, questionText, correct, scoreAwarded) {
|
||||
if (!window.FORGE_XAPI_CONFIG) return;
|
||||
forgeXapiSendStatement(
|
||||
"http://adlnet.gov/expapi/verbs/answered",
|
||||
'http://adlnet.gov/expapi/verbs/answered', // NOSONAR S5332
|
||||
{ success: correct, score: { raw: scoreAwarded || 0 } },
|
||||
forgeXapiQuestionObject(dialogueId, objectName, questionIndex, questionText)
|
||||
forgeXapiQuestionObject(dialogueId, objectName, questionIndex, questionText),
|
||||
);
|
||||
}
|
||||
|
||||
function forgeXapiNotifyStatusChanged(newStatus) {
|
||||
if (!window.FORGE_XAPI_CONFIG) return;
|
||||
var verb = FORGE_TO_XAPI_VERB[newStatus];
|
||||
const verb = FORGE_TO_XAPI_VERB[newStatus];
|
||||
if (!verb || newStatus === forgeXapiLastStatusSent) return;
|
||||
forgeXapiLastStatusSent = newStatus;
|
||||
forgeXapiSendStatement(verb.id, {
|
||||
completion: true,
|
||||
success: newStatus !== "echoue",
|
||||
success: newStatus !== 'echoue',
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("load", function () {
|
||||
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement("http://adlnet.gov/expapi/verbs/initialized");
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('load', () => {
|
||||
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement('http://adlnet.gov/expapi/verbs/initialized'); // NOSONAR S5332
|
||||
});
|
||||
window.addEventListener("beforeunload", function () {
|
||||
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement("http://adlnet.gov/expapi/verbs/terminated");
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement('http://adlnet.gov/expapi/verbs/terminated'); // NOSONAR S5332
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
forgeXapiSendStatement, forgeXapiNotifyScoreChanged, forgeXapiNotifyStatusChanged, FORGE_TO_XAPI_VERB,
|
||||
forgeXapiNotifyDialogueCompleted, forgeXapiNotifyQuestionAnswered,
|
||||
forgeXapiDialogueObject, forgeXapiQuestionObject, FORGE_XAPI_VERB_DISPLAY,
|
||||
forgeXapiSendStatement,
|
||||
forgeXapiNotifyScoreChanged,
|
||||
forgeXapiNotifyStatusChanged,
|
||||
FORGE_TO_XAPI_VERB,
|
||||
forgeXapiNotifyDialogueCompleted,
|
||||
forgeXapiNotifyQuestionAnswered,
|
||||
forgeXapiDialogueObject,
|
||||
forgeXapiQuestionObject,
|
||||
FORGE_XAPI_VERB_DISPLAY,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user