Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
78fa1c4
docs: add resilience demo engine and circuit-breaker page
lesnik512 Jul 18, 2026
ca1f989
docs: make demo breaker OPEN-state results no-ops to match httpware
lesnik512 Jul 18, 2026
593bd4f
docs: gate circuit-breaker recovery callout on observed state
lesnik512 Jul 18, 2026
6a03a5a
docs: show faithful-model disclaimer before interaction
lesnik512 Jul 19, 2026
7af56bc
docs: add retry + budget demo page
lesnik512 Jul 19, 2026
d04e6df
docs: make demo tour stops scenario-specific
lesnik512 Jul 19, 2026
c97a1c0
docs: fix demo mount crash and align RetryBudget with budget.py
lesnik512 Jul 19, 2026
c9df08c
docs: add bulkhead demo page
lesnik512 Jul 19, 2026
db2fc21
docs: hide bulkhead pool stat by default and hold slot across retries
lesnik512 Jul 19, 2026
9cbf53a
docs: add timeout demo page
lesnik512 Jul 19, 2026
2a158fc
docs: count breaker once per retry sequence, skip timed-out
lesnik512 Jul 19, 2026
d90ebff
docs: add full-stack compose demo page
lesnik512 Jul 19, 2026
ec17837
docs: add resilience demos landing page and cross-links
lesnik512 Jul 19, 2026
be988e1
chore: gitignore .superpowers scratch (plans, ledger, brainstorm)
lesnik512 Jul 19, 2026
b6f95f2
docs: make demo latB reflect bounded latency and outage bar cover the…
lesnik512 Jul 19, 2026
6f47ae8
docs: distinguish demo dots by shape and de-dup breaker stat write
lesnik512 Jul 19, 2026
ea65915
docs: defer demo mount() to DOMContentLoaded so engine.js loads first
lesnik512 Jul 19, 2026
884b8e6
docs: fix demo rendering and first-paint state from browser QA
lesnik512 Jul 19, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ wheels/
.venv
uv.lock
site/
.superpowers/
30 changes: 30 additions & 0 deletions docs/demos/bulkhead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Bulkhead

One slow dependency can sink a whole client: if every worker blocks on the slow call,
fast calls starve behind them. A bulkhead caps concurrency to that dependency — excess
calls fail fast with `BulkheadFullError` instead of piling up and exhausting the client.

<div class="hw-demo" id="bh-demo"></div>

<script>
document.addEventListener('DOMContentLoaded', function () {
HttpwareDemo.mount('#bh-demo', {
scenarios: [
{ id: 'slow', label: 'Dependency turns slow', dur: 12.5,
fault: (now) => (now >= 2.0 && now < 9.0)
? { ok: true, ms: 5.0, label: 'slow (5s)' } : { ok: true, ms: 0.05 },
chainB: { bulkhead: { maxConcurrent: 8, acquireTimeout: 0 } } },
],
buildStops: () => [
{ when: (s) => s.now >= 1.2, spot: ['ifA', 'poolB'], title: 'Fast calls, healthy pool',
body: 'Both clients are humming. The httpware client has a bulkhead: at most 8 calls to this dependency at once.' },
{ when: (s) => s.now >= 2.4, spot: ['ifA'], title: 'The dependency turns slow (5s)',
body: 'Every call now takes 5s. The plain client has no cap — watch in-flight climb without limit as workers block.' },
{ when: (s) => s.mw.rejected > 0, spot: ['poolB'], title: 'The bulkhead holds the line',
body: 'The httpware pool fills to 8 and STOPS admitting more — excess calls fail fast instead of piling up. The client stays responsive for everything else.' },
{ when: (s) => s.now >= 6.0, spot: ['ifA', 'poolB'], title: 'Bounded vs unbounded',
body: 'Plain client: in-flight unbounded, whole client degraded. httpware: in-flight pinned at the pool size, blast radius contained to this one dependency.' },
],
});
});
</script>
38 changes: 38 additions & 0 deletions docs/demos/circuit-breaker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Circuit Breaker

When a backend goes down, a client without a breaker keeps sending every request
into a slow timeout, piling up in-flight work until it exhausts itself. The breaker
trips after repeated failures and **fast-fails** instead — keeping the client healthy
and probing for recovery.

<div class="hw-demo" id="cb-demo"></div>

