Add relay disconnect UX: friendly errors, reconnect, cached identity#1004
Conversation
0a5b3c7 to
79ebe01
Compare
|
Screenshots of the disconnected-state UX, captured by the new Sidebar — relay unreachableThe channels query fails with a classified Connection banner while reconnectingThe thin strip appears above the content pane once the WebSocket has been degraded for 2s, with its own Reconnect button. Home feed unreachableThe home feed error card uses the shared Channel canvas unreachableThe same single-sourced copy inside the channel management sheet's canvas section. Cached identity while offlineWith a seeded self-profile cache, the profile card keeps the display name and the cached avatar data URL even though No cache — npub fallbackWithout the cache, the same offline state falls back to the npub-derived name and initials. This is the contrast case for the shot above. Profile popover — reconnect to relayThe always-available "Reconnect to relay" action in the profile popover, shown while the connection is degraded. |
41a5566 to
54a4445
Compare
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.
54a4445 to
0c9bdbc
Compare
…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)
…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)







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.relay unreachable:prefix — detect Cloudflare Access intercepts (by final-response host), HTML bodies, and connection/timeout/DNS failures. This coversquery_relay_at()andsubmit_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.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 callsrelayClient.preconnect()+queryClient.invalidateQueries()without unmounting the workspace or clearing draftsuseRelayAutoHeal: 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 recoveredRELAY_UNREACHABLE_SHORT/RELAY_UNREACHABLE_MESSAGEinrelayError.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 detaillocalStoragekeyed by relay+pubkey viaselfProfileStorage;ProfileAvatarfalls 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 allavatarDataUrlis only accepted from storage when it starts withdata: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 pureshouldFetchAvatar/resolveAvatarDataUrlhelpers with unit testsrelay-connectivity-screenshotsPlaywright spec (smoke project) covering the disconnected-state surfaces, backed by new mock-bridge error knobs (channelsReadError,feedReadError,canvasReadError), aget_canvasmock (previously an unsupported-command throw), and a__BUZZ_E2E_SET_RELAY_CONNECTION_STATE__seam for driving theConnectionBannerdeterministically