# 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 is a hard pre-requisite gate** — a breaking `TrackEntity` normalization that must land before any UI work in §8.1–§8.5. It also dissolves the largest UI open question (the old `AlbumSummaryDto` widening — the new Release table supplies those fields directly). The Waveform Pre-Processing tab is **removed**, folded into an in-grid status column + per-row/page-level generate actions (see §8.2). Review decisions folded in 2026-06-11; §8.0's own open questions (nullable release FK, upload upsert flow, public `TrackDto` read shape) still need Daniel sign-off before that migration is cut.

### 8.0 `TrackEntity` normalization (pre-requisite — must land before §8.1–§8.5)

- **What:** Split the flat `TrackEntity` into two normalized tables. New **`ReleaseEntity`** holds release-cardinal data (`Title`, `Artist`, `Genre?`, `ReleaseDate?`, `ImagePath?`, `ReleaseType`, `CreatedByUserId?`). Slimmed **`TrackEntity`** holds track-cardinal data only (`Id`, `ReleaseId` FK, `Release` nav, `EntryKey`, `TrackName`, `TrackNumber`, `OriginalFileName?`) — the release fields are removed from it. New `ReleaseDto`; `TrackDto` slims and gains `ReleaseId` + a **nested `Release` (`ReleaseDto`)** (resolved 2026-06-11: nested, not a flat read model — flat fields are removed and every consumer is updated, not denormalized back); `AlbumSummaryDto` is retired in favour of `ReleaseDto`.
- **Why:** The flat schema duplicates release-level metadata on every track row — updating an album's cover art or artist means rewriting every track. Phase 8 introduces album-as-a-unit editing (Batch Edit, album-scoped delete), so the model should match the domain: a Release is first-class; Tracks belong to a Release. This collapses several §8 UI open questions (Album-mode parent rows become Release rows directly; no `GROUP BY`-derived summary).
- **Shape:** Sequenced as **five mergeable waves** (notes §0.6): (1) data model — `ReleaseEntity`/config/migration in `DeepDrftData`; (2) DTOs/services/repositories/API — `ReleaseDto`, slimmed `TrackDto`, JOIN-projecting repository, upload find-or-create Release; **Waves 1 + 2 are a single deployment unit** (removing the entity fields breaks compile until the DTO/service layer lands — never merge Wave 1 alone); (3) public-client consumers (`TrackCard`, `TrackDetail`, `TrackMetaLabel`, `NowPlayingCard`) re-point to `track.Release.*`; (4) existing CMS surfaces (`TrackEdit`, `TrackNew`, `BatchUpload`, `TrackList`) minimally updated to compile on the normalized model — Waves 3 + 4 run in parallel; (5) the Phase 8 UI (§8.1–§8.5) begins only after 1–4 are stable. The breaking migration: create `releases`, populate from distinct `(album, artist)` groups, add + populate `release_id` FK, drop redundant track columns. Remaining open questions for Daniel: nullable release FK for album-less tracks (recommend yes), upload auto-create-or-find Release (recommend yes — committed in Wave 2 shape). Full spec, wave breakdown, and per-file consumer list: notes §0 / §0.6.

### 8.1 URL scheme + mode toggle  *(depends on §8.0)*

- **What:** `/tracks` (Track mode, default), `/tracks/albums`, `/tracks/genres` as route segments; a toggle inside the existing "Tracks" tab switches mode and pushes the matching URL. The Waveform Pre-Processing tab is untouched.
- **Why:** The public home page hard-codes these as cross-host deep-links; a route segment reads as a stable address and matches the app's existing segment-based routing (`/tracks/upload`, `/tracks/{id}`). Query-param mode (`?mode=`) was the alternative — rejected as transient-looking view state, optionally tolerated as an alias.
- **Shape:** One `TrackList` component carrying three `@page` directives (or three thin wrappers passing an `InitialMode`); the toggle drives `Mode` + `NavigationManager.NavigateTo`. See notes §3, §9.

### 8.2 `CmsTrackGrid` — the reusable flat track table (DRY core)  *(depends on §8.0)*

