Skip to content

Build circuit breaker emergency pause status banner 993 - #1090

Merged
Sadeequ merged 3 commits into
StellarFlow-Network:mainfrom
owohdora-stack:build-circuit-breaker-emergency-pause-status-banner-993
Oct 3, 2026
Merged

Sadeequ merged 3 commits into
StellarFlow-Network:mainfrom
owohdora-stack:build-circuit-breaker-emergency-pause-status-banner-993

Conversation

@owohdora-stack

Copy link
Copy Markdown
Contributor

feat(#993): real-time circuit breaker emergency pause status banner

Summary

Adds a high-visibility emergency alert banner to the top of the root layout that activates the instant the indexer mirrors an on-chain CircuitBreakerTriggered event. The banner names every halted protocol module and states the two numbers a user needs during an incident — how long the risk review is expected to take and when the module is estimated to recover.

Alongside the banner, module views can now lock their primary action CTA behind the same pause state, with an informative tooltip explaining why the action died.

What Changed

New — src/types/circuitBreaker.ts

  • CircuitBreakerModule union (swaps, liquidations, bridges, lending, staking, remittance, governance, payments) plus display labels.
  • CircuitBreakerIndexerEvent — the indexer wire envelope. Only module and type are guaranteed; everything else is optional and defaulted during normalisation.
  • ActiveModulePause — the normalised, render-ready record shared by the banner and the CTA lock-down.

New — src/lib/circuitBreakerEvents.ts
Pure, React-free and transport-free parsing/formatting helpers:

  • parseCircuitBreakerFrame / toActiveModulePause — validate and normalise raw frames; returns null for anything malformed so a bad frame can never take the banner down.
  • normalizeCircuitBreakerModule — resolves contract naming variants (swaps, Pause Swaps, pause_liquidations, swap_router) onto a known module; unknown modules are ignored rather than locking down an unrelated view.
  • hasPauseElapsed — auto-resume safety net for a missed CircuitBreakerCleared event.
  • buildPauseTooltip, formatDurationMinutes, formatRecoveryCountdown, formatRecoveryClock — user-facing copy.

New — src/components/circuit-breaker/CircuitBreakerContext.tsx

  • CircuitBreakerProvider owns the single realtime subscription and the set of active pauses, keyed by module so a re-trigger replaces its entry instead of stacking duplicate banners.
  • useCircuitBreakerState() for the banner, useModulePause(module) for module views.
  • A pause clears on CircuitBreakerCleared, or when a self-resuming breaker passes its recovery ETA.

New — src/components/circuit-breaker/CircuitBreakerBanner.tsx (the deliverable)

  • Renders only while a pause is active, above all other content.
  • role="alert" + aria-live="assertive" + aria-atomic so the pause is announced the moment it lands.
  • Lists each halted module with its on-chain reason, a live 1s recovery countdown, and the absolute UTC recovery time.
  • Surfaces a "Reconnecting to indexer" chip when the feed drops mid-pause.

New — src/components/circuit-breaker/PausedActionButton.tsx

  • disabled strips a button from the tab order and stops it emitting pointer/focus events, which would leave a dead control with no explanation. This wrapper hard-disables the CTA via cloneElement, then wraps it in a focusable carrier carrying both a native title tooltip and a styled popover wired through aria-describedby.
  • On the happy path the child is returned untouched — no wrapper, no clone.

Modified — src/utils/WebSocketManager.ts

  • Added a generic subscribeToIndexerEvents fan-out for non-price channels so circuit breaker frames ride the existing centralised socket rather than opening a second connection. The socket already drops unrecognised frame types; they now reach channel owners who own their own payload parsing.

Modified — src/app/layout.tsx

  • CircuitBreakerProvider wraps the whole tree (a pause must be able to lock a CTA on any route, not just the one that happens to be open) and CircuitBreakerBanner is mounted above it in document order.

Modified — src/app/swap/page.tsx

  • The "Swap & Send" CTA is wrapped in PausedActionButton module="swaps", relabels to "Swaps Paused", and handleSwap bails early as defence in depth.

Modified — src/app/globals.css

  • .circuit-breaker-flash keyframes — a slow 1.8s low-contrast flash that reads as urgent without strobing. Added to the existing prefers-reduced-motion: reduce block, alongside the status-ping / status-pulse guards.

Why This Matters

  • A user who is mid-swap when the market gaps needs to know why the button died, not just that it is grey. The tooltip names the trigger reason, the review window and the recovery ETA.
  • Doing this without a shared owner would mean every module view opening its own socket subscription and disagreeing about which modules are halted. The provider is the single source of truth.
  • Counting the banner as a subscription consumer means no second WebSocket is opened on routes that have no live price data.

Accessibility

  • role="alert" / aria-live="assertive" for immediate announcement.
  • Flash suppressed under prefers-reduced-motion: reduce; the pulsing icon uses motion-safe:animate-pulse.
  • A disabled button is not focusable, so the explanatory tooltip lives on a tabIndex={0} carrier — keyboard and touch users can reach it, not just mouse users.
  • Countdown text renders only after mount, so server and client markup agree on first paint.

Files Changed

File Change
src/types/circuitBreaker.ts new — domain types
src/lib/circuitBreakerEvents.ts new — parse/normalize/format helpers
src/components/circuit-breaker/CircuitBreakerContext.tsx new — provider + hooks
src/components/circuit-breaker/CircuitBreakerBanner.tsx new — emergency banner
src/components/circuit-breaker/PausedActionButton.tsx new — CTA lock-down
src/components/circuit-breaker/index.ts new — barrel
src/utils/WebSocketManager.ts modified — indexer event channel
src/app/layout.tsx modified — mount provider + banner
src/app/swap/page.tsx modified — wire swap CTA to guard
src/app/globals.css modified — flash keyframes + reduced-motion guard

Testing

This repo has no wired test runner (npm test runs scripts/test-lang-cache.js; the *.test.tsx files are not attached to a runner), so verification was done with two throwaway harnesses that drove the real modules and were removed before commit.

18 logic checks over circuitBreakerEvents.ts — module alias resolution, malformed-frame rejection, protocol default substitution, auto-resume semantics, and every formatter/tooltip string. This surfaced a real defect: Math.ceil made an minutes < 1 fallback unreachable, so the final minute would have displayed in 0m. Fixed to round up (in 1m).

27 integration checks in jsdom against a fake socket — provider → banner → CTA lock-down end to end:

  • no banner and an enabled CTA in the calm state
  • CircuitBreakerTriggered renders the banner instantly with role="alert"/aria-live="assertive", the module name, the on-chain reason, the review window, a live countdown and the UTC recovery time
  • the CTA becomes disabled + aria-disabled, relabels, and gains the pause marker + a focusable carrier with a native tooltip
  • hover reveals the popover with the reason
  • CircuitBreakerCleared clears the banner and re-enables the CTA
  • malformed frames (connection, price_update, unknown module, invalid JSON) never break the banner

Static checks

  • tsc --noEmit output is byte-identical to the pre-change baseline — 33 pre-existing errors, 0 new.
  • eslint on all changed and new files reports only the 5 pre-existing problems, confirmed by diffing against a git stashed baseline.

Notes / Out of Scope

  • No liquidation module exists in this repo — there is no liquidation view or CTA to lock down. The mechanism supports module="liquidations" and any view can opt in with one line, but I did not invent a module to satisfy the issue's example. swaps is the one real module view wired up.
  • Nothing emits these events yet. The indexer lives outside this frontend and no Soroban circuit-breaker contract is deployed against, so server.js was deliberately left untouched. The client is the deliverable and degrades to "no banner" rather than erroring. Wiring a producer is a follow-up once the indexer exists.
  • Pre-existing issues left untouched, per scope: the 33 tsc syntax errors in PoolTable.tsx, TransactionHistoryTable.tsx, settings/page.tsx, dashboard/transactions/page.tsx and app/services/api.ts; the MobileBottomNav is not defined lint error in layout.tsx; and the unused-var warnings in layout.tsx / swap/page.tsx.

Closes #993

@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

@owohdora-stack Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Sadeequ
Sadeequ merged commit 697ae93 into StellarFlow-Network:main Oct 3, 2026
3 of 10 checks passed

This branch had an error being deployed

1 failed deployment
github-pages — adf4299c Deployed Oct 2, 2026 by owohdora-stack via build-and-preview #119
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🚨 Build Real-Time Circuit Breaker Emergency Pause Status Banner

3 participants