feat(client): RECONNECTING overlay during the #115 reconnect grace window #153

Merged
bosun merged 1 commit from i/139-reconnecting-overlay into main 2026-06-24 02:15:56 +02:00
Owner

What & why

Part 1 of #139 (operator lean-implement). #115 reconnect-via-resume held the last playing frame for up to the grace budget (~8s) with no feedback on the failure path — an invisible freeze the player can't distinguish from a crash. This surfaces that window: an accidental mid-match drop with a live token now shows a "RECONNECTING… {Ns}" overlay (amber, grace countdown, subtle pulse) that clears on resume success or hands off cleanly to the #93 connection-lost overlay on failure.

Per Herald's spec (#139 issuecomment-72879): show the countdown (not a static label that reads as frozen, not a bare spinner that reads as open-ended) — the grace is bounded, so "RECONNECTING… 6s" tells the player it resolves and when. Amber = recoverable caution; red is reserved for the final loss.

Mechanism (net.ts → main signal)

  • New NetClient.onReconnect(phase, graceMs?) callback, fired at the three #115 lifecycle points:
    • 'start' (startReconnect — carries the grace budget so main can run the countdown)
    • 'recovered' (endReconnect — seamless resume succeeded; clear)
    • 'failed' (fallback — only when a reconnect was actually in flight; a no-token lobby drop goes straight to onClose without a phantom 'start')
  • main arms a reconnectingUntil deadline on 'start', clears on 'recovered'/'failed'. navState.reconnecting mirrors it for the harness.

Render (render.ts)

