Files
Forge-Engine/templates/base.html
T
william 50835a18e2
Build and deploy / test-python (push) Successful in 9m41s
Build and deploy / test-js (push) Successful in 1m0s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Supprime le type d'écran "document" et recentre le produit sur le jeu 2D
Le document de cadrage produit cible des formateurs non techniques créant
des serious games/quiz gamifiés — l'éditeur générique "document" (blocs de
logique en nœuds, timeline d'animation, définitions d'objets/relations,
templates réutilisables) est une complexité hors cible que l'effort
d'ingénierie récent avait déjà abandonnée au profit du jeu_2d.

- Onboarding : ne garde que le parcours "RPG" (jeu_2d), retire
  Quiz/Embranchement/Créer mon jeu de A à Z (tous document-only)
- Suppression en bloc des modules exclusifs au document : routes/elements,
  routes/element_types, routes/objects, routes/legacy_actions,
  screens/elements, screens/element_types, screens/widgets,
  screens/legacy_actions, le rendu render_element_html.py et son cluster,
  templates/screen_edit.html, templates/game_dashboard.html,
  flow-editor.js/tabs-and-blocks.js/animation-timeline.js
- Dashboard toujours simplifié (un seul mode possible désormais)
- Tests document-only supprimés, tests de logique partagée (flow,
  événements personnalisés, animations) retargetés sur des écrans jeu_2d
- Aucune régression jeu_2d : 299 tests passent

Carte d'onboarding retravaillée : argumentaire RH non technique (liste à
coche, badge "Compatible LMS"), taille et interaction de retournement
ajustées.
2026-09-04 20:56:32 +02:00

