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
+30
View File
@@ -6,6 +6,36 @@ Newest entries at the top. Group by phase/wave header (mirroring `PLAN.md` / `CM
---
## Phase 13 — CMS Public Landing (landed 2026-06-17)
**Landed:** 2026-06-17 on dev.
- **What:** Gave `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. Previously `/` was the `[Authorize]`-gated catalogue dashboard, so an anonymous hit fell straight through to the login form with no front door. Pattern borrowed from the `MainHomeLayout` / `Home.razor` idiom (dedicated public layout + `HierarchicalRoleAuthorizeView` redirect-the-authed-user), branded to the DeepDrft navy/green/off-white identity (`DeepDrftPalettes.Cms`). Additive — the admin experience is intact; only the catalogue's route moved. Routing decision: **Option A — splash owns `/`, catalogue moves to `/catalogue`** (Options B and C were weighed and rejected). New files: `Components/Pages/Home.razor` (`@page "/"`, no `[Authorize]`, `CmsHomeLayout`) wraps its body in `<HierarchicalRoleAuthorizeView>`: `Authorized``<RedirectToCatalogue />`; `NotAuthorized` → hero (`img/cms-hero.png`) + Login CTA (returnUrl → `/catalogue`). `Components/Layout/CmsHomeLayout.razor` — lean public layout (`DeepDrftPalettes.Cms` theme, "Deep Drft — Admin" AppBar, centered narrow `MudContainer`, `MudPopoverProvider` only). `Components/RedirectToCatalogue.razor` — inline `NavigationManager.NavigateTo("/catalogue")` redirect, mirroring `RedirectToAccessDenied`. Changed: `Index.razor` route `@page "/"``@page "/catalogue"`; `CmsLayout.razor` "Back to site" home button `Href` and tooltip updated to `/catalogue` / "Catalogue". AppBar wording resolved: "Deep Drft — Admin" in the bar, "Deep Drft" as the hero title.
- **Why:** An anonymous visitor hitting the CMS root landed directly on the AuthBlocks login form with no DeepDrft context, branding, or explanation. The splash provides a proper front door while keeping the admin surface fully intact.
- **Shape:** `DeepDrftManager/Components/Pages/Home.razor` (new); `DeepDrftManager/Components/Layout/CmsHomeLayout.razor` (new); `DeepDrftManager/Components/RedirectToCatalogue.razor` (new); `DeepDrftManager/Components/Pages/Index.razor` (route changed to `/catalogue`); `DeepDrftManager/Components/Layout/CmsLayout.razor` (home-button href + tooltip updated). Hero asset: `DeepDrftManager/wwwroot/img/cms-hero.png` (Daniel-supplied; page compiles and renders without it). Full spec: `product-notes/cms-public-landing.md`.
---
## Phase 12 — Waveform Visualizer Generalization + NowPlayingHero Rewire (all tracks landed 2026-06-17)
**Landed:** 2026-06-17 on dev. Six tracks (12.A, 12.B1, 12.B2, 12.E, 12.C, 12.D) plus a bridge live-track fix, all merged.
- **What:** Took the landed Mix WebGL2 lava visualizer (Phase 10 reframe) and made 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. Two deliverables: (1) the generalized engine serving three hosting modes, (2) the NowPlayingHero rewire. Full design, extraction analysis, per-track model, Direction B compute, wave decomposition: `product-notes/phase-12-waveform-visualizer-generalization.md`.
- **12.A — Rename to the abstraction.** `MixWaveformVisualizer``WaveformVisualizer`, `MixVisualizerControls``WaveformVisualizerControls`, `MixVisualizerControlState``WaveformVisualizerControlState`, `MixZoomMapping``WaveformZoomMapping`, `MixVisualizer.ts``WaveformVisualizer.ts`. Mechanical rename across the five C#/Razor files + TS module + import path + DI registration. No behavior change; Mix detail identical after.
- **12.B1 — Generalize high-res compute to every track + backfill (Direction B).** `MixWaveformResolution``WaveformResolution`. Vault `mix-waveforms``track-waveforms` (`VaultConstants.TrackWaveforms`), keyed per-track by `EntryKey`. New `WaveformProfileService.ComputeAndStoreHighResAsync` is the shared compute seam — upload path, CMS generate action, and Mix trigger all funnel through it. `UnifiedTrackService.UploadAsync` now computes the high-res datum for every new track. CMS generate action generalized to any track; a re-runnable "backfill high-res" batch action added in the CMS `TrackList`. `WaveformStatusDto.HasHighRes` added alongside the existing `HasProfile`. Backfill is Daniel-gated (CMS batch action; fetch 404s gracefully for not-yet-backfilled tracks).
- **12.B2 — Per-track datum fetch + bridge rewire.** New track-cardinal endpoint `GET api/track/{trackEntryKey}/waveform/high-res` (unauthenticated) + public proxy; `ITrackDataService.GetTrackWaveform`; bridge resolves the current track's `EntryKey` and re-fetches on track change. Client `GetMixWaveform` read path retired; API-side release waveform endpoint kept as a caller-less legacy delegate. Mix renders the same high-res lava via the track-cardinal fetch.
- **12.E — Popover-hosted control panel.** `WaveformVisualizerControls` became the panel content; new `WaveformVisualizerControlPopover` pairs the lava-lamp icon with the panel as overlay content (`MudPopover`). Panel styled to the NowPlaying Hero look from `deepdrft-tokens.css` (no hardcoded hex). A `PanelChrome` flag scopes panel chrome to the popover mount. One popover placed by the lava-lamp icon on every host — full parity across Mix, Cut, Session, and NowPlaying card.
- **Bridge live-track fix.** The visualizer now follows the live playing track (keys on host `TrackId` match OR shared host `ReleaseEntryKey`), not the fixed host `TrackId`.
- **12.C — `Ambient` slot on `ReleaseDetailScaffold` + mount on detail pages (mode B).** New optional `Ambient` slot on `ReleaseDetailScaffold` (full-bleed layer behind content; absent slot = no regression). Cut mounts the ambient visualizer + the lava-lamp icon → popover. Session mounts the engine directly behind its hero (it doesn't compose the scaffold) + the popover. Mix swapped its inline controls bar for the lava-lamp icon → popover, keeping its own full-bleed mode-A mount.
- **12.D — NowPlayingHero rewire (mode C).** `NowPlayingCard` replaced the 20 synthetic CSS bars with a contained `<WaveformVisualizer>` driven by the live cascaded player, pointed at the current track. Added a `Fill` container-sizing mode (CSS-only, defaults off). Placed the lava-lamp icon → popover on the card for full parity. Visualizer runs at-rest on the home page even before playback (deliberate; perf tuning deferred).
- **Why:** The landed Mix visualizer was structurally track-cardinal below the surface (bridge keyed on `TrackId`; renderer a pure function of a loudness datum + duration) but named `Mix*` throughout and restricted to Mix-only data. "Generalize" was a rename + per-track high-res compute extension, not a rebuild. Direction B (high-res for all media) was chosen over the cheaper 512-bucket-fallback Direction A to deliver uniform waveform quality. Controls moved from per-page inline knob bars to a single popover-hosted panel to achieve zero-cost placement on any host including the small NowPlaying card.
- **Shape:** `DeepDrftPublic.Client/Controls/`: `WaveformVisualizer.razor` (+ `.razor.cs`, `.razor.css`) — renamed engine, added `[Parameter] bool Fill`; `WaveformVisualizerControls.razor` — renamed, now panel content with `PanelChrome` flag; `WaveformVisualizerControlPopover.razor` — new, lava-lamp icon + `MudPopover` wrapping the panel; `WaveformZoomMapping.cs` — renamed; `ReleaseDetailScaffold.razor` (+ `.razor.cs`) — new optional `Ambient` `RenderFragment` slot; `NowPlayingCard.razor` — synthetic bars replaced, `<WaveformVisualizer Fill="true">` + `<WaveformVisualizerControlPopover>`. `DeepDrftPublic.Client/Services/`: `WaveformVisualizerControlState.cs` — renamed. `DeepDrftPublic.Client/Pages/`: `CutDetail.razor` — mounts ambient visualizer + popover; `SessionDetail.razor` — mounts engine + popover directly; `MixDetail.razor` — swaps inline controls bar for popover. `DeepDrftPublic/Interop/visualizer/WaveformVisualizer.ts` — renamed TS module. `DeepDrftContent/Processors/`: `WaveformResolution.cs` — renamed; `WaveformProfileService.cs``ComputeAndStoreHighResAsync` added, medium-neutral. `DeepDrftContent/Constants/VaultConstants.cs``TrackWaveforms = "track-waveforms"`. `DeepDrftAPI/Controllers/TrackController.cs``GET api/track/{trackEntryKey}/waveform/high-res` (unauthenticated) + `POST api/track/{trackId}/waveform/high-res` (ApiKey, generalized generate); `WaveformStatusDto.HasHighRes` populated. `DeepDrftAPI/Services/UnifiedTrackService.cs``UploadAsync` now calls `ComputeAndStoreHighResAsync` for every new track. `DeepDrftPublic/Controllers/TrackProxyController.cs` — proxy for the new high-res endpoint.
---
## Phase 10 — Mix Visualizer Reframe: Waves R1R4 (Lava tuning + eight-knob controls)
**Landed:** 2026-06-17 on dev.
+25 -7
View File
@@ -6,7 +6,7 @@ See the root `CLAUDE.md` for full architecture overview. This file covers what i
## One-line purpose
Dual-database authority for tracks (SQL metadata + FileDatabase binary), releases (SQL metadata with media-specific satellites), and images (FileDatabase binary); AuthBlocks API host (JWT auth, role/admin seed). Track endpoints expose CRUD with upload+persist, delete+cleanup, paged listing with filters, metadata operations, waveform profiles, and release associations. Release endpoints provide paged listing with medium filter, single-release read, and media-specific operations (mix waveform compute, session hero-image upload). Image endpoints provide authenticated upload and unauthenticated streaming. ApiKey middleware for authenticated endpoints, JWT + AuthBlocks for auth. CORS, forwarded headers. **FileDatabase implementation lives in `DeepDrftContent`; SQL services in `DeepDrftData`.**
Dual-database authority for tracks (SQL metadata + FileDatabase binary), releases (SQL metadata with media-specific satellites), and images (FileDatabase binary); AuthBlocks API host (JWT auth, role/admin seed). Track endpoints expose CRUD with upload+persist, delete+cleanup, paged listing with filters, metadata operations, waveform profiles (512-bucket player-bar seeker + per-track high-res visualizer datum), and release associations. Release endpoints provide paged listing with medium filter, single-release read, and media-specific operations (session hero-image upload; mix waveform is a caller-less legacy delegate — the track-cardinal `GET api/track/{entryKey}/waveform/high-res` is the live fetch path). Image endpoints provide authenticated upload and unauthenticated streaming. ApiKey middleware for authenticated endpoints, JWT + AuthBlocks for auth. CORS, forwarded headers. **FileDatabase implementation lives in `DeepDrftContent`; SQL services in `DeepDrftData`.**
## What lives here now (only)
@@ -77,6 +77,23 @@ Admin backfill: computes and stores a waveform profile for an existing track fro
- Fetches audio from vault, decodes it, computes a loudness profile, and stores the profile in the `waveform-profiles` vault.
- Returns 200 on success. Returns 404 if no audio is stored under that key. Returns 500 if WAV decoding or vault write fails.
### GET api/track/{trackId}/waveform/high-res (unauthenticated)
Track-cardinal high-res datum fetch. Returns the per-track duration-derived high-res waveform datum (~333 samples/sec) from the `track-waveforms` vault. This is the live read path for the `WaveformVisualizer` bridge — the release-level mix waveform endpoint is a caller-less legacy delegate.
- **Route parameter `trackId`** (string): the entry id (TrackEntity.EntryKey).
- **Response**: `WaveformProfileDto` with `BucketCount` and `Data` (base64).
- Returns 200 on success. Returns 404 if no high-res datum is stored (graceful — not-yet-backfilled tracks fall back to no visualizer data). Returns 500 on vault error.
### POST api/track/{trackId}/waveform/high-res ([ApiKeyAuthorize])
Server-side trigger: compute and store the per-track high-res datum for any track from its vault audio, keyed by `EntryKey` in the `track-waveforms` vault. Drives the CMS per-row "Generate high-res" action and the CMS batch backfill action. Generalised off Mix-only in Phase 12.
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
- **Route parameter `trackId`** (string): the entry id (TrackEntity.EntryKey).
- Calls `WaveformProfileService.ComputeAndStoreHighResAsync` via `UnifiedTrackService`.
- Returns 200 on success. Returns 404 if no audio stored under that key. Returns 500 on compute/storage failure.
### GET api/track/meta/by-key/{entryKey} (unauthenticated)
Single track metadata by vault entry key (EntryKey). Unauthenticated, reachable through the public proxy.
@@ -87,10 +104,10 @@ Single track metadata by vault entry key (EntryKey). Unauthenticated, reachable
### GET api/track/waveform-status ([ApiKeyAuthorize])
Admin backfill view: returns every track with a flag indicating whether a waveform profile is stored. Used by the CMS PreProcessing panel to flag tracks needing waveform computation.
Admin backfill view: returns every track with flags indicating whether each waveform type is stored. Used by the CMS track list to flag tracks needing waveform computation.
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
- **Response**: `List<WaveformStatusDto>` with `TrackId`, `EntryKey`, `TrackName`, and `HasProfile` (bool).
- **Response**: `List<WaveformStatusDto>` with `TrackId`, `EntryKey`, `TrackName`, `HasProfile` (bool — 512-bucket player-bar seeker profile in `waveform-profiles` vault), and `HasHighRes` (bool — duration-derived high-res visualizer datum in `track-waveforms` vault).
- Returns 200 on success. Returns 500 on query error.
### DELETE api/track/release/{id:long} ([ApiKeyAuthorize])
@@ -228,9 +245,9 @@ Single release with both metadata navs (nulls for non-matching media). Public, s
- **Response**: `ReleaseDto` with `Id`, `EntryKey`, `Title`, `Artist`, `Genre`, `ReleaseDate`, `Medium`, `ImagePath`, and media-specific metadata satellites (`MixMetadata` for Cut/Mix, `SessionMetadata` for Session; others null).
- Returns 200 on success. Returns 404 if not found. Returns 500 on query error.
### GET api/release/{entryKey}/mix/waveform (unauthenticated)
### GET api/release/{entryKey}/mix/waveform (unauthenticated — caller-less legacy delegate)
Serves the high-res waveform datum for a Mix release as base64-encoded bytes. Mirrors `GET api/track/{id}/waveform` but reads from the `mix-waveforms` vault. Public read — addresses by the release `EntryKey` (§3e).
Legacy endpoint: formerly served the high-res waveform datum for a Mix release from the `mix-waveforms` vault. **No longer called by the client** — the live fetch path is now the track-cardinal `GET api/track/{trackId}/waveform/high-res` (Phase 12). The endpoint is retained in the API but has no active callers. `UnifiedReleaseService.TriggerMixWaveformAsync` now delegates to `WaveformProfileService.ComputeAndStoreHighResAsync` (the same shared seam used by the upload path and the generalized CMS generate action).
- **Route parameter `entryKey`** (string): the release's `EntryKey`.
- **Response**: `WaveformProfileDto` with `BucketCount` and `Data` (base64).
@@ -238,7 +255,7 @@ Serves the high-res waveform datum for a Mix release as base64-encoded bytes. Mi
### POST api/release/{id:long}/mix/waveform ([ApiKeyAuthorize])
Server-side trigger: fetch the Mix's track audio from the vault, compute a 2048-bucket waveform, store it in the `mix-waveforms` vault, and link it via `MixMetadata.WaveformEntryKey`. No request body.
Server-side trigger: fetch the Mix's track audio from the vault, compute the duration-derived high-res datum, store it in the `track-waveforms` vault under the track's `EntryKey`, and link it via `MixMetadata.WaveformEntryKey`. Delegates to `WaveformProfileService.ComputeAndStoreHighResAsync` — the same shared seam used by the upload path and the generalized CMS generate action. No request body.
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
- **Route parameter `id`** (long): the SQL release ID.
@@ -289,7 +306,8 @@ Configured in `Startup.ConfigureDomainServices()`, applied to all endpoints via
3. Register `FileDatabase` as singleton.
4. Ensure the `tracks` vault exists (type `MediaVaultType.Audio`, created on first boot if missing).
5. Ensure the `images` vault exists (type `MediaVaultType.Image`, created on first boot if missing) via `InitializeImageVault`.
6. Register singletons: `AudioProcessor`, `ImageProcessor`, `TrackService` (the `DeepDrftContent` version for vault operations).
5a. Ensure the `track-waveforms` vault exists (type `MediaVaultType.Media`, created on first boot if missing) — holds per-track high-res visualizer datum keyed by `TrackEntity.EntryKey`.
6. Register singletons: `AudioProcessor`, `ImageProcessor`, `TrackService` (the `DeepDrftContent` version for vault operations), `WaveformProfileService`.
**In `Program.cs`** (SQL + AuthBlocks + wiring):
+3 -1
View File
@@ -82,6 +82,8 @@ Multi-format support via router pattern. All processors live in `DeepDrftContent
- `Mp3AudioProcessor.ProcessMp3FileAsync(filePath)`: MP3 processor. Skips ID3v2 tag, finds first valid MPEG frame sync, decodes frame header (bitrate, sample rate, channels). Reads Xing/Info header for VBR total-frame count (accurate duration); VBRI header as fallback; CBR estimate from file size otherwise. Returns `AudioBinary` with original bytes and `.mp3` extension. On parse failure, falls back to defaults (180s / 320 kbps).
- `FlacAudioProcessor.ProcessFlacFileAsync(filePath)`: FLAC processor. Validates `fLaC` magic, reads STREAMINFO metadata block (20-bit sample rate, 3-bit channels, 5-bit bits-per-sample, 36-bit total samples — all bit-packed). Computes duration from `totalSamples / sampleRate`; average bitrate from file size. Returns `AudioBinary` with original bytes and `.flac` extension. On parse failure, falls back to defaults (180s / 1411 kbps).
- `AudioProcessorRouter.ProcessAudioFileAsync(filePath)`: Routes by extension — `.wav``AudioProcessor`, `.mp3``Mp3AudioProcessor`, `.flac``FlacAudioProcessor`. Throws `ArgumentException` for unsupported extensions.
- `WaveformProfileService.ComputeAndStoreHighResAsync(entryKey)`: The shared compute seam for the duration-derived high-res waveform datum (~333 samples/sec). Medium-neutral — computes for any track by `EntryKey`, stores in the `track-waveforms` vault. Called by the upload path (`UnifiedTrackService.UploadAsync` for every new track), the CMS per-row generate action, and the Mix release trigger (now a legacy delegate). Phase 12 generalization of the former Mix-only compute.
- `WaveformResolution`: Enum / constants controlling bucket density for the high-res compute. Renamed from `MixWaveformResolution` in Phase 12.
Vault stores original bytes with correct extension and MIME type (inferred from file extension or content-type header at upload time).
@@ -120,7 +122,7 @@ Safety call to ensure the `tracks` vault exists (creates if missing). Called on
## Vault constants
`VaultConstants.Tracks = "tracks"` and `VaultConstants.Images = "images"` — the vault names in production use. New vault names go here when adding new vault types.
`VaultConstants.Tracks = "tracks"`, `VaultConstants.Images = "images"`, and `VaultConstants.TrackWaveforms = "track-waveforms"` — the vault names in production use. `TrackWaveforms` holds the per-track high-res waveform datum keyed by `TrackEntity.EntryKey` (Phase 12; renamed from the former `mix-waveforms`, which was Mix-only). New vault names go here when adding new vault types.
## Service registration
+9 -3
View File
@@ -10,7 +10,7 @@ All interactive UI for the site. Blazor WebAssembly. Pages, controls, the stream
## Actual structure
- `Pages/`: Routable components. `Home.razor` (hero/about), `TracksView.razor` (track gallery with pagination/sorting), `TrackDetail.razor` (single-track detail view with cover, metadata, play affordance), `SessionDetail.razor` (session detail — hero-dominant overlay composition rendered via `<ReleaseHeroOverlay>`: large background hero image with darkening gradient shim, cover thumbnail + title + play button overlaid near the hero's bottom, genre/date/share overlaid at the top; uses `MudContainer MaxWidth="Large"`; **does not compose `ReleaseDetailScaffold`**`PlayTrack` is wired directly in its own `@code` block), `MixDetail.razor` (mix detail — composes `ReleaseDetailScaffold` with `TopRowCenter` controls + `TopRightAction` lava-lamp; hero+meta rendered via `<ReleaseHeroOverlay Class="mix-hero">` in the scaffold's `Hero` slot with `ShowHeader="false"` suppressing the duplicate masthead; square ~600px cover-as-background with metadata overlaid). **No demo pages** (`Counter.razor`, `Weather.razor` do not exist).
- `Pages/`: Routable components. `Home.razor` (hero/about), `SessionDetail.razor` (session detail — hero-dominant overlay composition rendered via `<ReleaseHeroOverlay>`: large background hero image with darkening gradient shim, cover thumbnail + title + play button overlaid near the hero's bottom, genre/date/share overlaid at the top; uses `MudContainer MaxWidth="Large"`; **does not compose `ReleaseDetailScaffold`**`PlayTrack` is wired directly in its own `@code` block; mounts `<WaveformVisualizer>` ambient engine + `<WaveformVisualizerControlPopover>` directly), `MixDetail.razor` (mix detail — composes `ReleaseDetailScaffold` with `TopRightAction` lava-lamp `<WaveformVisualizerControlPopover>`; hero+meta rendered via `<ReleaseHeroOverlay Class="mix-hero">` in the scaffold's `Hero` slot with `ShowHeader="false"` suppressing the duplicate masthead; square ~600px cover-as-background with metadata overlaid; full-bleed `<WaveformVisualizer>` is the mode-A centerpiece mounted by the page directly), `CutDetail.razor` (album detail — composes `ReleaseDetailScaffold` with the `Ambient` slot carrying `<WaveformVisualizer>` + `<WaveformVisualizerControlPopover>` for mode-B ambient layer). **No demo pages** (`Counter.razor`, `Weather.razor` do not exist).
- `Layout/`: `MainLayout.razor` (root layout, wraps in `AudioPlayerProvider`, hosts theme switcher), `DeepDrftMenu.razor` (branded menu bar), `NavMenu.razor` (nav list), `Pages.cs` (centralised nav index — `MenuPages` for header, `AllPages` for exhaustive list).
- `Controls/`: Reusable components.
- `TrackCard.razor`: Individual track display (image, name, artist, album, genre, release date). Play/pause icon controlled via `IsPaused` parameter.
@@ -24,7 +24,12 @@ All interactive UI for the site. Blazor WebAssembly. Pages, controls, the stream
- `AudioPlayerBar/LevelMeterFab.razor`: Floating-action button replacing the static FAB in the minimized dock. Renders a continuous vertical fill inside the music-note silhouette that tracks live audio level (0100%), with fixed three-zone gradient (green 060%, yellow 6085%, orange 85100%). Note silhouette always visible at 25% opacity; idle when paused/stopped. Reuses spectrum-callback infrastructure.
- `SpectrumVisualizer.razor`: Bar-graph spectrum display, driven by `getSpectrumData` JS callback.
- `ReleaseHeroOverlay.razor`: Shared presentational overlay shell consumed by both `SessionDetail` and `MixDetail`. Renders a background-image hero region with genre/date/share overlaid at the top and title/artist/play at the bottom. Parameters: `HeroImageKey`, `PlaceholderIcon`, `CoverThumbKey` (optional cover thumb in bottom row), `Title`, `Artist`, `Genre`, `ReleaseDate`, `ShareContent` (slot), `PlayContent` (slot), `Class` (per-page aspect/sizing override). Owns no player logic or data fetch; each consuming page passes its own play and share slots. Overlay shell is plain `<div>`s; background-image surface is a `<div class="release-hero-img">` (no `MudPaper`).
- `MixVisualizerControls.razor`: Eight-knob RadialKnob control bar for the Mix detail lava-lamp visualizer. Controls (in order): waveform scroll speed, color gradient rotation speed, lava gravity, lava heat, fluid amount, fluid viscosity (cohesion), collision strength, waveform width. `[Parameter] bool Visible` — the host always renders this component and feeds the lava-lamp toggle into `Visible`; the knobs are `@if`-gated on `Visible` while the container holds a reserved `min-height` so content below never pops when the lamp toggles. Owns no JS interop: mutates the injected `MixVisualizerControlState` and raises `Changed`; the backdrop bridge (`MixWaveformVisualizer`) subscribes and pushes each changed dial to the WebGL module. No control is a seek surface (read-only contract).
- `WaveformVisualizer.razor`: The single WebGL2 lava-lamp visualizer engine. Hosts the waveform of whatever track is currently playing/selected. Three hosting modes: mode A (Mix detail — full-bleed centerpiece), mode B (Cut/Session detail — ambient layer behind hero+content via `ReleaseDetailScaffold`'s `Ambient` slot), mode C (NowPlaying card — container-relative). `[Parameter] bool Fill` switches from fixed-viewport positioning to container-relative sizing (CSS-only; the renderer is identical in both modes). The bridge resolves the current track's `EntryKey` and re-fetches the high-res datum on track change. Subscribes to `WaveformVisualizerControlState.Changed` and pushes each updated dial to the WebGL module via JS interop. Follows the live playing track (keys on host `TrackId` match OR shared host `ReleaseEntryKey`).
- `WaveformVisualizerControls.razor`: Eight-knob RadialKnob control panel (the panel content hosted by `WaveformVisualizerControlPopover`). Controls (in order): waveform scroll speed, color gradient rotation speed, lava gravity, lava heat, fluid amount, fluid viscosity (cohesion), collision strength, waveform width. `[Parameter] bool PanelChrome` scopes panel chrome (title bar, background) to the popover mount — set `true` when placed in a popover, `false` when embedded directly. Owns no JS interop: mutates the injected `WaveformVisualizerControlState` and raises `Changed`. No control is a seek surface (read-only contract).
- `WaveformVisualizerControlPopover.razor`: Pairs the lava-lamp icon button with `WaveformVisualizerControls` as `MudPopover` overlay content. This is the unit every host places — one icon anywhere gives the full eight-knob panel on demand. Styled to the NowPlaying Hero look from `deepdrft-tokens.css` (no hardcoded hex). Placed identically on Mix, Cut, Session, and NowPlaying card (full parity).
- `WaveformZoomMapping.cs`: Maps the `WaveformVisualizerControlState.Resolution` fraction to an integer zoom level for the WebGL renderer.
- `NowPlayingCard.razor`: Home-page hero card showing the currently playing track. Hosts `<WaveformVisualizer Fill="true">` (mode C — container-relative sizing) driven by the live cascaded player and pointed at the current track's high-res datum. Places `<WaveformVisualizerControlPopover>` for full parity with the detail pages. The former 20 synthetic CSS-bounce bars are gone. The visualizer runs at-rest on the home page even before playback.
- `ReleaseDetailScaffold.razor`: Shared scaffold for release detail pages. Gained an optional `Ambient` `RenderFragment` slot (Phase 12) — a full-bleed layer rendered behind the main content. Absent slot = no regression. Cut mounts `<WaveformVisualizer>` + `<WaveformVisualizerControlPopover>` here; Mix uses its own full-bleed mount outside the scaffold.
- `Helpers/`: Utilities and mapper functions.
- `PlaybackIcons.cs`: Static `Resolve(isPlaying, isPaused, trackId, currentTrackId)` method — the sole glyph-mapping source for transport icons across all surfaces. Returns `(Icon, IsActive, IsPaused)` tuple.
- `Services/`: Audio player + dark-mode services.
@@ -33,10 +38,11 @@ All interactive UI for the site. Blazor WebAssembly. Pages, controls, the stream
- `StreamingAudioPlayerService`: Production implementation. Chunked stream from `TrackMediaClient`, adaptive 1664 KB buffer, early-playback, **seek-beyond-buffer** via offset request to the content API.
- `AudioInteropService`: JS interop wrapper over `window.DeepDrftAudio`. Manages `DotNetObjectReference` lifetimes for progress, end-of-playback, spectrum callbacks.
- Dark-mode services: `DarkModeServiceBase` (cookie name constant), `DarkModeCookieService` (JS cookie read/write).
- `MixVisualizerControlState`: Scoped session-persistent holder for the Mix visualizer's **eight** control positions: `ScrollSpeed`, `GradientRotationSpeed`, `LavaGravity`, `LavaHeat`, `FluidAmount` (wax count/volume), `FluidViscosity` (cohesion — the second half of the Phase 10 "bubbles" split; `BlobDensity` is gone), `CollisionStrength`, `WaveformWidth`. Each has a matching `Default*` const. `Changed` event is the decoupling seam — controls mutate state + raise `Changed`; the bridge (`MixWaveformVisualizer`) subscribes and pushes the affected uniform. Scoped DI so state survives SPA nav within a session and resets on fresh page load.
- `WaveformVisualizerControlState`: Scoped session-persistent holder for the visualizer's **eight** control positions: `ScrollSpeed`, `GradientRotationSpeed`, `LavaGravity`, `LavaHeat`, `FluidAmount` (wax count/volume), `FluidViscosity` (cohesion — the second half of the Phase 10 "bubbles" split; `BlobDensity` is gone), `CollisionStrength`, `WaveformWidth`. Each has a matching `Default*` const. `Changed` event is the decoupling seam — controls mutate state + raise `Changed`; the bridge (`WaveformVisualizer`) subscribes and pushes the affected uniform. Scoped DI so state survives SPA nav within a session and resets on fresh page load.
- `Clients/`: HTTP API clients (both target DeepDrftAPI).
- `TrackClient`: SQL metadata API. Uses named `IHttpClientFactory` client `"DeepDrft.API"`. Sends `page` param (not `pageNumber`). Deserializes response as bare `PagedResult<TrackDto>` (not wrapped in ApiResultDto envelope).
- `TrackMediaClient`: Content API. Uses named `IHttpClientFactory` client `"DeepDrft.Content"`. Methods like `GetAudioStreamAsync(trackId, byteOffset?)``Stream` with optional Range header support for seek-beyond-buffer.
- `Services/ITrackDataService`: Contract used by the visualizer bridge and other consumers. Includes `GetTrackWaveform(entryKey)` → high-res `WaveformProfileDto` (calls `GET api/track/{entryKey}/waveform/high-res`); used by `WaveformVisualizer` to re-fetch the datum on track change.
- `ViewModels/`: Component state.
- `TracksViewModel`: Scoped. Holds current page, page size, sort column, descending flag. `SetPage(pageNumber)` calls `TrackClient.GetPageAsync` and updates. Registered in `Startup.ConfigureDomainServices`.
- `TrackDetailViewModel`: Scoped. Holds loaded track, loading flag, not-found flag. `Load(entryKey)` fetches via `ITrackDataService` and resets all flags per call (prevents cross-navigation bleed). Registered in `Startup.ConfigureDomainServices`.
-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.