Aller au contenu principal

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 :

  1. Animateur clique le bouton « Animer » dans la fiche atelier LeHub.
  2. Le Hub génère un JWT signé avec BOARD_ADMIN_JWT_SECRET, inclut aud: 'board-admin' et l'ID animateur.
  3. Redirection vers https://board.fresquesystemique.org/b/[token]?admin=<jwt>.
  4. LeBoard fetch l'état initial via GET /api/board/[token] (validation JWT + board access).
  5. Rendu page React avec état initial (cartes, post-its, étape active, etc.).
  6. Socket.io connect depuis le client → création AnimatorSession côté serveur (durée 24h).
  7. Broadcast board-state à la connexion pour synchroniser l'état avec tous les participants actuels.

Participant rejoint le plateau​

  1. Participant reçoit/clique URL publique https://board.fresquesystemique.org/b/[token].
  2. LeBoard valide le token (lookup Board par token).
  3. Page charge avec l'état initial board (cartes déjà posées, post-its, étape masquée).
  4. Modal PseudoModal demande le pseudo (ou utilise pseudo stocké localement).
  5. Socket.io connect → broadcast board-state à la connexion pour synchroniser l'état, affichage pseudo + curseur.

Distribution lot (Distribuer)​

Détails :

  1. Animateur clique « Distribuer » pour un lot depuis le LotPanel.
  2. Émission Socket lot:distribute avec l'ID lot.
  3. Serveur :
    • Récupère le lot du Hub (cardIds JSON).
    • Upsert BoardLotDistribution avec mode='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).
  4. Broadcast lot:distributed avec payload {[participantId]: [cardIds]} (chiffré par socket room).
  5. Chaque participant voit ses cartes dans le DeckPanel (bas écran, max 5 visibles).

Lancement lot (Pop-corn)​

Détails :

  1. Animateur clique « Pop-corn » pour un lot avec matrice (ex: étape "Système A").
  2. Serveur upsert BoardLotDistribution avec mode='popcorn'.
  3. Pour chaque carte du lot avec mapping matrice (col, row) :
    • Récupère la cellule correspondante.
    • Calcule position de snap (snap-to-grid via findSnapPosition()).
    • Crée/met à jour CardPlacement avec (x, y) calculées.
  4. Broadcast lot-distribute avec liste cardIds et placements.
  5. Animateur voit les cartes auto-posées dans la grille, verrouillées.
  6. 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 :

  1. Participant drag une carte depuis le plateau.
  2. Calcul optionnel de snap (si la carte doit rester dans une cellule de matrice).
  3. Émission card-move avec (cardId, x, y).
  4. Serveur UPDATE la CardPlacement en DB.
  5. Broadcast card-move à tous les autres (le client émetteur anticipe via optimistic update, donc pas de re-broadcast).
  6. 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.

  1. Lot "Émergence Actions" lancé en pop-corn.
  2. Chaque carte du lot a un emergenceSlot (1 ou 2) et un anchorCardId.
  3. Serveur calcule position d'émergence via emergencePosition(anchorCard) + slot offset.
  4. Cartes s'ancrent à la carte base et la suivent en cas de déplacement.
  5. 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)​

  1. Animateur/participant active l'outil Flèche.
  2. Clique sur anchorpoint de carte A → arrowDraft state.
  3. Clique sur anchorpoint de carte B → création Connection en DB.
  4. Broadcast arrow:created → rendu KonvaArrow ligne droite.

Mode flèche avancée (BoardArrow)​

  1. Pareil, mais crée un BoardArrow avec couleur/direction/strokeWidth.
  2. Supprimer flèche : clic droit → arrow:delete → suppression Connection ou BoardArrow.

Undo/Redo​

