Ajoute un suivi xAPI optionnel (bolt-on) au paquet SCORM exporté
Build and deploy / test-python (push) Successful in 9m41s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Greffe l'envoi de statements xAPI vers un LRS configurable par le
créateur du jeu, en parallèle du reporting SCORM existant : réglages
stockés en base (_meta), formulaire dans la modale d'export, injection
dans le paquet exporté. Relié aussi bien à l'action de flow "Modifier
un score/statut" qu'au parcours quête/quiz (qui alimentait déjà le
SCORM classique via un chemin séparé).

L'export ne se lance plus automatiquement à l'ouverture de la modale,
pour laisser le temps d'enregistrer les réglages xAPI avant de générer
le paquet.
This commit is contained in:
william
2026-09-05 10:00:57 +02:00
parent 7ebc9b143f
commit f7a50e5afe
13 changed files with 422 additions and 3 deletions
+83 -3
View File
@@ -114,14 +114,44 @@
<div class="box">
<h2 class="title is-4">Exporter « {{ game.name }} » en Web/SCORM</h2>
<p class="hint mb-4">Construit un paquet SCORM 1.2 (imsmanifest.xml + page web autonome, aucun serveur requis) : importe le .zip obtenu directement dans ton LMS (Moodle...) — le score et le statut de la partie remontent automatiquement.</p>
<div id="scormProgress">
<!-- Suivi xAPI (voir publish/build_scorm_package.py, routes/publish/
xapi_settings.py) : greffé sur le SCORM ci-dessus, jamais à sa
place — un réglage jamais renseigné (endpoint vide) désactive
entièrement xAPI pour ce jeu, le SCORM continue seul. Réglé
séparément du bouton d'export : ces valeurs sont déjà en base
au moment où on clique "Exporter", jamais retapées à chaque export. -->
<details class="propGroup mb-4" id="xapiSettingsGroup">
<summary>Suivi xAPI (optionnel)</summary>
<div class="propGroupBody">
<p class="help mb-2">Envoie en plus les résultats vers un LRS (Learning Record Store) externe, en parallèle du SCORM ci-dessus. Utilise une clé API dédiée à cet export, jamais tes identifiants LRS principaux.</p>
<div class="field">
<label class="label is-small">URL du LRS</label>
<div class="control"><input class="input is-small" type="text" id="xapiEndpoint" placeholder="https://mon-lrs.exemple.com/xAPI"></div>
</div>
<div class="field">
<label class="label is-small">Login</label>
<div class="control"><input class="input is-small" type="text" id="xapiLogin" autocomplete="off"></div>
</div>
<div class="field">
<label class="label is-small">Mot de passe</label>
<div class="control"><input class="input is-small" type="password" id="xapiPassword" autocomplete="new-password"></div>
<p class="help" id="xapiPasswordHint"></p>
</div>
<div class="field is-grouped" style="align-items:center;">
<div class="control"><button type="button" class="button is-small" onclick="window.forgeSaveXapiSettings()">Enregistrer</button></div>
<span class="hint" id="xapiSaveStatus"></span>
</div>
</div>
</details>
<div id="scormProgress" style="display:none;">
<p class="hint" id="scormStepText">Préparation du jeu...</p>
<div class="passwordStrengthBar" style="margin-top:10px;"><div class="passwordStrengthFill strong" id="scormProgressFill" style="width:100%; animation:forgePublishPulse 1.2s ease-in-out infinite;"></div></div>
</div>
<div id="scormError" class="notification is-danger" style="display:none;"></div>
<div id="scormDone" class="notification is-success" style="display:none;">Téléchargement lancé — importe directement le .zip dans ton LMS.</div>
<div class="field mt-4" style="display:flex; justify-content:flex-end;">
<div class="field mt-4" style="display:flex; justify-content:flex-end; gap:8px;">
<button type="button" class="button is-small" onclick="window.forgeCloseScormModal()">Fermer</button>
<button type="button" class="button primary is-small" id="scormExportButton" onclick="window.forgeStartScormExport()">Exporter (Web/SCORM)</button>
</div>
</div>
</div>
@@ -136,13 +166,63 @@
var steps = ["Préparation du jeu...", "Génération du paquet hors ligne...", "Écriture du manifeste SCORM..."];
var stepTimer = null;
var xapiEndpoint = document.getElementById('xapiEndpoint');
var xapiLogin = document.getElementById('xapiLogin');
var xapiPassword = document.getElementById('xapiPassword');
var xapiPasswordHint = document.getElementById('xapiPasswordHint');
var xapiSaveStatus = document.getElementById('xapiSaveStatus');
function forgeLoadXapiSettings() {
fetch("{{ url_for('xapi_settings_get', slug=game.slug) }}")
.then(function (r) { return r.json(); })
.then(function (json) {
xapiEndpoint.value = json.endpoint || '';
xapiLogin.value = json.login || '';
xapiPassword.value = '';
xapiPasswordHint.textContent = json.password_set
? 'Mot de passe déjà enregistré — laisse ce champ vide pour le garder.' : '';
});
}
// Réglages, jamais retapés à chaque export : sauvegardés indépendamment
// du bouton "Exporter" ci-dessous (voir routes/publish/xapi_settings.py).
window.forgeSaveXapiSettings = function () {
xapiSaveStatus.textContent = 'Enregistrement...';
fetch("{{ url_for('xapi_settings_set', slug=game.slug) }}", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
endpoint: xapiEndpoint.value.trim(), login: xapiLogin.value.trim(),
password: xapiPassword.value || null,
}),
})
.then(function (r) { return r.json(); })
.then(function (json) {
xapiSaveStatus.textContent = json.ok ? 'Enregistré ✓' : 'Échec de l\'enregistrement.';
forgeLoadXapiSettings();
})
.catch(function () { xapiSaveStatus.textContent = 'Échec de l\'enregistrement.'; });
};
// Ouvrir la modale ne lance PLUS l'export tout de suite : le temps que
// l'utilisateur renseigne (et surtout enregistre, bouton séparé
// ci-dessus) ses réglages xAPI serait sinon une course contre le fetch
// d'export, qui pourrait partir avec des réglages pas encore sauvegardés.
// L'export est déclenché explicitement via forgeStartScormExport().
window.forgeOpenScormModal = function () {
progressBox.style.display = 'none';
errorBox.style.display = 'none';
doneBox.style.display = 'none';
xapiSaveStatus.textContent = '';
forgeLoadXapiSettings();
modal.classList.add('is-active');
};
window.forgeStartScormExport = function () {
var stepIndex = 0;
stepText.textContent = steps[0];
progressBox.style.display = '';
errorBox.style.display = 'none';
doneBox.style.display = 'none';
modal.classList.add('is-active');
stepTimer = setInterval(function () {
stepIndex = (stepIndex + 1) % steps.length;