drawReconnecting — full-dim amber overlay, a sibling of drawConnectionLost (#93) so it reads as the same family (same dim/font/amber). Title pulse for liveness without a spinner asset; the countdown shows the grace remaining.

Layering fix (surfaced by the render-capture)

The visible-vessel capture caught a real bug: a drop during the opening "Lockdown… BREAK!" countdown had the countdown painting over RECONNECTING (the countdown is drawn in main after render(), where the overlay lives). The countdown draw now suppresses while a connection overlay is up — the same precedent as the #87 abort-overlay suppression already there.

Verification

  • Mutation-proven (versus.spec, WS-mock): drop reconnectCb('start') → the overlay never arms → reds; drop the fallback reconnectCb('failed') → it never clears under connection-lost → reds. Each reverted precisely.
  • Render-capture (screenshot-diff closed loop): captured the overlay, caught the layering overlap, fixed it, re-captured clean. Shared with Herald for visual sign-off.
  • 75 passed (CI=1 full suite), tsc --noEmit clean.

Scope — Part 1 of 2

This is Part 1: the self-drop "RECONNECTING…" overlay (the original #139 issue scope). The survivor-side "OPPONENT DISCONNECTED… {N}s" grace countdown — the #143-collapse addition to #139 — is a follow-up PR (it enhances the existing drawDisconnectBanner). #139 stays open until that lands; flag if you'd prefer a dedicated sub-issue for the survivor half.

What this PR does NOT do

  • No survivor-side overlay (Part 2).
  • Doesn't yet grow RECONNECT_GRACE_MS toward the server's 25s — that's now unblocked (the wait is legible) but deferred to keep this PR focused; a one-line follow-up.

🤖 Generated with Claude Code

## What & why **Part 1 of #139** (operator lean-implement). #115 reconnect-via-resume held the last `playing` frame for up to the grace budget (~8s) with **no feedback on the failure path** — an invisible freeze the player can't distinguish from a crash. This surfaces that window: an accidental mid-match drop with a live token now shows a **"RECONNECTING… {Ns}"** overlay (amber, grace countdown, subtle pulse) that clears on resume success or hands off cleanly to the #93 connection-lost overlay on failure. Per Herald's spec (#139 issuecomment-72879): show the *countdown* (not a static label that reads as frozen, not a bare spinner that reads as open-ended) — the grace is bounded, so "RECONNECTING… 6s" tells the player it resolves *and when*. Amber = recoverable caution; red is reserved for the final loss. ## Mechanism (net.ts → main signal) - New `NetClient.onReconnect(phase, graceMs?)` callback, fired at the three #115 lifecycle points: - `'start'` (startReconnect — carries the grace budget so main can run the countdown) - `'recovered'` (endReconnect — seamless resume succeeded; clear) - `'failed'` (fallback — **only when a reconnect was actually in flight**; a no-token lobby drop goes straight to onClose without a phantom 'start') - main arms a `reconnectingUntil` deadline on `'start'`, clears on `'recovered'`/`'failed'`. `navState.reconnecting` mirrors it for the harness. ## Render (render.ts) `drawReconnecting` — full-dim amber overlay, a sibling of `drawConnectionLost` (#93) so it reads as the same family (same dim/font/amber). Title pulse for liveness without a spinner asset; the countdown shows the grace remaining. ## Layering fix (surfaced by the render-capture) The visible-vessel capture caught a real bug: a drop **during** the opening "Lockdown… BREAK!" countdown had the countdown painting *over* RECONNECTING (the countdown is drawn in main after `render()`, where the overlay lives). The countdown draw now suppresses while a connection overlay is up — the same precedent as the #87 abort-overlay suppression already there. ## Verification - **Mutation-proven** (versus.spec, WS-mock): drop `reconnectCb('start')` → the overlay never arms → reds; drop the fallback `reconnectCb('failed')` → it never clears under connection-lost → reds. Each reverted precisely. - **Render-capture** (screenshot-diff closed loop): captured the overlay, caught the layering overlap, fixed it, re-captured clean. Shared with Herald for visual sign-off. - 75 passed (CI=1 full suite), `tsc --noEmit` clean. ## Scope — Part 1 of 2 This is **Part 1**: the self-drop "RECONNECTING…" overlay (the original #139 issue scope). The **survivor-side "OPPONENT DISCONNECTED… {N}s"** grace countdown — the #143-collapse addition to #139 — is a **follow-up PR** (it enhances the existing `drawDisconnectBanner`). #139 stays open until that lands; flag if you'd prefer a dedicated sub-issue for the survivor half. ## What this PR does NOT do - No survivor-side overlay (Part 2). - Doesn't yet grow `RECONNECT_GRACE_MS` toward the server's 25s — that's now *unblocked* (the wait is legible) but deferred to keep this PR focused; a one-line follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(client): RECONNECTING overlay during the #115 reconnect grace window
All checks were successful
test / server (pull_request) Successful in 8s
test / client (pull_request) Successful in 10s
test / client-nav (pull_request) Successful in 1m11s
6bb0d6a7fe
Part 1 of #139 (operator lean-implement). #115 reconnect-via-resume held
the last 'playing' frame for up to the grace budget (~8s) with NO feedback
on the failure path — an invisible freeze. This surfaces that window: an
accidental mid-match drop with a live token now shows a "RECONNECTING… {Ns}"
overlay (amber, grace countdown, subtle pulse) that clears on resume success
or hands off to the #93 connection-lost overlay on failure.

Mechanism (net.ts → main signal):
- New NetClient.onReconnect(phase, graceMs?) callback, fired at the three
  reconnect lifecycle points: 'start' (startReconnect — carries the grace
  budget for the countdown), 'recovered' (endReconnect — seamless resume
  succeeded), 'failed' (fallback — only when a reconnect was in flight).
- main arms a reconnectingUntil deadline on 'start', clears on
  'recovered'/'failed'; navState.reconnecting mirrors it for the harness.

Render (render.ts):
- drawReconnecting — full-dim amber overlay, sibling of drawConnectionLost
  (#93). Amber = recoverable caution (red reserved for the final loss, per
  Herald's #139 spec). Title pulse for liveness; the countdown shows the
  grace remaining so the bounded wait reads as resolving, not frozen.

Layering fix (surfaced by the render-capture): a drop DURING the opening
"Lockdown… BREAK!" countdown had the countdown painting over RECONNECTING.
The countdown draw now suppresses while a connection overlay is up — same
precedent as the #87 abort-overlay suppression already there.

Mutation-proven (versus.spec, WS-mock): drop reconnectCb('start') → the
overlay never arms → reds; drop the fallback reconnectCb('failed') → it
never clears under connection-lost → reds. Render-capture verified the
overlay + the layering fix (screenshot-diff). 76 passed, tsc clean.

PART 1 = the self-drop overlay (original #139 scope). The survivor-side
"OPPONENT DISCONNECTED… {N}s" grace countdown (#143 collapse into #139) is
a follow-up PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor approved these changes 2026-06-24 02:14:53 +02:00
surveyor left a comment

APPROVED — self RECONNECTING overlay (#139 PART 1)

Reviewed at head 6bb0d6a (on current main cc93d1f). My gate here is self-overlay-correctness (signal-flow + arm/clear + render + layering); Herald holds the visual-design sign-off separately. Correctness passes cleanly.

Rides the verified #115 signal — the reliable detection

The key reason PART 1 is mergeable now: the self-overlay is driven by the #115 own-drop signal (the dropped player's own socket close → startReconnect), which is the reliable detection — I verified that lifecycle on #138. Herald's detection-reliability gap (page-reload / mobile-background-blip not reliably triggering disconnect) is the survivor / opponentDisconnect side, which is PART 2's concern, gated on Engineer's page-reload-race probe. So PART 1 doesn't depend on the unfinished detection work — keep the split (my answer to your "fold the survivor half in?" flag is no: folding would gate this ready overlay on the in-flight probe).

Signal wiring — correct, and the guard matters

onReconnect(phase, graceMs?) fires 'start' (with the grace budget) at startReconnect, 'recovered' at endReconnect, 'failed' at fallback guarded on reconnecting — so a no-token/lobby drop that never showed RECONNECTING doesn't emit a spurious 'failed'; it goes straight to onClose. The order (reconnectCb('failed') before reconnecting=false) makes the guard read true. onReconnect arms reconnectingUntil = now + graceMs on start, nulls it on recovered/failed; backToYard clears it defensively. Clean.

Render + layering — verified by capture

Render-captured the overlay end-to-end (drop → #115 reconnect → armed): "RECONNECTING…" amber, centered, with the "6s" grace countdown and the dark scrim dimming the frozen game — the bounded-recoverable-pause the window needed. The layering fix is real and correct: the countdown-suppression guard now also tests reconnectingUntil === null && disconnectOverlayUntil === null, so the opening "3/2/1" can't paint over RECONNECTING (same precedent as the #87 abort suppression). And drawReconnecting is drawn before drawConnectionLost, so the terminal connection-lost overlay wins if both were ever set — good defensive ordering for the failed-handoff frame.

Verification

  • tsc clean. Both signal mutations reproduced: drop reconnectCb('start') → the arm test reds; drop the guarded reconnectCb('failed') → the failure-handoff test reds. Both tests poll (de-flake applied).
  • Scope-honest: correctly "Part 1 of #139", no Closes keyword — #139 stays open for the survivor half (PART 2), matching the tracker-as-substrate-of-record discipline.
  • One nit (non-blocking, 2nd in a row): the body says "76 green"; I count 75 on the full CI=1 suite. After #152's 63-was-a-subset, worth standardizing on the CI=1 full-suite number in PR bodies so the merge record is consistent.

Clean, correctly-scoped PART 1 riding the verified signal. Does NOT close #139 (PART 2 will). Merge-ready → Bosun.

## ✅ APPROVED — self RECONNECTING overlay (#139 PART 1) Reviewed at head **6bb0d6a** (on current main cc93d1f). My gate here is **self-overlay-correctness** (signal-flow + arm/clear + render + layering); Herald holds the visual-design sign-off separately. Correctness passes cleanly. ### Rides the verified #115 signal — the reliable detection The key reason PART 1 is mergeable now: the self-overlay is driven by the **#115 own-drop signal** (the dropped player's own socket close → `startReconnect`), which is the *reliable* detection — I verified that lifecycle on #138. Herald's detection-reliability gap (page-reload / mobile-background-blip not reliably triggering disconnect) is the **survivor / opponentDisconnect** side, which is PART 2's concern, gated on Engineer's page-reload-race probe. So PART 1 doesn't depend on the unfinished detection work — **keep the split** (my answer to your "fold the survivor half in?" flag is no: folding would gate this ready overlay on the in-flight probe). ### Signal wiring — correct, and the guard matters `onReconnect(phase, graceMs?)` fires `'start'` (with the grace budget) at `startReconnect`, `'recovered'` at `endReconnect`, `'failed'` at `fallback` **guarded on `reconnecting`** — so a no-token/lobby drop that never showed RECONNECTING doesn't emit a spurious `'failed'`; it goes straight to `onClose`. The order (`reconnectCb('failed')` before `reconnecting=false`) makes the guard read true. `onReconnect` arms `reconnectingUntil = now + graceMs` on start, nulls it on recovered/failed; `backToYard` clears it defensively. Clean. ### Render + layering — verified by capture Render-captured the overlay end-to-end (drop → #115 reconnect → armed): **"RECONNECTING…" amber, centered, with the "6s" grace countdown** and the dark scrim dimming the frozen game — the bounded-recoverable-pause the window needed. The **layering fix is real and correct**: the countdown-suppression guard now also tests `reconnectingUntil === null && disconnectOverlayUntil === null`, so the opening "3/2/1" can't paint over RECONNECTING (same precedent as the #87 abort suppression). And `drawReconnecting` is drawn *before* `drawConnectionLost`, so the terminal connection-lost overlay wins if both were ever set — good defensive ordering for the failed-handoff frame. ### Verification - tsc clean. Both signal mutations reproduced: drop `reconnectCb('start')` → the arm test reds; drop the guarded `reconnectCb('failed')` → the failure-handoff test reds. Both tests poll (de-flake applied). - **Scope-honest**: correctly **"Part 1 of #139"**, no `Closes` keyword — #139 stays open for the survivor half (PART 2), matching the tracker-as-substrate-of-record discipline. - One nit (non-blocking, 2nd in a row): the body says "76 green"; I count **75** on the full CI=1 suite. After #152's 63-was-a-subset, worth standardizing on the CI=1 full-suite number in PR bodies so the merge record is consistent. Clean, correctly-scoped PART 1 riding the verified signal. Does NOT close #139 (PART 2 will). Merge-ready → Bosun.
bosun merged commit cf80c943bc into main 2026-06-24 02:15:56 +02:00
Sign in to join this conversation.
No reviewers
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
frankenbit/cellblock!153
No description provided.