docs: move PLAN 2.4 to COMPLETED — interactivity-gap loading guards landed

This commit is contained in:
daniel-c-harvey
2026-06-08 14:11:42 -04:00
parent 0392ef6954
commit 095b49701f
4 changed files with 45 additions and 363 deletions
-20
View File
@@ -97,26 +97,6 @@ These follow from `CONTEXT.md §5`. Direction is strongly implied but no specifi
- **Shape:** Same extension to `GetPaged` as 2.2. UI is a debounced text input bound to the VM's filter property. EF Core translates `Contains` to SQLite `LIKE`.
- **Prerequisite:** Fold into 2.2 if both are being done — the same `GetPaged` extension serves both. Doing them separately doubles the API churn.
### 2.4 Interactivity-gap loading guard on dead-during-prerender controls
- **What:** Under `InteractiveAuto` (`App.razor`), the static SSR prerender ships a fully-rendered, clickable-*looking* page before any circuit exists. Every Blazor event-bound control is dead in that window (12s fast, 5s+ on a cold first load with no WASM cache). The listener reaches for **play** first — and nothing happens. Guard these controls so they *look* inactive until the runtime attaches, then re-render into their live form.
- **Why it matters:** The play button on a track is the single most-reached-for control on the site, and it is the most prominent dead one. A play button that looks armed but eats the click is the worst version of this bug — it reads as "the site is broken," not "the site is loading." This is a credibility/perceived-quality fix on the primary action, not a nicety.
- **The pattern already exists — extend it, do not invent a new layer.** `PlayStateIcon.razor` is the reference implementation: gate on `!RendererInfo.IsInteractive`, render a `MudProgressCircular` in place of the live button until hydration, then re-render into the wired control (it carries an explanatory comment). `DeepDrftHero.razor` uses the same `RendererInfo.IsInteractive` seam for animation gating. This item generalises that established idiom across the remaining unguarded controls. **A global overlay/scrim was considered and rejected** — it fights the prerender's purpose (the page is visible and partly usable; plain `<a>` links already work), needs its own teardown, and risks colliding with Blazor's built-in `#components-reconnect-modal`. Per-control guarding leaves the working parts live.
- **`RendererInfo.IsInteractive` returns `false` during static SSR/prerender, `true` once Server circuit or WASM runtime is up.** It is per-component (no app-global flag), available in any component's `@code` / codebehind. Each control below carries its own inline guard — mild duplication of the gate expression, accepted deliberately over a shared `<InteractivityGate>` wrapper (over-engineering for ~4 call sites; would obscure the per-control rendering differences). Consistent with how `PlayStateIcon` and `DeepDrftHero` already do it.
- **Controls to touch, with per-control implementation notes:**
- **`TrackCard.razor` play `MudFab` (grid + list mode) — HIGHEST PRIORITY.** Today a raw `MudFab StartIcon="@PlayPauseIcon" OnClick="@PlayClick"` in both the grid (`deepdrft-track-info-bottom`) and list (`deepdrft-track-row-fab`) branches — it does **not** route through `PlayStateIcon`, so it is unguarded. `TrackCard` is a `ComponentBase` (codebehind `TrackCard.razor.cs`), so read `RendererInfo.IsInteractive` directly there. **Design call (diverges from `PlayStateIcon`'s whole-button-swap on purpose):** do *not* replace each fab with a full `MudProgressCircular` — a 12-card grid would render 12 spinner circles and read as "everything is broken/loading." Instead keep the `MudFab` visually present but `Disabled="true"` during the gap (greyed, non-interactive via MudBlazor's built-in disabled state), optionally with the play glyph dimmed or a small inline busy hint. The card should look *composed but not-yet-armed*, not alarmed. Re-enable (the existing `OnClick` wiring takes over) once `RendererInfo.IsInteractive` flips. Note: `/tracks` already bridges *data* across the seam via `PersistentComponentState` (`WASM_SEAMS.md` S1) — but bridging data ≠ wiring handlers; the gap still exists on a cold WASM-cache load even though the cards are populated.
- **`TracksView.razor` `MudToggleGroup` (grid/list switch) + `MudPagination`.** Both have a `Disabled` property. Gate both on `!RendererInfo.IsInteractive``Disabled="true"` during the gap. Lower priority than play (a user who can't switch view modes for 2s is mildly inconvenienced, not misled), but cheap to include in the same pass and visually consistent.
- **`SharePopover.razor` (on `TrackDetail`).** The Share `MudIconButton` (`OnClick="@Toggle"`) and the copy buttons inside the popover are all dead during the gap. `SharePopover` is a `ComponentBase` (codebehind). Gate the trigger `MudIconButton` to `Disabled="true"` until interactive; the in-popover copy buttons are moot while the trigger is disabled (popover can't open), so the single guard on the trigger suffices.
- **`DeepDrftMenu.razor` "Stream Now" CTA.** Already has a `_streamLoading` disabled-guard, but that only covers the *post-click* in-flight window — it does **not** cover the pre-circuit gap, so a click during the gap silently no-ops. Fold `!RendererInfo.IsInteractive` into the existing `disabled="@(...)"` expression (e.g. `disabled="@(_streamLoading || !RendererInfo.IsInteractive)"`) on both the desktop and mobile button. The label-swap precedent here ("Finding a track…") is the house voice — consider a parallel "Warming up…"/spinner affordance during the gap if cheap, but disabling is the floor. The dark-mode toggle in this file is **commented out** — not a live concern; leave it.
- **What is already correct — DO NOT TOUCH (mirrors `WASM_SEAMS.md` §2 discipline):**
- **Minimized `AudioPlayerBar` dock** — default state shows only `LevelMeterFab`, which is idle (untinted, no animation) until audio actually plays. It reads correctly during the gap; nothing to guard.
- **Expanded `AudioPlayerBar` transport zone** — already routes its play/pause glyph through the guarded `PlayStateIcon`. Already covered by the existing pattern.
- **`NowPlaying` / `NowPlayingCard`** — reflect live player state; show "Nothing playing" on both passes on a cold load (per `WASM_SEAMS.md` G2). No dead control; the player is gesture-gated and intentionally non-persisted. Leave it.
- **Plain `<a href>` links** (track titles → `/track/{key}`, nav links, hero CTAs) — work in static SSR. Out of scope by construction.
- **Coexistence constraint:** This guard targets the *initial* SSR→interactive handoff (`RendererInfo.IsInteractive`). It must not duplicate or interfere with Blazor's built-in `#components-reconnect-modal` (dropped-circuit recovery, a different lifecycle event). The two are orthogonal — `RendererInfo.IsInteractive` does not flip back to `false` on a *reconnect*, so the guards correctly stay inactive during a reconnect. Do not wire any custom reconnect UI into this work.
- **Optional brand polish (not the spine of the work):** if a palette-tinted busy affordance is wanted, tint the existing `MudProgressCircular` / disabled-fab glyph toward the accent ("Lowcountry"/"Charleston") rather than introducing a new atmospheric loading layer. Keep the skeleton/spinner vocabulary already established in `TracksView` and `PlayStateIcon`.
- **Prerequisite:** None. Pure client-side rendering work in `DeepDrftPublic.Client`; no API or data-layer change. Can land independently of any Phase 14 item.
- **Reference:** `WASM_SEAMS.md` (the sibling SSR→WASM seam audit) is the precedent doc and idiom source; this item is the *control-interactivity* counterpart to that doc's *state-persistence* focus. Worth a glance before implementation for the `RendererInfo.IsInteractive` rationale already written up there.
---