From 86db4da888fb42459debdc8d848ccbc150931ff0 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 23 Jul 2026 01:11:18 +0200 Subject: [PATCH] feat(web): auto-retry transient "Cannot reach Maple API" errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Network blips previously failed straight to the error UI and stuck there until a manual reload: transport errors were explicitly excluded from the HTTP clients' retry predicates, and nothing ever re-probed a failed atom. HTTP layer: both atom clients now retry transient transport failures via a shared retry policy — up to 3 extra attempts with 300ms/600ms/1.2s backoff, idempotent methods only (mutations never replay), excluding the 45s client timeout and aborted requests. The existing 5xx retries gain the same backoff. UI layer: formatBackendError tags connectivity failures with kind: "network", and a new useNetworkAutoRetry hook probes on a jittered 5s→10s→20s→30s backoff plus immediately on the window `online` event / tab-visible, pausing while hidden or offline. ErrorState shows "Retrying automatically…" and RouteError self-recovers route-loader failures; the manual "Try again" button stays as an instant probe. Verified end-to-end: killed the API mid-session, watched GETs retry with backoff and POSTs fail fast, then restarted the API and the dashboard repopulated with zero user interaction. Co-Authored-By: Claude Fable 5 --- .../web/src/components/common/error-state.tsx | 14 ++++- apps/web/src/components/route-error.tsx | 14 ++++- apps/web/src/hooks/use-network-auto-retry.ts | 62 +++++++++++++++++++ apps/web/src/lib/error-messages.test.ts | 22 +++++++ apps/web/src/lib/error-messages.ts | 5 ++ .../src/lib/services/common/atom-client.ts | 7 ++- .../lib/services/common/retry-policy.test.ts | 40 ++++++++++++ .../src/lib/services/common/retry-policy.ts | 24 +++++++ .../src/lib/services/common/v2-atom-client.ts | 5 ++ 9 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/hooks/use-network-auto-retry.ts create mode 100644 apps/web/src/lib/services/common/retry-policy.test.ts create mode 100644 apps/web/src/lib/services/common/retry-policy.ts diff --git a/apps/web/src/components/common/error-state.tsx b/apps/web/src/components/common/error-state.tsx index 46dac4747..515f36409 100644 --- a/apps/web/src/components/common/error-state.tsx +++ b/apps/web/src/components/common/error-state.tsx @@ -1,4 +1,5 @@ import { Button } from "@maple/ui/components/ui/button" +import { useNetworkAutoRetry } from "@/hooks/use-network-auto-retry" import { formatBackendError } from "@/lib/error-messages" import { cn } from "@maple/ui/lib/utils" @@ -33,6 +34,13 @@ interface ErrorStateProps { export function ErrorState({ error, title, onRetry, variant = "panel", className }: ErrorStateProps) { const formatted = formatBackendError(error) const heading = title ?? formatted.title + // Connectivity blips self-heal: probe on a backoff poll + the `online` + // event, so recovery doesn't require the user to click or reload. + const autoRetrying = formatted.kind === "network" && onRetry !== undefined + useNetworkAutoRetry(autoRetrying, onRetry) + const description = autoRetrying + ? `${formatted.description} Retrying automatically…` + : formatted.description if (variant === "inline") { return ( @@ -46,7 +54,7 @@ export function ErrorState({ error, title, onRetry, variant = "panel", className

{heading}

-

{formatted.description}

+

{description}

{onRetry && (