Thèmes finaux¶
Une fois le parcours principal (100 énigmes sur 5 niveaux) complété, l'étudiant est redirigé vers la page de succès qui lui propose cinq thèmes pour une ultime énigme. Chaque thème est un composant React autonome chargé dynamiquement et qui valide sa réponse contre un hash SHA-256 côté serveur. La réussite d'un thème débloque l'accès à un hub de mini-jeux pédagogiques.
Cette page décrit les cinq thèmes, leur mécanique commune, et accorde une section dédiée au thème Logique & Mathématiques qui utilise une architecture à deux phases spécifique (V38 et V39).
Vue d'ensemble des cinq thèmes¶
| Thème | Id | Composant | Couleur | Icône | Thématique |
|---|---|---|---|---|---|
| Performance | performance |
ThemePerformance.jsx |
#34d399 (vert) |
⚡ | Métriques, débit, latence |
| Fonctionnel | fonctionnel |
ThemeFonctionnel.jsx |
#60a5fa (bleu) |
🔧 | Exceptions, chaos applicatif |
| Métier | metier |
ThemeMetier.jsx |
#f59e0b (orange) |
💼 | Anomalies commerciales, TVA |
| Sécurité | securite |
ThemeSecurite.jsx |
#f87171 (rouge) |
🔒 | OWASP, vulnérabilités web |
| Logique & Maths | logique |
ThemeLogique.jsx |
#a78bfa (violet) |
🧠 | Pool de 25 questions, tirage déterministe |
L'ordre dans la grille de la page de succès est fixé par THEMES_LIST
dans frontend/src/pedagogique/themes/index.js — les quatre thèmes
« concepts » en premier (performance, fonctionnel, métier, sécurité)
et Logique en cinquième. Les libellés et descriptions sont résolus
via les clés i18n themes.<id>.label et themes.<id>.description.
Mécanique commune aux quatre thèmes « concepts »¶
Les thèmes Performance, Fonctionnel, Métier et Sécurité partagent la
même structure : un énoncé illustré par un exemple concret, un
bouton 💡 qui développe trois indices progressifs, un champ de
réponse, et un bouton de validation qui appelle directement
POST /pedagogique/finale/validate.
sequenceDiagram
autonumber
actor E as Étudiant
participant UI as Theme{Concept}.jsx
participant API as sha256hex()
participant CTR as ChaosStudentController
E->>UI: Lit l'énoncé et les indices
E->>UI: Tape une réponse<br/>(1 mot-clé concept)
E->>UI: Clique "Valider"
UI->>API: sha256hex(answer)
API-->>UI: hash hex 64 chars
UI->>CTR: POST /pedagogique/finale/validate<br/>header X-Student-Token<br/>{theme:"<id>", answerHash:"..."}
CTR->>CTR: getSession(token)
CTR->>CTR: switch(theme) { case "<id>" → sha256(keyword) }
alt hash match
CTR-->>UI: {valid:true, gameUrl}
UI->>UI: setTimeout 1800ms
UI->>UI: onSuccess(gameUrl)
UI->>E: window.location = gameUrl
else hash mismatch
CTR-->>UI: {valid:false}
UI->>E: "Réponse incorrecte"
end
Endpoint POST /pedagogique/finale/validate¶
@PostMapping("/pedagogique/finale/validate")
public ResponseEntity<?> validateFinale(
@RequestBody Map<String, Object> body,
@RequestHeader(value = STUDENT_TOKEN_HEADER, required = false)
String studentToken) {
// vérifs token...
String theme = (String) body.getOrDefault("theme", "");
String submittedHash = (String) body.getOrDefault("answerHash", "");
String expectedHash = switch (theme) {
case "performance" -> PedagogiqueSessionService.sha256("<keyword>");
case "fonctionnel" -> PedagogiqueSessionService.sha256("<keyword>");
case "metier" -> PedagogiqueSessionService.sha256("<keyword>");
case "securite" -> PedagogiqueSessionService.sha256("<keyword>");
case "logique" -> session.logiqueExpectedHash; // ou fallback
default -> "";
};
if (expectedHash.isEmpty())
return ResponseEntity.badRequest().body(...);
if (!expectedHash.equalsIgnoreCase(submittedHash)) {
return ResponseEntity.ok(Map.of("valid", false));
}
String targetUrl = /* URL du hub — lue depuis l'environnement */;
return ResponseEntity.ok(Map.of("valid", true, "gameUrl", targetUrl));
}
| Propriété | Valeur |
|---|---|
| Méthode | POST |
| URL | /api/chaos/student/pedagogique/finale/validate |
| Authentification | Header X-Student-Token |
| Body | {"theme": "<id>", "answerHash": "<sha256 hex>"} |
| Réponse 200 OK | {"valid": true, "gameUrl": "..."} ou {"valid": false} |
| Réponse 400 | Thème inconnu |
| Réponse 401 | Token manquant ou invalide |
Les quatre mots-clés des thèmes « concepts » sont définis en clair
dans le code source du backend (ChaosStudentController). Ils ne
sont pas exposés via API et ne sont pas stockés en base ; seul le hash
calculé à la volée est utilisé pour la comparaison. Cette documentation
ne les divulgue pas : chaque thème a un indice fort dans son énoncé et
une série d'indices progressifs accessibles via le bouton 💡.
Le thème Performance¶
- Classe React :
ThemePerformance.jsx - Couleur dominante : vert
#34d399 - Structure : un énoncé en trois paragraphes (illustration, observation, question), trois indices masquables, un champ de réponse à un mot, un bouton de validation.
Le thème invite l'étudiant à réfléchir à un concept central des tests de performance. L'indicateur attendu n'est pas un chiffre mais un mot-clé du vocabulaire métier — le genre de terme qu'on lit dans un dashboard Grafana ou dans une spec de tir de charge.
Les trois indices du bouton 💡 progressent du plus abstrait
(« c'est ce qu'on mesure quand on veut savoir combien la plateforme
peut encaisser ») au plus spécifique (« le terme anglais qu'on voit
sur les métriques Grafana »). Tous les textes sont externalisés en
clés i18n theme.perf.*.
Le thème Fonctionnel¶
- Classe React :
ThemeFonctionnel.jsx - Couleur dominante : bleu
#60a5fa - Structure : énoncé illustrant un incident JVM classique, trois indices, champ libre.
Le thème Fonctionnel renvoie aux chaos fonctionnels de PerfShop
(F1 à F4 — voir chaos/functional.md). Il
demande à l'étudiant d'identifier l'une des exceptions JVM terminales
que peut déclencher le Chaos Fonctionnel. Le contexte fourni
(trace réduite, type d'erreur, ligne de log) est conçu pour
rappeler ce que l'étudiant a pu observer pendant le parcours s'il a
activé le chaos correspondant.
Les clés i18n sont theme.fonc.*.
Le thème Métier¶
- Classe React :
ThemeMetier.jsx - Couleur dominante : orange
#f59e0b - Structure : énoncé en quatre paragraphes (contexte commercial, exemple chiffré, observation, question), trois indices dont un indice par énumération de lettres (« les lettres sont : T, V, A »).
Le thème Métier exige une réponse courte en trois lettres. Le choix de l'énumération comme indice ultime est volontaire : il garantit que tout étudiant bloqué reçoit suffisamment d'info pour conclure, tout en forçant la réflexion si les deux premiers indices sont ignorés.
Les clés i18n sont theme.metier.*.
Le thème Sécurité¶
- Classe React :
ThemeSecurite.jsx - Couleur dominante : rouge
#f87171 - Structure : énoncé incluant un appel API concret
(
/api/orders/42), trois indices dont un par énumération de lettres, champ libre.
Le thème Sécurité renvoie à une famille de failles bien connue des listes OWASP. Comme pour le thème Métier, l'un des indices liste les lettres de la réponse dans le désordre — pédagogie : l'étudiant qui les voit ne peut pas deviner la réponse sans reconnaître le concept, il doit assembler les lettres mentalement.
Les clés i18n sont theme.secu.*.
Le thème Logique & Mathématiques¶
Le thème Logique est le seul thème qui propose cinq questions plutôt qu'une seule, et le seul qui utilise une architecture à deux phases spécifiquement conçue pour améliorer l'expérience utilisateur sans compromettre la sécurité.
Pool de 25 questions et tirage déterministe¶
Le backend contient un pool de 25 questions logiques et
mathématiques externalisé dans i18n/logique/logique_<lang>.json :
Chaque entrée a deux champs : text (énoncé de la question) et hint
(indice court, affichable à la demande). Les réponses attendues ne
sont pas dans le fichier JSON — elles sont dans un tableau Java
LOGIQUE_ANSWERS de DefaultPedagogiqueSessionService, indexé dans
le même ordre que le fichier :
public static final String[] LOGIQUE_ANSWERS = {
"…", "…", "…", "…", "…", // [0-4]
"…", "…", "…", "…", "…", // [5-9]
"…", "…", "…", "…", "…", // [10-14]
"…", "…", "…", "…", "…", // [15-19]
"…", "…", "…", "…", "…" // [20-24]
};
Chaque étudiant reçoit cinq questions tirées du pool via un générateur pseudo-aléatoire déterministe seedé sur son token UUID. L'algorithme est un LCG (Linear Congruential Generator) identique en Java et en JavaScript :
public static int[] computeLogiqueIndices(String token) {
int seed = 0;
for (int i = 0; i < token.length(); i++) {
seed = (seed << 5) - seed + token.charAt(i);
}
LinkedHashSet<Integer> used = new LinkedHashSet<>();
while (used.size() < 5) {
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
used.add(seed % LOGIQUE_ANSWERS.length);
}
int[] result = new int[5];
int i = 0;
for (int idx : used) result[i++] = idx;
return result;
}
Propriétés du tirage :
- Déterministe : deux étudiants avec le même token obtiennent les mêmes 5 questions dans le même ordre.
- Distinctes : l'usage d'un
LinkedHashSetgarantit que les 5 indices sont uniques. - Uniformément distribuées : sur une classe de 30 étudiants, les cinq tirages couvrent statistiquement la quasi-totalité du pool.
- Identique en JavaScript : la constante
1103515245et l'offset12345sont ceux du LCG de glibc, reproductibles octet pour octet en JS ((seed * 1103515245 + 12345) & 0x7fffffff).
Architecture V38 — calcul au /join¶
Avant la version V38, le frontend exécutait le LCG lui-même pour
choisir ses 5 questions à partir du token lu dans localStorage. Ce
pattern posait un problème subtil : si le token stocké dans
localStorage différait du token utilisé pour la session active
(par exemple après un reset partiel, un changement d'onglet, un
rechargement après expiration), le frontend tirait 5 questions sur une
seed, le backend en vérifiait 5 autres sur une autre seed, et la
validation finale échouait sans que l'étudiant puisse comprendre
pourquoi.
La V38 corrige ce problème en déplaçant le calcul côté backend
au moment du POST /pedagogique/join. Les cinq indices sont calculés
une seule fois, stockés en base dans la colonne
logique_question_indices (format "2,9,15,4,5"), et le hash attendu
— concaténation des cinq bonnes réponses dans l'ordre du tirage — est
stocké dans logique_expected_hash. Le frontend ne fait plus aucun
LCG : il récupère ses cinq questions via l'endpoint
GET /pedagogique/logique/questions.
// DefaultPedagogiqueSessionService.createSession()
int[] logiqueIndices = computeLogiqueIndices(session.token);
String logiqueHash = computeLogiqueHash(logiqueIndices);
session.logiqueQuestionIndices = indicesToString(logiqueIndices);
session.logiqueExpectedHash = logiqueHash;
// ... stockés en base via PedagogiqueSessionEntity
Un fallback existe pour les sessions créées avant la migration V38
(colonnes NULL en base) : ThemeLogique.jsx n'en dépend pas, mais
les endpoints logique recalculent les indices à la volée si besoin.
Endpoint GET /pedagogique/logique/questions¶
Retourne les cinq questions — textes et indices uniquement, jamais les réponses.
@GetMapping("/pedagogique/logique/questions")
public ResponseEntity<?> getLogiqueQuestions(
@RequestHeader(value = STUDENT_TOKEN_HEADER, required = false)
String studentToken) {
// vérifs token...
int[] indices = /* depuis session.logiqueQuestionIndices */;
String[][] pool = loadLogiquePool(); // i18n/logique/logique_XX.json
List<Map<String, Object>> questions = new ArrayList<>();
for (int i = 0; i < indices.length; i++) {
int idx = indices[i];
Map<String, Object> q = new LinkedHashMap<>();
q.put("index", idx);
q.put("text", pool[idx][0]);
q.put("hint", pool[idx][1]);
questions.add(q);
}
return ResponseEntity.ok(Map.of("questions", questions));
}
Réponse type :
{
"questions": [
{"index": 2, "text": "…", "hint": "…"},
{"index": 9, "text": "…", "hint": "…"},
{"index": 15, "text": "…", "hint": "…"},
{"index": 4, "text": "…", "hint": "…"},
{"index": 5, "text": "…", "hint": "…"}
]
}
Le champ index (0-24) permet au frontend de conserver un identifiant
stable par question, même si le format ou l'ordre change dans une
future version.
Chargement du pool avec double-checked locking¶
La méthode loadLogiquePool() utilise un cache statique initialisé
paresseusement via un double-checked locking pour éviter les
chargements concurrents :
private static volatile String[][] logiquePoolCache = null;
static String[][] loadLogiquePool() {
if (logiquePoolCache != null) return logiquePoolCache;
synchronized (ChaosStudentController.class) {
if (logiquePoolCache != null) return logiquePoolCache;
// charge i18n/logique/logique_<LANG>.json via Jackson
// ...
logiquePoolCache = pool;
return pool;
}
}
Le chargement se fait une seule fois au premier appel (lazy) — pas
au démarrage, contrairement aux énigmes BACx qui sont initialisées en
static dans PedagogiqueEnigme. Le volatile garantit la visibilité
entre threads, et le double-check évite la synchronisation une fois
le cache chaud.
Architecture V39 — validation à deux phases¶
Le thème Logique est le seul des cinq thèmes à utiliser une validation en deux phases :
- Phase 1 —
/logique/check: vérifie les cinq réponses individuellement et retourne un booléen par question. L'étudiant voit immédiatement quelles réponses sont correctes (✅) et lesquelles ne le sont pas (❌). Cet endpoint ne mute jamais la session et ne débloque jamais le jeu. - Phase 2 —
/finale/validate: vérifie le hash global de la concaténation des cinq réponses. C'est ce seul endpoint qui débloque le hub de jeux.
Cette séparation apporte une vraie amélioration UX — l'étudiant peut corriger uniquement les questions fausses au lieu de tout ressaisir — sans compromettre la sécurité : un attaquant qui brute-forcerait les 25 réponses individuellement ne pourrait toujours pas contourner la phase 2, qui exige le hash global. Le cout d'une attaque brute-force reste lié à la combinatoire des cinq questions ensemble.
Endpoint POST /pedagogique/logique/check¶
@PostMapping("/pedagogique/logique/check")
public ResponseEntity<?> checkLogiqueAnswers(
@RequestBody Map<String, Object> body,
@RequestHeader(value = STUDENT_TOKEN_HEADER, required = false)
String studentToken) {
// vérifs token...
List<String> submittedHashes = (List<String>) body.get("answerHashes");
if (submittedHashes == null || submittedHashes.size() != 5)
return ResponseEntity.badRequest().body(...);
int[] indices = /* depuis session */;
List<Boolean> results = new ArrayList<>(5);
for (int i = 0; i < 5; i++) {
int idx = indices[i];
String correctAnswer = LOGIQUE_ANSWERS[idx];
String correctHash = sha256(correctAnswer);
String submittedHash = submittedHashes.get(i);
results.add(submittedHash != null
&& correctHash.equalsIgnoreCase(submittedHash));
}
return ResponseEntity.ok(Map.of("results", results));
}
| Propriété | Valeur |
|---|---|
| Méthode | POST |
| URL | /api/chaos/student/pedagogique/logique/check |
| Auth | Header X-Student-Token |
| Body | {"answerHashes": ["hash1","hash2","hash3","hash4","hash5"]} |
| Réponse 200 | {"results": [true, false, true, true, false]} |
| Effets de bord | Aucun — lecture seule, pas de mutation de session |
La comparaison est faite hash par hash : les bonnes réponses ne transitent jamais en clair, ni dans la requête, ni dans la réponse. L'attaquant qui intercepte le trafic ne voit que des hashes SHA-256.
Séquence complète V39¶
sequenceDiagram
autonumber
actor E as Étudiant
participant UI as ThemeLogique.jsx
participant API as sha256hex()
participant CTR as ChaosStudentController
Note over UI: Mount — fetch questions
UI->>CTR: GET /pedagogique/logique/questions
CTR-->>UI: {questions: [{text,hint,index}×5]}
UI->>UI: Render 5 inputs + 5 boutons hint
E->>UI: Remplit les 5 champs
E->>UI: Clique "Valider tout"
Note over UI: Phase 1 — check individuel
loop Pour chaque réponse
UI->>API: sha256hex(answer[i])
end
UI->>CTR: POST /logique/check<br/>{answerHashes: [h1,...,h5]}
CTR->>CTR: Pour chaque i :<br/>compare sha256(LOGIQUE_ANSWERS[indices[i]])<br/>avec hash[i]
CTR-->>UI: {results: [true,false,true,true,false]}
UI->>UI: setResults([true,false,true,true,false])
UI->>E: ✅ ❌ ✅ ✅ ❌ affichés
alt Au moins une réponse fausse
UI->>E: "2 réponses incorrectes"
Note over E: L'étudiant corrige<br/>les questions fausses
E->>UI: Modifie les réponses KO
UI->>UI: setResults : neutralise ❌ des champs modifiés
E->>UI: Re-clique "Valider tout"
Note over UI: Retour au début de la Phase 1
else Toutes correctes
Note over UI: Phase 2 — validation finale
UI->>UI: combined = concat(5 réponses normalisées)
UI->>API: sha256hex(combined)
UI->>CTR: POST /finale/validate<br/>{theme:"logique", answerHash:"..."}
CTR->>CTR: compare session.logiqueExpectedHash
CTR-->>UI: {valid:true, gameUrl}
UI->>E: "Excellent !" puis onSuccess(gameUrl)
end
UX de correction en place¶
Le composant ThemeLogique.jsx implémente un détail d'UX important :
quand l'étudiant modifie une réponse marquée fausse, l'indicateur
❌ disparaît automatiquement et la bordure rouge repasse en neutre.
Ce comportement signale clairement que la réponse est « en cours de
correction » et n'a pas encore été revérifiée :
const setAnswer = (i, v) => {
setAnswers(prev => { const n = [...prev]; n[i] = v; return n; });
setResults(prev => {
if (!prev || prev[i] !== false) return prev;
const n = [...prev];
n[i] = null; // neutralise l'indicateur
return n;
});
};
Les indicateurs des réponses correctes (✅) ne sont pas neutralisés quand l'étudiant modifie d'autres champs — elles restent vertes tant que l'étudiant ne touche pas à ces inputs précis.
Sécurité du thème Logique¶
L'architecture V39 résiste au brute-force de plusieurs façons :
| Attaque | Défense |
|---|---|
| Deviner une seule réponse | /logique/check est silencieux mais ne débloque rien |
| Enumérer toutes les combinaisons de 5 réponses | /finale/validate attend le hash de la concaténation exacte — impossible de composer un hash global à partir des hashes individuels |
Récupérer les réponses via /logique/questions |
L'endpoint ne retourne que text et hint, jamais answer |
| Récupérer les hashes attendus | Aucun endpoint n'expose LOGIQUE_ANSWERS — les hashes sont calculés à la volée |
| Intercepter le trafic | Toutes les réponses sont hashées en SHA-256 avant envoi |
| Rejouer une validation volée | Le token est lié à une session unique — une URL /s/:token cesse de fonctionner à la désactivation du parcours |
Par construction, la seule façon de débloquer le hub par le thème Logique est de connaître réellement les 5 bonnes réponses et de les ressaisir correctement.
Redirection vers le hub¶
Quelle que soit le thème choisi, une validation réussie
({"valid": true}) inclut un champ gameUrl que le composant utilise
pour rediriger l'étudiant :
setTimeout(() => onSuccess(d.gameUrl), 1800);
// Dans PedagogiqueSucces
const handleSuccess = (gameUrl) => {
if (gameUrl) window.location.href = gameUrl;
};
Le délai de 1800 ms laisse l'étudiant voir le message de succès avant
la navigation. L'URL cible est injectée côté backend via une variable
d'environnement — les détails d'hébergement, de nom de domaine et de
port du hub ne sont pas documentés dans cette référence technique.
Du point de vue de PerfShop, tout ce qui se passe après
window.location.href est hors périmètre.
Ajouter un nouveau thème¶
L'ajout d'un sixième thème consiste à :
- Créer
frontend/src/pedagogique/themes/ThemeNewTheme.jsxexportant un composant par défaut avec la signature({ token, onSuccess }) => JSX - Ajouter une entrée dans
THEMESdu fichierthemes/index.jsavecid,labelKey,descKey,color, et une fonctionloadpointant vers l'import()dynamique - Ajouter la constante au tableau
THEMES_LISTexporté - Ajouter les clés i18n
themes.newtheme.labeletthemes.newtheme.descriptiondansfr.jsoneten.json - Ajouter un
case "newtheme" -> sha256("…")dansChaosStudentController.validateFinale()
Aucune migration de base de données n'est nécessaire — le thème est
entièrement stateless côté backend (à l'exception du cas Logique qui
utilise les colonnes logique_question_indices et
logique_expected_hash déjà présentes).
Pages précédentes : ← Page de succès · ← Concept et architecture · Retour au sommaire pédagogique