Skip to content

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:

[
  {"text": "…", "hint": "…"},
  {"text": "…", "hint": "…"},
   (23 others)
]

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 LinkedHashSet guarantees 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 1103515245 and the offset 12345 are 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:

  1. 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.
  2. 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:

  1. Create frontend/src/pedagogique/themes/ThemeNewTheme.jsx exporting a default component with the signature ({ token, onSuccess }) => JSX
  2. Add an entry in THEMES of the themes/index.js file with id, labelKey, descKey, color, and a load function pointing to the dynamic import()
  3. Add the constant to the exported THEMES_LIST array
  4. Add the i18n keys themes.newtheme.label and themes.newtheme.description in fr.json and en.json
  5. Add a case "newtheme" -> sha256("…") in ChaosStudentController.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