diff --git a/COMPLETED.md b/COMPLETED.md index 09498dd..73021f5 100644 --- a/COMPLETED.md +++ b/COMPLETED.md @@ -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 ``: `Authorized` → ``; `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 `` 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, `` + ``. `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 R1–R4 (Lava tuning + eight-knob controls) **Landed:** 2026-06-17 on dev. diff --git a/DeepDrftAPI/CLAUDE.md b/DeepDrftAPI/CLAUDE.md index b0738f9..6fd1dcd 100644 --- a/DeepDrftAPI/CLAUDE.md +++ b/DeepDrftAPI/CLAUDE.md @@ -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` with `TrackId`, `EntryKey`, `TrackName`, and `HasProfile` (bool). +- **Response**: `List` 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): diff --git a/DeepDrftContent/CLAUDE.md b/DeepDrftContent/CLAUDE.md index 0a1a35e..d29c78a 100644 --- a/DeepDrftContent/CLAUDE.md +++ b/DeepDrftContent/CLAUDE.md @@ -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 diff --git a/DeepDrftPublic.Client/CLAUDE.md b/DeepDrftPublic.Client/CLAUDE.md index 39b29ff..4201093 100644 --- a/DeepDrftPublic.Client/CLAUDE.md +++ b/DeepDrftPublic.Client/CLAUDE.md @@ -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 ``: 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 `` 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 ``: 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 `` ambient engine + `` directly), `MixDetail.razor` (mix detail — composes `ReleaseDetailScaffold` with `TopRightAction` lava-lamp ``; hero+meta rendered via `` in the scaffold's `Hero` slot with `ShowHeader="false"` suppressing the duplicate masthead; square ~600px cover-as-background with metadata overlaid; full-bleed `` is the mode-A centerpiece mounted by the page directly), `CutDetail.razor` (album detail — composes `ReleaseDetailScaffold` with the `Ambient` slot carrying `` + `` 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 (0–100%), with fixed three-zone gradient (green 0–60%, yellow 60–85%, orange 85–100%). 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 `
`s; background-image surface is a `
` (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 `` (mode C — container-relative sizing) driven by the live cascaded player and pointed at the current track's high-res datum. Places `` 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 `` + `` 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 16–64 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` (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`. diff --git a/PLAN.md b/PLAN.md index 9bc38f8..bd28d87 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 `` 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 - `` 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 -``: `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 1–5. Phase numbers are organisational, not sequencing.