194 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Forge Engine{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='branding/forge-favicon.svg') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script>window.CSRF_TOKEN = {{ csrf_token()|tojson }};</script>
<script src="{{ url_for('static', filename='csrf_fetch.js') }}"></script>
{% block extra_head %}{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
<!-- #pageChrome : tout ce qui est en dehors de <main> mais doit quand même
changer d'une page à l'autre (barre du jeu comprise) — un seul bloc
stable pour que pjax.js puisse le remplacer d'un coup à chaque
navigation (voir swapDocument() dans pjax.js). Avant l'ajout de ce
conteneur, pjax.js cherchait un <header class="topbar"> qui n'a jamais
existé (la barre est un <nav>, pas un <header>) : la barre de
navigation du jeu (ajoutée après pjax.js) ne changeait donc jamais
pendant la navigation pjax — absente en arrivant sur un jeu tant qu'on
ne rechargeait pas la page en entier (Ctrl+F5), et inversement
laissée en place en revenant sur l'accueil (liste des jeux) où elle
n'a pourtant rien à faire. -->
<div id="pageChrome">
<nav class="navbar topbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a href="{{ url_for('index') }}" class="navbar-item brand">
<img src="{{ url_for('static', filename='branding/forge-logo.svg') }}" alt="" width="32" height="32" style="border-radius:8px;">
<strong class="forge-gradient-title">Forge Engine</strong>
</a>
</div>
{% if g.get('current_user') %}
<div class="navbar-end" style="margin-left:auto; display:flex; align-items:center; gap:10px; padding-right:12px;">
<a href="{{ url_for('profile') }}" class="hint" style="text-decoration:underline;">{{ g.current_user.prenom }} {{ g.current_user.nom }}{{ ' (admin)' if g.current_user.role == 'admin' else '' }}</a>
<form method="post" action="{{ url_for('logout') }}"><button type="submit" class="button is-small">Déconnexion</button></form>
</div>
{% endif %}
</nav>
{% if game is defined and game %}
<!-- Barre toujours visible entre les différentes pages d'UN MÊME jeu.
Écrans/Éléments de jeu/Variables/Objets n'ont plus leur propre page —
tout ça vit désormais dans les onglets du tableau de bord (voir
game_dashboard.html) — donc un seul lien y mène. Remplace le fil
d'Ariane (retiré, devenu redondant) : n'apparaît que si la route a mis
un `game` dans le contexte du template (déjà fait par toutes les
pages d'un jeu) — absente sur l'accueil (liste des jeux). "Jouer" est
délibérément le DERNIER lien — une action à part (ouvre l'aperçu
jouable dans un nouvel onglet), pas une page de gestion de plus. -->
<nav class="gameNavBar" aria-label="Navigation du jeu">
<!-- Onboarding guidé (voir routes/onboarding/onboarding_new.py) : même
route pour tous les comptes — routes/games/game_dashboard.py y rend
une version simplifiée (juste les écrans) pour un type "restreint"
(quiz/embranchement/rpg), la version complète pour "custom". -->
<a href="{{ url_for('game_dashboard', slug=game.slug) }}" class="gameNavLink {{ 'active' if request.path == '/game/' ~ game.slug else '' }}" title="Écrans du jeu">Mes écrans</a>
<!-- "Jouer"/"Exporter" groupés à droite (gameNavRight, margin-left:auto
en CSS) et distingués par COULEUR (orange/vert, voir .gameNavLink--
play/--export dans static/style.css) — demande explicite, ce sont
des ACTIONS (lancer une partie, produire un paquet), pas des pages
de gestion comme "Mes écrans" à gauche. -->
<div class="gameNavRight">
<a href="{{ url_for('game_play', slug=game.slug) }}" target="_blank" class="gameNavLink gameNavLink--play" title="Aperçu jouable du jeu (une seule partie partagée, pour la conception)">Jouer</a>
<!-- Phase 1.2 (feuille de route produit) : seul export retenu — paquet
STATIQUE (aucun Python/Flask embarqué, voir
publish/build_scorm_package.py), pensé pour un import dans le LMS
du client (Moodle...). L'export exécutable Windows ("Publier",
publish/build_package.py) et l'export en ligne (route publique
/jouer/<slug>, routes/public_play/) ont été retirés, jugés
redondants avec celui-ci. -->
<button type="button" class="gameNavLink gameNavLink--export" onclick="window.forgeOpenScormModal && window.forgeOpenScormModal()" title="Exporte le jeu en paquet Web/SCORM importable dans un LMS">Exporter (Web/SCORM)</button>
</div>
</nav>
{% endif %}
</div>
<main class="content {% block content_class %}{% endblock %}">
{% set recovery_codes = pop_recovery_codes() %}
{% if recovery_codes %}
<!-- À L'INTÉRIEUR de <main> (et pas à côté de #pageChrome) : pjax.js ne
remplace jamais que #pageChrome et main.content d'une navigation à
l'autre (voir swapDocument() dans pjax.js) — posé ailleurs, ce modal
serait bien présent dans le HTML renvoyé par le serveur juste après
l'inscription, mais jamais copié dans le DOM réellement affiché
puisque la redirection qui suit la confirmation de la 2FA passe par
une navigation pjax (fetch), pas un vrai rechargement de page. -->
<div class="modal is-active" id="recoveryCodesModal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<h2 class="title is-4">Tes codes de récupération</h2>
<p class="hint mb-4">Note ces 10 codes en sécurité (gestionnaire de mots de passe, papier...) : chacun ne fonctionne qu'une seule fois et permet de te connecter si tu perds l'accès à ton application d'authentification. Ils ne seront plus jamais affichés.</p>
<div class="recoveryCodesGrid">
{% for code in recovery_codes %}
<code class="recoveryCode">{{ code }}</code>
{% endfor %}
</div>
<button type="button" class="button primary is-fullwidth mt-4" onclick="document.getElementById('recoveryCodesModal').remove()">J'ai noté mes codes</button>
</div>
</div>
</div>
{% endif %}
{% if game is defined and game %}
<!-- Même remarque que pour le modal des codes de récupération ci-dessus : à L'INTÉRIEUR
de <main> pour que pjax.js le remplace (et rejoue son script JS, voir
runScriptsIn()) à chaque navigation entre pages d'un même jeu — posé
dans #pageChrome (comme le bouton "🎓 Exporter (Web/SCORM)" qui
l'ouvre), son script ne serait rejoué qu'au tout premier chargement
complet de la page. -->
<style>
@keyframes forgePublishPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
</style>
<div class="modal" id="scormModal">
<div class="modal-background"></div>
<div class="modal-content">
<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">
<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;">
<button type="button" class="button is-small" onclick="window.forgeCloseScormModal()">Fermer</button>
</div>
</div>
</div>
</div>
<script>
(function () {
var modal = document.getElementById('scormModal');
var stepText = document.getElementById('scormStepText');
var progressBox = document.getElementById('scormProgress');
var errorBox = document.getElementById('scormError');
var doneBox = document.getElementById('scormDone');
var steps = ["Préparation du jeu...", "Génération du paquet hors ligne...", "Écriture du manifeste SCORM..."];
var stepTimer = null;
window.forgeOpenScormModal = 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;
stepText.textContent = steps[stepIndex];
}, 1200);
var filename = "{{ game.slug }}-scorm.zip";
fetch("{{ url_for('export_scorm', slug=game.slug) }}", { method: 'POST' })
.then(function (resp) {
if (!resp.ok) throw new Error('HTTP ' + resp.status);
var disposition = resp.headers.get('Content-Disposition') || '';
var match = /filename="?([^";]+)"?/.exec(disposition);
if (match) filename = match[1];
return resp.blob();
})
.then(function (blob) {
clearInterval(stepTimer);
progressBox.style.display = 'none';
doneBox.style.display = '';
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
})
.catch(function (err) {
clearInterval(stepTimer);
progressBox.style.display = 'none';
errorBox.style.display = '';
errorBox.textContent = "Échec de l'export (" + err.message + "). Réessaie.";
});
};
window.forgeCloseScormModal = function () {
modal.classList.remove('is-active');
if (stepTimer) clearInterval(stepTimer);
};
})();
</script>
{% endif %}
{% block content %}{% endblock %}
</main>
<script src="{{ url_for('static', filename='pjax.js') }}"></script>
</body>
</html>