Page de succès¶
Quand l'étudiant valide la dernière étape DYNAMIC de son parcours, le
PedagogiqueOrchestrator navigue automatiquement vers une URL de la
forme /s/:token. Cette page — la page de succès — est entièrement
hors de la navbar et du footer habituels de la boutique PerfShop : elle
occupe tout l'écran avec un fond dégradé sombre, un grand affichage
d'étoiles, les statistiques du parcours et un sélecteur de thème final.
Cette page documente son rendu, ses appels API, et son rôle dans le
cycle de vie d'un parcours.
URL et authentification par token¶
L'URL /s/:token utilise le token de session comme seule preuve
d'authentification. Le token est un UUID v4 (non-devinable), présent
uniquement :
- dans le
localStoragedu navigateur de l'étudiant (cléped_student_token) - dans l'URL de la page de succès (paramètre de route
:token) - dans la base
pedagogique_sessions(clé primairetoken)
Cette approche a plusieurs propriétés utiles :
- Pas besoin de session HTTP : l'étudiant peut partager l'URL à un camarade ou la rouvrir depuis un autre navigateur, et la page s'affiche si et seulement si le token existe et correspond à un parcours complété.
- Expiration naturelle : quand le formateur clique sur Désactiver,
clearAll()supprime toutes les sessions de la base — toutes les URLs/s/:tokenrenvoient alors404. - Contrôle fin : un étudiant ayant perdu son
localStoragene retrouve pas sa page de succès, ce qui est voulu — les scores sont transitoires et liés à la vie de la session formateur.
Flux de navigation¶
sequenceDiagram
autonumber
actor E as Étudiant
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: Valide la dernière étape
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 détecte 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
Points à retenir :
- Le token n'est pas dans la réponse de
/validate: il est déjà dans lelocalStorageet dans l'URL cible construite par le hook (state.successUrl = '/s/' + tok). L'overlay n'a donc pas besoin de connaître le token. - Un garde-fou
navigatedRefempêche lePedagogiqueOrchestratorde naviguer plus d'une fois, même si plusieurs polls arrivent en rafale aveccompleted:true. - Si
successUrlest absent (token perdu entre le join et la completion), l'orchestrator affiche un message discret « parcours terminé, rechargez la page » au lieu de laisser un écran blanc.
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()
));
}
| Propriété | Valeur |
|---|---|
| Méthode | GET |
| URL | /api/chaos/student/pedagogique/succes/{token} |
| Authentification | Aucune (le token {token} est l'auth) |
| Réponse 200 | Voir tableau ci-dessous |
| Réponse 404 | Token inconnu, ou parcours non complété |
Structure de la réponse¶
| Champ | Type | Description |
|---|---|---|
alias |
string |
Alias saisi au /join, ou Agent-XXXX si anonyme |
level |
string |
Identifiant du niveau (bac1…bac5) |
stars |
int |
Étoiles obtenues (incluant le malus hors temps) |
maxStars |
int |
Étoiles maximales pour ce niveau |
durationSeconds |
long |
Temps effectif entre joinedAt et completedAt |
completedAt |
long |
Timestamp Unix en millisecondes |
totalSteps |
int |
Nombre total d'étapes du niveau |
Pourquoi renvoyer totalSteps
Le frontend l'utilise pour afficher un récapitulatif « 20 étapes
résolues en 42 min ». Comme level est présent, il pourrait
le calculer côté client, mais ce serait dupliquer la logique du
catalogue Java — le serveur reste source unique.
Composant PedagogiqueSucces.jsx¶
Le composant, monté sur la route /s/:token de react-router, orchestre
trois états de rendu :
- Chargement — affiche un spinner pendant le fetch de
/pedagogique/succes/{token}. - Erreur — si le token est invalide (404), affiche un cadenas 🔒
et un lien vers
/products. - Succès sans thème — affiche les étoiles, les statistiques, et les cartes des cinq thèmes à choisir.
- Succès avec thème sélectionné — charge dynamiquement le
composant
Theme*.jsxcorrespondant et le rend à la place des cartes (gardant les étoiles visibles en haut).
Hiérarchie React¶
flowchart TD
ROOT["PedagogiqueSucces"]
SHELL["PageShell<br/>(dégradé sombre plein écran)"]
STARS["SuccesStars<br/>(étoiles + métriques)"]
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
Le composant racine est contenu dans un PageShell qui impose un
dégradé sombre plein écran (#0d0d20 → #0d1a2e), une carte centrée
de 560 px de large avec bordure verte tenue (rgba(52,211,153,0.25))
et un blur de fond. Cette esthétique « terminal futuriste » marque
visuellement la rupture avec le reste de la boutique — l'étudiant sait
qu'il est « sorti » du tunnel classique.
Lazy-loading des thèmes¶
Les cinq modules de thème sont chargés à la demande via
import() dynamique déclaré dans 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'), ... },
};
L'étudiant qui choisit Logique ne télécharge pas le JS des quatre autres thèmes. Cette séparation accélère la première peinture de la page et permet d'ajouter un 6e thème plus tard sans alourdir le bundle principal.
Changement de thème¶
Un bouton « Changer de thème » sous le composant chargé permet à l'étudiant de revenir à la grille de cartes sans perdre sa session. Cette option est utile si l'étudiant découvre qu'un thème ne l'intéresse pas ou qu'il bloque sur une question — il peut retenter avec un autre thème tant que la session existe en base.
Composant SuccesStars¶
Le bloc d'étoiles du haut est un sous-composant dédié qui reçoit directement les données de l'API :
<SuccesStars
alias={data.alias}
level={data.level}
stars={data.stars}
maxStars={data.maxStars}
durationSeconds={data.durationSeconds}
totalSteps={data.totalSteps}
/>
Il affiche :
- L'alias de l'agent en grand (
Agent-3F7Eou le nom saisi) - Le niveau atteint (
bac3) formaté en libellé lisible - Une rangée d'étoiles :
starspleines (⭐) +(maxStars - stars)vides (☆) - La durée formatée (
42 min 17 s) - Le ratio étapes résolues (
20 / 20)
L'esthétique reprend la palette verte (#34d399) du mode « chaos » de
la boutique, avec une ombre portée sur les étoiles pleines pour
l'emphase.
Hub de mini-jeux¶
En bas de la page de succès, une fois qu'un thème final est validé,
l'étudiant est redirigé vers un hub de mini-jeux pédagogiques
déployé à part. La redirection est déclenchée par la réponse de
POST /pedagogique/finale/validate qui contient un champ gameUrl.
Le frontend effectue un window.location.href = gameUrl pour quitter
l'application PerfShop principale.
La documentation technique du hub lui-même n'est pas exposée dans cette
référence. L'URL cible est injectée côté backend via la variable
d'environnement PUBLIC_GAMES_URL — un administrateur souhaitant
désactiver l'accès au hub peut positionner cette variable sur une URL
de fallback (page d'information, page d'erreur contrôlée, etc.).
Avant validation d'un thème
Tant que l'étudiant n'a pas validé de thème final, la page de succès affiche simplement le récapitulatif et les cinq cartes. La notion de hub n'apparaît pas dans l'UI — l'étudiant doit d'abord choisir un thème et résoudre son énigme.
Gestion des erreurs¶
Le composant gère trois cas d'erreur :
| Cas | Cause | Affichage |
|---|---|---|
error.unauthorized |
GET /succes/{token} retourne 404 |
Cadenas 🔒, message « Lien invalide », bouton retour catalogue |
theme.loadError |
L'import() dynamique du Theme*.jsx échoue (réseau, cache) |
Message « Erreur de chargement » + bouton retour sélecteur de thème |
| Thème KO | La validation finale est refusée (hash invalide) | Feedback en rouge dans le composant du thème, l'étudiant peut réessayer |
Le composant gèle le token dans une useRef au premier rendu :
const tokenRef = useRef(tokenParam); const token = tokenRef.current;.
Ce gel évite qu'un changement d'URL en cours de route (improbable mais
possible sur navigation manuelle) ne provoque un nouveau fetch avec un
token différent.
Clés i18n concernées¶
| Clé | Utilisation |
|---|---|
succes.unauthorized |
Titre du cas erreur 404 |
succes.invalidLink |
Sous-titre du cas erreur 404 |
succes.backToCatalog |
Bouton retour |
succes.finalChallenge |
Bandeau au-dessus des cartes de thème |
succes.chooseTheme |
Titre de la grille de thèmes |
succes.oneRiddleLeft |
Sous-titre « une dernière énigme » |
succes.changeTheme |
Bouton « Changer de thème » |
succes.back |
Bouton retour générique |
succes.loadError |
Erreur de chargement du composant Theme |
themes.{id}.label / themes.{id}.description |
Libellé et description de chaque carte |
Toutes les clés sont définies dans frontend/src/i18n/fr.json et son
équivalent en.json. Ajouter une langue à la page de succès consiste
à cloner l'un des deux fichiers et à le traduire.
Pages suivantes : ← Indices · Thèmes finaux →