Aller au contenu principal

Architecture technique

Stack

CoucheTechnologieVersion
FrameworkNext.js16 (App Router, RSC)
Serveur temps réelSocket.io4 (custom server.ts)
CanvasKonva.js + react-konvaDernière
StylesTailwind CSSv4
DatabasePostgreSQL + Prisma18 + 5 (partagée avec LeHub)
Visite guidéedriver.jsDernière
TestsJest + Testing LibraryVersions std
InfrastructureDocker + Nginx + VPSReverse proxy Let's Encrypt
CI/CDGitHub ActionsDeploy sur push master

Où vit quoi

Répertoires de premier niveau

CheminResponsabilité
src/app/Routes Next.js App Router (pages, layouts, API)
src/components/Composants React (Board, Canvas, Toolbar, etc.)
src/lib/Utilitaires métier (plateaux, matrices, émergeances, socket, authentification admin)
server/Serveur Socket.io custom (socket-handler.ts, gestion d'état temps réel)
prisma/Schéma de données (modèles Board, CardPlacement, etc.), migrations, seed
public/cards/Assets des cartes (images WebP recto/verso, par langue : fr/, en/, es/)
data/Métadonnées des cartes (cards.json) et configuration
.github/workflows/CI/CD (deploy.yml)

Structure de src/app/

Routes authentifiées

CheminRôleComposants clés
app/b/[token]/page.tsxPage principale du plateauBoardCanvas, Socket.io setup, état global du board
app/api/board/[token]/route.tsFetch état initial du board (cartes, sticky notes, etc.)

Routes API

CheminResponsabilité
app/api/board/[token]/État initial board (GET)
app/api/board/[token]/upload/Upload assets (TODO)

Structure de src/components/

FichierContenu
BoardCanvas.tsxConteneur principal : Stage Konva, gestion viewport (zoom/pan), rendu couches (cartes, flèches, post-its, minimap)
CardNode.tsxRendu d'une carte Konva : image recto/verso, shadow bitmap-cachée, drag/drop, flipped state
MatrixLayer.tsxRendu grille matricielle (lignes/colonnes, cellules, slot markers)
RowMatrixLayer.tsxRendu matrice en lignes ancrées (variante lignes au lieu de grille)
StickyNoteNode.tsxRendu post-it Konva : texte éditable, redimensionnable, coloré
DeckPanel.tsxPanel pioche en bas : affichage cartes participant (max 5 visibles)
LotPanel.tsxPanel lots : boutons Distribuer/Pop-corn par lot
LotPanelFloating.tsxVariante floating du panel lots
LeftToolbar.tsxToolbar outils : Flèche, Post-it, Pointer, etc. (modes)
Toolbar.tsxBarre actions : Lock/Unlock, forcer suivi, étapes
Header.tsxHeader : titre atelier, pseudo participant, menu
BottomNav.tsxNavigation étapes (séquenceur)
BottomCenter.tsxZoom controls, minimap
ParticipantBar.tsxAffichage pseudos en ligne et suivi
CardViewer.tsxVisualiseur plein écran carte : zoom indépendant, recto/verso
SlideOverlay.tsx, SlidePresenterPanel.tsxPrésentation diaporama synchronisée (lots diapositives)
BoardTour.tsxVisite guidée (driver.js)
PseudoModal.tsxModal saisie pseudo participant
Tooltip.tsxTooltip utilitaire (labels actions)
BoardCanvasLoader.tsxSkeleton loading plateau
admin/LotEditor.tsxComposant éditeur lots (présent dans le code mais sans route accessible)

Structure de src/lib/

FichierResponsabilité
plateaux.tsCatalogue étapes/matrices : codé dur, dupliqué avec Hub (lib/plateaux.ts). Interfaces Plateau, MatrixStage, RowMatrixStage, etc.
matrix.tsCalcul grille matricielle : computeSlots(), findSnapPosition(), cell coordinates, snap to grid
row-matrix.tsVariante ligne : slots ancrés à des lignes, pas à une grille fixe
emergence.tsPositionnement émergences : carte ancrée à une carte base, suivante en cas de déplacement
matrix-stack.tsStack des étapes visitées (pour retour rapide)
lot-cards.tsExtraction IDs cartes d'un lot
lot-slides.tsGestion diapositives : parsing données, navigation
cards.tsUtilitaires cartes : getCardImageUrl(), ID→URL mapping
socket.tsClient Socket.io : setup, listeners événements, emit actions
admin-auth.tsVérification JWT admin (token signé par Hub)
board-access.tsVérification accès board (token public)
z-index.tsGestion z-index couches et éléments
boardTour.tsConfiguration visite guidée (driver.js)
prisma.tsSingleton Prisma client

⚠️ Piège de maintenance : catalogue de plateaux dupliqué

Le catalogue de plateaux vit en dur dans src/lib/plateaux.ts côté LeBoard, et dans lib/plateaux.ts côté LeHub (attention, les deux chemins diffèrent : src/lib/ d'un côté, lib/ de l'autre). Les deux fichiers sont censés rester identiques. Tout changement effectué d'un côté sans l'autre crée une divergence silencieuse : les étapes, les matrices, leurs noms et leurs identifiants de cartes deviennent incohérents entre les deux applications en production, ce qui provoque des cartes mal placées ou des erreurs à la distribution des lots.

La divergence existe déjà. Au 20 juillet 2026, la version de LeHub porte en plus une fonction listPlateaux() absente de celle de LeBoard. L'écart est aujourd'hui sans conséquence, puisqu'il s'agit d'un utilitaire de lecture, mais il montre que la synchronisation manuelle ne tient pas dans la durée.

Risque : après un commit, oublier de répliquer la modification dans l'autre application.

Recommandation : à chaque modification du catalogue (ajout ou suppression d'étape, changement de matrice) :

  1. Modifier dans LeBoard : src/lib/plateaux.ts.
  2. Comparer les deux versions : diff LeBoard/src/lib/plateaux.ts LeHub/lib/plateaux.ts.
  3. Répliquer la modification dans LeHub (idéalement dans un commit séparé pour la traçabilité).
  4. Tester les deux apps avec le même JDD (même DB en dev).

À terme, cette duplication devrait être centralisée (par ex: une API partagée ou un fichier de configuration externe).

Structure de server/

FichierResponsabilité
socket-handler.tsGestionnaire Socket.io : listeners connection, disconnection, update:card, update:sticky, update:stage, lock:board, etc. Gestion état temps réel en mémoire (per-room) : positionnement cartes, post-its, étape active, locks. Broadcasts aux participants.

Conventions de routage

Next.js App Router

  • Routes dynamiques [token] : paramètres d'URL, accessible via params.token.
  • Pas de route groups : structure plate pour LeBoard.

Layout hierarchy

app/layout.tsx (RootLayout)
├── (auth)/layout.tsx (si ajout futur)
├── app/b/[token]/page.tsx (plateau principal)
└── app/api/board/[token]/route.ts (API fetch état)

Conventions UI

  • Tokens CSS : uniquement Tailwind CSS (pas de tokens personnalisés).
  • Konva : Group Node par élément (CardNode, StickyNoteNode), Layer par type (cartes, flèches, post-its).
  • Minimap : vue répliquée du Stage principal, tailles réduites.

Rendu Konva et pièges de performance

Piège 1 : Cartes floues au zoom

Symptôme : les cartes deviennent floues quand on zoom in (zoom > 100 %), même si les images source sont haute résolution.

Cause : Konva rasterise les nodes complexes en bitmap pour les accélérer. Ce cache bitmap a une résolution fixe déterminée au moment du caching. Quand on zoom, le bitmap est étiré sans réinterpolation, d'où le flou.

Solution : invalider le cache bitmap lors des changements d'échelle (appel group.clearCache()). Dans CardNode.tsx, un useEffect surveille le changement du flag bitmapCache (qui bascule selon la plage de zoom) et appelle clearCache() quand le cache doit passer d'actif à inactif.

Code :

// CardNode.tsx
if (bitmapCache !== bitmapCacheRef.current) {
if (bitmapCacheRef.current && innerGroupRef.current) {
innerGroupRef.current.clearCache()
}
bitmapCacheRef.current = bitmapCache
}

La stratégie globale est : cacher au zoom < 95 % (performance), invalider au zoom > 115 % (clarté). Entre 95 % et 115 % : bande morte pour éviter les oscillations.

Piège 2 : Zoom saccadé (haché)

Symptôme : le zoom molette/pinch progresse par crans discrets au lieu d'être fluide et continu.

Cause : un zoom quantifié à un facteur fixe (ex: ×1,2 par cran) avec rendu complet à chaque cran. Le trackpad émet beaucoup de petits deltas, ce qui crée de petits crans visibles. Une souris émet un gros delta par cran, d'où un grand saut.

Solution : zoom continu avec une sensibilité dépendant de l'amplitude du delta. Dans BoardCanvas.tsx :

  • Pour une souris (delta ~100 px par cran) : sensibilité = 0,0035, facteur ~×1,42 par cran.
  • Pour un trackpad (petits deltas ~4 px) : sensibilité = 0,015, facteur ~×1,06 par petit delta.

Le seuil WHEEL_NOTCH_THRESHOLD = 40 px sépare les deux régimes. La fonction wheelZoomFactor() applique une exponentielle clamped pour éviter les débordements.

Code :

// BoardCanvas.tsx
function wheelZoomFactor(e: WheelEvent) {
const delta = normalizeWheelDelta(e)
const sensitivity = Math.abs(delta) >= WHEEL_NOTCH_THRESHOLD
? ZOOM_SENSITIVITY_NOTCH // Souris
: ZOOM_SENSITIVITY_PIXEL // Trackpad
return Math.min(2, Math.max(0.5, Math.exp(-delta * sensitivity)))
}

L'état du zoom est tenu en refs (scaleRef, positionRef) et animé de manière découplée du rendu React. Un timer RAF (lastAnimTickRef) commit l'état React tous les 120 ms environ, ce qui lisse l'animation et découple le zoom du render.

Partage de base de données avec LeHub

LeBoard utilise la même base PostgreSQL que LeHub. Le schéma de LeBoard (modèles Board, CardPlacement, Connection, BoardLotDistribution, StickyNote, BoardArrow) vit dans le même prisma/schema.prisma hérité de LeHub. Le Hub est l'outil d'admin unique pour composer les modèles de plateau (WorkshopModel) et les lots (Lot), y compris leur mapping vers les cellules de matrice (Lot.cardIds est une chaîne JSON).

Important : les migrations de schéma Board sont appliquées depuis ce repo via npx prisma migrate deploy au déploiement.

Authentification animateur

L'accès animateur est sécurisé via JWT signé par LeHub :

  1. Animateur clique « Animer » dans la fiche atelier du Hub.
  2. Le Hub génère un token JWT signé avec BOARD_ADMIN_JWT_SECRET (shared) et redirige vers board.fresquesystemique.org/b/[token]?admin=<jwt>.
  3. LeBoard valide le JWT (signature + aud claim) dans app/b/[token]/page.tsx et initialise le rôle isAnimateur.
  4. Le token est stocké en session (localStorage) et réutilisé pour les actions admin Socket.io.

Token public : les participants reçoivent juste l'URL board.fresquesystemique.org/b/[token], pas de JWT. Leur accès est en lecture-écriture complète (collab participative), juste pas d'actions admin (stage/lock).