Files
deepdrft/PLAN.md
T

27 KiB
Raw Blame History

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 concurrencyVaultIndexDirectory no longer drops the lock before its async disk write; the index file can no longer be clobbered by interleaved writers.
  • Repository semanticsTrackRepository.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.

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 entryKeys for a periodic maintenance pass. Lighter than full transactional rollback (which the dual-database split fundamentally cannot give us).
  • Prerequisite: None. Worth landing alongside or just before 2.4.

Phase 5 — Documentation backlog

5.1 Folder-level CLAUDE.md sweep

  • What: Eight folder-level CLAUDE.md files need writing/rewriting per the brief in DOC_PLAN.md. Five are rewrites (drift from the .NET 10 upgrade and structural moves); three are new (DeepDrftWeb.Services, DeepDrftContent.Services — the two libraries where most domain logic now lives — plus the open question on DeepDrftContent.Services/FileDatabase/README.md).
  • Why it matters: The agent guidance files are how every future implementer (human or agent) gets oriented in a directory. They are currently misleading in ways that will cause wrong assumptions on first contact — claiming .NET 9, referencing MediaPath that has been EntryKey for two migrations, describing a FileDatabase/ tree inside DeepDrftContent that has moved out, and missing entirely for the two *.Services libraries.
  • Shape: Doc-keeper executes against DOC_PLAN.md. Order of operations and the per-folder briefs are already specified there.
  • Prerequisite: None. Can run fully in parallel with any feature work.
  • Constraint: Wait on Daniel for the DeepDrftContent.Services/FileDatabase/README.md judgement call before that file changes (retire, keep + refresh, or replace with a CLAUDE.md). The other seven can proceed without that decision.

Phase 7 — Shared UI Components

Reusable presentational components in DeepDrftShared.Client (the RCL consumed by both the public site and the CMS). Distinct from the player stack and CMS surfaces — these are host-agnostic building blocks both apps compose.


Phase 8 — CMS Track Browser

Three browse modes for the CMS /tracks page — Track, Album, Genre — selected by a toggle, each deep-linkable so the public home page can link straight into a mode. One view-model (DI-scoped, matching the TracksViewModel pattern) feeds all three views; the divergence is in rendering, not data paths (per the standing "same data, different uses" preference). This supersedes the deferred §6.2 — Album and Genre modes are the intermediate browse surface that item was waiting on. Full spec: product-notes/phase-8-cms-track-browser.md (normalization gate, component decomposition, VM design, URL scheme, data contracts, open questions).

§8.0 landed on 2026-06-11 — a breaking TrackEntity normalization has been completed and is stable on dev. §8.1–§8.5 are now unblocked. The Waveform Pre-Processing tab is removed, folded into an in-grid status column + per-row/page-level generate actions (see §8.2).


Phase 8.6 — "Music through Every Medium" Section

Replaces the "Genres & Moods" block in DeepDrftPublic.Client/Pages/Home.razor (current lines 4386 — 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 3657) 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 &mdash; 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 &mdash; 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 &mdash; 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 4386. Header grid block (lines 4457) 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 &amp; 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 &mdash; 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 makes cover-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 TODO comment 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-card hover styles already assume cursor affordance 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 106165 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-desc are 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 ::after is copied from .genre-card::after verbatim except for the added z-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.
600959px 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.jpeg
  • medium-live.jpeg
  • medium-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 to 1.05 over 0.5s, clipped by the card's overflow: 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-navy at 0%→35% alpha, top→bottom) sits over the image at opacity: 0.7, deepening to 1.0 on 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-body below 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 to 0.4 rather 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 .jpeg assets render identically. The navy scrim also reads correctly against the off-white card in both modes.
  • Text & backgrounds: --deepdrft-navy text on --deepdrft-white card in both modes. No .deepdrft-theme-dark overrides needed or wanted for this section.

9. Out of scope / deferred

  • Format-filtered routing. Cards are non-navigating today (no /tracks?format= route exists). The TODO comment 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 a Format/ReleaseType discriminator — 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.
  • 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 14 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 15. 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.