Historique animateur (global)​

  1. Animateur fait action (déplacer carte, créer post-it, etc.).
  2. Changement enregistré en mémoire dans undoStack (serveur Socket.io).
  3. 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)​

  1. Participant fait action.
  2. Changement enregistré localement (pas de broadcast serveur).
  3. Ctrl+Z → socket undo:personal → serveur marque comme "local-undo" (pas broadcast).
  4. 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​

  1. Animateur clique l'étape suivante dans BottomNav.
  2. Socket stage-change → serveur update Board.activeStageId.
  3. Broadcast stage-change à tous pour synchroniser l'étape active.
  4. Suivi de viewport (optionnel) : animateur peut envoyer viewport-change avec ses coordonnées zoom/pan → les participants en suivi reçoivent et adaptent leur viewport.

Participant suit l'animateur (volontaire)​

  1. Participant clique pseudo de l'animateur dans ParticipantBar → émet start-follow (pseudo de l'animateur).
  2. Serveur broadcast start-follow à tous.
  3. À chaque viewport-change de l'animateur → les followers s'alignent.
  4. Reclique pseudo pour arrêter (stop-follow).

Réinitialisation plateau​

  1. Animateur clique bouton Réinitialiser (reset).
  2. Socket board-reset → serveur efface l'état temps réel (cartes, post-its, étape).
  3. Broadcast board-reset à tous.
  4. 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 : suppression AnimatorSession, suppression de l'utilisateur de la liste des participants.
  • Timeout inactivité : 24h → auto-suppression AnimatorSession.

Événements clés​

ÉvénementÉmetteurRécepteurPayload
joinClientServeur{pseudo, role, avatarUrl, authSessionId}
card-moveClientServeur{cardId, x, y}
card-moveServeurClients{cardId, x, y} (broadcast)
card-flipClientServeur{cardId, flipped}
card-flipServeurClients{cardId, flipped} (broadcast)
card-drag-startClientServeur{cardId, pseudo}
card-drag-endClientServeur{cardId}
card-drag-endServeurClients{cardId} (broadcast)
card-placedClientServeur{cardId, x, y, flipped, zIndex}
arrow-addClientServeur{fromId, toId, fromAnchor, toAnchor, ...}
arrow-addServeurClientsDonnées flèche créée (broadcast)
arrow-updateClientServeur{arrowId, ...updates}
arrow-updateServeurClientsDonnées flèche modifiée (broadcast)
arrow-deleteClientServeur{arrowId}
arrow-deleteServeurClients{arrowId} (broadcast)
lot-distributeClient (animateur)Serveur{lotId}
deck-updatedServeurClients{[participantId]: [cardIds]}
lot-correct-handsServeurClientsCorrection distribution mains
deck-assignedServeurClients{participantId, cardIds}
board-resetClient (animateur)Serveur{}
board-stateServeurClientÉtat complet du plateau (connexion)
cursor-moveClientServeur{pseudo, x, y} (throttlé)
cursor-moveServeurClients{pseudo, x, y} (broadcast)
start-followClientServeur{pseudo}
start-followServeurClients{pseudo} (broadcast)
stop-followClientServeur{}
stop-followServeurClients{} (broadcast)
slides-presentClient (animateur)Serveur{lotId}
slides-presentServeurClients{lotId, slides: [...]} (broadcast)
slides-navigateClientServeur{index}
slides-navigateServeurClients{index, pointer: {x, y}} (broadcast)
slides-pointerClient (animateur)Serveur{x, y}
slides-pointerServeurClients{x, y} (broadcast)
slides-closeClientServeur{}
slides-closeServeurClients{} (broadcast)
undoClient (animateur)Serveur{}
undoServeurClientsAction inversée (broadcast)
redoClient (animateur)Serveur{}
redoServeurClientsAction réexécutée (broadcast)
role-downgradedServeurClient{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)​

  1. Lot marqué contentType='slides' lancé en pop-corn.
  2. Le serveur lit Lot.slidesData (JSON : images et notes).
  3. Le client affiche SlideOverlay en plein écran.
  4. Navigation : flèches gauche/droite, clic sur un numéro de diapositive.
  5. Pointeur partagé : la position de la souris de l'animateur est visible de tous.
  6. 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.