Success page¶
When the student validates the final DYNAMIC step of their journey,
the PedagogiqueOrchestrator automatically navigates to a URL of the
form /s/:token. This page — the success page — is entirely
outside the usual PerfShop navbar and footer: it takes up the whole
screen with a dark gradient background, a large star display, the
journey statistics and a final theme selector. This page documents its
rendering, its API calls, and its role in the lifecycle of a journey.
URL and token authentication¶
The /s/:token URL uses the session token as the only proof of
authentication. The token is a UUID v4 (non-guessable), present only:
- in the student's browser
localStorage(keyped_student_token) - in the success page URL (route parameter
:token) - in the
pedagogique_sessionsdatabase (primary keytoken)
This approach has several useful properties:
- No need for an HTTP session: the student can share the URL with a classmate or reopen it from another browser, and the page displays if and only if the token exists and corresponds to a completed journey.
- Natural expiration: when the instructor clicks Deactivate,
clearAll()removes all sessions from the database — all/s/:tokenURLs then return404. - Fine-grained control: a student who lost their
localStoragecannot find their success page again, which is intentional — scores are transient and tied to the life of the instructor session.
Navigation flow¶
sequenceDiagram
autonumber
actor E as Student
participant ORC as PedagogiqueOrchestrator
participant OVR as PedagogiqueOverlay
participant HOK as usePedagogiqueState
participant CTR as ChaosStudentController
participant R as react-router
participant SUC as PedagogiqueSucces
E->>OVR: Validates the final step
OVR->>CTR: POST /pedagogique/validate
CTR-->>OVR: {valid:true, completed:true, stars}
OVR->>HOK: refresh()
HOK->>CTR: GET /status
CTR-->>HOK: pedagogique.completed=true
HOK->>HOK: state.successUrl = "/s/<token>"
ORC->>ORC: useEffect detects completed + successUrl
ORC->>R: navigate("/s/<token>")
R->>SUC: mount PedagogiqueSucces
SUC->>CTR: GET /pedagogique/succes/<token>
CTR-->>SUC: {alias, level, stars, duration, ...}
SUC->>SUC: render SuccesStars + ThemeCards
Key points:
- The token is not in the
/validateresponse: it is already in thelocalStorageand in the target URL built by the hook (state.successUrl = '/s/' + tok). The overlay therefore does not need to know the token. - A
navigatedRefsafeguard prevents thePedagogiqueOrchestratorfrom navigating more than once, even if several polls arrive in rapid succession withcompleted:true. - If
successUrlis absent (token lost between the join and completion), the orchestrator displays a discreet "journey finished, reload the page" message instead of leaving a blank screen.
Endpoint GET /pedagogique/succes/{token}¶
@GetMapping("/pedagogique/succes/{token}")
public ResponseEntity<?> getSuccesPage(@PathVariable String token) {
PedagogiqueSessionService.MutableSession session =
pedagogiqueSessionService.getSession(token);
if (session == null)
return ResponseEntity.status(404).body(Map.of("error",
i18n.t("student.pedagogique.error.session_not_found")));
List<PedagogiqueEnigme.Enigme> enigmes = pedagogiqueLevel != null
? PedagogiqueEnigme.forLevel(pedagogiqueLevel) : List.of();
boolean completed = session.completedAt > 0
&& session.step.get() >= enigmes.size();
if (!completed)
return ResponseEntity.status(404).body(Map.of("error",
i18n.t("student.pedagogique.error.not_completed")));
long durationSeconds = (session.completedAt - session.joinedAt) / 1000L;
int stars = computeStars(pedagogiqueLevel,
durationSeconds <= pedagogiqueTimer);
int maxStars = computeStars(pedagogiqueLevel, true);
return ResponseEntity.ok(Map.of(
"alias", session.displayAlias(),
"level", pedagogiqueLevel != null ? pedagogiqueLevel : "",
"stars", stars,
"maxStars", maxStars,
"durationSeconds", durationSeconds,
"completedAt", session.completedAt,
"totalSteps", enigmes.size()
));
}
| Property | Value |
|---|---|
| Method | GET |
| URL | /api/chaos/student/pedagogique/succes/{token} |
| Authentication | None (the {token} is the auth) |
| Response 200 | See table below |
| Response 404 | Unknown token, or journey not completed |
Response structure¶
| Field | Type | Description |
|---|---|---|
alias |
string |
Alias entered at /join, or Agent-XXXX if anonymous |
level |
string |
Level identifier (bac1…bac5) |
stars |
int |
Stars obtained (including the overrun penalty) |
maxStars |
int |
Maximum stars for this level |
durationSeconds |
long |
Effective time between joinedAt and completedAt |
completedAt |
long |
Unix timestamp in milliseconds |
totalSteps |
int |
Total number of steps in the level |
Why return totalSteps
The frontend uses it to display a summary "20 steps solved in
42 min". Since level is present, it could compute it
client-side, but that would duplicate the Java catalog logic —
the server remains the single source of truth.
PedagogiqueSucces.jsx component¶
The component, mounted on the /s/:token route of react-router,
orchestrates three rendering states:
- Loading — displays a spinner while fetching
/pedagogique/succes/{token}. - Error — if the token is invalid (404), displays a padlock 🔒
and a link to
/products. - Success without theme — displays the stars, statistics, and the cards of the five themes to choose from.
- Success with theme selected — dynamically loads the
corresponding
Theme*.jsxcomponent and renders it in place of the cards (keeping the stars visible at the top).
React hierarchy¶
flowchart TD
ROOT["PedagogiqueSucces"]
SHELL["PageShell<br/>(full-screen dark gradient)"]
STARS["SuccesStars<br/>(stars + metrics)"]
SEP["separator"]
CARDS["Grid 2×3<br/>ThemeCard × 5"]
THEME["ThemeComponent<br/>(lazy-loaded)"]
SPIN["Spinner"]
ROOT --> SHELL
SHELL --> STARS
STARS -.-> SEP
SEP -.-> CARDS
SEP -.-> THEME
SHELL -.-> SPIN
The root component is contained in a PageShell that imposes a
full-screen dark gradient (#0d0d20 → #0d1a2e), a centered card
560 px wide with a tight green border
(rgba(52,211,153,0.25)) and a background blur. This "futuristic
terminal" aesthetic visually marks the break with the rest of the
store — the student knows they have "exited" the classic funnel.
Lazy-loading of themes¶
The five theme modules are loaded on demand via dynamic import()
declared in themes/index.js:
export const THEMES = {
performance: { id: 'performance', load: () => import('./ThemePerformance.jsx'), ... },
fonctionnel: { id: 'fonctionnel', load: () => import('./ThemeFonctionnel.jsx'), ... },
metier: { id: 'metier', load: () => import('./ThemeMetier.jsx'), ... },
securite: { id: 'securite', load: () => import('./ThemeSecurite.jsx'), ... },
logique: { id: 'logique', load: () => import('./ThemeLogique.jsx'), ... },
};
A student who chooses Logic does not download the JS of the four other themes. This separation speeds up the first paint of the page and allows adding a 6th theme later without bloating the main bundle.
Switching themes¶
A "Change theme" button below the loaded component lets the student return to the card grid without losing their session. This option is useful if the student discovers that a theme does not interest them or that they are stuck on a question — they can retry with another theme as long as the session exists in the database.
SuccesStars component¶
The top star block is a dedicated subcomponent that directly receives the API data:
<SuccesStars
alias={data.alias}
level={data.level}
stars={data.stars}
maxStars={data.maxStars}
durationSeconds={data.durationSeconds}
totalSteps={data.totalSteps}
/>
It displays:
- The agent alias in large (
Agent-3F7Eor the entered name) - The level reached (
bac3) formatted as a readable label - A row of stars:
starsfilled (⭐) +(maxStars - stars)empty (☆) - The formatted duration (
42 min 17 s) - The ratio of steps solved (
20 / 20)
The aesthetic reuses the green palette (#34d399) of the store's
"chaos" mode, with a drop shadow on the filled stars for emphasis.
Mini-games hub¶
At the bottom of the success page, once a final theme is validated,
the student is redirected to a pedagogical mini-games hub
deployed separately. The redirection is triggered by the response of
POST /pedagogique/finale/validate which contains a gameUrl field.
The frontend performs a window.location.href = gameUrl to leave
the main PerfShop application.
The technical documentation of the hub itself is not exposed in this
reference. The target URL is injected backend-side via the
PUBLIC_GAMES_URL environment variable — an administrator wishing
to disable access to the hub can set this variable to a fallback URL
(info page, controlled error page, etc.).
Before validating a theme
As long as the student has not validated a final theme, the success page simply displays the summary and the five cards. The notion of a hub does not appear in the UI — the student must first choose a theme and solve its enigma.
Error handling¶
The component handles three error cases:
| Case | Cause | Display |
|---|---|---|
error.unauthorized |
GET /succes/{token} returns 404 |
Padlock 🔒, "Invalid link" message, back-to-catalog button |
theme.loadError |
The dynamic import() of Theme*.jsx fails (network, cache) |
"Loading error" message + back-to-theme-selector button |
| Theme KO | Final validation is rejected (invalid hash) | Red feedback in the theme component, the student can retry |
The component freezes the token in a useRef at first render:
const tokenRef = useRef(tokenParam); const token = tokenRef.current;.
This freezing prevents a URL change mid-route (unlikely but possible
on manual navigation) from causing a new fetch with a different
token.
Relevant i18n keys¶
| Key | Usage |
|---|---|
succes.unauthorized |
Title of the 404 error case |
succes.invalidLink |
Subtitle of the 404 error case |
succes.backToCatalog |
Back button |
succes.finalChallenge |
Banner above the theme cards |
succes.chooseTheme |
Title of the theme grid |
succes.oneRiddleLeft |
Subtitle "one last enigma" |
succes.changeTheme |
"Change theme" button |
succes.back |
Generic back button |
succes.loadError |
Theme component loading error |
themes.{id}.label / themes.{id}.description |
Label and description of each card |
All keys are defined in frontend/src/i18n/fr.json and its equivalent
en.json. Adding a language to the success page consists of cloning
one of the two files and translating it.
Next pages: ← Hints · Final themes →