feat(reconnect): visible reconnect indicator with backoff and soft-refresh #62

Merged
longjacksonle merged 7 commits from feat/reconnect-ui into main 2026-05-10 16:09:23 +02:00

Summary

  • Visible connection-status banner above the layout: hidden when healthy, yellow
    "Connection lost. Reconnecting..." on drop, escalates to red after 5min, briefly
    green-flashes "Connected" on recovery.
  • Capped jittered exponential backoff (1s, 2s, 4s, 8s, 16s, 30s+) overriding the
    htmx-ext-ws default (uncapped full-jitter, exceeds 17min after ~10 retries).
  • Half-open detection via a 30s <!-- ping --> text-frame heartbeat (replaces the old
    protocol-level Ping which was invisible to JS); client resets a 60s watchdog on every
    htmx:wsAfterMessage.
  • Companion lc-ws-reconnect htmx extension exposes wrapper.init() so the watchdog can
    force a reconnect for close codes the extension would otherwise ignore (1006/1012/1013 are
    auto-handled; everything else - clean closes, our own 4000 from the watchdog - now
    reconnects via this path).
  • On recovery, soft-refreshes the current page via htmx.ajax(GET pathname, select '#main'). The smoke directive in the plan asks the reviewer to verify whether this is
    load-bearing vs. the per-page subscribe re-fire being sufficient; if the latter, drop the
    soft-refresh in a follow-up commit.

Architectural decisions

  • Kept htmx-ext-ws, did not replace it. Inbound OOB-swap processing uses
    extension-internal API; replacing would have meant reimplementing the swap loop. Per-room
    subscribe frames already attach to htmx:wsOpen and re-fire automatically on reconnect
    for free.
  • Replaced protocol Ping with text-frame heartbeat. Browsers handle protocol pings
    transparently and give JS no signal. Single mechanism keeps NAT alive AND drives the
    client watchdog.
  • select: '#main' for soft-refresh. Verified in vendored htmx; extracts just the
    <main> from the full-page response, avoiding nested-layout issues.

Test plan

Automated: just check, just test, just test-saas, just verify all pass.

Manual smoke (run from a browser; see plan section "Final verification" for the full
list):

  • Hard server restart: banner appears within ~1s, retries with backoff, flashes green
    on recovery
  • DevTools Offline -> Online cycle
  • 5min outage -> banner escalates to "Still trying to reconnect"
  • Half-open simulation (comment out the heartbeat tick): watchdog fires within 60s
  • Multi-tab cross-check: only the dropped tab shows the banner
  • Subscribe re-fires on reconnect: messages from other users arrive live without
    manual refresh
  • Soft-refresh load-bearing check: with subscribe re-fire alone (soft-refresh
    commented out), do users see messages sent during the disconnect window? If no,
    soft-refresh stays. If yes (or the gap is acceptable), follow-up commit drops the
    htmx.ajax call.
  • No banner on a healthy load; no console errors
