Claude: changes for issue #36 #39

Merged
Grandiras merged 2 commits from claude/issue-36 into main 2026-07-27 14:48:09 +00:00
Collaborator

Closes #36.

Closes #36.
Stop the reconnect dialog greeting anyone who opens the app
All checks were successful
CI / build (pull_request) Successful in 34s
7a79112765
Backgrounding a page ends its circuit: 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, opening
the moment the connection drops, before the first attempt is made.

The reconnect itself is quick and unremarkable. Being met by "rejoining
the server" for opening the app is not. So ReconnectModal.razor.js
schedules the dialog rather than showing it: a reconnect that lands
inside the grace window puts nothing on screen, and one that doesn't
still gets the dialog, because past that point there is a Retry button
the user needs.

The window is longer just after the page comes back, and that is the
part not to simplify away — a frozen tab runs no script, so the socket's
close is usually first observed after the page is visible again and the
"show" arrives with the page already in the foreground. What marks a
resume is having been hidden recently, not being hidden now.

Circuits are also held for 30 minutes rather than the default three.
Past the retention period the server no longer has the circuit, the
reconnect is rejected and blazor.web.js reloads — so the default turns
"back after lunch" into a full reload with the open dialog and the
scroll position gone. Cheap for a single shared board, capped by
DisconnectedCircuitMaxRetained.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Collaborator

🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.

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

🤖 Reviewing this PR — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/124); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-39/run-124.md) — updating every ~5 minutes while the task runs.
claude-reviewer left a comment

💬 Verdict: comment — notes only, nothing blocking.

Solid, well-documented fix for the reconnect-modal flash, and the reasoning in the comments/CLAUDE.md tracks the actual code. Program.cs's retention bump and the e2e coverage both look reasonable. One real logic gap in ReconnectModal.razor.js: the immediate flag passed to scheduleShow(true) for failed/resume-failed/retry-exception is silently dropped whenever the tab is hidden (but not fully frozen) at the moment those states fire, so a genuinely-exhausted reconnect surfaces its Retry dialog only after the ordinary grace delay on return instead of immediately. Worth a look before merge, though the blast radius is small (an extra ~1-3s before an unavoidable dialog appears, only in the non-frozen-hidden-tab case).


⏱ 5m 3s · 16 turns · tokens: 58.8k in / 29.4k out (+832.3k cache-read)
📜 Full session transcript

💬 **Verdict: comment** — notes only, nothing blocking. Solid, well-documented fix for the reconnect-modal flash, and the reasoning in the comments/CLAUDE.md tracks the actual code. Program.cs's retention bump and the e2e coverage both look reasonable. One real logic gap in `ReconnectModal.razor.js`: the `immediate` flag passed to `scheduleShow(true)` for `failed`/`resume-failed`/retry-exception is silently dropped whenever the tab is hidden (but not fully frozen) at the moment those states fire, so a genuinely-exhausted reconnect surfaces its Retry dialog only after the ordinary grace delay on return instead of immediately. Worth a look before merge, though the blast radius is small (an extra ~1-3s before an unavoidable dialog appears, only in the non-frozen-hidden-tab case). --- ⏱ 5m 3s · 16 turns · tokens: 58.8k in / 29.4k out (+832.3k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-39/run-124.md) <!-- claude-verdict:comment --> <!-- claude-reviewed-head:7a79112765ea1a669db1001cf0fc22ea1ab0e890 -->
@ -11,0 +56,4 @@
cancelShow();
if (reconnectModal.open) return;
if (document.visibilityState !== "visible") {
Collaborator

scheduleShow(immediate) drops the immediate argument entirely when the document is hidden at call time — it just sets showWhenVisible = true and returns (line 62), with no memory of whether the pending show was meant to bypass the grace window.

When the tab later becomes visible, the visibilitychange listener (line 41-53) calls scheduleShow() with no argument (line 51), so the request loses its urgency. Concretely: failed/resume-failed and the retry() exception path all call scheduleShow(true) specifically so an already-exhausted reconnect (there's a Retry button the user needs — see the comment at line 105-106) shows without delay. But if that call happens while the tab is hidden-but-not-frozen (e.g. a background desktop tab, where JS keeps running so Blazor's retries can actually run out while hidden), the urgency is lost, and on return to the foreground the dialog goes through the full RESUME_GRACE_MS/DROP_GRACE_MS delay just like a plain reconnect attempt — even though there's no prospect of a silent reconnect at that point.

A fix would need showWhenVisible to remember whether the deferred show was immediate, and have the visibilitychange handler pass that through to the scheduleShow() call at line 51.

`scheduleShow(immediate)` drops the `immediate` argument entirely when the document is hidden at call time — it just sets `showWhenVisible = true` and returns (line 62), with no memory of whether the pending show was meant to bypass the grace window. When the tab later becomes visible, the `visibilitychange` listener (line 41-53) calls `scheduleShow()` with **no** argument (line 51), so the request loses its urgency. Concretely: `failed`/`resume-failed` and the `retry()` exception path all call `scheduleShow(true)` specifically so an already-exhausted reconnect (there's a Retry button the user needs — see the comment at line 105-106) shows without delay. But if that call happens while the tab is hidden-but-not-frozen (e.g. a background desktop tab, where JS keeps running so Blazor's retries can actually run out while hidden), the urgency is lost, and on return to the foreground the dialog goes through the full `RESUME_GRACE_MS`/`DROP_GRACE_MS` delay just like a plain reconnect attempt — even though there's no prospect of a silent reconnect at that point. A fix would need `showWhenVisible` to remember whether the deferred show was `immediate`, and have the `visibilitychange` handler pass that through to the `scheduleShow()` call at line 51.
Owner

@claude-bot Please pick up the PR review feedback.

@claude-bot Please pick up the PR review feedback.
Author
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/126); 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-39/run-126.md) — updating every ~5 minutes while the task runs.
Grandiras force-pushed claude/issue-36 from 7a79112765
All checks were successful
CI / build (pull_request) Successful in 34s
to ce76490c9b
All checks were successful
CI / build (pull_request) Successful in 34s
2026-07-27 14:38:06 +00:00
Compare
Keep a deferred reconnect dialog's urgency across the foreground
All checks were successful
CI / build (pull_request) Successful in 33s
9aef62581c
A show that arrives while the page is hidden is deferred to the next
foreground, and `showWhenVisible` was a plain boolean — so it forgot
whether the show had been asked for without a grace window.

