docs(phase-12): record waveform-visualizer generalization landing

Move the landed Phase 12 section from PLAN.md to COMPLETED.md; update DeepDrftAPI/Content/Public.Client CLAUDE.md for the WaveformVisualizer rename, per-track high-res datum + track-waveforms vault, track-cardinal fetch, popover controls, Ambient slot, and NowPlaying host.
This commit is contained in:
daniel-c-harvey
2026-06-17 12:36:45 -04:00
parent 8a187a3ed8
commit f00758dc47
5 changed files with 67 additions and 197 deletions
-186
View File
@@ -239,192 +239,6 @@ Sequenced as **eight waves**; the critical path is `11.A → 11.B → 11.C → 1
---
## Phase 12 — Waveform Visualizer Generalization + NowPlayingHero Rewire
Take the landed Mix waveform visualizer (the WebGL2 lava renderer + its eight-knob controls, Phase 10
reframe) and **make it the one track-cardinal visualizer** — serving Mix detail, all Release Detail
pages, *and* the home-page NowPlaying card — rendering the waveform of **whatever track is currently
playing/selected**, instead of a Mix-only treatment forked three ways. **Two deliverables, one engine in
three hosting modes, DRY/SOLID the explicit ask.** Full design, the extraction analysis, the per-track
model, Direction B compute, wave decomposition, and open questions:
`product-notes/phase-12-waveform-visualizer-generalization.md`.
**Keystone model correction (Daniel, 2026-06-17): the datum is PER-TRACK, not per-release.** *"Each track
in the release must get the metadata… the release is just the host."* Every track carries its own high-res
waveform datum; the visualizer renders the *currently playing/selected* track's datum, and the release is
merely the host surface. This *simplifies* the design — it aligns with the bridge already keying on
`TrackId`, and it **dissolves** the old "what is a multi-track Cut's waveform?" question (no release-level
datum to choose). Threaded through the datum source, the endpoint shape, the bridge, and acceptance.
**Central finding (verified read, 2026-06-17): the engine is already track-cardinal below the surface.**
`MixWaveformVisualizer`'s bridge keys on `ReleaseEntryKey` + `TrackId` (not Mix); the renderer is a pure
function of a loudness datum + duration; the controls/state are renderer-agnostic. The *only* genuinely
Mix-coupled surface is (1) the datum **fetch** (per-release, `GET api/release/{entryKey}/mix/waveform` 404s
unless `Medium == Mix`) and (2) the high-res datum **source** (the `mix-waveforms` vault, Mix-track-only).
Everything else is just *named* `Mix*`. So "generalize from Mix to all tracks" is a **rename + a per-track
high-res compute generalization, not a rebuild** — the renderer, bridge, controls, read-only contract all
carry forward from the Phase 10 reframe unchanged.
**Datum decision (Daniel, 2026-06-17): Direction B — high-res for ALL media.** Today every uploaded track
gets a **512-bucket** profile (`UnifiedTrackService.UploadAsync``waveform-profiles` vault, consumed by
the player-bar `WaveformSeeker`); only **Mix tracks** *additionally* get the duration-derived **high-res**
datum (~333 samples/sec, `mix-waveforms` vault, CMS-triggered). Direction B **generalizes the high-res
compute to every track**: the content compute path goes medium-neutral, the upload path computes a per-track
high-res datum for every new track, the CMS generate action generalizes off Mix-only, and a **backfill**
populates existing tracks. The cheaper road (serve the existing 512-bucket profile to non-Mix, zero new
compute — old "Direction A") is **declined** in favor of uniform high-res. So 12.B is no longer "a new
endpoint" — it is a content + upload + CMS + backfill + fetch slice (split into 12.B1 / 12.B2 below).
**Three hosting modes of the one engine (Daniel corrected "backdrop").** *"backdrop?? MIXES doesn't really
have a backdrop?"* — right: on Mix the visualizer is the full-bleed **centerpiece that IS the page**, not
something behind content. The one engine is hosted three ways (spec §3f): **mode A — visualizer-is-the-page**
(Mix detail, full-bleed centerpiece); **mode B — ambient environment** (Cut/Session detail, living
texture *behind* the hero+content — this is the only mode that is genuinely a "backdrop"); **mode C —
contained live element** (NowPlaying card, a bounded live readout, `Fill`-sized to the card). Same engine,
same datum contract — variance is entirely in hosting composition. **Controls (Daniel, full parity, §8b):**
the lava controls ride **every host** — Mix, Cut, Session, **and** the NowPlaying card — via the single
popover-hosted panel (below); controls are no longer a per-mode discriminator.
**Controls-hosting revision (Daniel, 2026-06-17 — supersedes the inline knob-bar model).** *"We have enough
[controls] now that I want to design a panel to be hosted in a popover for the visualizer controls. The
lava-lamp toggle should be wired to this popover, so anywhere we can put one Icon we can put the control
surface."* The eight knobs no longer ride an inline *bar* per page — they move into a **single
popover-hosted panel** triggered by the **lava-lamp icon** (click icon → panel pops over). This is **more
DRY than the per-page bar** (one `<icon → popover → panel>` composition reused verbatim, not three-to-four
per-host bar layouts) and it **dissolves §8b-followup**: with a popover, the small NowPlaying card places
the *same* icon as every other host and the panel floats on demand, so the "is the card too small for the
bar?" question evaporates — **full parity on all four surfaces, the popover way**. The SOLID seam: **one
panel component (`WaveformVisualizerControls` becomes the panel content), one popover host
(`WaveformVisualizerControlPopover`), placed by an icon anywhere.** Panel styled to the **NowPlaying Hero
look** — dark-navy ground, green-accent knobs, light icons, muted-navy filler — pulled from the
`deepdrft-tokens.css` source of truth (no hardcoded hex; spec §3g). New open item the popover creates: its
anchor/positioning per host (§8e) — a layout detail, not a presence decision.
**Deliverable 2 — NowPlayingHero overhaul (mode C).** `NowPlayingCard.razor` today animates **20 hardcoded
CSS-bounce bars** with no audio coupling (the "stochastic" visualizer). Replace them with the *same*
`WaveformVisualizer`, mounted inside the existing player cascade and pointed at the **current track** — so
the home card shows the **real** high-res waveform of the live track, Mix or not. The payoff of the
generalization: the NowPlaying card is *just another host* of the one engine. The one genuine engineering
wrinkle is that the renderer assumes full-viewport (`position: fixed; inset: 0`, clip-to-footer) and the
card needs it container-relative — recommend a `Fill` mode parameter (spec §6c).
**Design discipline.** Rename the engine to its abstraction (`MixWaveformVisualizer``WaveformVisualizer`,
etc.) — a `Mix`-named component on a Cut page is a lie that cements the wrong model. Variance rides
**composition** (a new optional `Ambient` slot on `ReleaseDetailScaffold` for mode B; Mix keeps its own
mode-A mount; the card is a mode-C contained mount; per-host control suppression), never a `switch (medium)`
in the engine (memory *One source, multiple views*; scaffold's "variance rides a slot, never a flag" idiom,
Phase 9 §5.3). The slot is named `Ambient` not `Backdrop` precisely because Mix doesn't use it. **The lava
controls are now one popover-hosted panel placed by the lava-lamp icon on every host** (Mix, Cut, Session,
NowPlaying card — full parity, the popover dissolving the old card-suppression sub-question); the panel and
its NowPlaying-Hero styling are built once and reused (memory *Design for adaptability up front* — the
popover seam makes "place the controls anywhere there's an icon" a zero-cost composition).
Sequenced as **six waves**: `12.A → {12.B1 → 12.B2, 12.E}`, then `(12.B2 ∧ 12.E) → (12.C ‖ 12.D)`
**12.B1 a parallel server-side track** and **12.E (the popover controls panel) a third parallel track**,
both startable cold day one off the rename.
- **12.A — Rename to the abstraction (mechanical, no behavior change).** `Mix*``Waveform*` across the
five C#/Razor files + the TS module + its import path + the DI registration. **Load-bearing
prerequisite** — every later wave references the generalized names. Acceptance: Mix detail identical;
diff is identifiers only.
- **12.B1 — Generalize the high-res compute to every track + backfill (Direction B, the data change).**
Generalize the duration-derived compute off Mix-only (`WaveformProfileService` / `MixWaveformResolution`),
store per-track keyed by `EntryKey` in a (renamed) `track-waveforms` vault, add per-track high-res compute
to `UnifiedTrackService.UploadAsync`, generalize the CMS generate action to any track, and run the
**Daniel-gated backfill** for existing tracks (§8a-new). **Independent of 12.A** (server/content-side).
The new load-bearing heavy. Acceptance: every track has a high-res datum; new uploads get one; the
generate action works for any track.
- **12.B2 — Per-track datum fetch + bridge rewire.** New track-cardinal `GET
api/track/{trackEntryKey}/waveform` (spec §5b); `GetTrackWaveform`; bridge resolves the *current track's*
`EntryKey` and re-fetches on **track** change (not release change). **Depends on 12.A + 12.B1.**
Acceptance: Mix renders the same high-res lava via the track-cardinal fetch; a non-Mix track returns
high-res.
- **12.E — Popover-hosted control panel (the controls revision).** Turn the renamed
`WaveformVisualizerControls` into the **panel content** and build `WaveformVisualizerControlPopover`
pairing the lava-lamp trigger icon with that panel as overlay content (`MudPopover`). Style the panel to
the **NowPlaying Hero look** from `deepdrft-tokens.css` (no hardcoded hex; spec §3g). Make the
state-scoping call (one shared `WaveformVisualizerControlState`). **Depends on 12.A only** — no per-track
datum needed, so runs **parallel to 12.B**. The unit every host then places. Acceptance: lava-lamp icon
opens a Hero-styled popover with all eight knobs; turning a knob drives the visualizer via the unchanged
`Changed` seam; one panel reused everywhere.
- **12.C — `Ambient` slot on `ReleaseDetailScaffold` + mount on detail pages (mode B, full parity).**
Promote the full-bleed / foreground-stacking / dynamic-footer-clip pattern into the scaffold as an optional
`Ambient` slot; Cut mounts the ambient layer **and places the lava-lamp icon → popover** (full parity);
Session mounts directly **also full-parity** (it doesn't compose the scaffold — spec §3e). Mix is
**unchanged as a layer** (mode A keeps its own full-bleed mount); its only controls change is swapping the
inline `TopRowCenter` bar for the lava-lamp icon → popover (12.E's affordance). **Depends on 12.B2 + 12.E.**
**§8b resolved (full parity) — no longer gated**; Cut and Session ship with both the ambient layer and the
popover controls.
- **12.D — NowPlayingHero rewire (mode C).** Replace the synthetic bars with a contained
`<WaveformVisualizer>` driven by the live cascaded player, pointed at the current track; add the
`Fill`/container-sizing mode (spec §6c); **place the lava-lamp icon → popover on the card** (full parity —
the popover dissolves the old suppression). **Depends on 12.A + 12.B2 + 12.E; independent of 12.C**
(different host). Acceptance: home card shows the real playing-track high-res waveform, at-rest when
nothing plays, and carries the lava-lamp icon → popover like every other host; no synthetic bars remain.
**Resolved by Daniel (2026-06-17), kept visible per file convention:** datum resolution → **Direction B**
(high-res all media; 512-bucket-fallback "Direction A" declined); multi-track-Cut datum → **dissolved by
the per-track model** (renders the current track's datum, no album-representative choice); Cut/Session
hosting + controls → **full parity (option 3)**: all three hosting modes ship **and** the lava controls ride
every host — the three-mode *layout* framing is retained, the change is that controls are no longer
Mix-suppressed (the old "mode 1 Mix-only" and "controls Mix-only" alternatives are both closed);
**controls hosting → popover-hosted panel** (2026-06-17 revision): the controls move from an inline knob bar
to a single popover-hosted panel triggered by the lava-lamp icon, placed identically on every host;
**§8b-followup is dissolved by this** — the NowPlaying card gets the icon → popover like everywhere else, so
full parity now spans all four surfaces (Mix, Cut, Session, NowPlaying card). **Open (created by the popover
revision + Direction B + per-track):** (a) **§8e — popover anchor/positioning per host**: where the
lava-lamp icon sits and the panel anchors on each host (Mix's `TopRightAction` corner is cleanest; the small
NowPlaying card is the tightest case and may look cramped) — recommend one popover with a per-host
`AnchorOrigin` parameter, not a fork; staff-engineer-owned layout call, flagged for a glance in review.
(b) **§8a-new — backfill shape + gate**: one-shot migration/script vs. a CMS
batch action over the generalized generate action (recommend the CMS action; Daniel-gated to *run* either
way; the fetch 404s gracefully for not-yet-backfilled tracks so it can ship before the backfill completes).
(c) **§8b-new — per-track high-res compute cost** (flag only): upload latency (recommend inline; deferral is
the escape hatch) + storage growth (every track now stores a high-res datum, a multi-track Cut stores N —
modest, surfaced not blocking). (d) **§8d — NowPlaying container-sizing + home-page perf** —
staff-engineer-owned (`Fill` mode; `isPlaying`-gated rAF means an idle home page pays nothing), flagged so
a lava lamp on the landing page is no surprise.
---
## Phase 13 — CMS Public Landing
Give `DeepDrftManager` (the CMS) a true public face: an unauthenticated splash at `/` with DeepDrft
branding and a single **Login** CTA; authenticated admins are redirected past it to the catalogue.
Today `/` is the `[Authorize]`-gated catalogue, so an anonymous hit falls straight through to the login
form — there is no front door. Pattern borrowed from Skipper's `MainHomeLayout` / `Home.razor`
(dedicated public layout + `HierarchicalRoleAuthorizeView` redirect-the-authed-user idiom), branded to
the DeepDrft navy/green/off-white identity (`DeepDrftPalettes.Cms`), **not** Skipper's nautical look.
Additive — the admin experience is intact; only the catalogue's *route* moves. Full spec, routing-reshape
rationale, file responsibilities, hero/CTA composition, asset path, and acceptance criteria:
`product-notes/cms-public-landing.md`.
**Routing decision (recommended, spec §2): Option A — splash owns `/`, catalogue moves to `/catalogue`.**
A new `Home.razor` (`@page "/"`, no `[Authorize]`, new `CmsHomeLayout`) wraps its body in
`<HierarchicalRoleAuthorizeView>`: `Authorized` → redirect to `/catalogue`; `NotAuthorized` → hero +
Login CTA. `Index.razor` changes `@page "/"` → `@page "/catalogue"` (one-line; otherwise untouched).
`Routes.razor` and `Program.cs` need no change — the host is already `AllowAnonymous` at the endpoint and
page auth is owned by `AuthorizeRouteView`, so a no-`[Authorize]` page renders for everyone. The entire
cost of Option A is a small, mechanical link-repoint (every internal `/` that meant *catalogue* →
`/catalogue`; spec §6). Options B (layout-by-auth-state, conflates page concerns) and C (splash at a
side route, defeats the goal) were weighed and rejected.
- **New files:** `Components/Layout/CmsHomeLayout.razor` (lean public layout — same `DeepDrftPalettes.Cms`
theme, front-door AppBar, centered narrow `MudContainer`), `Components/Pages/Home.razor` (the splash),
`Components/RedirectToCatalogue.razor` (mirrors existing `RedirectToAccessDenied`; inline redirect
acceptable). **Changed:** `Index.razor` route line; `CmsLayout.razor` "Back to site" `Href="/"` →
`/catalogue`.
- **Hero asset (Daniel-supplied):** `DeepDrftManager/wwwroot/img/cms-hero.png` (creates the `wwwroot/img/`
dir, net-new); referenced `Src="img/cms-hero.png"`. The page must compile/render without it.
- **No new shared component, no new palette, no `@rendermode` override, no Register CTA** (CMS is
invite/seed-only — single Login button). DRY/MudBlazor-first throughout; the only bespoke CSS is the
layout's one viewport `min-height`.
- **One open copy question for Daniel (non-structural):** AppBar/title wording — "Deep Drft" vs.
"Deep Drft — Admin" (spec §4 recommends "Deep Drft — Admin" in the bar, "Deep Drft" as the hero title).
Implementable either way without rework.
---
## Working with this file
- **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 15. Phase numbers are organisational, not sequencing.