Flux métier
Les workflows critiques du Board : lancement d'atelier, distribution et gestion des lots, undo/redo, collaboration temps réel.
Lancement d'atelier et chargement initial
Détails :
- Animateur clique le bouton « Animer » dans la fiche atelier LeHub.
- Le Hub génère un JWT signé avec
BOARD_ADMIN_JWT_SECRET, inclutaud: 'board-admin'et l'ID animateur. - Redirection vers
https://board.fresquesystemique.org/b/[token]?admin=<jwt>. - LeBoard fetch l'état initial via
GET /api/board/[token](validation JWT + board access). - Rendu page React avec état initial (cartes, post-its, étape active, etc.).
- Socket.io connect depuis le client → création
AnimatorSessioncôté serveur (durée 24h). - Broadcast
board-stateà la connexion pour synchroniser l'état avec tous les participants actuels.
Participant rejoint le plateau
- Participant reçoit/clique URL publique
https://board.fresquesystemique.org/b/[token]. - LeBoard valide le token (lookup Board par token).
- Page charge avec l'état initial board (cartes déjà posées, post-its, étape masquée).
- Modal
PseudoModaldemande le pseudo (ou utilise pseudo stocké localement). - Socket.io connect → broadcast
board-stateà la connexion pour synchroniser l'état, affichage pseudo + curseur.
Distribution lot (Distribuer)
Détails :
- Animateur clique « Distribuer » pour un lot depuis le
LotPanel. - Émission Socket
lot:distributeavec l'ID lot. - Serveur :
- Récupère le lot du Hub (cardIds JSON).
- Upsert
BoardLotDistributionavecmode='distribute'. - Mélange aléatoire des cardIds.
- Répartition round-robin : participant 1 reçoit cartes 1,3,5,… / participant 2 reçoit 2,4,6,…
- Chaque participant a sa pioche unique (invisible des autres).
- Broadcast
lot:distributedavec payload{[participantId]: [cardIds]}(chiffré par socket room). - Chaque participant voit ses cartes dans le
DeckPanel(bas écran, max 5 visibles).
Lancement lot (Pop-corn)
Détails :
- Animateur clique « Pop-corn » pour un lot avec matrice (ex: étape "Système A").
- Serveur upsert
BoardLotDistributionavecmode='popcorn'. - Pour chaque carte du lot avec mapping matrice (col, row) :
- Récupère la cellule correspondante.
- Calcule position de snap (
snap-to-gridviafindSnapPosition()). - Crée/met à jour
CardPlacementavec (x, y) calculées.
- Broadcast
lot-distributeavec liste cardIds et placements. - Animateur voit les cartes auto-posées dans la grille, verrouillées.
- Les cartes restent dans la main de l'animateur jusqu'à révélation manuelle au clic (toggle
hidden).
Déplacement carte (drag-drop)
Détails :
- Participant drag une carte depuis le plateau.
- Calcul optionnel de snap (si la carte doit rester dans une cellule de matrice).
- Émission
card-moveavec (cardId, x, y). - Serveur UPDATE la
CardPlacementen DB. - Broadcast
card-moveà tous les autres (le client émetteur anticipe via optimistic update, donc pas de re-broadcast). - Affichage temps réel (~< 100ms latence socket).
Émergences indésirables
Certains lots sont marqués kind='emergence' au Hub. Leurs cartes s'ancrent à une carte de base déjà posée.
- Lot "Émergence Actions" lancé en pop-corn.
- Chaque carte du lot a un
emergenceSlot(1 ou 2) et unanchorCardId. - Serveur calcule position d'émergence via
emergencePosition(anchorCard)+ slot offset. - Cartes s'ancrent à la carte base et la suivent en cas de déplacement.
- Si carte base déplacée →
card:movedémis → serveur recalcule toutes les émergences liées et broadcast.
Flèches de causalité
Mode lien classique (Connection)
- Animateur/participant active l'outil Flèche.
- Clique sur anchorpoint de carte A →
arrowDraftstate. - Clique sur anchorpoint de carte B → création
Connectionen DB. - Broadcast
arrow:created→ rendu KonvaArrow ligne droite.
Mode flèche avancée (BoardArrow)
- Pareil, mais crée un
BoardArrowavec couleur/direction/strokeWidth. - Supprimer flèche : clic droit →
arrow:delete→ suppressionConnectionouBoardArrow.
Undo/Redo
Historique animateur (global)
- Animateur fait action (déplacer carte, créer post-it, etc.).
- Changement enregistré en mémoire dans
undoStack(serveur Socket.io). - Clique Ctrl+Z → serveur cherche l'action précédente, la réverse, broadcast aux autres (lecture seule).
Note : l'undo global animateur impacte le state serveur (vérité), broadcast aux participants qui voient le changement.
Historique participant (personnel)
- Participant fait action.
- Changement enregistré localement (pas de broadcast serveur).
- Ctrl+Z → socket
undo:personal→ serveur marque comme "local-undo" (pas broadcast). - Le changement est annulé localement seulement.
Remarque : LeBoard n'a pas (encore) d'historique persistant stocké en BD. L'undo/redo vit en mémoire serveur et es perdu au redéploiement. C'est acceptable pour un atelier qui dure 2-3h.
Étape active et forçage suivi
Animateur change l'étape
- Animateur clique l'étape suivante dans
BottomNav. - Socket
stage-change→ serveur updateBoard.activeStageId. - Broadcast
stage-changeà tous pour synchroniser l'étape active. - Suivi de viewport (optionnel) : animateur peut envoyer
viewport-changeavec ses coordonnées zoom/pan → les participants en suivi reçoivent et adaptent leur viewport.
Participant suit l'animateur (volontaire)
- Participant clique pseudo de l'animateur dans
ParticipantBar→ émetstart-follow(pseudo de l'animateur). - Serveur broadcast
start-followà tous. - À chaque
viewport-changede l'animateur → les followers s'alignent. - Reclique pseudo pour arrêter (
stop-follow).
Réinitialisation plateau
- Animateur clique bouton Réinitialiser (reset).
- Socket
board-reset→ serveur efface l'état temps réel (cartes, post-its, étape). - Broadcast
board-resetà tous. - Plateau revient à l'état vide, prêt pour un nouvel atelier.
Collaboration temps réel (Socket.io)
Gestion des connexions
connection(joinémis) : création AnimatorSession (si JWT valide) ou simple user join.disconnection: suppressionAnimatorSession, suppression de l'utilisateur de la liste des participants.- Timeout inactivité : 24h → auto-suppression
AnimatorSession.
Événements clés
| Événement | Émetteur | Récepteur | Payload |
|---|---|---|---|
join | Client | Serveur | {pseudo, role, avatarUrl, authSessionId} |
card-move | Client | Serveur | {cardId, x, y} |
card-move | Serveur | Clients | {cardId, x, y} (broadcast) |
card-flip | Client | Serveur | {cardId, flipped} |
card-flip | Serveur | Clients | {cardId, flipped} (broadcast) |
card-drag-start | Client | Serveur | {cardId, pseudo} |
card-drag-end | Client | Serveur | {cardId} |
card-drag-end | Serveur | Clients | {cardId} (broadcast) |
card-placed | Client | Serveur | {cardId, x, y, flipped, zIndex} |
arrow-add | Client | Serveur | {fromId, toId, fromAnchor, toAnchor, ...} |
arrow-add | Serveur | Clients | Données flèche créée (broadcast) |
arrow-update | Client | Serveur | {arrowId, ...updates} |
arrow-update | Serveur | Clients | Données flèche modifiée (broadcast) |
arrow-delete | Client | Serveur | {arrowId} |
arrow-delete | Serveur | Clients | {arrowId} (broadcast) |
lot-distribute | Client (animateur) | Serveur | {lotId} |
deck-updated | Serveur | Clients | {[participantId]: [cardIds]} |
lot-correct-hands | Serveur | Clients | Correction distribution mains |
deck-assigned | Serveur | Clients | {participantId, cardIds} |
board-reset | Client (animateur) | Serveur | {} |
board-state | Serveur | Client | État complet du plateau (connexion) |
cursor-move | Client | Serveur | {pseudo, x, y} (throttlé) |
cursor-move | Serveur | Clients | {pseudo, x, y} (broadcast) |
start-follow | Client | Serveur | {pseudo} |
start-follow | Serveur | Clients | {pseudo} (broadcast) |
stop-follow | Client | Serveur | {} |
stop-follow | Serveur | Clients | {} (broadcast) |
slides-present | Client (animateur) | Serveur | {lotId} |
slides-present | Serveur | Clients | {lotId, slides: [...]} (broadcast) |
slides-navigate | Client | Serveur | {index} |
slides-navigate | Serveur | Clients | {index, pointer: {x, y}} (broadcast) |
slides-pointer | Client (animateur) | Serveur | {x, y} |
slides-pointer | Serveur | Clients | {x, y} (broadcast) |
slides-close | Client | Serveur | {} |
slides-close | Serveur | Clients | {} (broadcast) |
undo | Client (animateur) | Serveur | {} |
undo | Serveur | Clients | Action inversée (broadcast) |
redo | Client (animateur) | Serveur | {} |
redo | Serveur | Clients | Action réexécutée (broadcast) |
role-downgraded | Serveur | Client | {reason: 'session-invalid' | 'no-session'} |
Convention : les événements émis par le client vers le serveur déclenchent un broadcast du serveur aux autres clients avec le même nom ou une variante (ex: card-move côté client → card-move broadcast aux autres). Voir socket-handler.ts pour le détail de chaque handler.
Diapositives (SlideOverlay)
- Lot marqué
contentType='slides'lancé en pop-corn. - Le serveur lit
Lot.slidesData(JSON : images et notes). - Le client affiche
SlideOverlayen plein écran. - Navigation : flèches gauche/droite, clic sur un numéro de diapositive.
- Pointeur partagé : la position de la souris de l'animateur est visible de tous.
- Broadcast
slides:navigate→ mise à jour de l'index et du pointeur.
Les images passent par un relais. Chaque image est demandée à /api/uploads/[...path] de LeBoard, qui va la chercher dans les uploads du Hub par son URL interne et la renvoie depuis l'origine du plateau. Les données de lot portent donc un chemin relatif, jamais une URL absolue vers le Hub : une URL absolue est refusée par la politique de sécurité du navigateur, sans erreur serveur ni trace. Le relais ne sert que les chemins réellement cités par les lots du modèle de ce plateau, et met en cache de façon privée et immuable.
Feuillet « Régénération »
Animateur arrive sur l'étape « Régénération »
│
└─→ Le bouton Régénération apparaît (centre bas)
│
└─ Ouverture du feuillet (RegenerationSheet)
│
├─ GET /api/regeneration?workshopModelId=…
│ laissez-passer : le token du plateau
│ (et le modèle doit être celui de CE plateau)
│
├─ Écran 1 : familles de pratiques
├─ Écran 2 : pratiques de la famille
│ titres lus dans les titres de cartes du modèle
│ → sans titres, le feuillet refuse de s'afficher
└─ Écran 3 : carte en grand (CardViewer) + ses exemples
Détails clés :
- Lecture en direct : le contenu vient de la base à chaque ouverture. Ce qui est corrigé dans le Hub est visible à l'atelier suivant sans redéploiement.
- Scopé à la version de cartes : deux versions de cartes peuvent porter des familles et des exemples différents.
- Mémoire de navigation : le feuillet rouvre sur la dernière carte consultée.
- Le bouton n'existe que sur cette étape : chercher pourquoi il n'apparaît pas commence toujours par vérifier l'étape active.