Discrete Event Simulation (DES) — chantier complet 8 phases#30
Open
PPCM wants to merge 16 commits into
Open
Conversation
added 12 commits
May 11, 2026 23:26
Premier lot du chantier "simulation à événements discrets" (option C
décidée avec l'utilisateur). Aucune ligne de code applicatif : uniquement
la spec, l'ADR et deux exemples pour cadrer les phases suivantes.
- ADR-006 (Proposed) : justification du passage de l'animation continue
à un simulator DES, décisions verrouillées (routing pondéré, dt clampé
à 33 ms, stats reset au Start, V1 stricte sur nodeRole), alternatives
écartées (visual-only, modulated flow, server-driven).
- Spec DOT 3D — triple invariant respecté en couche doc :
* nodeRole (enum: generator|relay|sink, défaut relay) — V1 stricte,
pas de fallback "tout émet"
* dropPolicy (enum: tail|head|reject, défaut tail) — n'a de sens
qu'avec queue_size défini
* queue_size, processing_time, failure_rate gagnent une sémantique
runtime (ils étaient déjà acceptés par le validator mais ignorés)
* Cross-checks de cohérence en warnings (pas erreurs) : dropPolicy
sans queue_size, particleGeneration sur relay/sink
- Exemples :
* generators.dot — démo minimale 2 sources → routeur → sink, pas
de saturation, pour comprendre les rôles
* saturation.dot — source rapide → goulot étroit → sink, pour
valider la visu d'accumulation et les drops en phase 5/7
Validator backend et renderer frontend seront mis à jour en phases 1
et 4 respectivement. Cf. plan de chantier en discussion utilisateur.
Implémente le côté validator du triple invariant (ADR-005) pour les deux nouveaux attributs DES introduits en ADR-006. La spec a été posée en Phase 0 (commit 0ac995a) ; cette PR exécute la même décision sur backend/src/utils/dotValidator.js. Ajouts dans le validator : - nodeRole (enum: generator|relay|sink) — warning sur valeur hors énum - dropPolicy (enum: tail|head|reject) — warning sur valeur hors énum - Méthode validateDESCoherence(ast) appelée après les checks d'extension : * warn si dropPolicy défini sans queue_size (file unbounded, drop inopérant) * warn si particleGeneration > 0 sur nodeRole=relay|sink (ignoré au runtime parce que seul "generator" émet) - Les deux attributs sont déclarés dans this.keywords et dans la liste vortexFlow3DAttrs pour qu'ils déclenchent hasVortexFlowExtensions=true Tests Jest (17 nouveaux dans tests/unit/utils/dotValidator.test.js) : - accept des 3 valeurs nodeRole + reject d'une valeur inconnue - accept des 3 valeurs dropPolicy + reject d'une valeur inconnue - 6 cas de cohérence (warns ON pour les cas problématiques, warns OFF pour les cas légitimes : generator+particleGeneration, relay+0, dropPolicy+queue_size) - 2 régressions sur les attributs existants (failure_rate hors [0,1], queue_size négatif) Coverage dotValidator.js : 86.91 % lines (threshold 80 %). 395 tests backend passent au total. Exemples DOT de Phase 0 (generators.dot, saturation.dot) validés sans warning — la grammaire et le validator sont cohérents. Phase 2 (architecture du simulator côté frontend) à suivre.
Suite du commit cf0d110 ("doublon rail") : ce commit avait supprimé le bouton Play/Pause du rail vertical mais laissé en place la prop onToggleSimulation, ses imports et les tests qui s'y rattachaient. ESLint flaggait deux vars unused et deux tests étaient devenus rouges parce qu'ils cherchaient un bouton qui n'existe plus. - GraphRenderer3D.tsx : retire la prop onToggleSimulation (interface + destructuring) et les imports PlayArrowIcon / PauseIcon. - GraphViewer.tsx : retire la fonction handleToggleSimulation (devenue dead code) et son passage en prop au renderer. - GraphRenderer3D.test.tsx : supprime le bloc describe sur "Start Simulation button delegates to onToggleSimulation" (2 tests). Le contrôle Start/Pause unique reste dans la toolbar horizontale de GraphViewer (handleStartSimulation / handlePauseSimulation), comme documenté dans CLAUDE.md §"Single Start/Pause control in the toolbar". Frontend après nettoyage : 320/320 tests passent, lint clean.
Squelette de la classe ParticleSimulator côté frontend, conforme au plan
adopté en ADR-006. Aucune implémentation à ce stade : les mutators
publics (start, pause, stop, tick, getStats, onTick, dispose) lèvent
NotImplementedError. Objectif de cette phase : verrouiller l'API et les
types avant d'écrire la logique en Phase 3.
frontend/src/services/particleSimulator.ts (208 lignes):
- Types exportés:
* NodeRole = 'generator' | 'relay' | 'sink'
* DropPolicy = 'tail' | 'head' | 'reject'
* DropReason = 'queue_full' | 'failure_rate' | 'no_outlet'
* Particle, NodeQueue, SimulatorStats, NodeInput, LinkInput, GraphInput
* SimulatorOptions (maxDtMs, random, defaultGenerationPerSecond,
onParticleReleased)
* StatsListener
- Classe ParticleSimulator avec sa constructor signature stable
(graph: GraphInput, options?: SimulatorOptions). Pas de coupling
React/Three — pure data layer, testable en isolation.
- NotImplementedError exporté pour que tests / callers puissent
asserter explicitement sur l'état "stub".
- Documentation JSDoc qui rappelle les décisions ADR-006 (V1 stricte,
routing pondéré, dt clampé 33 ms, stats reset au start) là où elles
s'appliquent dans le code.
frontend/src/services/particleSimulator.test.ts (12 tests):
- Instantiation avec graphe minimal et avec SimulatorOptions complet.
- Vérification que les 7 mutators publics throw NotImplementedError
via test.each, et que le message inclut le nom de la méthode +
référence Phase 3.
- Compile-time assertions sur les énums exportés (NodeRole, DropPolicy).
Phase 3 (à venir, 2-3 jours) remplacera tous les throw par les
implémentations DES : emission, transit, queue+drop, routing.
Remplace les stubs Phase 2 par une simulation à événements discrets
fonctionnelle. La classe ParticleSimulator est désormais utilisable end-
to-end côté logique : émission → transit → arrivée → queue → processing
→ routage → drop. Reste à l'intégrer au renderer en Phase 4.
Implémentation (frontend/src/services/particleSimulator.ts ~430 lignes):
Phase 3a — Émission régulière déterministe :
Accumulator par générateur, 1 particule toutes les (1000/rate) ms.
Seuls les nœuds nodeRole=generator émettent (V1 stricte, ADR-006).
particleGeneration sur relay/sink est zeroed à la construction.
Default 1 p/s si générateur sans particleGeneration explicite.
Phase 3b — Transit + arrivée :
Calibration identique à handleEmitTrace : speed_internal = particleSpeed
× 0.003 (fraction-de-lien par tick 16.67ms), clampé à [0.001, 0.02].
À l'arrivée sur un sink : compte totalArrived + latence.
Sur un relay/generator : enqueue (handleArrival).
Phase 3c — Queue + dropPolicy + processing slots :
queue_size optionnel (∞ par défaut). À saturation :
- tail/reject → drop l'entrante
- head → drop la plus ancienne, accepte l'entrante
Slots parallèles : maxParticleProcessing slots simultanés, chacun
occupé processing_time ms. À la sortie d'un slot :
- failure_rate (0..1) → drop probabiliste avec raison 'failure_rate'
- pas de lien sortant → drop 'no_outlet'
- sinon → sendOnLink (sans incrémenter totalEmitted, qui ne compte
que les émissions générateur)
Phase 3d — Routing pondéré :
Quand un nœud a M liens sortants : tirage pondéré par maxParticleFlow
si au moins un lien a un poids > 0, sinon round-robin déterministe.
Random injectable via SimulatorOptions.random (seedable pour tests).
Lifecycle :
start() : reset stats + queues, running=true
pause() : suspend (state préservé)
stop() : reset complet
tick(dt): dt clampé à maxDtMs (33ms par défaut), no-op si paused
onTick(): subscribe au stream de stats, retourne unsubscribe
dispose(): instance définitivement inutilisable
Tests (37 tests, frontend/src/services/particleSimulator.test.ts ~480 lignes):
- Construction & defaults (3)
- Phase 3a Émission (8) : rate, deterministic timing, no-outlet drop,
default 1/s, ignore sur relay/sink, callback onParticleReleased
- Phase 3b Transit (5) : arrival time, end-to-end latency, particlesInFlight
- Lifecycle (6) : tick noop quand paused, stop reset, dispose throws,
onTick subscribe/unsubscribe, dt clamping
- Phase 3c Queue + drop + processing (9) : croissance queue,
tail/head/reject policies, queue_size ∞, processing_time delay,
maxParticleProcessing cap, failure_rate 0/1, no_outlet sur relay
- Phase 3d Routing (3) : round-robin sans poids, pondéré 80/20 exact
avec seed cyclique, déterminisme avec random fixé
- Type smoke (2) : enums NodeRole / DropPolicy
Coverage particleSimulator.ts : 97.11% lines / 92.47% branches / 100%
functions / 98.44% statements (37 tests, ~480 LOC de tests).
vite.config.ts : threshold per-module ajouté pour particleSimulator
(lines 90, branches 85, functions 95, statements 90) — locke le niveau
contre les régressions futures pendant Phase 4 (intégration renderer).
POST /api/public/parse-dot ne renvoyait que particleGeneration et maxParticleProcessing parmi les attributs DES. Ajoute nodeRole, dropPolicy, queue_size, processing_time, failure_rate à la réponse pour que le ParticleSimulator (côté navigateur) reçoive l'intégralité des paramètres déclarés en DOT. Pas de nouveau test backend : la fonction map() est triviale, et les tests d'intégration end-to-end (Phase 7) couvriront la chaîne complète parse → render → simulate. Le validator (Phase 1) garantit déjà la forme des attributs en amont.
Hook React qui owns une instance de ParticleSimulator (Phase 3), la pilote via requestAnimationFrame, surface les stats via useState et forwarde onParticleReleased au callback du consommateur (qui appellera forceGraphRef.current.emitParticle()). frontend/src/hooks/useParticleSimulator.ts (~145 lignes): - Recrée le simulator quand graphData change (ref stability requise côté caller — documentée). - rAF loop avec dt mesuré via performance.now(), clamped par options.maxDtMs (33ms par défaut dans le simulator). - Cleanup propre : cancelAnimationFrame puis dispose. Le pause() au cleanup du rAF effect est intentionnellement omis pour éviter le crash quand graphData change (create-effect dispose en premier). - Callback ref pattern pour onParticleReleased : la dernière référence est utilisée sans recréer le simulator à chaque render. - Retourne `hasGenerators` (boolean dérivé : présence d'au moins un nodeRole=generator) pour piloter l'UI du renderer en mode strict. frontend/src/hooks/useParticleSimulator.test.ts (9 tests): - Empty graph → null stats + hasGenerators=false - Detection des generators - Pas de frames si isRunning=false - Émission via le callback quand isRunning=true - Stats surface via React state après tick - Reset des stats quand isRunning flips true→false→true - Dispose au unmount stoppe les callbacks - Callback ref fraîche sans recréation du simulator - Stub déterministe de requestAnimationFrame pour tests reproductibles Coverage useParticleSimulator.ts: 100% lines / 75% branches / 100% functions / 100% statements (branches < 100% : la branche "graphData empty" est testée mais le coverage v8 ne marque pas toujours les early returns dans les if).
Câble le ParticleSimulator (via useParticleSimulator) au renderer.
Le simulator devient la source de vérité pour l'émission ET les stats
dès qu'au moins un nœud déclare nodeRole=generator (ADR-006).
Changements GraphRenderer3D.tsx (~80 lignes nettes) :
- Import useParticleSimulator + appel dans le composant principal.
- Callback onSimulatorParticleReleased : décode le linkId généré par
le simulator ("source->target#counter"), retrouve le link object
dans 3d-force-graph, et appelle emitParticle(link) pour matérialiser
visuellement chaque release. Pas d'animation parasite : 3d-force-graph
gère l'animation, le simulator gère la logique.
- linkDirectionalParticles (deux endroits : updateParticleProperties +
initializeGraph) : retourne 0 quand hasGenerators=true. Le continuous
flow ne s'active que comme fallback pour les graphes legacy sans
generator déclaré.
- ForceGraphNode étendu avec nodeRole / dropPolicy / queue_size /
processing_time / failure_rate.
- DotTo3DConverter.convertBackendDataToGraph + parseDotToGraphDataFrontend
propagent les attributs DES depuis le backend / le parsing local
(avec validation des énums côté frontend).
- useEffect des stats heuristiques : skip si hasGenerators (le
simulator alimente directement simulationStats via un nouvel effect).
- handleEmitTrace : règle stricte sur nodeRole=generator. Plus de
fallback "tout émetteur si aucun particleGeneration" — cohérent avec
V1 stricte (ADR-006). Sans generator déclaré, le clic ne fait rien
(geste assumé : le UI doit le signaler en exposant `hasGenerators`).
Tests GraphRenderer3D.test.tsx :
- SAMPLE_PARSE.A reçoit nodeRole='generator' pour rester compatible
avec la règle stricte (les tests d'émission s'appuient dessus).
- Test "linkDirectionalParticles emits >0 when running" renommé en
"still returns 0 when running with generators (DES mode)" et inversé :
vérifie maintenant que la callback retourne 0 quand le simulator
prend la main, et que emitParticle est appelée à la place via
onParticleReleased.
Tous les tests passent : 354 frontend (1 modifié, 0 ajouté), lint
clean, coverage particleSimulator/useParticleSimulator maintenu (>90%
lines, >75% branches).
Expose visuellement l'état du simulator DES (Phase 3-4) sur les nœuds du rendu 3D. Sans cette phase, le simulator tournait correctement mais visuellement on ne voyait que les particules en transit — pas l'accumulation, ni la saturation, ni les drops. GraphRenderer3D.tsx — 4 hooks visuels via les accesseurs 3d-force-graph : - **Queue growth** : nodeVal((node) => baseSize * (1 + min(1, pending/queue_size))) → un nœud vide reste à sa taille de base, un nœud saturé fait 2× sa taille. Read live à chaque frame depuis queueStatsByNodeRef. - **Halo de saturation** : nodeColor retourne orange #ff9800 quand la file atteint 80% de queue_size, rouge #d32f2f à 100%. Override la couleur utilisateur explicite (priorité saturation > color DOT). - **Drop flash** : pendant 200 ms après l'incrément de droppedCount, nodeColor retourne rouge vif #ff1744. Détection via diff per-tick sur la map de droppedCount fournie par simulatorStats.queues. Court pour ne pas se confondre avec le halo de saturation soutenu. - **Role tint léger** : si node.color n'est pas défini, generator → teal #80cbc4, sink → indigo #9fa8da, relay → couleur par défaut. Les couleurs DOT explicites ne sont jamais écrasées par cette teinte. Mécanique : - 3 refs (queueStatsByNodeRef, dropFlashTimeRef, previousDroppedCountRef) mises à jour à chaque tick du simulator dans un useEffect dédié. Pas de state React pour éviter les rerenders inutiles. - L'effect re-installe les accesseurs (fg.nodeVal(fg.nodeVal())) à chaque tick : 3d-force-graph re-évalue alors nodeVal/nodeColor pour tous les nœuds. Pas de rebuild de scène, coût négligeable. - Le HUD stats reçoit un 4e chip "Drops" qui n'apparaît que quand le simulator est en charge (hasGenerators && simulatorStats). Pas de nouveau test dédié à cette phase. Les comportements sont purement visuels (couleurs ANSI, scale 3D, frames timing) et seront validés en Phase 7 via des tests d'intégration sur saturation.dot qui pourront mesurer le déclenchement réel de halos et flashes. Les 354 tests frontend existants continuent de passer (rien cassé), lint clean. Doc : - frontend/doc/RENDERER.md : ajout d'une section 8.bis détaillant les 4 hooks visuels et leur mécanique (refs vs state, re-install des accesseurs). Checklist PR mise à jour.
Étend le HUD stats du renderer avec deux métriques de session et des
tooltips explicatifs sur chaque chip. Reset automatique aligné sur le
contrat start() du simulator (D5 : stats reset au démarrage).
GraphRenderer3D.tsx :
- simulationStats étend avec maxQueueSize + throughputPerSec.
- Deux nouveaux refs :
* maxQueueSeenRef : plus grande file constatée toutes nœuds confondus,
track via Math.max sur simulatorStats.queues à chaque tick.
* throughputSamplesRef : fenêtre glissante {time, totalEmitted} sur
les 2 dernières secondes, trimmée par timestamp dans le sync effect.
- Throughput instantané calculé dans le branch effect :
((emitted_last - emitted_first) / dtMs) * 1000 — débit en p/s sur
la fenêtre disponible. Plus précis qu'un rate "depuis le début"
parce qu'il réagit aux changements (pause/reprise, ralentissement).
- Nouvel effect dédié au reset session-scoped : detect false→true
edge sur simulationRunning, clear maxQueueSeenRef +
throughputSamplesRef + previousDroppedCountRef + dropFlashTimeRef.
Mirror du reset interne du simulator.
- HUD : chaque chip est désormais enveloppé dans un MUI Tooltip avec
un texte explicatif (la métrique, son périmètre, son unité).
cursor:help quand le tooltip est défini. Les 3 nouveaux chips
(Drops, File max, Débit) ne s'affichent que quand le simulator
est en charge (hasGenerators && simulatorStats) — fallback
heuristique inchangé pour les graphes legacy.
Pas de nouveaux tests dédiés. La logique est arithmétique pure
(min/max sur des stats déjà testées) et la couverture des effects
de sync est validée en Phase 7 (tests d'intégration end-to-end).
Les 354 tests frontend passent, lint clean.
Nouveau fichier dédié frontend/src/services/particleSimulator.integration.test.ts qui stresse le simulator sur des topologies réalistes, séparé des tests unitaires (particleSimulator.test.ts) pour la lisibilité de la PR et la facilité de naviguer dans la suite Vitest. Scénarios couverts : 1. Convergence — 3 générateurs (10 p/s chacun) → 1 relay (slot 1, processing_time 66ms ≈ 15 p/s) → sink. Vérifie qu'avec 30 p/s en entrée et 15 p/s en sortie, la queue grossit et totalArrived < totalEmitted. Le déficit cumulé est visible après 3 secondes. 2. Divergence pondérée — 1 générateur (100 p/s) vers 3 sinks avec maxParticleFlow 50/30/20. Avec un random cyclique déterministe (i/100 pour i=1..100), la distribution est exactement 50/30/20 sur 100 émissions. Vérifie que l'algorithme de routing pondéré respecte les ratios. 3. Cycle (A→B→C→B) — boucle dirigée sans sink dans le cycle. Vérifie que tick() ne throw pas pendant 200 ticks de 50ms, que les stats restent finies et que totalEmitted > 0. Sert de garde-fou contre les régressions où un changement de routing introduirait une cascade infinie ou un état corrompu. 4. Saturation — A (100 p/s) → B (queue_size=5, dropPolicy=tail, 5 p/s sortie) → sink. Vérifie que la queue plafonne à 5 et que les drops sont comptés (totalDropped > 50 sur 3 secondes). 5. Saturation avec dropPolicy=head — variante où la file reste à 3 particules mais c'est la plus ancienne qui est évacuée à chaque nouvelle arrivée. 6. Perf smoke — graphe à 100 nœuds (10 generators / 80 relays / 10 sinks) et ~180 liens (chaque non-sink fan-out vers 2 successeurs déterministes). Mesure 100 ticks de 16.67ms (≈ une seconde de simulation à 60 fps). Cible : < 500ms total, soit ~5ms/tick en moyenne. Largement assez de marge pour piloter le rAF du browser. Total : 360 tests frontend (354 + 6 intégration). Coverage du simulator inchangée (97.11% lines, 94.62% branches) — les tests d'intégration exercent du code déjà couvert par les tests unitaires mais valident les combinaisons que les tests unitaires ne couvrent pas individuellement (convergence, divergence end-to-end avec sink, etc.). Pas de nouveaux tests renderer Phase 7. L'apparition des chips HUD (File max, Débit, Drops) en mode DES dépend du rAF qui tourne et qui n'est pas naturellement déclenché en jsdom ; le stubber suffisamment finement pour driver le simulator depuis un test renderer pèserait plus en complexité que la valeur ajoutée. La chaîne logique end-to-end est validée par les tests unitaires (hook, simulator) + les tests d'intégration de scénarios ci-dessus.
Clôture le chantier DES en propageant le statut "livré" dans la doc : - doc/adr/006-particle-discrete-event-simulation.md : statut passé de Proposed → Accepted. Date de référence ajoutée (2026-05-11 proposed / 2026-05-12 accepted) avec rappel du périmètre couvert (11 commits, PR #30, branche feature/des-simulation). - doc/adr/README.md : index mis à jour avec le nouveau statut. - doc/dot-3d/user-guide.md : nouvel "Exemple 0 : Pipeline DES avec goulot et drops" en tête de la section "Exemples Pratiques Complets". Présente le pipeline source→goulot→sink avec les 7 attributs DES, décrit pas-à-pas ce qui s'affiche à l'écran après ▶ (accumulation, halos, flash drop, HUD), et propose 3 variantes pour explorer (slots multiples, failure_rate, convergence). - doc/changelog/2026-05-12.md : entrée du jour résumant l'ensemble des changements DES, par fichier touché, avec totaux tests/coverage et liste des décisions / non-decisions (cap particules off, tests DOM intentionnellement omis, etc.). CLAUDE.md (gitignored, donc backup versionné dans ~/claude-projet/VortexFlow/) : section "GraphRenderer3D behaviors" réécrite pour refléter les nouveaux invariants DES (simulator owns emission, visual hooks via refs, HUD chips gating, fallback heuristique pour graphes legacy, V1 stricte sur nodeRole pour handleEmitTrace). Tests : 395 backend + 360 frontend = 755 tests verts au total sur le chantier. Coverage particleSimulator 97.11% lines / 94.62% branches, useParticleSimulator 100% lines.
added 4 commits
May 13, 2026 10:55
Découvert via test navigateur live : aucun des 3 hooks visuels Phase 5
(grossissement queue, halo saturation, drop flash, role tint) ne
fonctionnait sur les graphes avec `geometry` défini, parce que :
1. Le backend (routes/public.js) appliquait une couleur par défaut
`#1976D2` à tous les nodes sans color explicite. La condition
`!node.color` dans resolveNodeColor était donc toujours false →
le role tint ne s'appliquait jamais.
2. Le DotTo3DConverter frontend forçait aussi `node.color || '#1976D2'`,
masquant l'absence de couleur user.
3. Pour les nodes avec geometry (3d-sphere, cone, etc.), c'est
nodeThreeObjectCallback qui crée le mesh. Le moteur 3d-force-graph
N'APPELLE PAS nodeColor sur ces meshes custom — il les utilise
tels quels. Le mesh est figé à la couleur initiale.
4. fg.refresh() ne re-évalue pas nodeColor non plus pour les meshes
custom (vérifié via evaluate_script dans Chrome devtools : la
callback nodeColor n'est jamais rappelée).
Fix :
- backend/src/routes/public.js : retire les défauts color (#1976D2 sur
nodes, #888 sur links). On passe through undefined si l'utilisateur
n'a pas spécifié, et le frontend gère le défaut.
- frontend DotTo3DConverter : idem (color: node.color au lieu de
node.color || '#1976D2').
- GraphRenderer3D :
* Extraction de resolveNodeColor + resolveNodeScale en helpers
useCallback (réutilisés par 4 endroits).
* nodeColor accessor utilise resolveNodeColor (pour les nodes sans
geometry, où le moteur appelle bien la callback).
* nodeThreeObjectCallback utilise resolveNodeColor à l'init du mesh
custom — pour que le role tint s'applique dès le baseline (avant
démarrage de la simulation).
* useEffect sync simulatorStats : remplace fg.refresh() (inopérant)
par une mutation directe de material.color + Group.scale via
node.__threeObj. Walk les 3 meshes et applique resolveNodeColor +
resolveNodeScale. Cheap (max N nodes par tick).
Test navigateur (chrome-devtools MCP) sur un graphe saturation.dot
sans couleurs explicites :
- FastSource (generator) : #80cbc4 teal ✓
- Bottleneck (relay, queue 5/5) : #d32f2f rouge saturé, scale 2.0 ✓
- Sink (sink) : #9fa8da indigo ✓
- Flash drop : #ff1744 détecté ~85% du temps sur 13 samples en 1.5s
(cohérent : ~10 drops/s, fenêtre 200ms se chevauche)
- HUD : Drops 101, File max 5, Débit 6/s — tous corrects
Tests : 360/360 frontend verts, lint clean. Test DotTo3DConverter
adapté (le backend ne renvoie plus la couleur par défaut → vérification
de `color: undefined` au lieu de `'#1976D2'`).
Note : `start-vortexflow.sh` lance `node server.js` sans nodemon,
donc toute modif backend nécessite un redémarrage manuel du script.
Problème observé en test navigateur sur la page d'accueil non-connecté :
- L'endpoint /api/auth/session répondait 401 quand le user n'était pas
connecté, ce qui est sémantiquement correct mais opérationnellement
bruyant : l'AuthContext frontend probe cet endpoint au mount de
chaque page (publique ou privée), et l'axios interceptor logue tous
les non-2xx comme erreurs. Résultat : 6 erreurs console à chaque
affichage de /login (401 × 2 doublé par React.StrictMode + AxiosError
× 2 + "Authentification requise" × 2).
Fix : /auth/session devient une probe "soft" qui répond toujours 200,
avec `{ authenticated: false, user: null }` quand pas de session ou
user inactif, et `{ authenticated: true, user: {...} }` sinon. Le
frontend (getCurrentUser dans api.ts) lit déjà `response.data.authenticated`
pour décider — pas de changement frontend nécessaire.
Sémantiquement c'est plus juste : "y a-t-il une session ?" est une
question, pas une opération à protéger. L'absence de session n'est
pas une erreur.
Tests intégration auth.test.js (3 cas pour /session) :
- 200 authenticated:false quand pas de session (anciennement 401)
- 200 authenticated:false quand user inactif (nouveau cas)
- 200 authenticated:true + payload user quand session valide (inchangé)
Backend : 396 tests verts (un test additionnel pour user inactif).
Note : la lenteur de premier chargement (~4s LCP) constatée en parallèle
est due à Vite dev qui transforme 40+ modules à la volée. En prod build
(npm run build) le LCP retombe sous la seconde. Pas un bug applicatif.
Couvre les 3 nodeRoles (generator/relay/sink), les 5 géométries 3D (Sphere/Box/Cylinder/Cone/Torus), les 2 dropPolicies (tail/head), failure_rate, routage maxParticleFlow pondéré 60/30/10, particleSpeed variable, saturation de file et attributs legacy bandwidth/capacity/latency.
Le MenuItem du menu kebab appelait handleMenuClose() — qui null-ait selectedGraphId — avant d'ouvrir le dialogue de confirmation. Au clic du bouton du dialogue, la garde `if (selectedGraphId)` échouait et aucune requête API n'était émise (effet visible: le bouton "scintille" mais rien ne se passe). On ne ferme plus que l'ancre du menu, la sélection reste posée pour le dialogue. 2 tests de régression couvrent les flows Supprimer et Dupliquer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contexte
Chantier Discrete Event Simulation (DES) pour la simulation de particules, décidé en ADR-006 (Accepted).
Remplace le rendu continu de
linkDirectionalParticlespar un simulator événementiel piloté par les attributs DOT déjà documentés mais ignorés au runtime. L'utilisateur voit désormais :Phases (12 commits)
0ac995afbd38c28fb6024NotImplementedError)7094ae32f94ce6330df95+dca6b5b+159bc07c0034e396920b4be3e326f1e8ab1Décisions actées (figées dans l'ADR-006)
nodeRoleénumgenerator | relay | sink(défautrelay). V1 stricte : pas de fallback "tout émet" — graphes legacy doivent être annotés.dropPolicyénumtail | head | reject(défauttail), nécessitequeue_size.maxParticleFlow, fallback round-robin.maxDtMs(33 ms par défaut).1000/ratems d'intervalle).maxParticleProcessing×processing_time.failure_ratetiré à la sortie du nœud.handleEmitTraceexistant.Couverture & tests
particleSimulator.tsuseParticleSimulator.tsdotValidator.jsparticleSimulator.ts(90/85/95/90)Effets de bord assumés (à valider en review)
doc/dot-3d/examples/(network-distribution, particle-physics, workflow-pipeline, etc.) n'animeront plus tant qu'ils ne sont pas annotésnodeRole=generator. C'est la V1 stricte assumée en ADR-006.handleEmitTracene fait plus rien si aucun générateur n'est déclaré. Pas de message d'erreur au clic — le UI doit utiliserhasGeneratorspour signaler le cas.maxTotalParticlesInFlightpeut être ajouté àSimulatorOptionssans breaking change.Périmètre encore ouvert (post-merge)
nodeRole=generator(1-2 heures, à faire au cas par cas).Test plan
particleSimulator.ts≥ 90 % lines (threshold per-module figé)generators.dotetsaturation.dotvalidés par le validator sans warningsaturation.dot, démarrer la simulation, observer accumulation → halo orange → halo rouge → drop flash → chips HUD à jour