# 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.2 Audio format diversity - **What:** Today `AudioProcessor`, `WavOffsetService`, and the JS decoder are PCM/WAV-only. `MimeTypeExtensions` already maps MP3, FLAC, Ogg, AAC, M4A — none are wired. - **Why it matters:** WAV-only is a real ceiling for any non-internal release. Distribution-grade formats (MP3, FLAC at minimum) are table stakes for a music site. - **Shape:** Two seams need a strategy pattern. - Server side: replace `AudioProcessor.ProcessWavFileAsync` with a format-router that selects a per-format processor; replace `WavOffsetService` with a per-format offset strategy (some formats — MP3, Ogg — have natural frame boundaries; FLAC has block headers; AAC has ADTS). - Client side: the JS decoder is currently a WAV byte-walker. For non-WAV, the simplest path is `decodeAudioData` over the full payload (loses streaming-start). The richer path is per-format chunked decoders. Worth a design pass before committing. - **Prerequisite:** None functionally, but consider settling **Phase 4 (HTTP Range)** first — native range/cache is much more important for large MP3s than for WAVs. - **Constraint:** Spectrum FFT tap currently relies on raw `AudioBuffer`s through `decodeAudioData`. If a future path uses `MediaElementAudioSourceNode` (see 4.1), the FFT tap still works but the early-playback story changes. ### 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. --- ## Track Gallery View Toggle ### Overview Give the track gallery two switchable view modes behind a page-level toggle: **Mode A — Album Art Grid** (the current responsive 4-column `MudGrid` of 250×250 cards, augmented so that art-bearing cards hide their info overlay at rest and reveal it on hover) and **Mode B — Track Detail List** (a vertical stack of full-width horizontal rows, each a compact track line with play FAB, art thumbnail, artist/title text block, and right-aligned genre/year). The toggle is a two-option control at the top of `TracksView`, defaulting to Grid, with ephemeral page-level state (not persisted). Both modes consume the same `ViewModel.Page.Items` and the same per-card play-state inputs — the only divergence is in `TrackCard`'s rendering, consistent with the "one source, multiple views" convention (`CONTEXT.md §6`). ### Component changes - **`TracksView.razor` / `.razor.cs` / `.razor.css`** — Add an ephemeral `ViewMode _viewMode = ViewMode.Grid` field and a handler that flips it and calls `StateHasChanged()`. Render the toggle control above `tracks-content` (see Toggle spec). Pass `ViewMode="@_viewMode"` into ``. No change to data flow, persistence, or player-state subscription. CSS: a flex row for the toggle header (`justify-content: flex-end`). - **`TracksGallery.razor` / `.razor.cs` / `.razor.css`** — Add `[Parameter] public ViewMode ViewMode { get; set; } = ViewMode.Grid;`. Branch the template: for `Grid`, keep the existing `MudGrid` / `MudItem` breakpoint layout unchanged; for `List`, render a single flex-column container (`deepdrft-track-list`) that `@foreach`-es the same `Tracks` into `` rows with no `MudGrid` wrapper. Pass `ViewMode="@ViewMode"` down to each `TrackCard`. The `ActiveTrack` / `IsPlaying` / `IsPaused` / `OnPlay` / `OnPause` wiring is identical in both branches. - **`TrackCard.razor` / `.razor.cs` / `.razor.css`** — Add `[Parameter] public ViewMode ViewMode { get; set; } = ViewMode.Grid;`. Branch the markup at the top: `ViewMode.Grid` renders the existing card body unchanged (plus the hover behaviour below); `ViewMode.List` renders the horizontal row layout (see Mode B spec). The `hasLink` / `trackHref` computation, `PlayClick`, and `PlayPauseIcon` are shared across both. The `ViewMode` enum lives in a small shared file (e.g. `Controls/GalleryViewMode.cs` or alongside `TrackCard.razor.cs` in the `DeepDrftPublic.Client.Controls` namespace) so both `TracksView`, `TracksGallery`, and `TrackCard` reference one definition. ### Mode A — hover spec (pure CSS, no JS) - Applies **only** when the card has album art (`deepdrft-track-card-bg` present). The no-art fallback path (`deepdrft-track-card-fallback`) is untouched — its `deepdrft-track-card-content` stays visible at all times exactly as today. - For art-bearing cards: give `deepdrft-track-card-content` an `opacity: 0` rest state and `opacity: 1` on `.deepdrft-track-card-container:hover .deepdrft-track-card-content`. Add `transition: opacity 180ms ease, background-color 180ms ease`. - Swap the rest gradient for a **solid navy panel on hover**: at rest the content overlay is transparent/hidden; on hover its background becomes `var(--deepdrft-navy-mid, #162437)` (opaque, full-card) so the info reads cleanly over the art rather than through a gradient. Implement by toggling the `background` on the content layer between transparent (rest) and solid navy (hover), or by fading in a sibling navy panel beneath the content — implementer's call; the observable result is a solid navy reveal, not the current always-on gradient. - Distinguish art vs. no-art in CSS without new markup by scoping the hide/reveal rules to a container modifier. Add a class to the container when art is present (e.g. `deepdrft-track-card-container--art`) and gate the `opacity: 0` rest rule on it, so fallback cards never pick up the hidden-at-rest behaviour. - Touch devices have no hover; on coarse pointers the overlay should default to visible. Guard the hidden-at-rest rule with `@media (hover: hover) and (pointer: fine)` so touch users always see the info. ### Mode B — list row spec - Container: `deepdrft-track-list` is `display: flex; flex-direction: column; gap: 8px;` inside the existing `MudContainer MaxWidth="Large"`. Rows are full-width. - Row (`deepdrft-track-row`): `display: flex; flex-direction: row; align-items: center; gap: 16px;` with `height: ~72–88px`, `padding: 8px 16px`, and the same glass treatment as grid cards — `background: var(--deepdrft-navy-mid, #162437)`, off-white text, `border: 1px solid rgba(250,250,248,0.12)`. This reads on both light and dark themes (matches the fallback-panel rationale already documented in `TrackCard.razor.css`). - Columns, left to right: 1. **Play FAB** — fixed-width column, vertically centered. Same `` as grid mode (reuse, do not duplicate logic). 2. **Art thumbnail** — square `~64px` (`flex: 0 0 64px`), vertically centered. Reuse the art `background-image` div for art-present; a `deepdrft-track-card-fallback`-style navy square for art-absent. 3. **Text block** — `flex: 1 1 auto; min-width: 0;` two stacked rows: Artist (`Typo.subtitle1`, `deepdrft-track-artist`-weight) on top, Track Name (`Typo.caption`/body, `deepdrft-track-title`) below. Both `text-truncate`. Note the visual order here is Artist-over-Title, inverse of the grid card — intentional per the row sketch. 4. **Right metadata** — fixed/`flex: 0 0 auto` column, `text-align: right`, two stacked rows: Genre chip (`MudChip`, same green-accent outline styling) top-right, Year caption bottom-right. - Linking: wrap the art + text columns in the same `` pattern used by the grid card, so the row navigates to `/track/{EntryKey}` while the FAB (outside the anchor) remains the sole playback entry point. Preserve the `display: contents` approach so the flex row layout is unaffected by the anchor. - The active-state icon (`PlayPauseIcon` driven by `IsPlaying`/`IsPaused`) works identically — no list-specific play-state logic. ### Toggle spec - Component: `MudToggleGroup` with two `MudToggleItem`s (icon-only), or a pair of `MudToggleIconButton`s — `MudToggleGroup` is the cleaner fit for a 2-value exclusive switch. Icons: `Icons.Material.Filled.ViewModule` (Grid) and `Icons.Material.Filled.ViewList` (List). - Placement: top of `TracksView`, above `tracks-content`, aligned right. Sits in its own header row; does not displace the existing centered gallery or the footer pagination. - Binding: `@bind-Value="_viewMode"` (or `SelectedValue` + `SelectedValueChanged`) on the toggle; the setter triggers re-render. State is a plain page field — **not** persisted to cookie or `PersistentComponentState`. - Default: `ViewMode.Grid`. - Skeleton/loading state (`ViewModel.Page == null`) is unaffected — keep the existing skeleton grid; the toggle may render disabled or hidden while loading (implementer's call). ### Acceptance criteria - The TracksView page shows a two-option grid/list toggle, right-aligned at the top, defaulting to grid. - **Grid mode, art card:** at rest the card shows only album art (no title/artist/genre/year/FAB overlay); on hover a solid navy panel fades in over the art revealing all info and the play FAB; moving the pointer away hides it again. Transition is smooth (~180ms), no flicker. - **Grid mode, no-art card:** the navy fallback card shows title/artist/genre/year/FAB at all times, with no hover change — identical to current behaviour. - **Touch / coarse-pointer devices:** grid art cards show their info overlay by default (no permanently hidden info). - **List mode:** tracks render as a vertical stack of full-width rows, each ≤~88px tall, with play FAB at far left, ~64px art thumbnail (or navy placeholder), artist-over-title text block, and right-aligned genre chip over year. - Clicking a row (outside the FAB) navigates to that track's detail page; clicking the FAB plays/pauses without navigating, in both modes. - The play/pause icon and active state reflect the live player exactly as in grid mode, in both modes. - List rows are legible on both light and dark themes. - Toggling between modes is instant, preserves the current page and player state, and resets to grid on page reload (no persistence). ### Out of scope - Persisting the selected view mode (cookie / `PersistentComponentState` / query string) — explicitly ephemeral this ticket. - Mobile-specific gestures (long-press, swipe) beyond the coarse-pointer hover fallback above. - Keyboard navigation beyond what the anchor + `MudFab` give by default; no roving-tabindex or arrow-key list traversal. - Any change to sorting, filtering, pagination, or the `TracksViewModel` data path. - Album/genre grouping views (covered separately under Phase 2.2). - Animation of mode transitions (cards/rows reflowing) — a plain re-render is acceptable. --- ## Phase 2 — Product surface: gallery, browsing, ingestion These follow from `CONTEXT.md §5`. Direction is strongly implied but no specific UI has been committed. ### 2.2 Album and genre views - **What:** `TrackCard` already renders album/genre/release date; the data is there. Missing are gallery groupings (album view, genre view), filters, and the API-side support for filter expressions in `TrackService.GetPaged`. - **Why it matters:** The track gallery is the only working content surface. Multiple views over the same library is how it earns the "gallery" name. - **Shape:** Per `CONTEXT.md §6`, the convention is one source of truth, multiple views over it. New views should consume the same `TracksViewModel` / `PagedResult` and differ only at the rendering layer. - `TrackService.GetPaged` extended to accept a filter expression (or a simple structured filter DTO). - `PagingParameters` extended with a `Where: Expression>?` or a parallel `FilterParameters` — pick one to avoid drift. - New routes (`/albums`, `/genres`) consume the same VM with different grouping / filter inputs. - **Prerequisite:** **2.1** for any view that prominently features cover art (album view especially is impoverished without it). ### 2.3 Search and filter on the gallery - **What:** `TracksViewModel` exposes sort but no filter. `TrackService.GetPaged` accepts only sort. Simple text search across `TrackName` / `Artist` / `Album` is the obvious first cut. - **Why it matters:** Once the library has more than ~30 entries, sort-only browsing is friction. - **Shape:** Same extension to `GetPaged` as 2.2. UI is a debounced text input bound to the VM's filter property. EF Core translates `Contains` to SQLite `LIKE`. - **Prerequisite:** Fold into 2.2 if both are being done — the same `GetPaged` extension serves both. Doing them separately doubles the API churn. --- ## 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.1 HTTP Range + CDN caching - **What:** Today's `?offset=` query parameter defeats HTTP caching — a CDN sees `?offset=1234567` as a distinct URL from the un-offset request. The architecture re-invents byte-range on top of a custom query param. - **Why it matters:** Material once the site has real listener traffic. Also relevant to non-WAV formats (1.2) where decoder-side seek is cheaper natively. - **Shape:** Two intertwined moves. - Server: `LoadResourceStreamAsync` returning an open `FileStream` instead of `LoadResourceAsync` materialising the whole buffer. `File(stream, mime, enableRangeProcessing: true)`. The `WavOffsetService` synthesised-header path becomes a special-case rather than the default. - Client: consider `MediaElementAudioSourceNode` instead of (or alongside) `decodeAudioData`-fed `AudioBufferSourceNode`s. Native seek, native range, native cache; FFT tap on the audio graph still works for the spectrum visualiser. - **Prerequisite:** None functionally, but the audit explicitly flagged this trade-off as architecture-intentional — the current path was chosen because spectrum analysis wants `AudioBuffer`s. Re-deciding the trade-off is itself part of the work. - **Constraint:** A move to `MediaElementAudioSourceNode` changes the early-playback story (the element handles buffering, not us). Worth a design pass. ### 4.2 Server-side stream from disk (no buffer materialisation) - **What:** `LoadResourceAsync` reads the entire file into memory before `File(file.Buffer, mimeType)` returns it. A 100 MB WAV is a 100 MB LOH allocation per request. - **Why it matters:** Scaling ceiling. Currently fine for a small audience and small library; not fine if either grows. - **Shape:** Folds into 4.1 — the same `LoadResourceStreamAsync` overload solves both. Listed separately because either could land without the other (you could stream from disk while still using the `?offset=` query path, or you could move to `Range` headers while still buffering). ### 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. --- ## Cross-cutting / not yet themed 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.