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
+44 -10
View File
@@ -6,6 +6,50 @@ Newest entries at the top. Group by phase/wave header (mirroring `PLAN.md` / `CM
---
## Phase 2.4 — Interactivity-gap loading guard on dead-during-prerender controls
**Status:** Fully landed on 2026-06-08 (implementation complete, reviewed and merged to dev).
Guard controls that are dead during the SSR→interactive handoff window (12s on fast loads, 5s+ on cold WASM cache) so they *look* inactive until the Blazor runtime attaches, then re-render into their live form. The listener reaches for **play** first — a play button that looks armed but eats the click reads as "the site is broken," not "the site is loading." This is a credibility/perceived-quality fix on the primary action.
**Implementation approach:** Extend the existing `RendererInfo.IsInteractive` pattern already established in `PlayStateIcon.razor` and `DeepDrftHero.razor`. Add `Disabled="@(!RendererInfo.IsInteractive)"` (or the HTML equivalent) to unguarded controls during the SSR phase. No global overlay/scrim (rejected — it fights the prerender's purpose and risks colliding with Blazor's `#components-reconnect-modal`); per-control guarding leaves the working parts (plain `<a>` links, idle UI) live. Each control carries its own inline gate — mild duplication over a shared `<InteractivityGate>` wrapper is deliberately accepted (over-engineering for ~4 call sites; would obscure the per-control rendering differences). Consistent with existing patterns.
**Guarded controls (as implemented):**
- **`TrackCard.razor` play `MudFab` (grid + list mode) — HIGHEST PRIORITY.** Disabled during the gap (greyed, non-interactive via MudBlazor's built-in disabled state). Card looks *composed but not-yet-armed*, not alarmed. Re-enables once `RendererInfo.IsInteractive` flips. Note: `/tracks` bridges *data* across the seam via `PersistentComponentState` — but bridging data ≠ wiring handlers; the gap still exists on a cold WASM cache load.
- **`TracksView.razor` `MudToggleGroup` (grid/list switch) + `MudPagination`.** Both gated to `Disabled="true"` during the gap. Lower priority than play, but cheap to include in the same pass and visually consistent.
- **`SharePopover.razor` (on `TrackDetail`).** The Share `MudIconButton` trigger gated to `Disabled="true"` until interactive; the in-popover copy buttons are moot while the trigger is disabled, so the single guard on the trigger suffices.
- **`DeepDrftMenu.razor` "Stream Now" CTA.** Folded `!RendererInfo.IsInteractive` into the existing `disabled="@(...)"` expression (e.g. `disabled="@(_streamLoading || !RendererInfo.IsInteractive)"`) on both desktop and mobile buttons. The label-swap precedent here ("Finding a track…") is the house voice — disabling is the floor.
**What was deliberately left untouched (mirrors `WASM_SEAMS.md` §2 discipline):**
- **Minimized `AudioPlayerBar` dock** — default state shows only `LevelMeterFab`, which is idle (untinted, no animation) until audio plays. 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. No dead control; the player is gesture-gated and intentionally non-persisted.
- **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. It does 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.
**Prerequisite:** None. Pure client-side rendering work in `DeepDrftPublic.Client`; no API or data-layer change.
---
## LevelMeterFab — Continuous vertical fill animation
**Status:** Fully landed on 2026-06-08 (feature complete, component + CSS animation, merged to dev).
Replaced the discrete three-band tint model with a **continuous vertical fill** inside the music-note SVG silhouette. The fill height tracks live audio level bottom-up (0100%); a fixed three-zone gradient (`linearGradient` with `gradientUnits="userSpaceOnUse"`) renders green (060% of note height), yellow (6085%), and orange (85100%) zones. The color at the fill line therefore changes naturally as the level rises. The note shape remains always visible as a dim silhouette at 25% opacity; idle (paused/stopped) shows the silhouette alone.
**Implementation details:**
- **C# side (`LevelMeterFab.razor.cs`)**: Removed discrete `_bandClass` field; replaced with continuous `_fillPercent` (0100). dB → fill % uses a linear map over a 30 to 0 dB window (30 dB = 0% fill, 0 dB = 100%, 12 dB = 60% / yellow boundary, 4.5 dB = 85% / orange boundary). Smoothing envelope operates on the continuous value (attack-fast / release-slow on dB, then map). Computed properties `FillY` and `FillH` expose the rect geometry to the SVG template.
- **SVG (`LevelMeterFab.razor`)**: Two layers — always-on dim silhouette (note path at 25% white) and a clipped fill group (rectangle revealed through the note via `clipPath`, painted with the zone gradient). No color cascade; explicit rgba on silhouette, explicit colors in gradient stops.
- **Gradient anchoring**: `linearGradient` with `gradientUnits="userSpaceOnUse"` (not `objectBoundingBox`) — x1="0" y1="24" x2="0" y2="0" (bottom to top in viewBox coordinates). This pins the zones to fixed heights so the fill line always crosses the same colors at the same levels.
- **CSS (`LevelMeterFab.razor.css`)**: Removed band-tint color transition (no longer applicable). Geometry attributes `y` and `height` are not CSS-animatable in a reliable way; animation is purely the 30fps C# value updates driven by smoothing envelope. Silhouette remains always-on idle visual when `_fillPercent = 0`.
- **Re-render gate**: 0.5% change threshold prevents churn on sub-pixel deltas; renders only on meaningful level swings.
- **Idle behavior**: `StopAnimation` resets `_fillPercent = 0` and `_smoothedDb = SilenceFloorDb`, dropping the column and leaving only the dim silhouette.
Supersedes the earlier discrete-tint `LevelMeterFab` entry from the same component. The new model is load-bearing for real-time level feedback on a commercial dance-music master (8 to 3 dBFS); the meter "breathes" through the green/yellow zones with peaks reaching orange, rather than holding in one band.
---
## Track Gallery View Toggle
**Status:** Fully landed on 2026-06-08 (feature complete, component + layout + CSS, merged to dev).
@@ -71,16 +115,6 @@ Give the track gallery two switchable view modes behind a page-level toggle: **M
---
## LevelMeterFab — Reactive audio-level-tint FAB
**Status:** Fully landed on 2026-06-08 (feature complete, component + integration, merged to dev).
- **What:** A new `LevelMeterFab` Blazor component that replaces the static music-note FAB in the minimized player dock (`AudioPlayerBar.razor`). Reactively tints the icon based on live audio output level: green (−∞ to 18 dB), yellow (18 to 6 dB), orange (above 6 dB). When idle (no track, paused, stopped), reverts to static untinted state.
- **Why it matters:** The minimized dock is always visible in the UI; adding a live level indicator gives real-time visual feedback on the audio stream's loudness, and the three-band color coding immediately communicates whether the output is quiet, normal, or hot.
- **Implementation:** No TypeScript or `AudioInteropService` changes — reuses the existing spectrum callback infrastructure (`StartSpectrumAnimationAsync` / `StopSpectrumAnimationAsync`). The component subscribes to live spectrum buckets at ~30fps, reduces the peak bucket to a reconstructed dB value via the inverse of the spectrum normalization formula, applies attack-fast/release-slow smoothing, and updates the icon color class. CSS transitions on the color (120ms ease-out) smooth the band changes. Follows the identical state-subscription pattern as `SpectrumVisualizer` — observes `IPlayerService.StateChanged` to toggle animation on play and off on pause/stop/track-end.
---
## Phase 2.5 — "Stream Now" — random-track instant play
**Status:** Fully landed on 2026-06-07 (feature complete, endpoints + service methods + menu wiring, merged to dev).