Skip to content

Add relay disconnect UX: friendly errors, reconnect, cached identity#1004

Merged
wpfleger96 merged 3 commits into
mainfrom
wpfleger/relay-connectivity-ux
Jun 12, 2026
Merged

Add relay disconnect UX: friendly errors, reconnect, cached identity#1004
wpfleger96 merged 3 commits into
mainfrom
wpfleger/relay-connectivity-ux

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

This PR improves the desktop app's behavior when the relay is unreachable — replacing raw network errors with friendly disconnected states, adding reconnect affordances, and caching the user's identity locally so the sidebar doesn't revert to their npub key.

When WARP VPN needs its daily reauth, Cloudflare Access intercepts relay HTTPS requests and returns an HTML login page with HTTP 200. Previously the Rust backend passed this through as failed to parse query response: error decoding response body for url (https://sqprod.cloudflareaccess.com/...), rendering verbatim in the sidebar — confusing and non-actionable.

  • Classify relay HTTP errors with a stable relay unreachable: prefix — detect Cloudflare Access intercepts (by final-response host), HTML bodies, and connection/timeout/DNS failures. This covers query_relay_at() and submit_event() as well as the media commands (upload_blob, fetch_blob_bytes), which previously surfaced raw reqwest errors (embedding full request URLs) and raw response bodies in upload/download toasts. Raw URLs and HTML bodies are never included in error strings.
  • Add ConnectionBanner — a thin warning strip above the content pane that auto-appears after 2s when degraded, with a Reconnect button; also add a "Reconnect to relay" item to the profile popover (always visible); reconnect calls relayClient.preconnect() + queryClient.invalidateQueries() without unmounting the workspace or clearing drafts
  • Auto-heal via useRelayAutoHeal: when the WebSocket recovers from a degraded state, invalidate all queries so errored queries (messages don't poll) refetch automatically without any user action — rate-limited to once per 15s so a flappy connection (e.g. VPN toggling) can't repeatedly fire an unfiltered invalidation at a relay that just recovered
  • Single-source the disconnected-state copy as RELAY_UNREACHABLE_SHORT / RELAY_UNREACHABLE_MESSAGE in relayError.ts, shared by the sidebar banner, home screen, and channel canvas; relayErrorDetail() falls back to the full message instead of an empty string when a classified error carries no detail
  • Cache the last-fetched self-profile (display name + avatar as a ≤256 KB data URL) in localStorage keyed by relay+pubkey via selfProfileStorage; ProfileAvatar falls back to the cached data URL when the live proxied URL fails to load, and renders initials immediately (instead of after the 200ms fallback delay) when there is no image source at all
  • Harden the self-profile cache: writes are skipped entirely when the serialized payload is unchanged (the 30s profile poll otherwise rewrote ~341KB and re-notified listeners every cycle); avatarDataUrl is only accepted from storage when it starts with data:image/ since it flows into an <img src> sink; removing a workspace garbage-collects all cached profiles for that relay; the avatar fetch/reuse policy is extracted into pure shouldFetchAvatar / resolveAvatarDataUrl helpers with unit tests
  • Add a relay-connectivity-screenshots Playwright spec (smoke project) covering the disconnected-state surfaces, backed by new mock-bridge error knobs (channelsReadError, feedReadError, canvasReadError), a get_canvas mock (previously an unsupported-command throw), and a __BUZZ_E2E_SET_RELAY_CONNECTION_STATE__ seam for driving the ConnectionBanner deterministically

@wpfleger96 wpfleger96 force-pushed the wpfleger/relay-connectivity-ux branch from 0a5b3c7 to 79ebe01 Compare June 12, 2026 15:49
@wpfleger96

Copy link
Copy Markdown
Collaborator Author

Screenshots of the disconnected-state UX, captured by the new relay-connectivity-screenshots.spec.ts (runs in the Playwright smoke project). To re-run locally: cd desktop && pnpm run build && pnpm exec playwright test tests/e2e/relay-connectivity-screenshots.spec.ts — PNGs land in desktop/test-results/relay-connectivity-screenshots/.

Sidebar — relay unreachable

The channels query fails with a classified relay unreachable: error, so the sidebar shows the friendly copy and a Reconnect affordance instead of a raw reqwest error string.

01-sidebar-unreachable

Connection banner while reconnecting

The thin strip appears above the content pane once the WebSocket has been degraded for 2s, with its own Reconnect button.

02-connection-banner

Home feed unreachable

The home feed error card uses the shared RELAY_UNREACHABLE_MESSAGE copy with a retry affordance.

03-home-unreachable

Channel canvas unreachable

The same single-sourced copy inside the channel management sheet's canvas section.

04-canvas-unreachable

Cached identity while offline

With a seeded self-profile cache, the profile card keeps the display name and the cached avatar data URL even though get_profile fails.

05-cached-identity-offline

No cache — npub fallback

Without the cache, the same offline state falls back to the npub-derived name and initials. This is the contrast case for the shot above.

06-no-cache-npub-fallback

Profile popover — reconnect to relay

The always-available "Reconnect to relay" action in the profile popover, shown while the connection is degraded.

07-profile-popover-reconnect

wpfleger96 added a commit that referenced this pull request Jun 12, 2026
@wpfleger96 wpfleger96 force-pushed the wpfleger/relay-connectivity-ux branch from 41a5566 to 54a4445 Compare June 12, 2026 16:18
When WARP VPN needs daily reauth, Cloudflare Access intercepts relay
HTTPS requests with a 200 HTML response. The Rust backend was passing
these through as raw error strings containing full URLs, which were
rendered verbatim in the sidebar channel list and home feed — confusing
and non-actionable for users.

Three improvements:
- Relay HTTP errors are now classified at the Rust layer with a stable
  "relay unreachable:" prefix (detect CF Access host, HTML content-type,
  connect/timeout/DNS failures); raw URLs and HTML bodies are never
  surfaced in the UI.
- A degraded-state banner (ConnectionBanner) appears above the content
  pane after 2s with a Reconnect button; a "Reconnect to relay" item is
  always available in the profile menu. Reconnect calls
  relayClient.preconnect() + queryClient.invalidateQueries() — no full
  workspace remount, so drafts are preserved. Errored queries also
  auto-refetch when the socket recovers without any manual action.
- The last-fetched self-profile (display name + avatar bytes as a
  size-capped data URL) is persisted to localStorage keyed by relay +
  pubkey so the identity panel renders correctly even when the relay is
  unreachable, instead of reverting to the truncated npub fallback.
Media upload/download leaked raw reqwest errors (embedding full request
URLs) and raw response bodies (Cloudflare HTML login pages) into UI
toasts; route them through the same classification helpers as relay
queries. The self-profile cache rewrote ~341KB to localStorage on every
30s refetch even when unchanged, never GC'd entries for removed
workspaces, and trusted avatarDataUrl into an <img src> sink. Also
single-source the relay-unreachable copy that had drifted across three
surfaces, rate-limit the reconnect auto-heal against flappy connections,
and render avatar initials immediately when no image source exists.
Mock bridge gains channelsReadError/feedReadError/canvasReadError knobs,
a get_canvas case (previously hit the unsupported-command throw), and a
connection-state seam — stall+disconnect can't drive ConnectionBanner
deterministically since reconnect emissions keep resetting the 2s
debounce. The seam reaches the TS-private emitter via a cast in the
test bridge so the production client carries no test-only method.
Covers sidebar/banner/home/canvas error states and the cached-identity
offline fallback.
@wpfleger96 wpfleger96 force-pushed the wpfleger/relay-connectivity-ux branch from 54a4445 to 0c9bdbc Compare June 12, 2026 17:54
@wpfleger96 wpfleger96 merged commit 8c9211f into main Jun 12, 2026
21 checks passed
@wpfleger96 wpfleger96 deleted the wpfleger/relay-connectivity-ux branch June 12, 2026 18:00
wpfleger96 added a commit that referenced this pull request Jun 12, 2026
…session-new

* origin/main:
  Add relay disconnect UX: friendly errors, reconnect, cached identity (#1004)
  feat(agents): add active turn indicators to Agents Menu (#1005)
  ci: add fork guards to docker, release, and auto-tag workflows (#1007)
  docs(nip-rs): add optional thread read context scheme (#1006)
tellaho added a commit that referenced this pull request Jun 12, 2026
…tate

* origin/main:
  Add relay disconnect UX: friendly errors, reconnect, cached identity (#1004)
  feat(agents): add active turn indicators to Agents Menu (#1005)
  ci: add fork guards to docker, release, and auto-tag workflows (#1007)
  docs(nip-rs): add optional thread read context scheme (#1006)
  fix(huddle): Pocket TTS quality overhaul — reference parity + cross-message pipelining (#997)
  Add manual ACP session rotation command (#932)
  fix(desktop): heal stale persona_team_dir paths in release builds (#1003)
  ci(docker): publish public ghcr.io/block/buzz image (native multi-arch) (#986)
  fix(buzz-agent): cap tool-result text at 50 KiB with middle elision (#952)
  feat(huddle): sentence-at-a-time voice-mode guidelines for lower TTS latency (#996)
  Shard desktop Playwright CI jobs (#992)
  chore(release): release version 0.3.18 (#995)
  Video Player Improvements  (#993)
  Improve first-run welcome setup (#970)
  fix(release): use legacy updater key secret (#991)
  Replace built-in personas with Fizz (#987)
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.

1 participant