From f1a984068e712d39251c9c7f493f36f4a14d250c Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 19 Jul 2026 16:47:09 +0300 Subject: [PATCH] docs: make retry demo failure count the visible payoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retry page's whole point is that httpware surfaces fewer failures to the caller, but the failed count was rendered in the muted label style, the blip stops spotlighted the in-flight counters (while the copy said 'compare the ✗ counts'), and the 1.2s blip left httpware recovering only ~5 of ~14 — a weak contrast. - ✗ failed is now a prominent stat (bold number, red when >0), wrapped in a spotlightable element, on all pages - retry blip stops spotlight the ✗ counts (plain climbing vs httpware ~0) - tighten the blip to 0.4s so httpware recovers all of it: plain ✗6, httpware ✗0 Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/demos/demos.css | 1 + docs/demos/engine.js | 14 ++++--- docs/demos/retry.md | 14 +++---- ...07-19.01-retry-demo-failures-visibility.md | 41 +++++++++++++++++++ 4 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 planning/changes/2026-07-19.01-retry-demo-failures-visibility.md diff --git a/docs/demos/demos.css b/docs/demos/demos.css index 27365c1..38e6b46 100644 --- a/docs/demos/demos.css +++ b/docs/demos/demos.css @@ -96,6 +96,7 @@ .hw-demo .score .k { color: var(--hw-muted); } .hw-demo .stat { padding: 2px 6px; border-radius: 6px; } .hw-demo .inflight-big { font-size: 1.15rem; font-weight: 700; transition: color .3s; } +.hw-demo .fail-big { font-size: 1.15rem; font-weight: 700; transition: color .3s; } .hw-demo .big { font-weight: 700; } .hw-demo .note { color: var(--hw-muted); font-size: .76rem; font-style: italic; margin-top: 6px; } diff --git a/docs/demos/engine.js b/docs/demos/engine.js index d5a764d..d5dbc21 100644 --- a/docs/demos/engine.js +++ b/docs/demos/engine.js @@ -213,7 +213,7 @@ window.HttpwareDemo = (function () {
in-flight 0 0 - 0 + ✗ failed 0 p99 40ms
@@ -229,7 +229,7 @@ window.HttpwareDemo = (function () {
in-flight 0 0 - 0 + ✗ failed 0 ⏻ fast-failed 0 @@ -262,16 +262,16 @@ window.HttpwareDemo = (function () { scenarios: $('scenarios'), play: $('play'), replay: $('replay'), scenLabel: $('scenLabel'), timeline: $('timeline'), outage: $('outage'), outageLabel: $('outageLabel'), playhead: $('playhead'), laneA: $('laneA'), badgeA: $('badgeA'), srvA: $('srvA'), trackA: $('trackA'), - ifA: $('ifA'), okA: $('okA'), badA: $('badA'), latA: $('latA'), + ifA: $('ifA'), okA: $('okA'), badA: $('badA'), badWrapA: $('badWrapA'), latA: $('latA'), laneB: $('laneB'), badgeB: $('badgeB'), srvB: $('srvB'), trackB: $('trackB'), brkB: $('brkB'), - ifB: $('ifB'), okB: $('okB'), badB: $('badB'), rejB: $('rejB'), latB: $('latB'), + ifB: $('ifB'), okB: $('okB'), badB: $('badB'), badWrapB: $('badWrapB'), rejB: $('rejB'), latB: $('latB'), poolWrap: $('poolWrap'), poolB: $('poolB'), elapsedWrap: $('elapsedWrap'), elapsedB: $('elapsedB'), note: $('note'), dimT: $('dimT'), dimB: $('dimB'), dimL: $('dimL'), dimR: $('dimR'), ring: $('ring'), coach: $('coach'), cArrow: $('cArrow'), cStep: $('cStep'), cTitle: $('cTitle'), cBody: $('cBody'), cGo: $('cGo'), }; - const ELS = { ifA: els.ifA, latA: els.latA, ifB: els.ifB, latB: els.latB, brkB: els.brkB, poolB: els.poolB, elapsedB: els.elapsedB }; + const ELS = { ifA: els.ifA, latA: els.latA, badWrapA: els.badWrapA, ifB: els.ifB, latB: els.latB, badWrapB: els.badWrapB, brkB: els.brkB, poolB: els.poolB, elapsedB: els.elapsedB }; // Built fresh inside run() from the selected scenario (STOPS is never read before a // scenario is played, so it's safe to leave empty until then) — pages with one @@ -401,7 +401,7 @@ window.HttpwareDemo = (function () { els.ifA.textContent = '0'; els.okA.textContent = '0'; els.badA.textContent = '0'; els.ifB.textContent = '0'; els.okB.textContent = '0'; els.badB.textContent = '0'; els.rejB.textContent = '0'; els.latA.textContent = '40ms'; els.latB.textContent = '40ms'; - [els.latA, els.latB, els.ifA, els.ifB].forEach((n) => { n.style.color = ''; }); + [els.latA, els.latB, els.ifA, els.ifB, els.badA, els.badB].forEach((n) => { n.style.color = ''; }); // Flow-diagram boxes reflect the selected scenario's CHAIN config, not the runtime // middleware objects (which aren't constructed until run()). Otherwise the breaker // box reads "no breaker" on a circuit-breaker page until the first Play. @@ -425,6 +425,8 @@ window.HttpwareDemo = (function () { els.ifB.textContent = B.if; els.okB.textContent = B.ok; els.badB.textContent = B.bad; els.rejB.textContent = B.rej; els.ifA.style.color = A.if > 10 ? 'var(--hw-bad)' : ''; els.ifB.style.color = B.if <= 6 ? 'var(--hw-ok)' : ''; + els.badA.style.color = A.bad > 0 ? 'var(--hw-bad)' : ''; + els.badB.style.color = B.bad > 0 ? 'var(--hw-bad)' : ''; if (bulk) els.poolB.textContent = bulk.inUse + '/' + bulk.max; let maxElapsed = 0; if (tmoCfg) { diff --git a/docs/demos/retry.md b/docs/demos/retry.md index c58eb30..c7cbb99 100644 --- a/docs/demos/retry.md +++ b/docs/demos/retry.md @@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', function () { HttpwareDemo.mount('#retry-demo', { scenarios: [ { id: 'blip', label: 'Brief blip (recovers)', dur: 12.5, - fault: (now, rnd) => (now >= 2.0 && now < 3.2) + fault: (now, rnd) => (now >= 2.0 && now < 2.4) ? { ok: false, ms: 0.05, label: 'blip' } : { ok: true, ms: 0.05 }, chainB: { retry: { maxAttempts: 3, baseDelay: 0.1, maxDelay: 5.0 }, budget: { ttl: 10.0, minRetriesPerSec: 10.0, percentCanRetry: 0.2 } } }, @@ -32,12 +32,12 @@ document.addEventListener('DOMContentLoaded', function () { { when: (s) => s.now >= 10.0, spot: ['ifA', 'ifB'], title: 'Blip: recovered. Outage: contained', body: 'Retry rescues transient errors without turning a real outage into a storm. That cap is the whole reason the budget exists.' }, ] : [ - { when: (s) => s.now >= 1.2, spot: ['ifA', 'ifB'], title: 'A backend blip appears', - body: 'Both clients hit the same transient errors. Watch how each responds.' }, - { when: (s) => s.now >= 2.4, spot: ['ifB'], title: 'httpware retries the blip', - body: 'The plain client surfaces the error immediately. httpware retries with backoff — most of these recover on attempt 2 or 3, invisibly to the caller.' }, - { when: (s) => s.now >= 10.0, spot: ['ifA', 'ifB'], title: 'Blip: recovered', - body: 'The backend healed and so did both clients — but compare the ✗ counts: httpware surfaced far fewer failures to the caller. That is what retry buys you on a transient blip.' }, + { when: (s) => s.now >= 1.2, spot: ['badWrapA', 'badWrapB'], title: 'A backend blip appears', + body: 'Both clients are about to hit the same transient errors. Keep your eye on the ✗ failed counts — they start equal at zero.' }, + { when: (s) => s.now >= 2.4, spot: ['badWrapA', 'badWrapB'], title: 'Plain surfaces every error; httpware retries', + body: 'The plain client surfaces each error straight to the caller — its ✗ is climbing. httpware retries with backoff, so most of these recover on attempt 2 or 3 and its ✗ barely moves.' }, + { when: (s) => s.now >= 10.0, spot: ['badWrapA', 'badWrapB'], title: 'Blip over — mind the ✗ gap', + body: 'The backend healed. Compare the ✗ failed counts: the plain client surfaced far more failures than httpware. That gap is exactly what retry buys you on a transient blip.' }, ], }); }); diff --git a/planning/changes/2026-07-19.01-retry-demo-failures-visibility.md b/planning/changes/2026-07-19.01-retry-demo-failures-visibility.md new file mode 100644 index 0000000..3a2c70e --- /dev/null +++ b/planning/changes/2026-07-19.01-retry-demo-failures-visibility.md @@ -0,0 +1,41 @@ +--- +summary: Make the retry demo's failure count the visible payoff — prominent ✗ stat, spotlighted at the blip stops, with a punchier recover-vs-surface contrast. +--- + +# Change: Make retry demo failures count obvious + +**Lane:** lightweight — docs demo only, no capability contract moves. + +## Goal + +On the retry demo the whole point is that httpware surfaces *fewer failures* to +the caller than a plain client. But the ✗ failure count is rendered in the same +muted style as every label, the blip stops spotlight the in-flight counters +(while the copy says "compare the ✗ counts"), and the 1.2 s blip lets httpware +recover only ~5 of ~14 failures — a weak contrast. Result: the failure count, +the metric that matters, is not obvious. + +## Approach + +- Make the ✗ failure count a prominent stat (larger number, red when > 0), + wrapped in a spotlightable element, on all pages. +- Register the failure-stat wrappers in the engine's spotlight lookup so stops + can point at them. +- Retry blip stops spotlight the ✗ counts (plain rising vs httpware staying + low) instead of in-flight. +- Tighten the blip window so httpware recovers nearly all of it, making the + recover-vs-surface contrast dramatic (verified by trace). + +## Files + +- `docs/demos/engine.js` — failure-stat wrappers in the template + ELS lookup; + colour ✗ count red when > 0. +- `docs/demos/demos.css` — prominent failure-number style. +- `docs/demos/retry.md` — blip stops spotlight the ✗ counts; tighten the blip. + +## Verification + +- [ ] `node --check docs/demos/engine.js`. +- [ ] `just docs-build` clean; scratchpad `verify-real-page.js` + `verify-demos.js` green. +- [ ] Trace the blip: plain ✗ high, httpware ✗ low (dramatic gap); sustained still + hits `budgetExhausted`; every stop reachable.