- **What:** Extract today's `MudTable<TrackDto>` into a standalone `CmsTrackGrid.razor` taking `AlbumFilter`/`GenreFilter` params. Apply the new column layout: Track # → 40×40 art thumb → Track Name → Artist → Album → Genre → Release Date (`d MMMM, yyyy`) → **Waveform Status** → Actions. Entry Key + File Name move out of the grid into an Info-icon tooltip (monospace). Art thumb reuses the public `TrackCard` fallback pattern, defined locally CMS-side.
- **Why:** Single source of truth for the track-table layout — consumed by both Track mode (no filter) and Genre mode (genre filter), so no duplicated table markup. Decluttering Entry Key / File Name into a tooltip keeps the grid scannable while the data stays reachable. The Waveform column replaces the removed Waveform Pre-Processing tab (status visible inline; per-row Generate when no profile; page-level "Generate All Missing" in the Track-mode header).
- **Shape:** Owns its own `MudTable` + `LoadServerData` + delete-confirm (lifted from `TrackList`). `GetPagedAsync` gains optional `album`/`genre` filter params — the one filter data-contract change (the endpoint already supports the filters); post-§0 the filter joins through `releases`. Waveform status comes from a new `HasWaveformProfile` bool on `TrackDto` (recommended over a second per-page lookup; fold into the §8.0 DTO pass). Display date format is presentation-only; sort key stays the raw `DateOnly`. See notes §8, §9, §11.

### 8.3 Album mode  *(depends on §8.0)*

- **What:** `CmsAlbumBrowser` — parent release rows (art, title, artist, track count, genre, release date, release-type chip, Edit + Delete) that expand to child track rows (track # + name only). Edit → Batch Edit page (§8.5); Delete → album-scoped delete of every track.
- **Why:** A scannable release catalogue is the CMS analogue of the public `AlbumsView`, and the natural place to manage a release as a unit.
- **Shape:** Post-§0, parent rows are `ReleaseEntity`/`ReleaseDto` rows — `GetReleasesAsync` (eager, once) supplies title/artist/genre/date/type directly, no derivation. Child tracks lazy via `GetPagedAsync(album:)` (joins through `releases`) on first expand, cached per row — no new endpoint. Expandable `MudTable` over `MudTreeView` (parent rows are multi-column, not tree-shaped). **The old `AlbumSummaryDto` widening question is dissolved by §8.0 normalization** — the Release table has all the fields, so the parent row is fully populated at rest with no DTO widening and no lazy derivation. See notes §6, §10, §0.5.

### 8.4 Genre mode  *(depends on §8.0)*

- **What:** `CmsGenreBrowser` — a responsive `MudCard` grid (one card per genre: name + track count); clicking a card expands it (accordion, one open at a time) to reveal a `CmsTrackGrid` filtered to that genre.
- **Why:** CMS analogue of the public `GenresView`; the card-to-grid expand is the cheapest second mode because the grid is already built (§8.2).
- **Shape:** `GetGenreSummariesAsync` once; the expanded panel renders `CmsTrackGrid` with `GenreFilter` set and the Add button suppressed — zero duplicated table markup. The embedded grid gets the waveform status column + per-row generate for free. See notes §7, §9.

### 8.5 Batch Edit page  *(depends on §8.0)*

- **What:** New page `/tracks/album/{albumName}/edit`, reached from an Album-mode row's Edit action. `BatchUpload`'s master-detail mechanics with the release's data preloaded; submit swaps per-row `UploadTrackAsync` for `UpdateAsync` on existing tracks (new tracks still upload). Distinct from the existing single-track edit at `/tracks/{id}`.
- **Why:** Editing a release as a unit (rename tracks, reorder, swap cover, add tracks) without round-tripping the single-track editor per track.
- **Shape:** **Confirmed:** a *new* `BatchEdit.razor` sharing extracted sub-components with `BatchUpload` — album-header fields block (post-§0 edits the `ReleaseDto`), batch track list (move-up/down/remove + status chips), track detail pane — over growing `BatchUpload` with an `isEdit` flag (the flag breeds conditional soup across preload/detail/submit). Cover art uses the established upload-once-then-link-via-`UpdateAsync` two-step. **Open:** does remove-in-edit delete an existing track (with confirm) or just detach? See notes §10, §12(8).

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.

---

## 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.

