@@ -402,11 +402,15 @@ window.HttpwareDemo = (function () {
402402 els . ifB . textContent = '0' ; els . okB . textContent = '0' ; els . badB . textContent = '0' ; els . rejB . textContent = '0' ;
403403 els . latA . textContent = '40ms' ; els . latB . textContent = '40ms' ;
404404 [ els . latA , els . latB , els . ifA , els . ifB ] . forEach ( ( n ) => { n . style . color = '' ; } ) ;
405- els . brkB . className = 'box' ; els . brkB . textContent = brk ? 'breaker CLOSED' : 'no breaker' ;
406- els . poolWrap . style . display = bulk ? '' : 'none' ;
407- els . poolB . textContent = bulk ? ( bulk . inUse + '/' + bulk . max ) : '—' ;
408- els . elapsedWrap . style . display = tmoCfg ? '' : 'none' ;
409- els . elapsedB . textContent = tmoCfg ? ( '0.0s / ' + tmoCfg . timeout . toFixed ( 1 ) + 's' ) : '—' ;
405+ // Flow-diagram boxes reflect the selected scenario's CHAIN config, not the runtime
406+ // middleware objects (which aren't constructed until run()). Otherwise the breaker
407+ // box reads "no breaker" on a circuit-breaker page until the first Play.
408+ const cfgB = selectedScenario ? selectedScenario . chainB : { } ;
409+ els . brkB . className = 'box' ; els . brkB . textContent = cfgB . circuitBreaker ? 'breaker CLOSED' : 'no breaker' ;
410+ els . poolWrap . style . display = cfgB . bulkhead ? '' : 'none' ;
411+ els . poolB . textContent = cfgB . bulkhead ? ( '0/' + cfgB . bulkhead . maxConcurrent ) : '—' ;
412+ els . elapsedWrap . style . display = cfgB . timeout ? '' : 'none' ;
413+ els . elapsedB . textContent = cfgB . timeout ? ( '0.0s / ' + cfgB . timeout . timeout . toFixed ( 1 ) + 's' ) : '—' ;
410414 els . srvA . className = 'box' ; els . srvA . textContent = 'server ✓' ;
411415 els . srvB . className = 'box' ; els . srvB . textContent = 'server ✓' ;
412416 els . laneA . className = 'lane' ; els . laneB . className = 'lane' ;
@@ -433,8 +437,15 @@ window.HttpwareDemo = (function () {
433437 const down = ! ! ( lastFault && ! lastFault . ok ) ;
434438 const slow = ! ! ( lastFault && lastFault . ok && lastFault . ms >= 1.0 ) ;
435439 const stressed = down || slow ;
436- els . latA . textContent = stressed ? ( A . if > 4 ? '12s+' : formatMs ( lastFault . ms ) ) : '40ms' ;
437- els . latA . style . color = stressed ? 'var(--hw-bad)' : '' ;
440+ // latA is the plain client's p99: reflect the ACTUAL in-flight latency tail, not
441+ // the last request. In a brownout (mixed fast successes + slow failures) the last
442+ // sample flickers, so a last-request p99 drops to 40ms whenever the last request
443+ // was a fast success — contradicting the "p99 blown" narration while slow requests
444+ // are still piled up in flight. The in-flight max is stable and honest.
445+ const aTailMs = A . pend . length ? A . pend . reduce ( ( m , p ) => Math . max ( m , p . ms ) , 0 ) : 0.04 ;
446+ const plainStress = aTailMs >= 1.0 || A . if > 4 ;
447+ els . latA . textContent = plainStress ? ( A . if > 4 ? '12s+' : formatMs ( aTailMs ) ) : '40ms' ;
448+ els . latA . style . color = plainStress ? 'var(--hw-bad)' : '' ;
438449 // On timeout pages, AsyncTimeout bounds every lane-B attempt at the deadline —
439450 // p99 must reflect that ACTUAL bounded latency, not the constant 40ms, or it
440451 // visually contradicts elapsedB as it climbs toward the same deadline. Pages
@@ -449,10 +460,10 @@ window.HttpwareDemo = (function () {
449460 : slow ? ( 'server ' + ( lastFault . label || 'slow' ) ) : 'server ✓' ;
450461 els . srvB . className = 'box' + ( stressed ? ' down' : '' ) ;
451462 els . srvB . textContent = down ? 'server ✗' : slow ? 'server slow' : 'server ✓' ;
452- els . laneA . classList . toggle ( 'hot' , stressed && A . if > 8 ) ;
463+ els . laneA . classList . toggle ( 'hot' , plainStress && A . if > 8 ) ;
453464 els . laneB . classList . toggle ( 'safe' , st === 'CLOSED' && ! stressed ) ;
454- els . badgeA . className = 'badge' + ( stressed ? ' bad' : '' ) ;
455- els . badgeA . textContent = stressed ? 'drowning' : 'no protection' ;
465+ els . badgeA . className = 'badge' + ( plainStress ? ' bad' : '' ) ;
466+ els . badgeA . textContent = plainStress ? 'drowning' : 'no protection' ;
456467 const okBadge = st === 'CLOSED' && brk && ! stressed ;
457468 els . badgeB . className = 'badge ' + ( st === 'OPEN' ? 'warn' : okBadge ? 'ok' : '' ) ;
458469 els . badgeB . textContent = st === 'OPEN' ? 'fast-failing' : st === 'HALF_OPEN' ? 'probing' : chainLabel ( selectedScenario . chainB ) ;
@@ -554,7 +565,7 @@ window.HttpwareDemo = (function () {
554565 const f = scenario . fault ( now , rnd ) ;
555566 lastFault = f ;
556567 const landTicks = Math . max ( 1 , Math . round ( f . ms * 1000 / TICK ) ) ;
557- A . if ++ ; A . pend . push ( { land : tick + landTicks , ok : f . ok , attempt : 0 } ) ;
568+ A . if ++ ; A . pend . push ( { land : tick + landTicks , ok : f . ok , attempt : 0 , ms : f . ms } ) ;
558569 spawnDot ( dotsA , els . trackA , f . ok ? 'ok' : 'bad' ) ;
559570 if ( brk && ! brk . allow ( now ) ) {
560571 B . rej ++ ; spawnDot ( dotsB , els . trackB , 'rej' ) ;
@@ -595,6 +606,10 @@ window.HttpwareDemo = (function () {
595606 } ) ;
596607 els . play . addEventListener ( 'click' , run ) ;
597608 els . replay . addEventListener ( 'click' , run ) ;
609+ // Auto-select the first scenario so first paint shows a coherent ready-to-play
610+ // state (correct flow-diagram boxes, Play enabled) instead of bare "—" placeholders.
611+ const firstScenarioBtn = els . scenarios . querySelector ( '.scenario-btn' ) ;
612+ if ( firstScenarioBtn ) firstScenarioBtn . click ( ) ;
598613 }
599614
600615 return { mount, _models : { makeCircuitBreaker, makeRetryBudget, makeBulkhead } , _util : { mulberry } , REAL , TICK , ADV } ;
0 commit comments