Skip to content

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 (key ped_student_token)
  • in the success page URL (route parameter :token)
  • in the pedagogique_sessions database (primary key token)

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/:token URLs then return 404.
  • Fine-grained control: a student who lost their localStorage cannot find their success page again, which is intentional — scores are transient and tied to the life of the instructor session.
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 /validate response: it is already in the localStorage and in the target URL built by the hook (state.successUrl = '/s/' + tok). The overlay therefore does not need to know the token.
  • A navigatedRef safeguard prevents the PedagogiqueOrchestrator from navigating more than once, even if several polls arrive in rapid succession with completed:true.
  • If successUrl is 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 (bac1bac5)
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:

  1. Loading — displays a spinner while fetching /pedagogique/succes/{token}.
  2. Error — if the token is invalid (404), displays a padlock 🔒 and a link to /products.
  3. Success without theme — displays the stars, statistics, and the cards of the five themes to choose from.
  4. Success with theme selected — dynamically loads the corresponding Theme*.jsx component 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-3F7E or the entered name)
  • The level reached (bac3) formatted as a readable label
  • A row of stars: stars filled (⭐) + (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 →