Web app reconnecting to server #36

Closed
opened 2026-07-27 14:14:28 +00:00 by Grandiras · 2 comments
Owner

When installing the app as a web app, whenever bringing that to the foreground, it triggers the reconnecting modal. Should not happen.

When installing the app as a web app, whenever bringing that to the foreground, it triggers the reconnecting modal. Should not happen.
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/121); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-36/run-121.md) — updating every ~5 minutes while the task runs.
Collaborator

Pushed to claude/issue-36.

What was happening

This isn't a bug in the reconnect logic — it's the framework doing exactly what it's designed to do. Backgrounding a page ends its circuit two ways over:

  • SignalR stops the hub connection on the browser's freeze event, and
  • blazor.web.js beacons /_blazor/disconnect on pagehide.

In an installed web app, switching away is how you leave the app, so every return to the foreground is a reconnect. And the framework's reconnect UI is unconditional: it opens the dialog the moment the connection goes down, before the first attempt has even been made. Hence "rejoining the server" as a greeting.

The fix

ReconnectModal.razor.js now schedules the dialog instead of showing it. A reconnect that lands inside the grace window puts nothing on screen at all; one that doesn't still gets the dialog, because past that point there's a Retry button the user actually needs. failed / resume-failed bypass the delay (but still wait for the foreground), and the retry() catch surfaces a held-back dialog, so a real outage can't end up silently invisible.

The grace window is longer just after the page comes back, and that's the part worth not simplifying later: visibilityState alone can't recognise a backgrounded page. A frozen tab runs no script, so the socket's close is usually first observed after the page unfreezes — the "show" arrives with the page already visible. What marks a resume is having been hidden recently, not being hidden now.

CircuitOptions in Program.cs now holds disconnected circuits for 30 minutes instead of the default three. Past the retention period the server no longer has the circuit, Blazor.reconnect() is rejected, and blazor.web.js reloads the page — so the default turns "back after lunch" into a full reload with your open card modal and scroll position gone. Cheap here for a single shared board, capped by DisconnectedCircuitMaxRetained.

Also documented in CLAUDE.md under a new Reconnecting section.

Verified

I brought up a real stack in the CI container (installed the .NET 10 SDK and Postgres; no container engine available here, so the app ran directly on :8090 in Development).

  • New tests/e2e/reconnect.spec.js, 4 tests, all passing. Playwright can't background a page, so it drives document.visibilityState directly — which is what both this file and Blazor's own retry backoff read — and counts HTMLDialogElement.prototype.showModal rather than polling open, since the complaint is a flash and a poll can miss one.
  • Confirmed the tests fail against the old code: I stashed the fix, rebuilt, restarted, and 3 of the 4 failed (showModal called once, immediately). The 4th — "a connection that is really gone still shows the dialog" — passes both ways by design; it's the guard against over-suppressing.
  • dotnet build clean, dotnet test 46/46 passing.
  • Existing e2e: auth-entrance, board-entrance, i18n, reconnect30/30; board + mobile40/41.

The one failure is environmental, not a regression: mobile.spec.js:103 waits for .card .tag-chip-static, which needs a pre-existing tagged card from the long-lived dev board — my database was freshly migrated and empty. requests.spec.js / pds.spec.js weren't run at all, since there's no PDS container in this environment.

One thing I couldn't exercise end-to-end: the >30-minute background, where the circuit is genuinely gone and the page reloads. That path is unchanged from before apart from being reached far less often.


⏱ 12m 55s · 58 turns · tokens: 92.7k in / 36.1k out (+3731.2k cache-read)
📜 Full session transcript
→ Opened PR: #39

Pushed to **`claude/issue-36`**. ## What was happening This isn't a bug in the reconnect logic — it's the framework doing exactly what it's designed to do. Backgrounding a page ends its circuit two ways over: - SignalR stops the hub connection on the browser's `freeze` event, and - `blazor.web.js` beacons `/_blazor/disconnect` on `pagehide`. In an installed web app, switching away *is* how you leave the app, so **every** return to the foreground is a reconnect. And the framework's reconnect UI is unconditional: it opens the dialog the moment the connection goes down, before the first attempt has even been made. Hence "rejoining the server" as a greeting. ## The fix **`ReconnectModal.razor.js` now schedules the dialog instead of showing it.** A reconnect that lands inside the grace window puts nothing on screen at all; one that doesn't still gets the dialog, because past that point there's a Retry button the user actually needs. `failed` / `resume-failed` bypass the delay (but still wait for the foreground), and the `retry()` catch surfaces a held-back dialog, so a real outage can't end up silently invisible. The grace window is longer just after the page comes back, and that's the part worth not simplifying later: **`visibilityState` alone can't recognise a backgrounded page.** A frozen tab runs no script, so the socket's close is usually first *observed* after the page unfreezes — the "show" arrives with the page already visible. What marks a resume is having been hidden *recently*, not being hidden *now*. **`CircuitOptions` in `Program.cs` now holds disconnected circuits for 30 minutes** instead of the default three. Past the retention period the server no longer has the circuit, `Blazor.reconnect()` is rejected, and `blazor.web.js` reloads the page — so the default turns "back after lunch" into a full reload with your open card modal and scroll position gone. Cheap here for a single shared board, capped by `DisconnectedCircuitMaxRetained`. Also documented in `CLAUDE.md` under a new **Reconnecting** section. ## Verified I brought up a real stack in the CI container (installed the .NET 10 SDK and Postgres; no container engine available here, so the app ran directly on `:8090` in Development). - **New `tests/e2e/reconnect.spec.js`, 4 tests, all passing.** Playwright can't background a page, so it drives `document.visibilityState` directly — which is what both this file and Blazor's own retry backoff read — and counts `HTMLDialogElement.prototype.showModal` rather than polling `open`, since the complaint is a *flash* and a poll can miss one. - **Confirmed the tests fail against the old code**: I stashed the fix, rebuilt, restarted, and 3 of the 4 failed (`showModal` called once, immediately). The 4th — "a connection that is really gone still shows the dialog" — passes both ways by design; it's the guard against over-suppressing. - `dotnet build` clean, `dotnet test` **46/46 passing**. - Existing e2e: `auth-entrance`, `board-entrance`, `i18n`, `reconnect` → **30/30**; `board` + `mobile` → **40/41**. The one failure is environmental, not a regression: `mobile.spec.js:103` waits for `.card .tag-chip-static`, which needs a pre-existing tagged card from the long-lived dev board — my database was freshly migrated and empty. `requests.spec.js` / `pds.spec.js` weren't run at all, since there's no PDS container in this environment. One thing I couldn't exercise end-to-end: the >30-minute background, where the circuit is genuinely gone and the page reloads. That path is unchanged from before apart from being reached far less often. --- ⏱ 12m 55s · 58 turns · tokens: 92.7k in / 36.1k out (+3731.2k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-36/run-121.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/39
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
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
Grandiras/Ponente#36
No description provided.