## Summary - Visible connection-status banner above the layout: hidden when healthy, yellow "Connection lost. Reconnecting..." on drop, escalates to red after 5min, briefly green-flashes "Connected" on recovery. - Capped jittered exponential backoff (1s, 2s, 4s, 8s, 16s, 30s+) overriding the htmx-ext-ws default (uncapped full-jitter, exceeds 17min after ~10 retries). - Half-open detection via a 30s `<!-- ping -->` text-frame heartbeat (replaces the old protocol-level Ping which was invisible to JS); client resets a 60s watchdog on every `htmx:wsAfterMessage`. - Companion `lc-ws-reconnect` htmx extension exposes `wrapper.init()` so the watchdog can force a reconnect for close codes the extension would otherwise ignore (1006/1012/1013 are auto-handled; everything else - clean closes, our own 4000 from the watchdog - now reconnects via this path). - On recovery, soft-refreshes the current page via `htmx.ajax(GET pathname, select '#main')`. **The smoke directive in the plan asks the reviewer to verify whether this is load-bearing vs. the per-page subscribe re-fire being sufficient; if the latter, drop the soft-refresh in a follow-up commit.** ## Architectural decisions - **Kept htmx-ext-ws, did not replace it.** Inbound OOB-swap processing uses extension-internal API; replacing would have meant reimplementing the swap loop. Per-room `subscribe` frames already attach to `htmx:wsOpen` and re-fire automatically on reconnect for free. - **Replaced protocol Ping with text-frame heartbeat.** Browsers handle protocol pings transparently and give JS no signal. Single mechanism keeps NAT alive AND drives the client watchdog. - **`select: '#main'` for soft-refresh.** Verified in vendored htmx; extracts just the `<main>` from the full-page response, avoiding nested-layout issues. ## Test plan Automated: `just check`, `just test`, `just test-saas`, `just verify` all pass. Manual smoke (run from a browser; see plan section "Final verification" for the full list): - [ ] Hard server restart: banner appears within ~1s, retries with backoff, flashes green on recovery - [ ] DevTools Offline -> Online cycle - [ ] 5min outage -> banner escalates to "Still trying to reconnect" - [ ] Half-open simulation (comment out the heartbeat tick): watchdog fires within 60s - [ ] Multi-tab cross-check: only the dropped tab shows the banner - [ ] Subscribe re-fires on reconnect: messages from other users arrive live without manual refresh - [ ] **Soft-refresh load-bearing check:** with subscribe re-fire alone (soft-refresh commented out), do users see messages sent during the disconnect window? If no, soft-refresh stays. If yes (or the gap is acceptable), follow-up commit drops the `htmx.ajax` call. - [ ] No banner on a healthy load; no console errors
Covers connection-lost banner (yellow/red/green states), capped jitter
backoff, half-open detection via a 30s text-frame heartbeat replacing
the protocol-level ping, the lc-ws-reconnect companion extension for
forcing reconnect on non-1006/1012/1013 close codes, and a soft-refresh
on recovery to surface missed messages.
Plan covers a visible reconnect indicator backed by capped/jittered backoff, a half-open watchdog driven by an HTML-comment heartbeat frame, and an optional soft-refresh on recovery. Three open questions resolved up-front: htmx.ajax supports `select`, login/register skip layout.html so the IIFE only runs where the banner element exists, and the deferred-script ordering keeps the new htmx extension definition ahead of body processing.

refactor(ws): emit text-frame heartbeat in place of protocol ping

Browsers handle protocol-level WebSocket pings transparently and give the JS no signal. A 30s \`<!-- ping -->\` text frame keeps NAT/proxies alive and gives the client a visible \"still alive\" event via \`htmx:wsAfterMessage\`, which the upcoming reconnect UI uses to detect half-open connections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixed-position element at the top of the viewport with three visible states (reconnecting / failed-long / connected-flash) keyed off a `data-state` attribute. Hidden by default so a healthy connection paints nothing. The included `<style>` block carries the data-state CSS because tailwind's data-attribute variant isn't enabled in this project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Includes the new partial as the first child of the WS-root div so the banner is in scope of the IIFE that drives it. Adds lc-ws-reconnect to the body's hx-ext alongside response-targets so the upcoming companion extension can register itself before htmx processes the body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drives the connection-status banner from htmx:wsOpen / wsClose / wsError, with a 60s half-open watchdog reset by htmx:wsAfterMessage. Caps the extension's reconnect-delay at 30s with jitter, and adds a tiny lc-ws-reconnect htmx extension so the watchdog can force a reconnect for close codes the extension would otherwise ignore. On successful reconnect after a drop, soft-refreshes the current page via htmx.ajax so the user sees content they missed; the soft-refresh is gated on a smoke-test directive in the plan and may be removed in a follow-up if the per-page subscribe re-fire turns out to cover the recovery story already.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cheap server-side guards: (1) GET / for an authenticated user includes the connection-status banner element with `data-state="hidden"`, so a future template rename does not silently delete the partial; (2) the same response carries the `<main id="main">` wrapper that the client-side soft-refresh extracts via `select: '#main'` on reconnect. Most of phase 18 is client-side JS lifecycle that needs a browser harness this repo does not have; those paths stay covered by the manual smoke list in the plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/reconnect-ui 2026-05-10 16:09:23 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!62
No description provided.