Add §8.0 TrackEntity → Release/Track normalization as a breaking pre-requisite before Phase 8 UI. Fold in review decisions: Waveform tab removed (in-grid status column + per-row/page-level generate), ViewModel is DI-scoped (TracksViewModel pattern), BatchEdit confirmed as a new page sharing extracted sub-components. Dissolve the AlbumSummaryDto widening question (Release table supplies the fields directly).
21 KiB
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 —
VaultIndexDirectoryno longer drops the lock before its async disk write; the index file can no longer be clobbered by interleaved writers. - Repository semantics —
TrackRepository.Updatenow fails-fast when anIdis not found instead of silently issuing anINSERT. - 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,
IAsyncDisposableon 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
HttpClientrequest kicked off when the current track passes a configurable threshold (e.g. last 10 seconds). Bytes accumulate into a stagedStreamDecoderinstance 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
IPlayerServiceor 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 futurePlaylistServiceorchestrates 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
PlaybackSchedulerinstances suffice — each owns its own gain node, crossfaded viaGainNode.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 firstAudioBufferSourceNode.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
processStreamingChunkaborts 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);awaitresolves immediately and the nextinitialize()can run against a not-yet-closed context.- iOS Safari < 15 had streaming-fetch quirks;
HttpCompletionOption.ResponseHeadersReadbehaviour 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 — detectwebkitAudioContextand pollstate === "closed"with a short timeout instead of trusting theawait. 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
/tracksview (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. TheGET api/track/pageendpoint already acceptsalbum=andgenre=query filters, so the API substrate is ready; the missing piece is the CMS browse UI and the filter plumbing inTrackList. - 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 intoGetPagedAsyncis 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 polymorphicMediaEntitywith discriminator. The choice affects how much code inTrackService/TrackControllercan be reused. - New vault type(s).
MediaVaultType.Mediaexists and is the obvious home for video; sessions are probably stillAudio. - New routes, new UI surfaces, new player considerations (video has its own playback element and does not go through the WAV decoder).
- Likely new entity table(s) sibling to
- 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
DeadLetterLogrecording orphanedentryKeys 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.mdfiles need writing/rewriting per the brief inDOC_PLAN.md. Five are rewrites (drift from the.NET 10upgrade and structural moves); three are new (DeepDrftWeb.Services,DeepDrftContent.Services— the two libraries where most domain logic now lives — plus the open question onDeepDrftContent.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, referencingMediaPaththat has beenEntryKeyfor two migrations, describing aFileDatabase/tree insideDeepDrftContentthat has moved out, and missing entirely for the two*.Serviceslibraries. - 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.mdjudgement 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
TrackEntityinto two normalized tables. NewReleaseEntityholds release-cardinal data (Title,Artist,Genre?,ReleaseDate?,ImagePath?,ReleaseType,CreatedByUserId?). SlimmedTrackEntityholds track-cardinal data only (Id,ReleaseIdFK,Releasenav,EntryKey,TrackName,TrackNumber,OriginalFileName?) — the release fields are removed from it. NewReleaseDto;TrackDtoslims and gainsReleaseId+ a Release access path;AlbumSummaryDtois retired in favour ofReleaseDto. - 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: Breaking migration — create
releases, populate from distinct(album, artist)groups intracks, add + populaterelease_idFK, drop the redundant track columns. Consumer impact reaches the public client (DeepDrftPublic.Clientreads the flatTrackDtofields for the gallery) as well as the CMS — flagged as the highest-risk consumer in the notes. Open questions: nullable release FK for album-less tracks (recommend yes), upload auto-create-or-find Release (recommend yes), flat-vs-nestedTrackDtoread shape for the public API (recommend flat read model). Full spec + migration steps + consumer-impact list: notes §0.
8.1 URL scheme + mode toggle (depends on §8.0)
- What:
/tracks(Track mode, default),/tracks/albums,/tracks/genresas 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
TrackListcomponent carrying three@pagedirectives (or three thin wrappers passing anInitialMode); the toggle drivesMode+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 standaloneCmsTrackGrid.razortakingAlbumFilter/GenreFilterparams. 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 publicTrackCardfallback 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 fromTrackList).GetPagedAsyncgains optionalalbum/genrefilter params — the one filter data-contract change (the endpoint already supports the filters); post-§0 the filter joins throughreleases. Waveform status comes from a newHasWaveformProfilebool onTrackDto(recommended over a second per-page lookup; fold into the §8.0 DTO pass). Display date format is presentation-only; sort key stays the rawDateOnly. 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/ReleaseDtorows —GetReleasesAsync(eager, once) supplies title/artist/genre/date/type directly, no derivation. Child tracks lazy viaGetPagedAsync(album:)(joins throughreleases) on first expand, cached per row — no new endpoint. ExpandableMudTableoverMudTreeView(parent rows are multi-column, not tree-shaped). The oldAlbumSummaryDtowidening 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 responsiveMudCardgrid (one card per genre: name + track count); clicking a card expands it (accordion, one open at a time) to reveal aCmsTrackGridfiltered 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:
GetGenreSummariesAsynconce; the expanded panel rendersCmsTrackGridwithGenreFilterset 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-rowUploadTrackAsyncforUpdateAsyncon 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.razorsharing extracted sub-components withBatchUpload— album-header fields block (post-§0 edits theReleaseDto), batch track list (move-up/down/remove + status chips), track detail pane — over growingBatchUploadwith anisEditflag (the flag breeds conditional soup across preload/detail/submit). Cover art uses the established upload-once-then-link-via-UpdateAsynctwo-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. ITrackServiceinterface. 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.mdrather 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.