Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur Support de formation (voir docs/plan/PLAN.md), sans toucher a l'architecture en couches existante : - screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D, avant l'arrivee d'un second "moteur" cote document) : ~85 imports corriges, contrat import-linter mis a jour, meme forme de couches. - routes/, scripts/, static/, templates/, tests/ : tout ce qui est propre au jeu 2D deplace dans un sous-dossier game/ de chacun (routes/game/, static/game/, templates/game/, tests/game/, scripts/game/) ; ce qui est partage par le site (auth, onboarding, dashboard, uploads, db/) reste a la racine de chaque dossier. Un sous-dossier document/ (vide) cree dans chacun pour le futur chantier. - styles/ volontairement inchange : les 3 fichiers sources sont concatenes en un seul static/style.css charge par tout le site, scinder leur CONTENU (editeur vs partage) serait un refactor CSS distinct, pas un deplacement mecanique. - Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en profondeur : copie des assets, URLs d'icones relatives a static/style.css (qui ne bouge pas), manifeste, wrapper SCORM. - Deux regressions d'un sweep de renommage anterieur corrigees au passage (screens.js/screens/scene-objects incorrectement convertis en game_engine.js/game_engine/scene-objects dans des commentaires). - Effet de bord Windows decouvert et corrige : git mv + Path.write_text convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers normalises en LF. - .eslintrc.json/package.json : uniquement les chemins de glob mis a jour (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7 reste volontairement non committee (package-lock.json restaure a la version precedente). Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit, lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ; demarrage serveur + requetes HTTP manuelles confirmant que les assets deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien. SKIP=djlint : backlog H021 (styles inline) deja documente comme dette assumee dans CODE_QUALITY.md section 6, aucun template touche par ce commit au-dela d'un deplacement de fichier. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
367 lines
17 KiB
HTML
367 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Consignes d'évacuation — Formation incendie</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{
|
|
--bg:#0b0e14;
|
|
--bg-2:#11151d;
|
|
--card:#161b25;
|
|
--border:#232a38;
|
|
--text:#e8ecf4;
|
|
--muted:#8b95a7;
|
|
--accent1:#ff5f2e;
|
|
--accent2:#ffb020;
|
|
--gradient:linear-gradient(120deg,#ff5f2e,#ffb020);
|
|
--glow:0 0 0 1px rgba(255,95,46,0.25), 0 10px 30px -8px rgba(255,95,46,0.25);
|
|
--overlay:rgba(11,14,20,0.9);
|
|
--success-bg:rgba(59,165,93,0.12);
|
|
--success-border:#3ba55d;
|
|
--success-text:#8fe3ab;
|
|
--danger-bg:rgba(255,95,46,0.12);
|
|
--danger-border:#ff5f2e;
|
|
--danger-text:#ffb3a0;
|
|
}
|
|
:root[data-theme="light"]{
|
|
--bg:#F6F5F2;
|
|
--bg-2:#FFFFFF;
|
|
--card:#FFFFFF;
|
|
--border:#E3E1DB;
|
|
--text:#14161C;
|
|
--muted:#6B7280;
|
|
--glow:0 0 0 1px rgba(255,95,46,0.3), 0 10px 30px -8px rgba(255,95,46,0.3);
|
|
--overlay:rgba(255,255,255,0.85);
|
|
--success-bg:#E4F5EA;
|
|
--success-border:#2E7D46;
|
|
--success-text:#1D4C2C;
|
|
--danger-bg:#FDEDE8;
|
|
--danger-border:#C1451F;
|
|
--danger-text:#7A1418;
|
|
}
|
|
*{box-sizing:border-box;}
|
|
html,body{margin:0;padding:0;}
|
|
body{
|
|
font-family:'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
background:var(--bg); color:var(--text);
|
|
min-height:100vh;
|
|
}
|
|
|
|
/* barre de progression du parcours */
|
|
.course-bar{
|
|
position:sticky; top:0; z-index:10;
|
|
background:var(--overlay); backdrop-filter:blur(6px);
|
|
border-bottom:1px solid var(--border);
|
|
padding:14px 24px;
|
|
display:flex; align-items:center; justify-content:space-between; gap:16px;
|
|
}
|
|
.course-bar .brand{ display:flex; align-items:center; gap:10px; min-width:0; }
|
|
.course-bar .label{ font-size:13px; font-weight:600; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
.theme-toggle{
|
|
width:34px; height:34px; border-radius:10px; border:1px solid var(--border); background:transparent;
|
|
display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--text); flex-shrink:0;
|
|
}
|
|
.theme-toggle:hover{ border-color:var(--muted); }
|
|
.forge-badge{
|
|
padding:0.35rem 0.85rem; border-radius:999px; background:rgba(255,95,46,0.1);
|
|
border:1px solid rgba(255,95,46,0.35); color:var(--accent1); font-size:0.72rem;
|
|
font-weight:600; letter-spacing:0.04em; flex-shrink:0;
|
|
}
|
|
.progress-track{ height:4px; background:var(--border); border-radius:2px; overflow:hidden; margin-top:10px; }
|
|
.progress-fill{ height:100%; background:var(--gradient); width:60%; }
|
|
|
|
main{ max-width:720px; margin:0 auto; padding:48px 24px 80px; }
|
|
|
|
.hero-title{
|
|
font-weight:800;
|
|
font-size:clamp(2.6rem, 6vw, 4.6rem);
|
|
line-height:1.05;
|
|
margin:0 0 18px;
|
|
background:var(--gradient);
|
|
-webkit-background-clip:text;
|
|
background-clip:text;
|
|
color:transparent;
|
|
}
|
|
.hero-intro{ font-size:17px; line-height:1.6; color:var(--muted); max-width:56ch; margin:0 0 40px; }
|
|
|
|
section{ margin-bottom:40px; }
|
|
|
|
.image-frame{
|
|
width:100%; border-radius:16px; background:var(--bg-2); border:1px solid var(--border);
|
|
height:260px; display:flex; align-items:center; justify-content:center; gap:10px; color:var(--muted);
|
|
margin-bottom:40px;
|
|
}
|
|
.image-frame span{ font-size:14px; font-weight:600; }
|
|
|
|
h2{ font-size:2rem; font-weight:800; margin:0 0 16px; line-height:1.15; }
|
|
|
|
.cols{ display:flex; gap:32px; flex-wrap:wrap; }
|
|
.cols .text{ flex:1; min-width:240px; font-size:16px; line-height:1.65; color:var(--text); }
|
|
.cols .steps{ flex:1; min-width:240px; display:flex; flex-direction:column; gap:14px; }
|
|
.step{ display:flex; gap:14px; align-items:flex-start; }
|
|
.step .num{
|
|
flex-shrink:0; width:30px; height:30px; border-radius:999px; background:var(--card);
|
|
border:1px solid var(--border); display:flex; align-items:center; justify-content:center;
|
|
font-weight:800; font-size:13px; color:var(--accent1);
|
|
}
|
|
.step .txt{ font-size:15px; line-height:1.5; padding-top:4px; }
|
|
|
|
.divider{ height:1px; background:var(--border); margin:40px 0; }
|
|
|
|
/* ===== QUIZ (forge-card) ===== */
|
|
.forge-card{
|
|
background:linear-gradient(180deg, var(--card), var(--bg-2));
|
|
border:1px solid var(--border); border-radius:16px; padding:1.8rem;
|
|
transition:box-shadow .2s ease;
|
|
}
|
|
.forge-card:hover{ box-shadow:var(--glow); }
|
|
.quiz-kicker{ font-size:12px; font-weight:700; letter-spacing:0.04em; color:var(--accent1); margin-bottom:6px; }
|
|
.quiz-title{ font-size:1.15rem; font-weight:700; margin:0 0 20px; }
|
|
|
|
.progress-track.quiz{ margin-top:0; margin-bottom:10px; }
|
|
.quiz-meta{ display:flex; justify-content:space-between; font-size:13px; color:var(--muted); font-weight:600; margin-bottom:20px; }
|
|
|
|
.q-text{ font-size:17px; font-weight:700; line-height:1.3; margin:0 0 18px; }
|
|
.q-options{ display:flex; flex-direction:column; gap:10px; }
|
|
.q-option{
|
|
text-align:left; padding:13px 15px; border:1px solid var(--border); background:var(--bg-2);
|
|
color:var(--text); border-radius:10px; font-size:15px; font-weight:500; cursor:pointer;
|
|
display:flex; align-items:center; gap:12px; transition:border-color .15s ease;
|
|
}
|
|
.q-option:hover:not(:disabled){ border-color:var(--accent1); }
|
|
.q-option .letter{
|
|
flex-shrink:0; width:24px; height:24px; border-radius:50%; border:1px solid var(--border);
|
|
display:flex; align-items:center; justify-content:center; font-weight:700; font-size:12.5px; color:var(--muted);
|
|
}
|
|
.q-option.correct{ border-color:var(--success-border); background:var(--success-bg); }
|
|
.q-option.correct .letter{ border-color:var(--success-border); background:var(--success-border); color:#0b0e14; }
|
|
.q-option.wrong{ border-color:var(--danger-border); background:var(--danger-bg); }
|
|
.q-option.wrong .letter{ border-color:var(--accent1); background:var(--accent1); color:#0b0e14; }
|
|
.q-option:disabled:not(.correct):not(.wrong){ opacity:0.45; }
|
|
|
|
.q-feedback{ margin-top:16px; padding:13px 15px; border-radius:10px; font-size:14px; line-height:1.5; display:none; }
|
|
.q-feedback.show{ display:block; }
|
|
.q-feedback.ok{ background:var(--success-bg); color:var(--success-text); border:1px solid var(--success-border); }
|
|
.q-feedback.ko{ background:var(--danger-bg); color:var(--danger-text); border:1px solid var(--danger-border); }
|
|
|
|
.q-nextbar{ margin-top:20px; display:flex; justify-content:flex-end; }
|
|
.forge-btn{
|
|
padding:0.75rem 1.6rem; border-radius:10px; border:none;
|
|
background:var(--gradient); color:#14100c; font-weight:700; font-size:14px; cursor:pointer;
|
|
opacity:0; pointer-events:none; transition:opacity .2s ease;
|
|
}
|
|
.forge-btn.show{ opacity:1; pointer-events:auto; }
|
|
|
|
.q-result{ text-align:center; padding:6px 4px 4px; }
|
|
.q-result .big{ font-weight:800; font-size:52px; background:var(--gradient); -webkit-background-clip:text; background-clip:text; color:transparent; line-height:1; }
|
|
.q-result .sub{ font-size:13px; color:var(--muted); font-weight:600; margin-top:6px; }
|
|
.q-result p.msg{ font-size:15px; margin:18px 0 24px; color:var(--text); line-height:1.5; }
|
|
.forge-btn-secondary{
|
|
padding:0.7rem 1.5rem; border-radius:10px; background:transparent; border:1px solid var(--border);
|
|
color:var(--text); font-weight:600; font-size:14px; cursor:pointer;
|
|
}
|
|
|
|
footer{
|
|
max-width:720px; margin:0 auto; padding:0 24px 60px;
|
|
display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap;
|
|
}
|
|
.next-section{ font-size:14px; color:var(--muted); }
|
|
.next-section strong{ color:var(--text); }
|
|
|
|
@media (max-width:640px){
|
|
main{ padding:32px 18px 60px; }
|
|
.course-bar{ padding:12px 16px; }
|
|
h2{ font-size:1.5rem; }
|
|
.cols{ gap:24px; }
|
|
.forge-card{ padding:1.3rem; }
|
|
footer{ padding:0 18px 48px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="course-bar">
|
|
<div style="flex:1; min-width:0;">
|
|
<div class="brand">
|
|
<span class="label">Formation incendie · Module 3 sur 4</span>
|
|
<span class="forge-badge">En cours</span>
|
|
</div>
|
|
<div class="progress-track"><div class="progress-fill"></div></div>
|
|
</div>
|
|
<button class="theme-toggle" id="themeToggle" aria-label="Changer de thème"></button>
|
|
</div>
|
|
|
|
<main>
|
|
|
|
<h1 class="hero-title">Consignes d'évacuation</h1>
|
|
<p class="hero-intro">Ce module présente les étapes à suivre en cas de déclenchement de l'alarme incendie, ainsi que les bons réflexes à adopter avant l'arrivée des secours.</p>
|
|
|
|
<div class="image-frame">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="10" r="1.4"/><path d="M21 16.5l-5-5-4 4-3-3-5 5"/></svg>
|
|
<span>Plan d'évacuation de l'étage</span>
|
|
</div>
|
|
|
|
<section>
|
|
<div class="cols">
|
|
<div class="text">Dès le signal d'alarme, l'évacuation est immédiate. Aucune tâche en cours ne justifie un délai, même de quelques secondes. Les trois étapes ci-contre doivent devenir un réflexe.</div>
|
|
<div class="steps">
|
|
<div class="step"><div class="num">1</div><div class="txt">Donner l'alerte</div></div>
|
|
<div class="step"><div class="num">2</div><div class="txt">Évacuer par l'issue la plus proche</div></div>
|
|
<div class="step"><div class="num">3</div><div class="txt">Rejoindre le point de rassemblement</div></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<section>
|
|
<div class="forge-card" id="quizCard">
|
|
<div class="quiz-kicker">Quiz</div>
|
|
<div class="quiz-title">Vérifiez vos réflexes</div>
|
|
<div class="progress-track quiz"><div class="progress-fill" id="qFill" style="width:0%;"></div></div>
|
|
<div class="quiz-meta"><span id="qProg"></span><span id="qScore"></span></div>
|
|
<div class="q-text" id="qText"></div>
|
|
<div class="q-options" id="qOptions"></div>
|
|
<div class="q-feedback" id="qFeedback"></div>
|
|
<div class="q-nextbar"><button class="forge-btn" id="qNext">Question suivante</button></div>
|
|
</div>
|
|
|
|
<div class="forge-card hidden" id="qResultCard" style="display:none;">
|
|
<div class="q-result">
|
|
<div class="big" id="qBig">0/5</div>
|
|
<div class="sub">bonnes réponses</div>
|
|
<p class="msg" id="qMsg"></p>
|
|
<button class="forge-btn-secondary" id="qRestart">Recommencer le quiz</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<footer>
|
|
<div class="next-section">Section suivante : <strong>Utilisation des extincteurs</strong></div>
|
|
<button class="forge-btn">Continuer</button>
|
|
</footer>
|
|
|
|
<script>
|
|
const questions = [
|
|
{ q:"Vous découvrez un début d'incendie. Premier réflexe ?",
|
|
options:["Donner l'alerte et déclencher l'alarme","Éteindre seul sans prévenir","Récupérer ses affaires avant de sortir","Prendre l'ascenseur pour évacuer"],
|
|
correct:0,
|
|
ok:"C'est le bon réflexe : alerter avant d'agir permet de déclencher l'évacuation au plus vite.",
|
|
ko:"Le premier réflexe est toujours de donner l'alerte, avant toute autre action." },
|
|
{ q:"Quel extincteur utiliser sur un feu d'origine électrique ?",
|
|
options:["Extincteur à eau pulvérisée","Extincteur au CO2","Extincteur à mousse","Seau d'eau"],
|
|
correct:1,
|
|
ok:"Exact : le CO2 ne conduit pas l'électricité et ne laisse aucun résidu.",
|
|
ko:"Seul le CO2 convient : les autres types conduisent l'électricité ou abîment le matériel." },
|
|
{ q:"Le signal d'évacuation retentit. Que faites-vous ?",
|
|
options:["Je termine ma tâche puis je sors","Je sors immédiatement par l'issue la plus proche","J'attends une confirmation orale","Je vérifie d'abord l'origine de l'alarme"],
|
|
correct:1,
|
|
ok:"Correct : toute alarme impose une évacuation immédiate, sans délai.",
|
|
ko:"Il faut évacuer immédiatement, sans attendre ni vérifier la cause." },
|
|
{ q:"Où se retrouver après avoir évacué le bâtiment ?",
|
|
options:["Sur le parking le plus proche de sa voiture","Au point de rassemblement désigné","Devant l'entrée principale, peu importe où","Chacun rentre chez soi"],
|
|
correct:1,
|
|
ok:"Le point de rassemblement permet de faire l'appel du personnel.",
|
|
ko:"Il faut toujours se rendre au point de rassemblement désigné." },
|
|
{ q:"Une porte est chaude au toucher pendant l'évacuation. Que faites-vous ?",
|
|
options:["Je l'ouvre rapidement et je passe","Je ne l'ouvre pas, je cherche une autre issue","Je l'ouvre en la poussant du pied","J'attends 2 minutes puis j'ouvre"],
|
|
correct:1,
|
|
ok:"Bonne réaction : une porte chaude signale un feu derrière, il ne faut jamais l'ouvrir.",
|
|
ko:"Une porte chaude ne doit jamais être ouverte : cherchez une autre issue." }
|
|
];
|
|
|
|
let cur=0, score=0, answered=false;
|
|
const qText=document.getElementById('qText'), qOptions=document.getElementById('qOptions'),
|
|
qFeedback=document.getElementById('qFeedback'), qNext=document.getElementById('qNext'),
|
|
qFill=document.getElementById('qFill'), qProg=document.getElementById('qProg'), qScore=document.getElementById('qScore'),
|
|
qCard=document.getElementById('quizCard'), qResultCard=document.getElementById('qResultCard');
|
|
|
|
function render(){
|
|
answered=false;
|
|
const item = questions[cur];
|
|
qText.textContent = item.q;
|
|
qOptions.innerHTML='';
|
|
qFeedback.className='q-feedback'; qFeedback.textContent='';
|
|
qNext.classList.remove('show');
|
|
const letters=['A','B','C','D'];
|
|
item.options.forEach((opt,idx)=>{
|
|
const btn=document.createElement('button');
|
|
btn.className='q-option';
|
|
btn.innerHTML=`<span class="letter">${letters[idx]}</span><span>${opt}</span>`;
|
|
btn.addEventListener('click',()=>select(idx,btn));
|
|
qOptions.appendChild(btn);
|
|
});
|
|
qFill.style.width=(cur/questions.length*100)+'%';
|
|
qProg.textContent=`Question ${cur+1} sur ${questions.length}`;
|
|
qScore.textContent=`Score : ${score}`;
|
|
}
|
|
function select(idx,btn){
|
|
if(answered) return;
|
|
answered=true;
|
|
const item=questions[cur];
|
|
const all=qOptions.querySelectorAll('.q-option');
|
|
all.forEach(o=>o.disabled=true);
|
|
if(idx===item.correct){
|
|
btn.classList.add('correct'); score++;
|
|
qFeedback.classList.add('show','ok'); qFeedback.textContent=item.ok;
|
|
} else {
|
|
btn.classList.add('wrong'); all[item.correct].classList.add('correct');
|
|
qFeedback.classList.add('show','ko'); qFeedback.textContent=item.ko;
|
|
}
|
|
qScore.textContent=`Score : ${score}`;
|
|
qNext.classList.add('show');
|
|
qNext.textContent = (cur===questions.length-1)?'Voir mon résultat':'Question suivante';
|
|
}
|
|
qNext.addEventListener('click',()=>{
|
|
cur++;
|
|
if(cur<questions.length) render(); else showResult();
|
|
});
|
|
function showResult(){
|
|
qFill.style.width='100%';
|
|
qCard.style.display='none'; qResultCard.style.display='block';
|
|
document.getElementById('qBig').textContent=`${score}/${questions.length}`;
|
|
const msg=document.getElementById('qMsg');
|
|
if(score===questions.length) msg.textContent="Excellent ! Les bons réflexes sont parfaitement maîtrisés.";
|
|
else if(score>=questions.length-1) msg.textContent="Très bon résultat, une relecture rapide suffira.";
|
|
else if(score>=questions.length/2) msg.textContent="Résultat correct, certains réflexes essentiels restent à revoir.";
|
|
else msg.textContent="Il est recommandé de revoir la formation avant une situation réelle.";
|
|
}
|
|
document.getElementById('qRestart').addEventListener('click',()=>{
|
|
cur=0; score=0;
|
|
qResultCard.style.display='none'; qCard.style.display='block';
|
|
render();
|
|
});
|
|
render();
|
|
|
|
/* ---- thème clair / sombre ---- */
|
|
const SUN_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M2 12h2M20 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/></svg>';
|
|
const MOON_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"/></svg>';
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
|
|
function applyTheme(theme){
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
themeToggle.innerHTML = theme === 'light' ? MOON_ICON : SUN_ICON;
|
|
themeToggle.setAttribute('aria-label', theme === 'light' ? 'Passer en thème sombre' : 'Passer en thème clair');
|
|
}
|
|
|
|
let storedTheme = 'dark';
|
|
try {
|
|
const saved = localStorage.getItem('forge-doc-theme');
|
|
if(saved === 'light' || saved === 'dark') storedTheme = saved;
|
|
} catch(e) { /* stockage indisponible, on reste sur le thème par défaut */ }
|
|
applyTheme(storedTheme);
|
|
|
|
themeToggle.addEventListener('click', () => {
|
|
const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
|
applyTheme(next);
|
|
try { localStorage.setItem('forge-doc-theme', next); } catch(e) { /* ignore */ }
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|