Files
deepdrft/PLAN.md
T
daniel-c-harvey 5fb46bf5eb docs(product): spec CMS public landing page (Phase 13)
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.
2026-06-17 11:44:33 -04:00

435 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 14 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 24 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 17 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 17 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.A8.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 12 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 R1R4
**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.B11.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.A11.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.B11.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 20004000); `/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 15. 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.