Files
deepdrft/PLAN.md
T

269 lines
33 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
- **What:** 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.
- **Prerequisite:** Requires a notion of "next track" — today the player only knows the current one. That implies either a playlist/queue model in `IPlayerService` or a passive "what was the next row in the gallery" inference.
- **Open question:** Does a queue model belong in `IPlayerService`, or is the player a single-slot device that a future `PlaylistService` orchestrates above? Worth a design note before implementation. Capture in product notes when picked up.
### 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.
---
### 9.1 Wave 1 — Data model + migration `[prerequisite gate]`
- **What:** New `ReleaseMedium` enum (`Cut, Session, Mix`) in `DeepDrftModels/Enums/`. `ReleaseEntity` gains `ReleaseMedium Medium` (default `Cut`) plus 1:1 nav properties to two new metadata entities. New `SessionMetadata` (`HeroImageEntryKey`) and `MixMetadata` (`WaveformEntryKey`) entities, each 1:1 with `ReleaseEntity`. EF configurations + migration.
- **Why:** Every other wave reads this schema. The discriminator-plus-optional-table shape is the load-bearing decision of the phase; it must land first and land right.
- **Shape:**
- `ReleaseMedium` enum with `Cut = 0` (default — existing/migrated releases stay studio cuts with no discriminator data migration).
- `Medium` column on `releases`; `ReleaseConfiguration` documents the `ReleaseType`-only-for-`Cut` invariant *and* the named `CutMetadata`-rejected exception (see the phase intro above).
- `session_metadata` and `mix_metadata` tables, each with a unique FK to `releases` (1:1). `MixMetadata.WaveformEntryKey` is a vault entry key (resolved — see open question), not an inline blob.
- Migration is **additive only** — no data migration of existing rows beyond defaulting `Medium = Cut`. Lower risk than the Phase 8 normalization.
- **Prerequisite:** Phase 8 §8.0 normalization (`ReleaseEntity` exists) — already landed.
- **Acceptance criteria:**
- `ReleaseMedium` enum exists; `ReleaseEntity.Medium` defaults to `Cut`.
- `SessionMetadata` / `MixMetadata` entities + EF configs + migration applied; solution compiles and existing releases read back as `Cut`.
- The invariant is documented in `ReleaseConfiguration` (no DB constraint — a deliberate choice; EF supports check constraints, see the phase intro).
- **Open questions:**
- **Resolved — waveform storage:** vault blob + `WaveformEntryKey`. Settled by the server-side trigger design (9.2.B): the API computes and stores the datum vault-side; SQL holds only the entry key, so a JSON column never enters the flow. This wave adds only the SQL column — the vault write rides the existing vault abstraction server-side.
---
### 9.2 Wave 2 — API: medium reads + metadata uploads
A new `api/release` controller — the medium unit is the *release*, not the track, so medium browse and metadata uploads are release-cardinal rather than bolted onto `api/track/page`.
- **9.2.A — Release read endpoints (data layer + controller).**
- **What:** `GET api/release?medium={cut|session|mix}&page=&pageSize=&sort=` (unauth, paginated, medium filter additive — omitting returns all) and `GET api/release/{id}` (unauth, single release + medium metadata). The **list** read `Include`s the matching metadata table via a per-medium projection map; the **by-id** read always-`Include`s both metadata navs (two 1:1 unique-FK joins; non-matching media naturally yield nulls — no per-medium branching, no map).
- **Why:** The public CUTS/SESSIONS/MIXES surfaces and the CMS browsers all read releases by medium. One cohesive release-read family keeps `api/track/page` focused on Phase 8's track-list cases.
- **Shape:** Repository/service join through the metadata tables only for the relevant medium on list reads; base release reads never touch them. The projection map carries a dual responsibility: per-medium `Include` selection *and* the single enforcement point of the medium↔metadata correlation (a metadata DTO is populated iff the medium matches) — which is why it is not inlined in the controller. The honest extensibility guarantee is "one entry, one file," not "zero controller changes." `ReleaseDto` gains `Medium`, a **nullable** `ReleaseType?` (nulled at the mapping point for non-`Cut`), and optional nested `SessionMetadataDto?` / `MixMetadataDto?` (populated only for the matching medium — mirrors Phase 8's nested-`Release` choice, not denormalized flat fields).
- **Acceptance criteria:** `GET api/release?medium=session` returns Session releases with hero-image metadata included and no `MixMetadata`; `medium=cut` returns Cuts with neither metadata block and a non-null `ReleaseType`; non-Cut releases serialize `ReleaseType: null`; pagination + sort parity with `api/track/page`.
- **9.2.B — Metadata write endpoints.**
- **What:** `POST api/release/{id}/session/hero-image` (ApiKey, multipart — hero image → image vault → set `SessionMetadata.HeroImageEntryKey`) and `POST api/release/{id}/mix/waveform` (ApiKey, **no request body** — a server-side trigger: the API fetches the mix audio from its own vault, computes the high-resolution waveform via `WaveformProfileService` parameterized by resolution, stores the datum in the vault, sets `MixMetadata.WaveformEntryKey`). Both routes are resource-addressed — the release id rides the route.
- **Why:** The CMS authoring flows (Wave 3 B/C) need write paths for the medium-specific data, and the waveform is a *derived* datum the server can compute from audio it already owns. Mirroring the existing body-less `POST api/track/{trackId}/waveform` idiom makes the datum correct by construction (no trusting a client blob) and keeps the CMS free of any in-process data layer (its standing constraint). Splitting these from the track-upload endpoint keeps each endpoint single-responsibility.
- **Shape:** Hero-image upload mirrors the existing cover-art `UploadImageAsync` → image-vault → link pattern, targeting `HeroImageEntryKey`. The waveform trigger includes the `WaveformProfileService` refactor: a per-call resolution/profile parameter (today fixed via injected `WaveformProfileOptions.BucketCount = 512`) plus a distinct entry-key/vault target for the high-res datum — one pipeline, two resolutions (*One source, multiple views*). Both endpoints find-or-create the metadata row for the release.
- **Acceptance criteria:** Posting a hero image to a Session release sets `HeroImageEntryKey` and the image is served back through the existing image proxy; the body-less waveform trigger on a Mix release computes + stores a high-res datum, sets `WaveformEntryKey`, and the datum is retrievable.
- **Prerequisite:** 9.1.
- **Open questions:**
- **New endpoints vs. `api/track/page` query-param extension.** Recommend the new `api/release` family (release-cardinal browse, medium metadata `Include`); `api/track/page` can gain a cheap `medium=` passthrough later if a track-level filter is ever needed.
---
### 9.3 Wave 3 — CMS: Release Archive tab, medium selector, medium browsers
- **9.3.A — Release Archive tab + medium selector.**
- **What:** Rename `TrackList.razor`'s third tab **Genre → Release Archive**. Inside it, render a **medium card group** (one card per `ReleaseMedium`, styled like the existing `CmsGenreBrowser` cards) where each card *navigates* to a medium-specific browser. Add a `ReleaseMedium` selector to `TrackNew` / `TrackEdit` / `BatchUpload` / `BatchEdit` / `AlbumHeaderFields`; show `ReleaseType` only when `Medium == Cut`, hide it (and surface medium-specific fields) for Session/Mix.
- **Why:** The CMS needs to author medium per release and browse the archive by medium. The card-group-of-media is the CMS analogue of the home page's three-medium block.
- **Shape:** Cards driven by `Enum.GetValues<ReleaseMedium>()` + a display-metadata lookup (label/descriptor/swatch) — **no hardcoded card switch**. Cut card → `CmsAlbumBrowser` (reused, with a `MediumFilter`); Session card → `CmsSessionBrowser`; Mix card → `CmsMixBrowser`. Selector-driven conditional fields ride **per-medium section components** (`CutFields` / `SessionFields` / `MixFields` — plain explicit markup inside, no clever generics) behind a **single dispatch point** (a `MediumFields` component holding the one `@switch`) embedded by all five forms — one dispatch, not five scattered conditional blocks. A new medium is one section component + one dispatch entry.
- **Acceptance criteria:** The third tab reads "Release Archive" and shows one card per medium; each card navigates to its browser; the upload/edit forms show `ReleaseType` only for `Cut`.
- **9.3.B — `CmsSessionBrowser` + hero-image authoring.**
- **What:** New `CmsSessionBrowser.razor` — a flat list of Session releases (`Medium == Session`) with cover + hero thumbnail, session name, artist; row Edit + hero-image management. Wire the Session upload/edit path to the hero-image upload endpoint (9.2.B).
- **Why:** Sessions are single-track releases with a distinct hero image; the album parent/child expansion of `CmsAlbumBrowser` is the wrong shape for them.
- **Shape:** Reuse `CmsTrackGrid` parameterized by `MediumFilter` where the layout fits; the hero thumbnail is an additive column / thin wrapper, not a forked table. Hero upload reuses the cover-art one-shot pattern against `HeroImageEntryKey`.
- **Acceptance criteria:** Session browser lists only Session releases; uploading a hero image persists it and renders the thumbnail.
- **9.3.C — `CmsMixBrowser` + waveform trigger wiring.**
- **What:** New `CmsMixBrowser.razor` — a flat list of Mix releases (`Medium == Mix`) with an in-grid waveform-generation **status** column (mirroring Phase 8's `HasWaveformProfile` idiom) and a per-row **Generate Waveform** action. Wire the Mix upload to call the server-side waveform trigger (9.2.B) — the CMS never computes or carries the datum.
- **Why:** A Mix without a generated high-res waveform is incomplete; status-in-grid + generate-action is the Phase 8-established pattern for waveform readiness. The CMS has no in-process data layer by convention, so all it does is fire the trigger.
- **Shape:** Upload flow: `UploadTrackAsync``POST api/release/{id}/mix/waveform` (body-less; the API computes and stores server-side, 9.2.B). The per-row Generate action is the same trigger — recovery costs one POST, with no download/recompute/re-upload of the catalogue's longest audio files.
- **Acceptance criteria:** Mix browser lists only Mix releases and shows per-row waveform status; uploading a Mix fires the trigger and the stored high-res waveform appears as generated; the per-row Generate action recovers a missing waveform.
- **Prerequisite:** 9.2.
- **Open questions:**
- **Genre browse fate.** Resolved: the Genre tab slot is taken by Release Archive (Wave 3A as specced); the existing genre browse functionality is deprioritized and stays route-reachable as-is — no active development, no retirement. The team should not remove it.
- **Waveform preprocessor reuse.** Resolved: one server-side parameterized pipeline (player-bar peek = low-res, Mix = high-res; *One source, multiple views*). The `WaveformProfileService` resolution-parameter refactor lands in **Wave 2 with the trigger endpoint (9.2.B)**, not in this wave.
- **Single-track invariant.** Resolved: hard constraint. One track per Session/Mix release is enforced at upload — the CMS form for those media drops the multi-track master list entirely.
---
### 9.4 Wave 4 — Public site: ARCHIVE nav, CUTS / SESSIONS / MIXES, waveform visualizer
- **9.4.A — ARCHIVE nav + popover.**
- **What:** Replace the current RELEASES / SESSIONS / MIXES nav items (in `DeepDrftPublic.Client/Layout/Pages.cs`) with a single **ARCHIVE** item. Desktop: hover shows a MudBlazor popover with CUTS / SESSIONS / MIXES → `/cuts`, `/sessions`, `/mixes`. Mobile / direct nav: ARCHIVE → an overview page `/archive` (three medium cards, reusing the §8.6 card idiom). Fixes the current **dead** Sessions/Mixes links.
- **Why:** The nav must route into the new medium surfaces; today's Sessions/Mixes links point nowhere.
- **Shape:** `DeepDrftMenu.razor` renders `Pages.MenuPages` as a flat `<a>` list today with no dropdown mechanism. Recommend extending the nav model with an optional `Children` collection (generalizes to future dropdowns) over a bespoke hardcoded popover. Pinned semantics (spec §5.1): dual-role nodes — desktop hover opens children, desktop click navigates to the parent's route (`/archive`), mobile renders the parent as a link with children indented; depth cap of **one level** — deeper nesting is a redesign, not a recursion.
- **Acceptance criteria:** ARCHIVE replaces the three flat items; desktop hover reveals the three sub-links; mobile routes to `/archive`; no dead links remain.
- **9.4.B — CUTS (`/cuts`).**
- **What:** New `/cuts` route reusing the existing `AlbumsView` layout, filtered to `Medium == Cut`. Studio Singles/EPs/Albums appear as they do on the current Releases page.
- **Why:** Honour the existing studio-release browse under the new medium taxonomy. Lowest-effort of the three media.
- **Shape:** Parameterize `AlbumsView`'s data load with a medium filter rather than forking a component. `/cuts` = `AlbumsView` with `Medium == Cut`.
- **Acceptance criteria:** `/cuts` shows only `Cut` releases with the current AlbumsView ergonomics.
- **Resolved:** When `/cuts` lands, the existing `/albums` route issues a redirect to `/cuts`. Old URLs keep working; no hard 404.
- **9.4.C — SESSIONS (`/sessions` + `/sessions/{id}`).**
- **What:** Gallery of session cards (cover, session name, artist) at `/sessions`; detail at `/sessions/{id}` mirroring `TrackDetail` but with the **hero image dominant above the fold**, cover secondary.
- **Why:** Sessions are an authored content kind the home page advertises; the hero image is their distinctive visual.
- **Shape:** Gallery borrows `AlbumsView`'s card-gallery skeleton with a session card face. Detail composes a shared `ReleaseDetailScaffold` (extracted common metadata + play + player wiring) with a hero-image hero slot — see 9.4.D open question.
- **Acceptance criteria:** `/sessions` lists Session releases; `/sessions/{id}` renders hero-dominant with the play affordance intact.
- **9.4.D — MIXES (`/mixes` + `/mixes/{id}`) + `MixWaveformVisualizer`.**
- **What:** Gallery at `/mixes`; detail at `/mixes/{id}` whose defining visual is a **`MixWaveformVisualizer`** component fed by the preprocessed waveform datum from `MixMetadata`, rendered as the **full-page background** of the detail page. The visualizer is a **named, reusable** component.
- **Why:** Mixes are long continuous sets; the waveform is their signature visual and the brief calls for a reusable visualizer.
- **Shape:** `MixWaveformVisualizer` takes the waveform datum (via `WaveformEntryKey` → content endpoint) + optional playback-position binding; renders a high-resolution, sophisticated **full-page background** visual in **its own visual language** — explicitly *not* the `SpectrumVisualizer` / `LevelMeterFab` peak-bar idiom, which is **reserved for the player bar**. The two are siblings in subject matter (waveforms) with entirely separate design treatments; they share a data pipeline (9.2.B), never a look. Detail composes the same `ReleaseDetailScaffold`, with the visualizer as the page-background layer.
- **Acceptance criteria:** `/mixes` lists Mix releases; `/mixes/{id}` renders the waveform visualizer as the page background fed by real datum (seedable via the 9.2.B trigger, no CMS required); the visualizer is a standalone reusable component visually distinct from the player-bar idiom.
- **Open question:** Design the visualizer's seek-on-click position-binding seam now even if click-to-seek ships later? Recommend yes — design the seam, defer the feature (*Design for adaptability up front*).
- **Prerequisite:** 9.2 (the `api/release` read family). Independent of Wave 3 for both **build and acceptance** — the body-less 9.2.B waveform trigger seeds real Mix datum and a script can seed hero images, with no CMS in existence.
- **Open questions:**
- **Detail-page strategy.** Three separate detail pages vs. one branching `TrackDetail` vs. a shared `ReleaseDetailScaffold` + per-medium hero slot. Recommend the scaffold (DRY-by-composition, the Phase 8 `BatchUpload`/`BatchEdit` extraction move; honours *One source, multiple views*). Sets the shape of 9.4.C and 9.4.D. Scaffold contract (spec §5.3): it owns exactly the invariant trio — metadata block, play affordance, player wiring; all per-medium variance rides slots (a boolean layout parameter on the scaffold is a design failure). `TrackDetail` is refactored onto the scaffold in this wave (it is the extraction source — nearly free); if deferred, record the fork as deliberate debt with a retirement note.
---
## 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.