Stage 4 — Capacity gate snapshot route + dashboard cell¶
Motivation¶
Stages 1–3 ship the gate logic (2026-05-31-capacity-gate-stage1-tests.md),
per-job workdir isolation (2026-05-31-stage2-workdir-isolation.md),
the cached capacity signal resolver
(2026-05-31-stage3a-capacity-signals.md),
and the worker wiring (2026-05-31-stage3-submit-wiring.md).
The gate's verdict is now observable only inside worker logs — operators have
to grep blast_gate_admit to know if a cluster is congested.
Stage 4 surfaces the gate so the dashboard answers one question without opening a terminal: why did submit just wait, and how many slots are already in use?
User-facing change¶
- The cluster bento on the dashboard grows a new "Capacity Gate" cell next to the recent-runtime summary. It shows:
- State pill (
Admitting/Holding/Preview only) tinted bycapacityGateBandClass(ok / warning / danger / disabled / degraded). - Slots
N / maxwith a thin progress bar. - CPU + Memory request% vs configured watermark, with a watermark tick mark.
- Pending pods count.
- decision_reason code (e.g.
cpu_watermark) when the gate would deny, and a "Signals degraded" warning when the K8s payload is missing. - The cell uses TanStack Query with a 30 s
refetchInterval, so it picks up env flips and node-pool churn without a page reload. - When
BLAST_GATE_ENABLED=falsethe cell renders the same data but labels itself Preview only and uses the muted disabled tone — that matches the rollout plan in Charter §12a Rule 4 where the gate ships default-OFF and can be enabled per environment.
API / IaC diff summary¶
- New route
GET /api/blast/capacity(api/routes/blast/capacity.py) guarded byrequire_caller, query params:subscription_id, resource_group, cluster_name, program=blastn, database=nt. - Response shape:
{ "data": { "enabled": false, "pool": "blastpool", "slots": { "in_use": 0, "max": 1 }, "cpu_request_pct": 10, "memory_request_pct": 15, "watermark_cpu_pct": 75, "watermark_memory_pct": 75, "pending_pods": 0, "decision_preview": "admit", "decision_reason": null, "decision_retryable": false, "predicted_demand": { "cpu_m": 1000, "mem_mib": 4096 }, "active_reservations": [], "signals_degraded": false, "signals_error": null }, "meta": { "generated_at": "...", "warnings": [] } } - Wired into
api/routes/blast/__init__.pybetween the existing_results_routesand the implicit catch-all (no prefix collision — the blast router itself owns/api/blast/*). - The route is read-only and never raises. K8s degradation folds
into
signals_degraded=true+signals_error="<ExceptionClass>"; Redis reservation lookup failures fall back to an empty list. This matches the existing/api/monitor/*"_graceful" contract used elsewhere on the dashboard. - No Bicep changes — the gate already has env defaults from Stage 3c
(
BLAST_GATE_ENABLED=falseon api/worker/beat). Operators flip the env per environment to enforce.
Frontend¶
blastApi.getCapacityGate(...)typed client + exportedCapacityGateSnapshotinterface inweb/src/api/blast.ts.- Pure helper
capacityGateBandClass(snapshot)lives next to the client so vitest can import it without jsdom. - New
CapacityGateCellcomponent inweb/src/components/cards/ClusterBento/CapacityGateCell.tsx, wired intoClusterBentoafter the recent-runtime cell.
Validation evidence¶
uv run pytest -q api/tests/test_blast_capacity_route.py→ 6 passed (default-disabled admit preview, degraded signals deny, reservation list, missing query param 422, auth required, exception is caught).cd web && npm test -- --run CapacityGateCell.test→ 7 passed (capacityGateBandClassenabled / disabled / degraded / danger / warning matrix).cd web && npm run build→ ✓ built in 7.76s, no new TS errors.uv run ruff check api/routes/blast/capacity.py api/tests/test_blast_capacity_route.py api/routes/blast/__init__.py→ All checks passed.
Notes for Stage 5¶
Stage 5 (telemetry) will wire blast_gate_admit / deny / release into
the audit log + add lightweight in-memory counters that the
/api/monitor/sidecars SSE stream surfaces. The Stage 4 snapshot route
is intentionally not the place to emit counters — it stays
idempotent and side-effect-free.