`failed`, `resume-failed` and a manual retry that throws all call
scheduleShow(true) because the retries are already spent and there is a
Retry button the user has to press. A hidden page is not necessarily a
frozen one: a background desktop tab keeps running script, so those
states can genuinely be reached with nobody looking. The deferred show
then came back as an ordinary one and sat out the full resume window on
a return where there was no reconnect left to wait for.

Carry the flag through the deferral, clear it in cancelShow, and pin it
in reconnect.spec.js. The test drives `resume-failed` rather than
`failed` deliberately: the latter also arms a retry for the next
foreground, whose own failure opens the dialog at once by a different
route, so a test built on it passes with or without this fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Collaborator

Picked up the review's finding — it was real. Pushed to claude/issue-36 (commit 9aef625).

The fix

scheduleShow(immediate) deferred to the next foreground by setting a plain boolean showWhenVisible, so the immediate argument was lost. On return, the visibilitychange handler called scheduleShow() with no argument and the show went through the full RESUME_GRACE_MS — even though failed / resume-failed / the retry-exception path pass true precisely because the retries are already spent and there is a Retry button the user has to press. As the review noted, this is reachable because a hidden page isn't necessarily a frozen one: a background desktop tab keeps running script, so Blazor's retries can run out with nobody looking.

ReconnectModal.razor.js now carries the flag through the deferral (showWhenVisibleImmediate), on both defer paths — the hidden-at-schedule-time branch and the hidden-when-the-timer-fires branch, which had the same gap — and clears it in cancelShow so urgency can't leak into an unrelated later show.

Test

Added a reconnect that ran out while away comes out at once on return to tests/e2e/reconnect.spec.js. It drives resume-failed rather than failed on purpose: failed also arms a retry for the next foreground, and that retry's failure opens the dialog immediately by a different route — a test built on failed passes with or without this fix. I found that out the hard way (see below).

What I actually verified

This runner has no container engine and no dotnet, so I could not bring up the compose stack or run Playwright — that verification is still owed to CI. Instead I drove ReconnectModal.razor.js itself under Node with a fake DOM (fresh module instance per scenario) and ran it against both the pre-fix and post-fix file:

scenario pre-fix post-fix
exhausted while hidden: nothing shown pass pass
exhausted while hidden: shows at once on return fail pass
ordinary drop: still re-graced on return pass pass
ordinary drop: lands after the resume grace pass pass
urgency does not survive a hide pass pass
foreground drop: held for the drop grace pass pass
foreground drop: lands after the drop grace pass pass
exhausted while visible: no wait at all pass pass

So the bug reproduces, the fix closes it, and the other four scheduling behaviours are unchanged. Both changed files also pass node --check. No C# was touched, so the build is unaffected.

CLAUDE.md's Reconnecting section now records why the deferral carries urgency, since a boolean is the obvious "simplification" back into this bug.


⏱ 7m 2s · 36 turns · tokens: 60.5k in / 24.6k out (+1969.6k cache-read)
📜 Full session transcript

