Final themes¶
Once the main journey (100 enigmas across 5 levels) is completed, the student is redirected to the success page which offers five themes for a final enigma. Each theme is a self-contained React component loaded dynamically that validates its answer against a server-side SHA-256 hash. Successfully completing a theme unlocks access to a pedagogical mini-games hub.
This page describes the five themes, their common mechanics, and dedicates a section to the Logic & Math theme which uses a specific two-phase architecture (V38 and V39).
Overview of the five themes¶
| Theme | Id | Component | Color | Icon | Focus |
|---|---|---|---|---|---|
| Performance | performance |
ThemePerformance.jsx |
#34d399 (green) |
⚡ | Metrics, throughput, latency |
| Functional | fonctionnel |
ThemeFonctionnel.jsx |
#60a5fa (blue) |
🔧 | Exceptions, application chaos |
| Business | metier |
ThemeMetier.jsx |
#f59e0b (orange) |
💼 | Commercial anomalies, VAT |
| Security | securite |
ThemeSecurite.jsx |
#f87171 (red) |
🔒 | OWASP, web vulnerabilities |
| Logic & Math | logique |
ThemeLogique.jsx |
#a78bfa (purple) |
🧠 | Pool of 25 questions, deterministic draw |
The order in the success page grid is fixed by THEMES_LIST in
frontend/src/pedagogique/themes/index.js — the four "concept"
themes first (performance, fonctionnel, metier, securite) and Logic
fifth. Labels and descriptions are resolved via the i18n keys
themes.<id>.label and themes.<id>.description.
Common mechanics of the four "concept" themes¶
The Performance, Functional, Business and Security themes share the
same structure: a statement illustrated by a concrete example, a
💡 button that expands three progressive hints, an answer field, and
a validation button that directly calls
POST /pedagogique/finale/validate.
sequenceDiagram
autonumber
actor E as Student
participant UI as Theme{Concept}.jsx
participant API as sha256hex()
participant CTR as ChaosStudentController
E->>UI: Reads the statement and hints
E->>UI: Types an answer<br/>(1 concept keyword)
E->>UI: Clicks "Validate"
UI->>API: sha256hex(answer)
API-->>UI: 64-char hex hash
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: "Wrong answer"
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) {
// token checks...
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; // or fallback
default -> "";
};
if (expectedHash.isEmpty())
return ResponseEntity.badRequest().body(...);
if (!expectedHash.equalsIgnoreCase(submittedHash)) {
return ResponseEntity.ok(Map.of("valid", false));
}
String targetUrl = /* hub URL — read from the environment */;
return ResponseEntity.ok(Map.of("valid", true, "gameUrl", targetUrl));
}
| Property | Value |
|---|---|
| Method | POST |
| URL | /api/chaos/student/pedagogique/finale/validate |
| Authentication | Header X-Student-Token |
| Body | {"theme": "<id>", "answerHash": "<sha256 hex>"} |
| Response 200 OK | {"valid": true, "gameUrl": "..."} or {"valid": false} |
| Response 400 | Unknown theme |
| Response 401 | Missing or invalid token |
The four keywords of the "concept" themes are defined in clear in
the backend source code (ChaosStudentController). They are not
exposed via the API and are not stored in the database; only the
hash computed on the fly is used for comparison. This documentation
does not disclose them: each theme has a strong hint in its statement
and a series of progressive hints accessible via the 💡 button.
The Performance theme¶
- React class:
ThemePerformance.jsx - Dominant color: green
#34d399 - Structure: a three-paragraph statement (illustration, observation, question), three hidable hints, a one-word answer field, a validation button.
The theme invites the student to reflect on a central concept of performance testing. The expected indicator is not a number but a business vocabulary keyword — the kind of term read on a Grafana dashboard or in a load testing spec.
The three hints of the 💡 button progress from the most abstract
("what we measure to know how much the platform can handle") to the
most specific ("the English term we see on Grafana metrics"). All
texts are externalized in i18n keys theme.perf.*.
The Functional theme¶
- React class:
ThemeFonctionnel.jsx - Dominant color: blue
#60a5fa - Structure: statement illustrating a classic JVM incident, three hints, free field.
The Functional theme refers to PerfShop's functional chaos (F1 to
F4 — see chaos/functional.md). It asks
the student to identify one of the terminal JVM exceptions that
Functional Chaos can trigger. The provided context (reduced trace,
error type, log line) is designed to recall what the student may
have observed during the journey if they activated the corresponding
chaos.
The i18n keys are theme.fonc.*.
The Business theme¶
- React class:
ThemeMetier.jsx - Dominant color: orange
#f59e0b - Structure: four-paragraph statement (commercial context, numeric example, observation, question), three hints including a letter enumeration hint ("the letters are: T, V, A").
The Business theme requires a short three-letter answer. The choice of enumeration as the ultimate hint is deliberate: it guarantees that any stuck student gets enough info to conclude, while still forcing reflection if the first two hints are ignored.
The i18n keys are theme.metier.*.
The Security theme¶
- React class:
ThemeSecurite.jsx - Dominant color: red
#f87171 - Structure: statement including a concrete API call
(
/api/orders/42), three hints including one by letter enumeration, free field.
The Security theme refers to a well-known family of flaws from OWASP lists. As with the Business theme, one of the hints lists the letters of the answer in random order — pedagogy: the student who sees them cannot guess the answer without recognizing the concept, they must assemble the letters mentally.
The i18n keys are theme.secu.*.
The Logic & Math theme¶
The Logic theme is the only theme that offers five questions rather than one, and the only one that uses a two-phase architecture specifically designed to improve user experience without compromising security.
Pool of 25 questions and deterministic draw¶
The backend contains a pool of 25 logic and math questions
externalized in i18n/logique/logique_<lang>.json:
Each entry has two fields: text (question statement) and hint
(short hint, displayable on demand). The expected answers are
not in the JSON file — they are in a Java LOGIQUE_ANSWERS
array of DefaultPedagogiqueSessionService, indexed in the same
order as the file:
public static final String[] LOGIQUE_ANSWERS = {
"…", "…", "…", "…", "…", // [0-4]
"…", "…", "…", "…", "…", // [5-9]
"…", "…", "…", "…", "…", // [10-14]
"…", "…", "…", "…", "…", // [15-19]
"…", "…", "…", "…", "…" // [20-24]
};
Each student receives five questions drawn from the pool via a deterministic pseudo-random generator seeded on their UUID token. The algorithm is an LCG (Linear Congruential Generator) identical in Java and 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;
}
Draw properties:
- Deterministic: two students with the same token get the same 5 questions in the same order.
- Distinct: using a
LinkedHashSetguarantees that the 5 indices are unique. - Uniformly distributed: over a class of 30 students, the five draws statistically cover most of the pool.
- Identical in JavaScript: the constant
1103515245and the offset12345are those of the glibc LCG, reproducible byte-for-byte in JS ((seed * 1103515245 + 12345) & 0x7fffffff).
V38 architecture — computation at /join¶
Before version V38, the frontend executed the LCG itself to choose
its 5 questions from the token read in localStorage. This pattern
posed a subtle problem: if the token stored in localStorage
differed from the token used for the active session (for example
after a partial reset, a tab change, a reload after expiration), the
frontend drew 5 questions on one seed, the backend verified 5 other
questions on another seed, and the final validation failed without
the student being able to understand why.
V38 fixes this problem by moving the computation to the
backend at the time of POST /pedagogique/join. The five indices
are computed once, stored in the database in the
logique_question_indices column (format "2,9,15,4,5"), and the
expected hash — concatenation of the five correct answers in the
order of the draw — is stored in logique_expected_hash. The
frontend does no more LCG: it retrieves its five questions via
the GET /pedagogique/logique/questions endpoint.
// DefaultPedagogiqueSessionService.createSession()
int[] logiqueIndices = computeLogiqueIndices(session.token);
String logiqueHash = computeLogiqueHash(logiqueIndices);
session.logiqueQuestionIndices = indicesToString(logiqueIndices);
session.logiqueExpectedHash = logiqueHash;
// ... stored in the database via PedagogiqueSessionEntity
A fallback exists for sessions created before the V38 migration
(NULL columns in the database): ThemeLogique.jsx does not depend
on it, but the logic endpoints recompute the indices on the fly if
needed.
Endpoint GET /pedagogique/logique/questions¶
Returns the five questions — texts and hints only, never the answers.
@GetMapping("/pedagogique/logique/questions")
public ResponseEntity<?> getLogiqueQuestions(
@RequestHeader(value = STUDENT_TOKEN_HEADER, required = false)
String studentToken) {
// token checks...
int[] indices = /* from 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));
}
Typical response:
{
"questions": [
{"index": 2, "text": "…", "hint": "…"},
{"index": 9, "text": "…", "hint": "…"},
{"index": 15, "text": "…", "hint": "…"},
{"index": 4, "text": "…", "hint": "…"},
{"index": 5, "text": "…", "hint": "…"}
]
}
The index field (0-24) allows the frontend to keep a stable
identifier per question, even if the format or order changes in a
future version.
Loading the pool with double-checked locking¶
The loadLogiquePool() method uses a static cache initialized
lazily via double-checked locking to avoid concurrent loads:
private static volatile String[][] logiquePoolCache = null;
static String[][] loadLogiquePool() {
if (logiquePoolCache != null) return logiquePoolCache;
synchronized (ChaosStudentController.class) {
if (logiquePoolCache != null) return logiquePoolCache;
// loads i18n/logique/logique_<LANG>.json via Jackson
// ...
logiquePoolCache = pool;
return pool;
}
}
Loading happens once on the first call (lazy) — not at startup,
unlike BACx enigmas which are initialized in static in
PedagogiqueEnigme. The volatile guarantees visibility between
threads, and the double-check avoids synchronization once the cache
is warm.
V39 architecture — two-phase validation¶
The Logic theme is the only one of the five themes to use a two-phase validation:
- Phase 1 —
/logique/check: checks the five answers individually and returns a boolean per question. The student immediately sees which answers are correct (✅) and which are not (❌). This endpoint never mutates the session and never unlocks the game. - Phase 2 —
/finale/validate: verifies the global hash of the concatenation of the five answers. This is the only endpoint that unlocks the games hub.
This separation brings a real UX improvement — the student can correct only the wrong questions instead of re-entering everything — without compromising security: an attacker who would brute-force the 25 answers individually still could not bypass phase 2, which requires the global hash. The cost of a brute-force attack remains tied to the combinatorics of the five questions together.
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) {
// token checks...
List<String> submittedHashes = (List<String>) body.get("answerHashes");
if (submittedHashes == null || submittedHashes.size() != 5)
return ResponseEntity.badRequest().body(...);
int[] indices = /* from 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));
}
| Property | Value |
|---|---|
| Method | POST |
| URL | /api/chaos/student/pedagogique/logique/check |
| Auth | Header X-Student-Token |
| Body | {"answerHashes": ["hash1","hash2","hash3","hash4","hash5"]} |
| Response 200 | {"results": [true, false, true, true, false]} |
| Side effects | None — read-only, no session mutation |
The comparison is done hash by hash: the correct answers never travel in clear, neither in the request nor in the response. An attacker intercepting traffic only sees SHA-256 hashes.
Complete V39 sequence¶
sequenceDiagram
autonumber
actor E as Student
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 hint buttons
E->>UI: Fills the 5 fields
E->>UI: Clicks "Validate all"
Note over UI: Phase 1 — individual check
loop For each answer
UI->>API: sha256hex(answer[i])
end
UI->>CTR: POST /logique/check<br/>{answerHashes: [h1,...,h5]}
CTR->>CTR: For each i:<br/>compare sha256(LOGIQUE_ANSWERS[indices[i]])<br/>with hash[i]
CTR-->>UI: {results: [true,false,true,true,false]}
UI->>UI: setResults([true,false,true,true,false])
UI->>E: ✅ ❌ ✅ ✅ ❌ displayed
alt At least one wrong answer
UI->>E: "2 wrong answers"
Note over E: The student corrects<br/>the wrong questions
E->>UI: Modifies the KO answers
UI->>UI: setResults: neutralizes ❌ of modified fields
E->>UI: Re-clicks "Validate all"
Note over UI: Back to the beginning of Phase 1
else All correct
Note over UI: Phase 2 — final validation
UI->>UI: combined = concat(5 normalized answers)
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!" then onSuccess(gameUrl)
end
In-place correction UX¶
The ThemeLogique.jsx component implements an important UX detail:
when the student modifies an answer marked wrong, the ❌
indicator disappears automatically and the red border returns to
neutral. This behavior clearly signals that the answer is "being
corrected" and has not yet been rechecked:
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; // neutralizes the indicator
return n;
});
};
The indicators of correct answers (✅) are not neutralized when the student modifies other fields — they stay green as long as the student does not touch those specific inputs.
Logic theme security¶
The V39 architecture resists brute-force attacks in several ways:
| Attack | Defense |
|---|---|
| Guess a single answer | /logique/check is silent but unlocks nothing |
| Enumerate all combinations of 5 answers | /finale/validate expects the hash of the exact concatenation — impossible to compose a global hash from individual hashes |
Retrieve the answers via /logique/questions |
The endpoint only returns text and hint, never answer |
| Retrieve the expected hashes | No endpoint exposes LOGIQUE_ANSWERS — hashes are computed on the fly |
| Intercept traffic | All answers are SHA-256 hashed before sending |
| Replay a stolen validation | The token is tied to a unique session — a /s/:token URL stops working at journey deactivation |
By construction, the only way to unlock the hub through the Logic theme is to actually know the 5 correct answers and to re-enter them correctly.
Redirection to the hub¶
Regardless of the chosen theme, a successful validation
({"valid": true}) includes a gameUrl field that the component
uses to redirect the student:
setTimeout(() => onSuccess(d.gameUrl), 1800);
// In PedagogiqueSucces
const handleSuccess = (gameUrl) => {
if (gameUrl) window.location.href = gameUrl;
};
The 1800 ms delay lets the student see the success message before
navigation. The target URL is injected backend-side via an
environment variable — hosting, domain name and port details of the
hub are not documented in this technical reference. From
PerfShop's point of view, everything that happens after
window.location.href is out of scope.
Adding a new theme¶
Adding a sixth theme consists of:
- Create
frontend/src/pedagogique/themes/ThemeNewTheme.jsxexporting a default component with the signature({ token, onSuccess }) => JSX - Add an entry in
THEMESof thethemes/index.jsfile withid,labelKey,descKey,color, and aloadfunction pointing to the dynamicimport() - Add the constant to the exported
THEMES_LISTarray - Add the i18n keys
themes.newtheme.labelandthemes.newtheme.descriptioninfr.jsonanden.json - Add a
case "newtheme" -> sha256("…")inChaosStudentController.validateFinale()
No database migration is necessary — the theme is entirely stateless
backend-side (except for the Logic case which uses the existing
logique_question_indices and logique_expected_hash columns).
Previous pages: ← Success page · ← Concept and architecture · Back to pedagogical summary