27 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 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).
Phase 8.6 — "Music through Every Medium" Section
Replaces the "Genres & Moods" block in DeepDrftPublic.Client/Pages/Home.razor (current lines 43–86 — the <section class="section"> containing the .genre-grid). The 6 text-only genre cards become 3 image-first cards keyed on release format: Studio, Live, DJ Mix. The pivot is taxonomy → medium: instead of "what scene is this," the section answers "in what form does the music reach you."
The section-divider tag stays "The Sound." The .section-divider and .section-header-grid wrappers (Home.razor lines 36–57) are untouched — only the header copy inside the grid and the card grid below it change. Everything from .section-dark onward (line 88+) is untouched.
Design intent. The current section is a flat, typographic palette grid — appropriate when the message was "we span many genres." The new message is fewer, weightier, photographic: three distinct ways the collective produces, each earning a full image pane. This trades the dense 6-up rhythm for a confident 3-up editorial spread, closer in spirit to the dark .features-grid (icon + title + desc) but image-led rather than icon-led. The card is the unit of interest now, not the grid texture.
1. Section header copy
| Slot | Class | Copy |
|---|---|---|
| Label | .section-label |
Format & Medium |
| Title | .section-title |
Music through<br /><em>Every</em><br />Medium |
| Body | .section-body |
The same hands, three different rooms. A studio cut is built; a live set is risked; a DJ mix is woven. We release in every form the music asks for — each one a different relationship between the moment and the record of it. |
The <em>Every</em> carries the italic-green emphasis the existing .section-title em rule already styles — no change needed there. (Title echoes the prior "Every Frequency Explored" cadence deliberately, so the replacement reads as an evolution of the same voice, not a rewrite.)
2. Card copy
| Card | Type label (.medium-type, mono) |
Title (.medium-name, serif) |
One-line descriptor (.medium-desc) |
|---|---|---|---|
| Studio | Produced |
Studio Releases |
Composed, layered, and finished — tracks built to be returned to. |
| Live | Captured |
Live Releases |
Performances caught in the moment, unrepeatable and unedited. |
| DJ Mix | Continuous |
DJ Mix Releases |
Uninterrupted sets — one track bleeding into the next, start to finish. |
The type labels (Produced / Captured / Continuous) play the same one-word-essence role the genre .genre-count labels did ("Foundation," "Architecture," …) — kept deliberately to preserve that tic of the original design.
3. HTML structure sketch
Replaces Home.razor lines 43–86. Header grid block (lines 44–57) keeps its existing structure with only the copy swapped; the grid below is new:
@* Medium section *@
<section class="section">
<div class="section-header-grid">
<MudGrid Style="margin-bottom: 5rem;">
<MudItem xs="12" md="4">
<div class="section-label">Format & Medium</div>
<h2 class="section-title">Music through<br /><em>Every</em><br />Medium</h2>
</MudItem>
<MudItem xs="12" md="8">
<p class="section-body"> ...body copy from §1... </p>
</MudItem>
</MudGrid>
</div>
<div class="medium-grid">
@* TODO Phase 3.x: wire each card to its format-filtered browse route once /tracks?format= exists *@
<div class="medium-card">
<div class="medium-image" style="background-image: url('img/medium-studio.jpeg');">
<div class="medium-scrim"></div>
</div>
<div class="medium-body">
<div class="medium-type">Produced</div>
<div class="medium-name">Studio Releases</div>
<div class="medium-desc">Composed, layered, and finished — tracks built to be returned to.</div>
</div>
</div>
@* …Live card (medium-live.jpeg) and DJ Mix card (medium-djmix.jpeg) follow the same shape… *@
</div>
</section>
Notes for the implementer:
- Image as CSS
background-image, not<img>. This makescover-cropping, the scrim overlay, and the hover scale trivial without a wrapper-overflow dance, and keeps these decorative-but-branded photos out of the document's content image flow. (If alt-text/SEO is later wanted, revisit — but these are mood images, not informational, so background is the right call here.) The card is one block: image pane on top, text body below, matching the brief's "image area + text below." - The three cards are structurally identical — implementer can author one and repeat. Leave the
TODOcomment so the future format-filter routing has a home (mirrors the existing@* TODO Phase 2.2 *@convention in the current genre grid). - Whether the card is a
<div>or an<a>is deferred: there is no format-filtered route yet (the genre grid had the same unresolved/genres/{slug}TODO). Author as<div>now; the.medium-cardhover styles already assumecursoraffordance so promoting to<a>later is a one-line change.
4. CSS additions (Home.razor.css)
Add a new block after the genre-grid rules (lines 106–165 can stay or be removed once the genre markup is gone — recommend removing the now-dead .genre-grid / .genre-card / .genre-name / .genre-count rules in the same change to avoid dead CSS, since nothing else on the page uses them; confirm no other consumer with a grep before deleting). New classes:
/* ── MEDIUM GRID (Music through Every Medium) ── */
.medium-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: var(--deepdrft-border);
border: 1px solid var(--deepdrft-border);
margin-bottom: 4rem;
}
.medium-card {
background: var(--deepdrft-white); /* fixed white ground — matches .section, see §9 */
cursor: pointer;
overflow: hidden; /* clips the hover image scale */
text-decoration: none;
display: block;
}
.medium-image {
position: relative;
width: 100%;
aspect-ratio: 4 / 3; /* consistent crop across all three; ~240px tall at 1-col, scales with column width */
background-size: cover;
background-position: center;
transition: transform 0.5s ease;
}
.medium-card:hover .medium-image { transform: scale(1.05); }
.medium-scrim {
position: absolute;
inset: 0;
background: linear-gradient(to bottom,
rgba(17, 35, 56, 0.0) 40%,
rgba(17, 35, 56, 0.35) 100%); /* navy scrim, weighted to the lower edge near the text seam */
transition: opacity 0.3s;
opacity: 0.7;
}
.medium-card:hover .medium-scrim { opacity: 1; }
.medium-body {
padding: 2rem 1.5rem;
position: relative;
}
/* Green underline sweep — same mechanic as the old .genre-card::after */
.medium-card::after {
content: '';
position: absolute;
bottom: 0; left: 0; right: 0;
height: 2px;
background: var(--deepdrft-green-accent);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s;
z-index: 1;
}
.medium-card:hover::after { transform: scaleX(1); }
.medium-type {
font-family: var(--deepdrft-font-mono);
font-size: 0.58rem;
letter-spacing: 0.2em;
color: var(--deepdrft-muted);
text-transform: uppercase;
margin-bottom: 0.6rem;
}
.medium-name {
font-family: var(--deepdrft-font-display);
font-size: 1.6rem;
font-weight: 400;
color: var(--deepdrft-navy);
margin-bottom: 0.75rem;
line-height: 1.1;
}
.medium-desc {
font-family: var(--deepdrft-font-body);
font-size: 0.82rem;
line-height: 1.65;
color: var(--deepdrft-navy);
opacity: 0.6;
}
Reuse decisions:
.section,.section-divider,.section-header-grid,.section-label,.section-title,.section-body— all reused unchanged..medium-type/.medium-name/.medium-descare new but are deliberate near-clones of.genre-count/.genre-name(bumped from 1.5→1.6rem to suit the larger card) / a new descriptor line the genre cards never had. Kept as distinct classes rather than reusing the.genre-*names so the dead genre CSS can be removed cleanly.- The underline-sweep
::afteris copied from.genre-card::afterverbatim except for the addedz-index: 1(needed because the card now has a stacking context from the image).
5. Responsive breakpoints
| Viewport | .medium-grid columns |
Behaviour |
|---|---|---|
| ≥ 960px | repeat(3, 1fr) |
Three cards in a row — the primary editorial layout. |
| 600–959px | repeat(2, 1fr) + third card spans both columns |
Two on top, the third full-width below. Reads better than a lone 1-col orphan on tablet and keeps the image panes generous. |
| < 600px | 1fr |
Single column, cards stack. Each image pane is full content-width; aspect-ratio: 4/3 keeps them generous (~260px tall at a typical mobile width). |
@media (max-width: 959px) {
.medium-grid { grid-template-columns: repeat(2, 1fr); }
.medium-card:last-child { grid-column: 1 / -1; } /* third card spans full width */
}
@media (max-width: 599px) {
.medium-grid { grid-template-columns: 1fr; }
.medium-card:last-child { grid-column: auto; } /* reset the span at 1-col */
}
Note the breakpoint boundary is 959px here (the existing genre grid used 960px for its max-width query; .section-header-grid uses min-width: 960px). Using max-width: 959px avoids the 1px both-rules-fire overlap at exactly 960px. Implementer may keep 960 for consistency with the surrounding file if preferred — the last-child span makes the 960 edge case harmless either way.
6. Image placeholder names
All three in DeepDrftPublic.Client/wwwroot/img/ (same dir as existing hero images), referenced as img/<name> to match the existing Image1="img/..." convention:
medium-studio.jpegmedium-live.jpegmedium-djmix.jpeg
.jpeg extension matches every existing photo on the page (dd-duo-hero.jpeg, kp-shoulder-bw.jpeg). Recommend source images at least 800px wide (rendered up to ~430px wide at the 3-col desktop layout on a 1440px viewport, so 800px covers 2× displays). Consistent landscape orientation across all three — the 4/3 aspect-ratio crop will center-cover whatever is supplied, but landscape sources avoid heavy cropping.
7. Hover and overlay spec
- Underline sweep (preserved from genre cards): on
:hover, a 2px green-accent bar sweeps in from the left along the card's bottom edge (scaleX(0)→(1), 0.3s). Unchanged mechanic. - Image scale (new, additive): on
:hover, the background image scales to1.05over 0.5s, clipped by the card'soverflow: hidden. Slow and subtle — a breath, not a zoom. This is the "parallax-scale" the brief allowed; pure CSS transform, no JS. - Scrim (always-on, subtle): a navy gradient (
--deepdrft-navyat 0%→35% alpha, top→bottom) sits over the image atopacity: 0.7, deepening to1.0on hover. Two jobs: (a) it weights the image toward its lower edge so the transition into the text body feels intentional rather than abrupt, and (b) it future-proofs for overlaying white text on the image if a later iteration wants the title on the photo. Today all text sits in.medium-bodybelow the image, so the scrim is purely tonal — keep it light; it should never read as a dark box. If during implementation the supplied photos are already dark/low-key, dial the base opacity down to0.4rather than fighting them.
The hover bundle (underline + scale + scrim-deepen) fires together as one gesture. Don't stagger them.
8. Dark-mode awareness
The raw --deepdrft-white and --deepdrft-navy tokens are literal in both themes — they are not remapped under .deepdrft-theme-dark (verified in deepdrft-tokens.css; only the alias tokens like --deepdrft-surface/--theme-* flip). The existing .section and .genre-card both hardcode background: var(--deepdrft-white), so this whole section is a fixed off-white ground in both light and dark mode today — it does not invert.
The new .medium-card follows that same convention deliberately: white card ground, navy text, in both themes. This keeps Phase 8.6 consistent with its untouched siblings (.section above it stays white; only .section-dark below it is dark). Do not introduce theme-aware surface tokens here — that would make this one section invert while the rest of the white .section stays put, which is a larger and out-of-scope design decision (if Daniel wants the public home page to genuinely respond to dark mode, that is a separate roadmap item spanning every .section, not a Phase 8.6 concern).
- Images: unaffected by theme — same
.jpegassets render identically. The navy scrim also reads correctly against the off-white card in both modes. - Text & backgrounds:
--deepdrft-navytext on--deepdrft-whitecard in both modes. No.deepdrft-theme-darkoverrides needed or wanted for this section.
9. Out of scope / deferred
- Format-filtered routing. Cards are non-navigating today (no
/tracks?format=route exists). TheTODOcomment marks where it lands. This mirrors the genre grid's never-resolved/genres/{slug}TODO — don't build the route as part of 8.6. - A real format field on
TrackEntity. "Studio / Live / DJ Mix" is presentational copy here, not yet a data dimension. If these cards are ever to filter real tracks, the entity needs aFormat/ReleaseTypediscriminator — that is Phase 3 (new content kinds) territory, not this cosmetic swap. Flagging so the copy isn't mistaken for an existing capability.
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.