5fb46bf5eb
Splash owns /, catalogue moves to /catalogue, authed users redirected via HierarchicalRoleAuthorizeView. Skipper's public-layout pattern, branded to DeepDrft. Adds Phase 13 to PLAN.md.
435 lines
53 KiB
Markdown
435 lines
53 KiB
Markdown
# PLAN.md — DeepDrftHome forward roadmap
|
||
|
||
Forward-looking roadmap. Sits alongside `CONTEXT.md` (architecture orientation) and `COMPLETED.md` (history). Per `CONTEXT.md §6`, items move from here to `COMPLETED.md` when work lands; do not delete completed entries.
|
||
|
||
Organised by **theme**, not by date. Themes are roughly ordered by current product weight, not commitment. Nothing here carries a timeline unless it explicitly says so.
|
||
|
||
---
|
||
|
||
## 0. Baseline — what just landed
|
||
|
||
A two-part audit (design + streaming) ran on 2026-05-17 and the fixes for Critical, Major, and Minor findings are now on `dev`. The remainder of this plan assumes that baseline. In summary the audit-pass fixed:
|
||
|
||
- **Index concurrency** — `VaultIndexDirectory` no longer drops the lock before its async disk write; the index file can no longer be clobbered by interleaved writers.
|
||
- **Repository semantics** — `TrackRepository.Update` now fails-fast when an `Id` is not found instead of silently issuing an `INSERT`.
|
||
- **Streaming Criticals** — concurrent-seek race in the client, dirty trailing bytes leaking out of the `ArrayPool`-rented buffer, final-tail audio dropped at EOF below the minimum decode frame, and the assumption that the first network chunk contains the whole WAV header.
|
||
- **17 design and streaming Majors/Minors** across all eight projects — format-validation alignment between processor/offset/decoder, `IAsyncDisposable` on the player provider, cancellation tokens threaded through the HTTP path, structured logging into the FileDatabase subsystem, sort-sentinel cleanup, sundry DRY/SRP tightenings.
|
||
|
||
What this means for the roadmap: the streaming substrate is solid. Future work can build on top of it rather than around it. The remaining items in `TODO-V2.md` that did not land are **deferred as features, not bugs** — they are captured below under Phase 1.
|
||
|
||
---
|
||
|
||
## Phase 1 — Streaming features deferred from the audit
|
||
|
||
These were flagged during the audit but classified as feature work, not defect fixes. They are listed in rough order of user-visible impact.
|
||
|
||
### 1.3 Preload / prefetch of the next track
|
||
|
||
> **Split as of 2026-06-15.** This item bundled two things: (a) a **queue model** ("a notion of next
|
||
> track") and (b) **preload/prefetch** (begin the next track's bytes during the current tail). The
|
||
> **queue half (a) is now absorbed into Phase 11** (commitment 7 — Daniel: "now is the natural time
|
||
> for that"; full spec in `product-notes/phase-11-public-site-enhancements.md §3c`). The **preload
|
||
> half (b) remains deferred here** and still gates crossfade (1.4) and gapless (1.5). The open
|
||
> question below — queue in `IPlayerService` vs. a separate orchestrator — is **answered in the
|
||
> Phase 11 spec** (strong steer: a separate `IQueueService` above the single-slot player; final call
|
||
> staff-engineer's at implementation). When Phase 11's queue lands, the preload below becomes "add a
|
||
> subscriber to the queue's already-known next track," not a fresh queue design.
|
||
|
||
- **What (deferred — preload only):** No mechanism to begin the next track's stream during the tail of the current. Each play is a cold fetch.
|
||
- **Why it matters:** Prerequisite for both crossfade (1.4) and gapless (1.5). Also a perceived-latency win on its own — track-change feels instant when the bytes are already in flight.
|
||
- **Shape:** A second `HttpClient` request kicked off when the current track passes a configurable threshold (e.g. last 10 seconds). Bytes accumulate into a staged `StreamDecoder` instance rather than the live one. Promotion to "current" happens at end-of-stream or on user-selected next. **The "next track" it prefetches comes from Phase 11's `IQueueService`** — that dependency is now satisfied by the queue work, not an open question.
|
||
|
||
### 1.4 Crossfade
|
||
|
||
- **What:** Smooth A→B transition with overlapping fade-out / fade-in.
|
||
- **Why it matters:** DJ/mix aesthetic that fits the DeepDrft collective's electronic-music context. Distinguishing UX from generic "next track."
|
||
- **Shape:** Architecturally two simultaneous `PlaybackScheduler` instances suffice — each owns its own gain node, crossfaded via `GainNode.gain.linearRampToValueAtTime`. The wiring is the work, not the audio graph itself.
|
||
- **Prerequisite:** **1.3 (Preload)** — there is nothing to fade *into* without prefetch.
|
||
|
||
### 1.5 Gapless playback
|
||
|
||
- **What:** Eliminate the inter-track silence that exists today.
|
||
- **Why it matters:** Important for live-set rips, mix tapes, anything authored to flow continuously.
|
||
- **Shape:** The decoder must be able to start the next track's first buffer scheduled exactly at the end of the current one's last buffer (sample-accurate, not wall-clock). With `PlaybackScheduler`'s existing 500 ms lookahead this is mechanically achievable — the next track's first `AudioBufferSourceNode.start(t)` is set to the previous track's end time.
|
||
- **Prerequisite:** **1.3 (Preload)**. Also needs to play nicely with **1.2** because gapless across formats is hard (encoder padding/priming on MP3 in particular).
|
||
- **Constraint:** Truly sample-accurate gapless requires knowing the priming/padding sample counts of the source format. Out of scope for WAV-only; revisit when format diversity lands.
|
||
|
||
### 1.6 Track-skip on error
|
||
|
||
- **What:** A failed `processStreamingChunk` aborts the entire load with no recovery path.
|
||
- **Why it matters:** One corrupt frame at byte 4M of a 100 MB stream currently means the listener loses the entire track. Should at minimum surface a clear error and (optionally) skip past the bad region.
|
||
- **Shape:** Two-level response.
|
||
- Cheap: catch in the streaming loop, surface a user-visible error, advance the gallery to the next track if a queue exists.
|
||
- Richer: byte-scan forward to the next valid frame header for the format and resume. Format-dependent — only worth doing once **1.2** lands.
|
||
|
||
### 1.7 Safari compatibility
|
||
|
||
- **What:** Two known Safari edge cases.
|
||
- `webkitAudioContext.close()` is async-but-not-Promise on older Safari (≤ ~14); `await` resolves immediately and the next `initialize()` can run against a not-yet-closed context.
|
||
- iOS Safari < 15 had streaming-fetch quirks; `HttpCompletionOption.ResponseHeadersRead` behaviour is not guaranteed there.
|
||
- **Why it matters:** Real listener share. iOS in particular is a primary listening surface for music.
|
||
- **Shape:** For the `close()` race — detect `webkitAudioContext` and poll `state === "closed"` with a short timeout instead of trusting the `await`. For the fetch quirks — first decide the minimum supported iOS version; if pre-15 is in scope, fall back to a non-streaming fetch path and accept the latency.
|
||
- **Open question:** What's the floor? Decide before designing the fallback. iOS 15+ as the floor would let us drop the second concern entirely.
|
||
|
||
---
|
||
|
||
## Phase 2 — Product surface: gallery, browsing, ingestion
|
||
|
||
These follow from `CONTEXT.md §5`. Direction is strongly implied but no specific UI has been committed.
|
||
|
||
---
|
||
|
||
## Phase 6 — CMS Enhancements (Completed)
|
||
|
||
See `COMPLETED.md` for Phase 6 (§6.1, §6.3) and entity-prep (§6.2 model layer) which landed on dev in June 2026.
|
||
|
||
---
|
||
|
||
### 6.2 Card-contextual filtering of the Tracks page — `[superseded by §8]`
|
||
|
||
- **What:** Make the Album and Genre dashboard cards navigate into a *filtered* `/tracks` view (e.g. clicking an album card shows only that album's tracks), rather than the unfiltered table.
|
||
- **Why:** Turns the dashboard from a read-only summary into a navigation hub — the natural next step once the cards exist.
|
||
- **Why deferred:** The dashboard cards aggregate *across all* albums/genres — there is no single album/genre to filter to from a top-level count card. Meaningful per-album/per-genre navigation needs an intermediate browse surface (a list of albums, a list of genres) for the admin to pick from — i.e. it's really a CMS analogue of the public `AlbumsView`/`GenresView`, not a property of the summary cards. That's a larger surface than the dashboard itself and shouldn't be smuggled in. The `GET api/track/page` endpoint already accepts `album=` and `genre=` query filters, so the API substrate is ready; the missing piece is the CMS browse UI and the filter plumbing in `TrackList`.
|
||
- **Superseded:** **§8 (CMS Track Browser)** builds exactly the intermediate browse surface this item was waiting on — Album Mode and Genre Mode *are* the CMS analogue of `AlbumsView`/`GenresView`, and the filter plumbing into `GetPagedAsync` is part of §8's data contract. This item folds into §8; do not implement it separately.
|
||
|
||
---
|
||
|
||
## Phase 3 — New content kinds
|
||
|
||
### 3.1 Live / session content
|
||
|
||
- **What:** The home page advertises "Live Sessions" and "Video Content (coming soon)". No data model exists for these.
|
||
- **Why it matters:** Honour the home page copy. Also differentiates the site from a generic track gallery — live sessions and video are the collective's authored output.
|
||
- **Shape:** Speculative; no commitment yet.
|
||
- Likely new entity table(s) sibling to `TrackEntity` (`SessionEntity`, `VideoEntity`?) — or a polymorphic `MediaEntity` with discriminator. The choice affects how much code in `TrackService` / `TrackController` can be reused.
|
||
- New vault type(s). `MediaVaultType.Media` exists and is the obvious home for video; sessions are probably still `Audio`.
|
||
- New routes, new UI surfaces, new player considerations (video has its own playback element and does not go through the WAV decoder).
|
||
- **Prerequisite:** Probably **2.1** (vault wiring proof) and a decision on the entity model before any code lands.
|
||
- **`[speculative]`** — direction inferred from home-page copy, not a Daniel-confirmed commitment.
|
||
|
||
---
|
||
|
||
## Phase 4 — Infrastructure / delivery
|
||
|
||
### 4.3 Dual-write rollback / dead-letter log
|
||
|
||
- **What:** If content-side write succeeds and SQL-side write fails, audio is orphaned in the vault. No compensating mechanism exists.
|
||
- **Why it matters:** A latent data-integrity issue. Materially riskier once web upload (2.4) exists.
|
||
- **Shape:** Audit suggested a `DeadLetterLog` recording orphaned `entryKey`s for a periodic maintenance pass. Lighter than full transactional rollback (which the dual-database split fundamentally cannot give us).
|
||
- **Prerequisite:** None. Worth landing alongside or just before 2.4.
|
||
|
||
---
|
||
|
||
## Phase 5 — Documentation backlog
|
||
|
||
### 5.1 Folder-level CLAUDE.md sweep
|
||
|
||
- **What:** Eight folder-level `CLAUDE.md` files need writing/rewriting per the brief in `DOC_PLAN.md`. Five are rewrites (drift from the `.NET 10` upgrade and structural moves); three are new (`DeepDrftWeb.Services`, `DeepDrftContent.Services` — the two libraries where most domain logic now lives — plus the open question on `DeepDrftContent.Services/FileDatabase/README.md`).
|
||
- **Why it matters:** The agent guidance files are how every future implementer (human or agent) gets oriented in a directory. They are currently misleading in ways that will cause wrong assumptions on first contact — claiming `.NET 9`, referencing `MediaPath` that has been `EntryKey` for two migrations, describing a `FileDatabase/` tree inside `DeepDrftContent` that has moved out, and missing entirely for the two `*.Services` libraries.
|
||
- **Shape:** Doc-keeper executes against `DOC_PLAN.md`. Order of operations and the per-folder briefs are already specified there.
|
||
- **Prerequisite:** None. Can run fully in parallel with any feature work.
|
||
- **Constraint:** Wait on Daniel for the `DeepDrftContent.Services/FileDatabase/README.md` judgement call before that file changes (retire, keep + refresh, or replace with a CLAUDE.md). The other seven can proceed without that decision.
|
||
|
||
---
|
||
|
||
## Phase 7 — Shared UI Components
|
||
|
||
Reusable presentational components in `DeepDrftShared.Client` (the RCL consumed by both the public site and the CMS). Distinct from the player stack and CMS surfaces — these are host-agnostic building blocks both apps compose.
|
||
|
||
---
|
||
|
||
## Phase 8 — CMS Track Browser
|
||
|
||
Three browse modes for the CMS `/tracks` page — **Track**, **Album**, **Genre** — selected by a toggle, each deep-linkable so the public home page can link straight into a mode. One view-model (DI-scoped, matching the `TracksViewModel` pattern) feeds all three views; the divergence is in rendering, not data paths (per the standing "same data, different uses" preference). This supersedes the deferred §6.2 — Album and Genre modes *are* the intermediate browse surface that item was waiting on. Full spec: `product-notes/phase-8-cms-track-browser.md` (normalization gate, component decomposition, VM design, URL scheme, data contracts, open questions).
|
||
|
||
**§8.0 landed on 2026-06-11** — a breaking `TrackEntity` normalization has been completed and is stable on dev. §8.1–§8.5 are now unblocked. The Waveform Pre-Processing tab is **removed**, folded into an in-grid status column + per-row/page-level generate actions (see §8.2).
|
||
|
||
---
|
||
|
||
|
||
A small set of items that are real but don't fit a phase yet. Surface them when they become relevant rather than committing now.
|
||
|
||
- **Identity / accounts.** Currently no user concept. Needed before web upload (2.4); also a precondition for favourites, listening history, per-user playlists. Decide the shape before any of those lands. `[speculative]` until Daniel signals interest.
|
||
- **`ITrackService` interface.** Audit-suggested. Low value today (one consumer pair); higher value when the test surface expands beyond FileDatabase.
|
||
- **Test coverage outside FileDatabase.** Tests today cover the FileDatabase subsystem comprehensively and nothing else. As features in Phases 1–4 land, test scope should expand — at minimum `WavOffsetService`, `AudioProcessor`, `TrackService` (both sides), and the streaming player services. Not a phase of its own; an attached cost to feature work.
|
||
|
||
---
|
||
|
||
## Phase 9 — Release Medium Types
|
||
|
||
Releases gain a top-level **medium** discriminator above the existing `ReleaseType`. Three media: **Studio CUTS** (`Cut` — the only medium that uses Single/EP/Album), **Live SESSIONS** (`Session` — a single live track with a distinct hero image), **DJ MIXES** (`Mix` — a single long track with a preprocessed high-resolution waveform datum). This touches the data model, the API, the CMS, and the public site.
|
||
|
||
The public home page **already** carries the three-medium framing as editorial cards (Studio / Live / DJ Mix — `COMPLETED.md §8.6`, landed 2026-06-12), but those cards have no destinations and nothing below the copy layer knows what a medium is. Phase 9 makes the medium real and gives those cards somewhere to point.
|
||
|
||
**Architectural spine — discriminator enum + optional metadata table.** `ReleaseMedium` is a plain enum column on `ReleaseEntity`. A medium that needs data beyond the base release (Session's hero image, Mix's waveform datum) gets its own 1:1 metadata table; a medium that needs nothing extra (`Cut`) *is* the base `ReleaseEntity`. This is Open/Closed at the schema level — a future medium (e.g. Video, `§3.1`) adds an enum value and *optionally* one metadata table, and changes **zero** existing tables. The alternatives (one wide nullable table; an EF type hierarchy) both collapse to the god-table the Phase 8 normalization moved away from — rejected. Full design, contracts, and the SOLID rationale: `product-notes/phase-9-release-medium-types.md`.
|
||
|
||
**Design discipline throughout: extension, not modification.** Where a per-medium mapping is unavoidable (card → browser, medium → API projection, medium → detail hero), keep it in **one table per concern** — never a scattered three-arm `switch`. Drive CMS cards and nav sub-items off `Enum.GetValues<ReleaseMedium>()` + a display-metadata lookup, so a new medium surfaces automatically.
|
||
|
||
**The `ReleaseType`-only-for-`Cut` invariant.** Single/EP/Album is meaningful only when `Medium == Cut`. Enforce as a **domain rule** (service layer ignores/resets `ReleaseType` for non-`Cut`; CMS hides the field unless `Cut`; `ReleaseDto.ReleaseType` is **nullable**, nulled at the single entity→DTO mapping point for non-`Cut` so one producer enforces and no consumer needs the rule), **not** a DB constraint — **by choice, not necessity**: EF Core supports check constraints first-class (`HasCheckConstraint`, versioned in migrations, Npgsql-supported), but the invariant is advisory ("meaningless," not "invalid") and the read model enforces it at one point. The column stays on `ReleaseEntity` as a **named exception** to the metadata-table pattern: a `CutMetadata` table was considered and rejected because the `/cuts` hot path reads `ReleaseType` on every card and Phase 8 §8.0 just landed the column (see spec §1). Future media must not copy this — the default remains the metadata table.
|
||
|
||
Sequenced as four waves. Wave 1 is a prerequisite for everything; within Waves 2–4 the lettered tracks are parallel.
|
||
|
||
**Dependency summary:** `1 → 2 → 3 → 4`. Wave 4 (public site) can begin once Wave 2's `api/release` family is stable; both Wave 4 **build and acceptance** are independent of Wave 3 (CMS) — the body-less `POST api/release/{id}/mix/waveform` trigger (9.2.B) can seed real waveform datum for acceptance testing without any CMS in existence, and hero images seed via a script against 9.2.B likewise.
|
||
|
||
Waves 1–7 are landed (`COMPLETED.md §9`). Wave 6 closes two functional gaps a post-landing smoke-test survey surfaced — surfaces the medium taxonomy did *not* reach, not regressions. Wave 7 hardens the single-track-per-medium rule from a CMS-form convention into a real domain invariant — the one place the medium taxonomy is *declared but not enforced* below the UI.
|
||
|
||
### 9.8 Wave 8 — Remediation (fully landed; all tracks complete)
|
||
|
||
Daniel tested the landed Phase 9 surface end-to-end and produced a punch-list of corrections before the phase is called complete. These are **not new features** — they are the gap between what the Wave 1–7 specs *built* and what hands-on use *wants*. The theme is the same one Phase 9 has carried throughout: the medium taxonomy reaching every surface it should, and the browse surfaces matching the mental model rather than the implementation's first cut.
|
||
|
||
Two surfaces dominated: the **CMS Release Archive** (the card-grid landing is the wrong shape — Daniel wants medium *tabs*, not navigate-away cards) and the **public Archive** (the three-card overview is dead weight; the searchable all-**releases** view *is* the archive — release-cardinal, decided). The **Mix Visualizer redesign (8.K)** was pulled out of Phase-9-completion scope and ran as a post-Phase-9 wave from a finished spec (`product-notes/phase-9-mix-visualizer-redesign.md`); it has now also landed.
|
||
|
||
**Open questions resolved (Daniel, 2026-06-13):** 8.H is decided **H2** (a new release-cardinal searchable browser at `/archive`; cascade: `/tracks` demoted from nav, route kept; mobile ARCHIVE → the browser; three-card overview fully retired); 8.I drops GENRES from the nav only (route kept); 8.F makes the Session hero optional-but-warn-if-missing; 8.E defaults the `ALL`-tab Add Track to Cut with the medium selector staying user-changeable. A new track **8.L** consolidates the release-name/track-name pair into a single name for single-track media (derived track name **kept synced**, decided), and **8.M** (split off 8.L) retires the legacy `TrackNew`/`TrackEdit` forms by folding them into the batch forms to reduce code surface.
|
||
|
||
Full track decomposition, acceptance criteria, and parallel/dependent analysis: `product-notes/phase-9-wave-8-remediation.md`.
|
||
|
||
**Dependency shape:** 8.B is the foundation for the CMS tab work (8.A consumes the shared grid; 8.C/8.E layer on once 8.A lands). 8.L follows 8.G and coordinates with 8.E/8.F (same forms). 8.M (legacy-form retirement) follows 8.L and is architectural (route map + addressing decision). On the public side, 8.H (decided H2 — the new release-cardinal archive) gates 8.I. **All Wave 8 tracks are landed** — Phase-9-completion gate (8.A–8.J + 8.L), 8.M, and the post-Phase-9 8.K Mix Visualizer redesign. **Landed tracks:** 8.A, 8.B, 8.C, 8.D, 8.E, 8.F, 8.G, 8.H, 8.I, 8.J, 8.L (2026-06-13); 8.M (2026-06-14); 8.K (2026-06-14).
|
||
|
||
|
||
## Phase 10 — Mix Visualizer WebGL2 Renderer
|
||
|
||
The landed Canvas 2D Mix visualizer (8.K) renders at 1–2 FPS and **cannot afford the planned effects** — a staff-engineer analysis found the per-frame killers (full-viewport `shadowBlur`, CSS `backdrop-filter`, per-frame `getBoundingClientRect`) structural to the approach, and the planned effects (bulge, lava-lamp detach, a morphing 2D color field, glass) are all per-pixel/per-frame work — exactly what Canvas 2D is worst at and a fragment shader is best at.
|
||
|
||
**Decision (Daniel, explicit): rebuild as a WebGL2 fragment-shader renderer. No Canvas 2D stopgap** — "WebGL as step 1, no pussyfooting." This supersedes 8.K §E's Canvas-2D-default recommendation; the "industry-standard, well-commented, no tricks" discipline carries forward as *textbook WebGL2 with a commented shader*. Target a smooth **60 FPS**. Strictly read-only (no playback-control changes); the duration-derived ~333 samples/sec datum (8.K §F) and the existing Blazor↔JS bridge are both preserved — the datum now lands as a GPU texture rather than a CPU-walked array.
|
||
|
||
Adds a **controls row** above the mix details / below the back button: four continuous, session-persistent sliders — **resolution** (relocated 8.K zoom), **bubblyness** (box→liquid bulge), **detach** ("unleash the lava lamp" — blobs pinch off and rise), **color-shift speed** (gradient morph rate). The headline visual is a living 2D **navy↔moss** gradient field (theme tokens from `DeepDrftPalettes`) that varies per-bar *and* shifts along time, never static; plus an in-shader **glass** treatment (specular/Fresnel/frosted/refraction — no CPU backdrop-filter). Persistence mirrors `MixVisualizerZoomState` (widen to a `MixVisualizerControlState` holding all four).
|
||
|
||
Full design, renderer architecture, the four effects, acceptance criteria, and phasing: `product-notes/mix-visualizer-webgl-renderer.md`.
|
||
|
||
**Sequenced as four waves.** Wave 1 (renderer swap at parity — prove WebGL2 on screen at 60 FPS, bridge intact, no new effects) is the load-bearing prerequisite. Wave 2 (controls row + widened state) and Wave 3 (the four effects in the shader) both follow Wave 1; the four effects within Wave 3 are independently shippable and tunable. **Deferred (Daniel):** control-range guards and motion-speed coupling to bubblyness — he tunes bad ranges by hand once on screen. **Landed:** Wave 1 (2026-06-15). Wave 2 (2026-06-15). Wave 3 (2026-06-15).
|
||
|
||
**Wave 4 — detail-page polish + controls rework (presentation only; the final wave).** A UI/placement pass over the Mix detail page — **no renderer, state, bridge, or mapping change.** (1) The four controls move out of the always-visible row into a **popover** (`MudPopover`, `SharePopover`-idiom) opened by a new bespoke **lava-lamp icon button** anchored **top-right of the body, across from the `← Back` link** (recommend a new `TopRightAction` slot on `ReleaseDetailScaffold`, laid as a SpaceBetween row with the back link). (2) The lava-lamp SVG lives in `DeepDrftShared.Client/Common/DDIcons.cs` in the hand-rolled gas-lamp style (`currentColor`, 24×24 viewBox, raw-string const) — a recognizable lamp with two-three suspended blobs. (3) The four `MudSlider`s become four **`RadialKnob`s** (`DeepDrftShared.Client/Components/RadialKnob.razor`) **in a row in the popover**, each carrying its existing Material icon (`ZoomIn`/`BubbleChart`/`Air`/`Palette`) **as an adjacent `MudIcon` caption** — RadialKnob has **no icon slot** (its `Label` is SVG text), so icons sit beside each knob. Knobs bind `Value`/`ValueChanged` to the **unchanged** `MixVisualizerControlState` via the **same `OnXChanged` handlers + `NotifyChanged()` seam** the sliders use today (resolution via `MixZoomMapping` fraction; other three normalized [0,1]; `HoldValue=false` for live feel). (4) **Widen the Mix body** to match the Sessions detail page — `MudContainer MaxWidth="Large"` (~1280px, up from the scaffold's 760px), Mix-scoped so Track detail is unaffected. **Depends on Wave 3 merged** (the knobs drive the Wave 3 effects) and **supersedes the controls-row design** (`product-notes/mix-visualizer-webgl-renderer.md` §3 → §7). Read-only contract intact; no knob is a seek surface. Full design + acceptance: that spec's **§7**.
|
||
|
||
### Phase 10 — Reframe (Lava): Waves R1–R4
|
||
|
||
**Landed:** 2026-06-17 on dev. See `COMPLETED.md` for the full completion record.
|
||
|
||
---
|
||
|
||
## Phase 11 — Public Site Enhancements
|
||
|
||
The next pass over the public listening surface, after Phase 9 + Wave 8 moved the site to release-cardinal browse (`/archive`) and per-medium detail. The spine of the phase: **make the release the cardinal unit of the public site, make every navigation an addressable shareable URL, and make the album a first-class playable object** (ordered, queue-able, shareable). **Nine Daniel commitments** (the original four, plus four added 2026-06-15 when he resolved the open questions and expanded scope, plus a ninth added 2026-06-16): (1) a Cuts detail page `/cuts/{id}`; (2) the player-bar release-title resolves medium → dedicated detail page; (3) retire the **whole** track-cardinal stack **and** normalize release-card rendering into shared components; (4) encode Archive filters in the URL; (5) explicit track ordinal editable from the CMS; (6) release-level Share; (7) a play-queue system (absorbs the queue half of §1.3); (8) a release **Description** field — multiline free-text on the base release (all media), edited from the CMS add/edit forms and rendered as a text block on every detail page; (9) **release GUID identifiers** — front the release's transparent sequential int PK with an opaque app-minted GUID handle (the track-`EntryKey` model), swept across every public addressing site. Full design, framing corrections, wave decomposition, gap analysis: `product-notes/phase-11-public-site-enhancements.md`.
|
||
|
||
**State it inherits (verified 2026-06-15).** `/sessions/{id}` and `/mixes/{id}` detail pages **exist and are mature** (both inherit `ReleaseDetailBase`'s prerender bridge; `MixDetail` composes `ReleaseDetailScaffold`, `SessionDetail` deliberately diverges). `/archive` is **already** a release-cardinal searchable browser (search + medium + genre). `ReleaseGallery` is the shared release-card grid — but **only Sessions/Mixes use it**; Archive and Cuts re-implement equivalent card markup inline. The real gaps: **Cuts have no single-release detail page** (`/cuts` cards open `/tracks?album={title}`), and **`/archive` holds its filters in component fields, not the URL**. The queue/playlist **does not exist** (single-slot player).
|
||
|
||
**Headline correction — commitment 5 is already built.** The brief framed the track ordinal as a new column + EF migration + a Daniel-gated apply step. **The read shows it already shipped in Phase 8:** `TrackEntity.TrackNumber` (1-based, non-null), migration `20260611005700` **already applied**, `TrackDto` mirror, API write path (validated `> 0`), CMS reorder (`BatchEdit` assigns ordinal from list position on submit), and the read already `.OrderBy(t => t.TrackNumber)`. **No new schema, no migration to gate.** Commitment 5 collapses to *verify-and-consume*: confirm the public read projects/sorts `TrackNumber` and that `CutDetailViewModel` orders by it (a one-line fix if not). See spec §3a.
|
||
|
||
**Mirror-image on commitment 8 — the Description column is genuinely new.** Where commitment 5 turned out already-built, commitment 8 is the opposite: **no `Description` member exists** on `ReleaseEntity` or `ReleaseDto` (greps return nothing; the entity carries `Title`/`Artist`/`Genre`/`ReleaseDate`/`ImagePath`/`ReleaseType`/`Medium` + the two metadata satellites). So commitment 8 **is** the real cross-stack schema project — just for a different field: a new base-`ReleaseEntity.Description` column + EF migration (**Daniel-gated apply**), `ReleaseDto` mirror, `TrackConverter` round-trip, write-path plumbing (`UpdateTrackMetadataRequest` + upload form + the unified services — threaded **wherever `Genre` is**, since there is no dedicated release-update endpoint; release-cardinal fields ride the track update/upload path), the CMS `AlbumHeaderFields` multiline input, and the detail-page text block. It is a base field (uniform across media) so it lives on the base release, **not** a per-medium satellite (Phase 9 spine). See spec §3d.
|
||
|
||
**Framing corrections (brief vocabulary vs. live routes).** (1) There is **no `/tracks/{id}` route** — the track-cardinal detail is `/track/{EntryKey}`. The brief's "`/tracks/{id}` becomes a router" is best realized as a **medium→route resolver** at click sites (the player bar already carries release id + medium — no round-trip), plus a thin `/tracks/{id}` redirect page for deep links. (2) The new `/cuts/{id}` album page is the phase's center of gravity — the first **multi-track** release detail. (3) Requirement 4 is a **URL-binding pass over the existing `ArchiveView`**, borrowing the `TracksView` `[SupplyParameterFromQuery]` pattern — not a new browser.
|
||
|
||
**Design discipline.** The medium→route resolver is **one table** (`ReleaseRoutes.DetailHref`) consumed by the player bar, Archive, and Cuts cards. The shared `ReleaseGallery` becomes the **one** release-card grid across all four browse surfaces (Archive/Cuts fold in via a new per-card `HrefResolver`), not three inline copies (memory *One source, multiple views*). The `/cuts/{id}` page composes `ReleaseDetailScaffold` via a generalized `Header` slot + a `BodyContent` slot for the track list — **not** a boolean layout flag (Phase 9 §5.3). The queue is a separate `IQueueService` orchestrating above the single-slot player (strong steer; final call staff-engineer's). Header Play binds to a single handler that swaps single-track → `QueueService.PlayRelease` with no page change (memory *Design for adaptability up front*).
|
||
|
||
Sequenced as **eight waves**; the critical path is `11.A → 11.B → 11.C → 11.H`, with 11.D / 11.E / 11.F / 11.G hanging off the front and 11.H sitting at the tail (it re-types the public addressing surface that 11.B–11.E build on).
|
||
|
||
- **11.A — `/cuts/{id}` album-detail page.** Left header (name, artist, genre, year, Play + Share), right cover with theme border, ordered track list (by `TrackNumber`) with per-row play, header Play. New `CutDetailViewModel`; reuses `GetById` + the `releaseId`-filtered track page (both exist). Ordinal is a **verification** (§3a), not a dependency. Header/row Play consume 11.F when present, else degrade to single-track (§3.4 seam). **Load-bearing prerequisite for 11.B's Cut resolution.**
|
||
- **11.B — `ReleaseRoutes` resolver + repoint.** Promote `ArchiveView.DetailHref` to a shared `ReleaseRoutes.DetailHref`; Cut resolves to `/cuts/{id}` (needs 11.A); repoint player-bar title (→ release), Archive cards, `AlbumsView` cards; thin `/tracks/{id}` redirect page. **Depends on 11.A.**
|
||
- **11.C — retire + normalize (the heart).** With §2 removing every inbound link: **delete the whole track-cardinal stack** (`TracksView`/`TrackDetail`/`TrackCard`/`TracksGallery`/`GalleryViewMode` + `/tracks`, `/track/{EntryKey}` routes) **and** fold Archive + Cuts inline cards into the shared `ReleaseGallery` (new `HrefResolver`); consolidate the medium-label lookup. **Depends on 11.B.** (Cut track-row is a separate small `TrackRow`, not `ReleaseGallery`.)
|
||
- **11.D — Archive filters in the URL.** `/archive?q=&medium=&genre=`, history-driven (§5). Touches only `ArchiveView`. **Free-floating — but coordinate with 11.C** (both edit `ArchiveView`).
|
||
- **11.E — release-level Share.** `SharePopover` gains a release mode that copies `ReleaseRoutes.DetailHref(release)`; wire the Cut header Share to it. **Depends on 11.B** (resolver) + a release detail to share.
|
||
- **11.F — queue model.** `IQueueService` above the single-slot player + one new player `TrackEnded` hook + player-bar skip controls. **Free-floating, can start cold day one.** Gates the Cuts "play album" affordance (11.A header Play). **Preload (§1.3 half b) stays OUT** — design the seam, defer the feature.
|
||
- **11.G — release Description schema slice.** New `ReleaseEntity.Description` column + EF migration (**Daniel-gated apply**), `ReleaseDto` mirror, `TrackConverter` round-trip, write-path plumbing (`UpdateTrackMetadataRequest` + upload form + the unified services, threaded wherever `Genre` is), CMS `AlbumHeaderFields` multiline input (§3d). **Free-floating, can start cold day one** — the only gate is Daniel's migration go-ahead. The **detail-page render is NOT in this wave**: the Cut text block rides 11.A, the Session/Mix block is a small additive touch to those existing pages. Both degrade cleanly (null Description renders nothing), so render & schema can land in either order.
|
||
- **11.H — release GUID identifiers (terminal public-site wave).** Front the release `long` PK with an app-minted GUID-string `EntryKey` column — the **same pattern tracks use** (`TrackEntity.EntryKey` is `required string`, app-minted `Guid.NewGuid().ToString()`, keeping the int PK private). New `ReleaseEntity.EntryKey` (`string`, unique index, minted at `FindOrCreateRelease`) + EF migration that **backfills a GUID-string `EntryKey` for every existing release row at migration time** (**Daniel-gated apply**); `ReleaseDto.EntryKey`; `TrackConverter` round-trip; **re-type the public addressing surface from `long` to the `EntryKey` handle** — detail routes (`:long`→`{EntryKey}`), the `/tracks/{id}` redirect, `ReleaseRoutes.DetailHref`, `SharePopover.ReleaseId`, the public read path, and the public release API params (`GET api/release/{id}` + the `releaseId` track-page query). Internal FKs (track→release, satellite→release), the `long` int PK (unused by the app), and the ApiKey-gated CMS endpoints **stay on the int**. **Depends on 11.B (landed), 11.C, 11.D, 11.E** — it sweeps the routes/resolver/share/cards those waves create or edit, so it is the **last** public-site wave (spec §3e.7). **Gating decision (Daniel, spec §3e.5(1)) — RESOLVED (additive `EntryKey`, track-pattern):** additive app-level GUID-string `EntryKey` column matching tracks; the `long` PK stays DB-only and unused by the app; existing rows are backfilled at migration time (not a dev reset). Daniel's rationale (2026-06-16): "long at the DB level with an app-level guid `EntryKey` for the releases just like tracks; PK is not used by the app; migrate the existing data to provide the entry key at migration time." The true PK retype is **declined** (framework fork of `Cerebellum.BlazorBlocks.Models` — `BaseEntity.Id` hardwired `long` — plus full FK rewrite; recorded as considered-and-declined per file convention). Still open: raw-GUID URL (recommended) vs. slug, and migration ordering after 11.G's snapshot.
|
||
|
||
**Landed:** 11.A (2026-06-16); 11.F (2026-06-16); 11.G (2026-06-16); 11.B (2026-06-16); 11.C (2026-06-16); 11.E (2026-06-16); 11.D (2026-06-16); 11.H (2026-06-16). The §3.4 PlayAlbum→`IQueueService` seam (deferred in 11.A, awaiting 11.F) is now closed: `CutDetail.razor` consumes the cascaded `IQueueService` — header Play calls `Queue.PlayRelease(ViewModel.Tracks, 0)`, per-row play calls `Queue.PlayRelease(ViewModel.Tracks, index)`, currently-playing row toggles play/pause, null-safe fallback to `SelectTrackStreaming` when the queue cascade is absent (2026-06-16). **All Phase 11 tracks (11.A–11.H) are now landed; Phase 11 is complete.** Two release-table migrations are authored but not yet applied (Daniel-gated, apply in author order): `20260616035252_AddReleaseDescription` (11.G) then `20260616210143_AddReleaseEntryKey` (11.H).
|
||
|
||
**Dependency shape:** `11.A → 11.B → 11.C → 11.H`; `11.B → 11.E`; **11.D, 11.F, 11.G parallel** (11.D coordinates with 11.C on `ArchiveView`; 11.F's "play album" is consumed by 11.A; 11.G's Description render rides 11.A + a Session/Mix touch, degrading on null). **11.H is terminal** — it re-types the public release-addressing surface (routes, `ReleaseRoutes`, `SharePopover`, cards, public API params) that 11.B–11.E create/edit, so it follows all of them; its migration is authored after 11.G's so the EF snapshot stays linear. The cold-start items are **11.A**, **11.F**, and **11.G** — kick 11.A + 11.F off first so "play album" works on first ship of the Cut page; 11.G runs alongside on its own track; 11.H waits for the addressing surface to settle.
|
||
|
||
**Resolved by Daniel (2026-06-15), kept visible per file convention:** player-bar title → release detail (was OQ1); track ordinal in scope **and already built** (was OQ4, reversed then found done); retire the **whole** track-cardinal stack (was OQ5, full cut chosen); release-level Share in scope; play-queue in scope (queue half of §1.3 absorbed; preload half stays deferred); release **Description** field in scope (commitment 8 — a real new column, lands as schema slice 11.G with the render on 11.A + a Session/Mix touch). **Still open (spec §7.2):** `/cuts/{id}` scaffold strategy (generalized `Header` slot — recommended — vs. bespoke); Cut header affordance idiom (icon vs. labeled buttons); queue architecture (separate `IQueueService` — strong steer; staff-engineer's final call); whether release-share keeps "Embed player" (recommend copy-link-only); Description render plain-text vs. markdown (recommend plain text + preserved line breaks for v1) and column max-length (recommend 2000–4000); `/genres` fate (out of scope, flag as adjacent).
|
||
|
||
---
|
||
|
||
## 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 1–5. Phase numbers are organisational, not sequencing.
|
||
- **When something lands, move it to `COMPLETED.md`** rather than deleting it. Keep the original "What / Why / Shape" body intact so the history reads as a record of the decision, not just the outcome.
|
||
- **Mark genuinely uncertain items `[speculative]`** so future readers can tell what is direction vs. commitment.
|
||
- **Open questions belong in the item that raises them**, not in a separate "questions" list — they expire when the item does.
|
||
|