250 lines
31 KiB
Markdown
250 lines
31 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
|
||
|
||
- **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 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.
|
||
|
||
---
|
||
|
||
### 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 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.
|
||
|