Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/demos/demos.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
14 changes: 8 additions & 6 deletions docs/demos/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ window.HttpwareDemo = (function () {
<div class="score">
<span class="stat"><span class="k">in-flight</span> <span class="inflight-big" data-el="ifA">0</span></span>
<span class="k">&#10003; <span data-el="okA">0</span></span>
<span class="k">&#10007; <span data-el="badA">0</span></span>
<span class="stat" data-el="badWrapA"><span class="k">&#10007; failed</span> <span class="fail-big" data-el="badA">0</span></span>
<span class="stat"><span class="k">p99</span> <span class="big" data-el="latA">40ms</span></span>
</div>
</div>
Expand All @@ -229,7 +229,7 @@ window.HttpwareDemo = (function () {
<div class="score">
<span class="stat"><span class="k">in-flight</span> <span class="inflight-big" data-el="ifB">0</span></span>
<span class="k">&#10003; <span data-el="okB">0</span></span>
<span class="k">&#10007; <span data-el="badB">0</span></span>
<span class="stat" data-el="badWrapB"><span class="k">&#10007; failed</span> <span class="fail-big" data-el="badB">0</span></span>
<span class="k">&#9211; fast-failed <span data-el="rejB">0</span></span>
<span class="stat" data-el="poolWrap" style="display:none"><span class="k">pool</span> <span class="big" data-el="poolB">&mdash;</span></span>
<span class="stat" data-el="elapsedWrap" style="display:none"><span class="k">elapsed</span> <span class="big" data-el="elapsedB">&mdash;</span></span>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand Down
14 changes: 7 additions & 7 deletions docs/demos/retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 } } },
Expand All @@ -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.' },
],
});
});
Expand Down
41 changes: 41 additions & 0 deletions planning/changes/2026-07-19.01-retry-demo-failures-visibility.md
Original file line number Diff line number Diff line change
@@ -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.