Picked up the review's finding — it was real. Pushed to **`claude/issue-36`** (commit `9aef625`). ## The fix `scheduleShow(immediate)` deferred to the next foreground by setting a plain boolean `showWhenVisible`, so the `immediate` argument was lost. On return, the `visibilitychange` handler called `scheduleShow()` with no argument and the show went through the full `RESUME_GRACE_MS` — even though `failed` / `resume-failed` / the retry-exception path pass `true` precisely because the retries are already spent and there is a Retry button the user has to press. As the review noted, this is reachable because a hidden page isn't necessarily a *frozen* one: a background desktop tab keeps running script, so Blazor's retries can run out with nobody looking. `ReconnectModal.razor.js` now carries the flag through the deferral (`showWhenVisibleImmediate`), on both defer paths — the hidden-at-schedule-time branch and the hidden-when-the-timer-fires branch, which had the same gap — and clears it in `cancelShow` so urgency can't leak into an unrelated later show. ## Test Added `a reconnect that ran out while away comes out at once on return` to `tests/e2e/reconnect.spec.js`. It drives `resume-failed` rather than `failed` on purpose: `failed` also arms a retry for the next foreground, and *that* retry's failure opens the dialog immediately by a different route — a test built on `failed` passes with or without this fix. I found that out the hard way (see below). ## What I actually verified This runner has no container engine and no `dotnet`, so I could not bring up the compose stack or run Playwright — that verification is still owed to CI. Instead I drove `ReconnectModal.razor.js` itself under Node with a fake DOM (fresh module instance per scenario) and ran it against both the pre-fix and post-fix file: | scenario | pre-fix | post-fix | |---|---|---| | exhausted while hidden: nothing shown | pass | pass | | exhausted while hidden: shows at once on return | **fail** | pass | | ordinary drop: still re-graced on return | pass | pass | | ordinary drop: lands after the resume grace | pass | pass | | urgency does not survive a `hide` | pass | pass | | foreground drop: held for the drop grace | pass | pass | | foreground drop: lands after the drop grace | pass | pass | | exhausted while visible: no wait at all | pass | pass | So the bug reproduces, the fix closes it, and the other four scheduling behaviours are unchanged. Both changed files also pass `node --check`. No C# was touched, so the build is unaffected. `CLAUDE.md`'s Reconnecting section now records why the deferral carries urgency, since a boolean is the obvious "simplification" back into this bug. --- ⏱ 7m 2s · 36 turns · tokens: 60.5k in / 24.6k out (+1969.6k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-39/run-126.md)
Owner

@claude-bot /review

@claude-bot /review
Author
Collaborator

🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.

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

🤖 Reviewing this PR — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/127); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-39/run-127.md) — updating every ~5 minutes while the task runs.
claude-reviewer left a comment

Verdict: approve — no blocking issues found.

The logic gap flagged in my previous pass is fixed: scheduleShow() now takes and remembers a showWhenVisibleImmediate flag whenever a show request arrives while the tab is hidden, and the visibilitychange listener reads that flag back and passes it through to the re-scheduled scheduleShow(immediate) call. This is exactly what was missing before, and it's the scenario the new test a reconnect that ran out while away comes out at once on return pins directly.

Walked back through the rest of the state machine (show/hide/failed/resume-failed/rejected, retry()'s success/resume/catch paths, cancelShow/hideModal) and didn't find any remaining gaps — urgency and grace-window handling both look consistent now. Program.cs's retention bump and the new tests/e2e/reconnect.spec.js coverage (including the flash-guard test and the two "drop only noticed on foreground" cases) look reasonable and match the documented behavior. CLAUDE.md's new "Reconnecting" section tracks the actual code.

Good to merge.


⏱ 1m 17s · 10 turns · tokens: 36.7k in / 5.3k out (+533.5k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. The logic gap flagged in my previous pass is fixed: `scheduleShow()` now takes and remembers a `showWhenVisibleImmediate` flag whenever a show request arrives while the tab is hidden, and the `visibilitychange` listener reads that flag back and passes it through to the re-scheduled `scheduleShow(immediate)` call. This is exactly what was missing before, and it's the scenario the new test `a reconnect that ran out while away comes out at once on return` pins directly. Walked back through the rest of the state machine (`show`/`hide`/`failed`/`resume-failed`/`rejected`, `retry()`'s success/resume/catch paths, `cancelShow`/`hideModal`) and didn't find any remaining gaps — urgency and grace-window handling both look consistent now. `Program.cs`'s retention bump and the new `tests/e2e/reconnect.spec.js` coverage (including the flash-guard test and the two "drop only noticed on foreground" cases) look reasonable and match the documented behavior. CLAUDE.md's new "Reconnecting" section tracks the actual code. Good to merge. --- ⏱ 1m 17s · 10 turns · tokens: 36.7k in / 5.3k out (+533.5k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-39/run-127.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:9aef62581c3e8d37314c39cfe7282043fcfdf433 -->
Grandiras deleted branch claude/issue-36 2026-07-27 14:48:09 +00:00
Grandiras referenced this pull request from a commit 2026-07-27 14:48:10 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 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!39
No description provided.