<script>
document.addEventListener('DOMContentLoaded', function () {
HttpwareDemo.mount('#cb-demo', {
scenarios: [
{ id: 'down', label: 'Backend goes down', dur: 12.5,
fault: (now) => (now >= 2.0 && now < 8.0)
? { ok: false, ms: 3.0, label: 'DOWN' } : { ok: true, ms: 0.04 },
chainB: { circuitBreaker: { failureThreshold: 5, resetTimeout: 2.0, successThreshold: 1 } } },
{ id: 'brownout', label: 'Brownout (40% errors)', dur: 12.5,
fault: (now, rnd) => (now >= 2.0 && now < 9.0)
? (rnd() < 0.4 ? { ok: false, ms: 3.0, label: 'erroring' } : { ok: true, ms: 0.04 })
: { ok: true, ms: 0.04 },
chainB: { circuitBreaker: { failureThreshold: 5, resetTimeout: 2.0, successThreshold: 1 } } },
],
buildStops: () => [
{ when: (s) => s.now >= 1.2, spot: ['ifA', 'ifB'], title: 'Two clients, one backend',
body: 'Both are healthy — in-flight near zero on each. The backend is about to die. Keep your eye on these two in-flight counters.' },
{ when: (s) => s.now >= 2.35, spot: ['ifA'], title: 'Backend just went DOWN',
body: 'Every request now hangs ~3s then fails. This plain client keeps sending — watch this number start to climb.' },
{ when: (s) => s.mw.state === 'OPEN', spot: ['brkB', 'ifB'], title: 'The breaker tripped OPEN',
body: '5 failures in a row -> circuit OPEN. It now fast-fails instantly; its in-flight stays flat while the plain client keeps piling up.' },
{ when: (s) => s.now >= 5.6, spot: ['ifA', 'latA', 'ifB', 'latB'], title: 'The gap — this is the point',
body: 'Plain client: in-flight high AND p99 blown to 12s — drowning. Protected client: in-flight flat, p99 still 40ms. Same outage, two outcomes.' },
{ when: (s) => s.mw.recovered, spot: ['brkB'], title: 'Recovery via one probe',
body: 'Backend is back. The breaker admits exactly ONE probe, sees success, and closes — no thundering herd.' },
],
});
});
</script>
133 changes: 133 additions & 0 deletions docs/demos/demos.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* httpware resilience demos — shared stylesheet.
* Ported from the validated prototype (.superpowers/brainstorm/67614-1784395901/content/circuit-breaker-v6.html).
* Palette follows the mkdocs-material theme where an equivalent token exists
* (bg/fg/primary), falling back to the prototype's hex values otherwise.
* The --hw-ok/--hw-bad/--hw-warn/--hw-reject semantic colors have no material
* equivalent and are switched explicitly for the slate (dark) scheme.
*/
.hw-demo {
--hw-bg: var(--md-default-bg-color, #fff);
--hw-fg: var(--md-default-fg-color, #1b1b2e);
--hw-muted: #6b6b80;
--hw-line: #e4e4ee;
--hw-accent: var(--md-primary-fg-color, #5b4bd6);
--hw-ok: #12a150;
--hw-bad: #e5484d;
--hw-warn: #f5a623;
--hw-reject: #8e8ea0;
--hw-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
position: relative;
display: block;
}
[data-md-color-scheme="slate"] .hw-demo {
--hw-muted: #9a9ab0;
--hw-line: #2a2a3a;
--hw-ok: #3dd68c;
--hw-bad: #ff6369;
--hw-warn: #ffb84d;
--hw-reject: #6a6a80;
}

.hw-demo, .hw-demo *, .hw-demo *::before, .hw-demo *::after { box-sizing: border-box; }

.hw-demo .hw-wrap {
color: var(--hw-fg);
font-family: inherit;
line-height: 1.5;
}
.hw-demo h2 { font-size: 1.25rem; margin: 0 0 4px; }
.hw-demo .intro { color: var(--hw-muted); margin: 0 0 18px; max-width: 600px; }

.hw-demo .ctl { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 8px; }
.hw-demo .scenarios { display: flex; flex-wrap: wrap; gap: 8px; }
.hw-demo .scenario-btn {
background: transparent; color: var(--hw-fg); border: 1px solid var(--hw-line);
border-radius: 8px; padding: 7px 14px; font-size: .85rem; cursor: pointer;
}
.hw-demo .scenario-btn.active { border-color: var(--hw-accent); color: var(--hw-accent); font-weight: 600; }
.hw-demo .play {
background: var(--hw-accent); color: #fff; border: 0; border-radius: 8px; padding: 9px 22px;
font-size: .95rem; font-weight: 600; cursor: pointer;
}
.hw-demo .play:disabled { opacity: .5; cursor: not-allowed; }
.hw-demo .play.ghost { background: transparent; color: var(--hw-muted); border: 1px solid var(--hw-line); }
.hw-demo .scen { color: var(--hw-muted); font-size: .85rem; margin-bottom: 16px; }

.hw-demo .timeline {
position: relative; height: 26px; border-radius: 8px; overflow: hidden; margin-bottom: 14px;
background: var(--hw-bg); border: 1px solid var(--hw-line); font-family: var(--hw-mono); font-size: .68rem;
}
.hw-demo .outage {
position: absolute; top: 0; bottom: 0; background: rgba(229, 72, 77, .18);
border-left: 2px solid var(--hw-bad); border-right: 2px solid var(--hw-bad);
}
.hw-demo .outage span { position: absolute; top: 50%; transform: translateY(-50%); left: 8px; white-space: nowrap; color: var(--hw-bad); }
.hw-demo .playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--hw-fg); left: 0; transition: left .15s linear; }

.hw-demo .lane {
border: 2px solid var(--hw-line); border-radius: 12px; padding: 16px 18px; margin-bottom: 14px;
transition: border-color .3s; position: relative;
}
.hw-demo .lane.hot { border-color: var(--hw-bad); }
.hw-demo .lane.safe { border-color: var(--hw-ok); }
.hw-demo .lane h3 { margin: 0 0 10px; font-size: .95rem; display: flex; align-items: center; gap: 8px; }
.hw-demo .badge {
font-family: var(--hw-mono); font-size: .66rem; padding: 2px 7px; border-radius: 5px;
background: var(--hw-bg); border: 1px solid var(--hw-line); color: var(--hw-muted);
}
.hw-demo .badge.bad { color: var(--hw-bad); border-color: var(--hw-bad); }
.hw-demo .badge.warn { color: var(--hw-warn); border-color: var(--hw-warn); }
.hw-demo .badge.ok { color: var(--hw-ok); border-color: var(--hw-ok); }
.hw-demo .laneflow { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.hw-demo .box {
font-family: var(--hw-mono); font-size: .75rem; border: 1.5px solid var(--hw-line); border-radius: 8px;
padding: 8px 10px; background: var(--hw-bg); white-space: nowrap; transition: all .25s;
}
.hw-demo .box.down { border-color: var(--hw-bad); background: rgba(229, 72, 77, .12); color: var(--hw-bad); }
.hw-demo .box.open { border-color: var(--hw-bad); background: rgba(229, 72, 77, .12); color: var(--hw-bad); }
.hw-demo .box.half { border-color: var(--hw-warn); background: rgba(245, 166, 35, .12); color: var(--hw-warn); }
.hw-demo .arrow { flex: 1; height: 2px; background: var(--hw-line); }
.hw-demo .track { position: relative; height: 20px; margin: 6px 0 4px; }
.hw-demo .dot { position: absolute; top: 6px; width: 8px; height: 8px; border-radius: 50%; left: 0; transition: left .17s linear; }
.hw-demo .dot.ok { background: var(--hw-ok); }
.hw-demo .dot.bad { background: var(--hw-bad); border-radius: 0; }
.hw-demo .dot.rej { background: transparent; border: 2px solid var(--hw-reject); }
.hw-demo .score { display: flex; flex-wrap: wrap; gap: 6px 18px; font-family: var(--hw-mono); font-size: .82rem; margin-top: 6px; }
.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 .big { font-weight: 700; }
.hw-demo .note { color: var(--hw-muted); font-size: .76rem; font-style: italic; margin-top: 6px; }

/* spotlight overlay: 4 dim panels leave a bright hole around the target(s) */
.hw-demo .dim { position: fixed; background: rgba(8, 8, 16, .6); z-index: 40; display: none; }
.hw-demo .dim.show { display: block; }
.hw-demo .ring {
position: fixed; z-index: 45; border: 2px solid var(--hw-accent); border-radius: 8px;
box-shadow: 0 0 0 4px rgba(91, 75, 214, .28); pointer-events: none; display: none; transition: all .2s;
}
.hw-demo .ring.show { display: block; }
.hw-demo .coach {
position: fixed; z-index: 70; width: 300px; background: var(--hw-bg); border: 1px solid var(--hw-line);
border-radius: 12px; padding: 15px 17px; box-shadow: 0 16px 50px rgba(0, 0, 0, .35); display: none;
color: var(--hw-fg); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.hw-demo .coach.show { display: block; }
.hw-demo .coach .step {
font-family: var(--hw-mono); font-size: .68rem; color: var(--hw-accent); text-transform: uppercase;
letter-spacing: .06em; margin-bottom: 6px;
}
.hw-demo .coach h4 { margin: 0 0 6px; font-size: 1.02rem; }
.hw-demo .coach p { margin: 0 0 14px; font-size: .9rem; }
.hw-demo .coach .go {
background: var(--hw-accent); color: #fff; border: 0; border-radius: 8px; padding: 8px 16px;
font-weight: 600; cursor: pointer; font-size: .88rem;
}
.hw-demo .c-arrow {
position: absolute; width: 13px; height: 13px; background: var(--hw-bg);
border-left: 1px solid var(--hw-line); border-top: 1px solid var(--hw-line);
}

@media (prefers-reduced-motion: reduce) {
.hw-demo .playhead, .hw-demo .ring, .hw-demo .box, .hw-demo .lane, .hw-demo .dot { transition: none; }
}
Loading