47 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
Split as of 2026-06-15. This item bundled two things: (a) a queue model ("a notion of next track") and (b) preload/prefetch (begin the next track's bytes during the current tail). The queue half (a) is now absorbed into Phase 11 (commitment 7 — Daniel: "now is the natural time for that"; full spec in
product-notes/phase-11-public-site-enhancements.md §3c). The preload half (b) remains deferred here and still gates crossfade (1.4) and gapless (1.5). The open question below — queue inIPlayerServicevs. a separate orchestrator — is answered in the Phase 11 spec (strong steer: a separateIQueueServiceabove the single-slot player; final call staff-engineer's at implementation). When Phase 11's queue lands, the preload below becomes "add a subscriber to the queue's already-known next track," not a fresh queue design.
- What (deferred — preload only): 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. The "next track" it prefetches comes from Phase 11'sIQueueService— that dependency is now satisfied by the queue work, not an open question.
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).
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.
Phase 9 — Release Medium Types
Releases gain a top-level medium discriminator above the existing ReleaseType. Three media: Studio CUTS (Cut — the only medium that uses Single/EP/Album), Live SESSIONS (Session — a single live track with a distinct hero image), DJ MIXES (Mix — a single long track with a preprocessed high-resolution waveform datum). This touches the data model, the API, the CMS, and the public site.
The public home page already carries the three-medium framing as editorial cards (Studio / Live / DJ Mix — COMPLETED.md §8.6, landed 2026-06-12), but those cards have no destinations and nothing below the copy layer knows what a medium is. Phase 9 makes the medium real and gives those cards somewhere to point.
Architectural spine — discriminator enum + optional metadata table. ReleaseMedium is a plain enum column on ReleaseEntity. A medium that needs data beyond the base release (Session's hero image, Mix's waveform datum) gets its own 1:1 metadata table; a medium that needs nothing extra (Cut) is the base ReleaseEntity. This is Open/Closed at the schema level — a future medium (e.g. Video, §3.1) adds an enum value and optionally one metadata table, and changes zero existing tables. The alternatives (one wide nullable table; an EF type hierarchy) both collapse to the god-table the Phase 8 normalization moved away from — rejected. Full design, contracts, and the SOLID rationale: product-notes/phase-9-release-medium-types.md.
Design discipline throughout: extension, not modification. Where a per-medium mapping is unavoidable (card → browser, medium → API projection, medium → detail hero), keep it in one table per concern — never a scattered three-arm switch. Drive CMS cards and nav sub-items off Enum.GetValues<ReleaseMedium>() + a display-metadata lookup, so a new medium surfaces automatically.
The ReleaseType-only-for-Cut invariant. Single/EP/Album is meaningful only when Medium == Cut. Enforce as a domain rule (service layer ignores/resets ReleaseType for non-Cut; CMS hides the field unless Cut; ReleaseDto.ReleaseType is nullable, nulled at the single entity→DTO mapping point for non-Cut so one producer enforces and no consumer needs the rule), not a DB constraint — by choice, not necessity: EF Core supports check constraints first-class (HasCheckConstraint, versioned in migrations, Npgsql-supported), but the invariant is advisory ("meaningless," not "invalid") and the read model enforces it at one point. The column stays on ReleaseEntity as a named exception to the metadata-table pattern: a CutMetadata table was considered and rejected because the /cuts hot path reads ReleaseType on every card and Phase 8 §8.0 just landed the column (see spec §1). Future media must not copy this — the default remains the metadata table.
Sequenced as four waves. Wave 1 is a prerequisite for everything; within Waves 2–4 the lettered tracks are parallel.
Dependency summary: 1 → 2 → 3 → 4. Wave 4 (public site) can begin once Wave 2's api/release family is stable; both Wave 4 build and acceptance are independent of Wave 3 (CMS) — the body-less POST api/release/{id}/mix/waveform trigger (9.2.B) can seed real waveform datum for acceptance testing without any CMS in existence, and hero images seed via a script against 9.2.B likewise.
Waves 1–7 are landed (COMPLETED.md §9). Wave 6 closes two functional gaps a post-landing smoke-test survey surfaced — surfaces the medium taxonomy did not reach, not regressions. Wave 7 hardens the single-track-per-medium rule from a CMS-form convention into a real domain invariant — the one place the medium taxonomy is declared but not enforced below the UI.
9.8 Wave 8 — Remediation (fully landed; all tracks complete)
Daniel tested the landed Phase 9 surface end-to-end and produced a punch-list of corrections before the phase is called complete. These are not new features — they are the gap between what the Wave 1–7 specs built and what hands-on use wants. The theme is the same one Phase 9 has carried throughout: the medium taxonomy reaching every surface it should, and the browse surfaces matching the mental model rather than the implementation's first cut.
Two surfaces dominated: the CMS Release Archive (the card-grid landing is the wrong shape — Daniel wants medium tabs, not navigate-away cards) and the public Archive (the three-card overview is dead weight; the searchable all-releases view is the archive — release-cardinal, decided). The Mix Visualizer redesign (8.K) was pulled out of Phase-9-completion scope and ran as a post-Phase-9 wave from a finished spec (product-notes/phase-9-mix-visualizer-redesign.md); it has now also landed.
Open questions resolved (Daniel, 2026-06-13): 8.H is decided H2 (a new release-cardinal searchable browser at /archive; cascade: /tracks demoted from nav, route kept; mobile ARCHIVE → the browser; three-card overview fully retired); 8.I drops GENRES from the nav only (route kept); 8.F makes the Session hero optional-but-warn-if-missing; 8.E defaults the ALL-tab Add Track to Cut with the medium selector staying user-changeable. A new track 8.L consolidates the release-name/track-name pair into a single name for single-track media (derived track name kept synced, decided), and 8.M (split off 8.L) retires the legacy TrackNew/TrackEdit forms by folding them into the batch forms to reduce code surface.
Full track decomposition, acceptance criteria, and parallel/dependent analysis: product-notes/phase-9-wave-8-remediation.md.
Dependency shape: 8.B is the foundation for the CMS tab work (8.A consumes the shared grid; 8.C/8.E layer on once 8.A lands). 8.L follows 8.G and coordinates with 8.E/8.F (same forms). 8.M (legacy-form retirement) follows 8.L and is architectural (route map + addressing decision). On the public side, 8.H (decided H2 — the new release-cardinal archive) gates 8.I. All Wave 8 tracks are landed — Phase-9-completion gate (8.A–8.J + 8.L), 8.M, and the post-Phase-9 8.K Mix Visualizer redesign. Landed tracks: 8.A, 8.B, 8.C, 8.D, 8.E, 8.F, 8.G, 8.H, 8.I, 8.J, 8.L (2026-06-13); 8.M (2026-06-14); 8.K (2026-06-14).
Phase 10 — Mix Visualizer WebGL2 Renderer
The landed Canvas 2D Mix visualizer (8.K) renders at 1–2 FPS and cannot afford the planned effects — a staff-engineer analysis found the per-frame killers (full-viewport shadowBlur, CSS backdrop-filter, per-frame getBoundingClientRect) structural to the approach, and the planned effects (bulge, lava-lamp detach, a morphing 2D color field, glass) are all per-pixel/per-frame work — exactly what Canvas 2D is worst at and a fragment shader is best at.
Decision (Daniel, explicit): rebuild as a WebGL2 fragment-shader renderer. No Canvas 2D stopgap — "WebGL as step 1, no pussyfooting." This supersedes 8.K §E's Canvas-2D-default recommendation; the "industry-standard, well-commented, no tricks" discipline carries forward as textbook WebGL2 with a commented shader. Target a smooth 60 FPS. Strictly read-only (no playback-control changes); the duration-derived ~333 samples/sec datum (8.K §F) and the existing Blazor↔JS bridge are both preserved — the datum now lands as a GPU texture rather than a CPU-walked array.
Adds a controls row above the mix details / below the back button: four continuous, session-persistent sliders — resolution (relocated 8.K zoom), bubblyness (box→liquid bulge), detach ("unleash the lava lamp" — blobs pinch off and rise), color-shift speed (gradient morph rate). The headline visual is a living 2D navy↔moss gradient field (theme tokens from DeepDrftPalettes) that varies per-bar and shifts along time, never static; plus an in-shader glass treatment (specular/Fresnel/frosted/refraction — no CPU backdrop-filter). Persistence mirrors MixVisualizerZoomState (widen to a MixVisualizerControlState holding all four).
Full design, renderer architecture, the four effects, acceptance criteria, and phasing: product-notes/mix-visualizer-webgl-renderer.md.
Sequenced as four waves. Wave 1 (renderer swap at parity — prove WebGL2 on screen at 60 FPS, bridge intact, no new effects) is the load-bearing prerequisite. Wave 2 (controls row + widened state) and Wave 3 (the four effects in the shader) both follow Wave 1; the four effects within Wave 3 are independently shippable and tunable. Deferred (Daniel): control-range guards and motion-speed coupling to bubblyness — he tunes bad ranges by hand once on screen. Landed: Wave 1 (2026-06-15). Wave 2 (2026-06-15). Wave 3 (2026-06-15).
Wave 4 — detail-page polish + controls rework (presentation only; the final wave). A UI/placement pass over the Mix detail page — no renderer, state, bridge, or mapping change. (1) The four controls move out of the always-visible row into a popover (MudPopover, SharePopover-idiom) opened by a new bespoke lava-lamp icon button anchored top-right of the body, across from the ← Back link (recommend a new TopRightAction slot on ReleaseDetailScaffold, laid as a SpaceBetween row with the back link). (2) The lava-lamp SVG lives in DeepDrftShared.Client/Common/DDIcons.cs in the hand-rolled gas-lamp style (currentColor, 24×24 viewBox, raw-string const) — a recognizable lamp with two-three suspended blobs. (3) The four MudSliders become four RadialKnobs (DeepDrftShared.Client/Components/RadialKnob.razor) in a row in the popover, each carrying its existing Material icon (ZoomIn/BubbleChart/Air/Palette) as an adjacent MudIcon caption — RadialKnob has no icon slot (its Label is SVG text), so icons sit beside each knob. Knobs bind Value/ValueChanged to the unchanged MixVisualizerControlState via the same OnXChanged handlers + NotifyChanged() seam the sliders use today (resolution via MixZoomMapping fraction; other three normalized [0,1]; HoldValue=false for live feel). (4) Widen the Mix body to match the Sessions detail page — MudContainer MaxWidth="Large" (~1280px, up from the scaffold's 760px), Mix-scoped so Track detail is unaffected. Depends on Wave 3 merged (the knobs drive the Wave 3 effects) and supersedes the controls-row design (product-notes/mix-visualizer-webgl-renderer.md §3 → §7). Read-only contract intact; no knob is a seek surface. Full design + acceptance: that spec's §7.
Phase 10 — Reframe (Lava): Waves R1–R4
A major reframe of the Mix visualizer's effects, controls, and color model, folded under this same Phase 10 as a reframe wave-set (Waves R1–R4 — labelled to stay unambiguous against the landed Waves 1–4 above). It builds on the landed Phase 10 renderer infrastructure (pipeline, datum texture, playhead interp, bridge, widened body, lava-lamp trigger) but replaces what it paints. Daniel tested the Phase 10 effects end-to-end and rejected the visual result: the lava read as "giant disconnected circles," the colors drifted to cyan (an HSL saturation-boost artifact), and the waveform and lava read as two unrelated things sharing a canvas. The diagnosis (staff-engineer research pass) is that the rejected look is structural to the effect approach, not a tuning miss.
This supersedes the original Phase 10 (Waves 1–4) effects/controls/color design — product-notes/mix-visualizer-webgl-renderer.md §4 (effects) and §7 (popover-controls) are marked superseded with a pointer to the reframe spec. The renderer infrastructure carries forward unchanged.
The three reframes:
- Lava → CPU-physics wax blobs. Keep the single-pass WebGL2 fragment renderer; add a small CPU-side per-frame physics step modeling ~16–32 Lagrangian "wax blobs" (position/velocity/temperature/radius) uploaded as uniforms and blended with
sminSDF metaballs. The waveform and lava share the same plane WITH real 2D elastic collision (blob↔waveform-boundary + blob↔blob) — the waveform pushes the fluid out of its way (read-only authority preserved; the fluid never deforms the waveform). Refined by Daniel's Wave R2 eval: the fluid must read flat (an evenly-lit unified body, NOT blobs with bright pointed centers / cone-like radial gradients) and melt into one fluid (low viscosity / strong coalescence — "behave more like a fluid," not stiff globs in contact; the slow wax-like motion damping and the freely-coalescing surface are independent). Energy-coupled dynamics: higher heat/energy → smaller bubbles + more turbulence (many small lively turbulent bubbles at high heat; fewer, larger, calmer wax at low heat) — the lamp should feel dynamic and fun. At heat 0 the wax rests on the floor and only collision moves it (collision always on, independent of heat); at heat max many small turbulent bubbles rise/morph per second. Tuning anchor: Daniel's sweet spot is ~20% gravity / ~100% heat — calibrate defaults/ranges so that combination lands lively. Rejected: a full ping-pong FBO Navier-Stokes fluid sim — a lava lamp is high-viscosity/low-turbulence, the opposite regime; large rewrite for unwanted realism. Deliberate later upgrade only. - Color → three-color OKLab gradient with three motions. One source of truth (
DeepDrftPalettes), no hardcoded hexes. Always A→B linear from the center line outward. Three combined motions: (1) anchors A/B rotate among three theme colors X/Y/Z at the rotation-speed control's rate — OKLab interpolation, never through the rainbow (the cyan fix is structural, not a tuning dial); (2) per-bar sinusoidal variation baked at segment entry and fixed as the segment scrolls — realized by keying the sinusoid to mix-time so it travels with the segment by construction (decided 2026-06-16; the explicit ring-buffer alternative is rejected for maintainability); (3) per-bar gradient curve shifts with scroll height (mostly A at bottom → mostly B at top). The static noise/frost texture is removed (Daniel: makes the screen look dirty). - Controls → seven knobs in an inline collapse/expand knob-bar. Replaces the four: (1) waveform scroll speed [replaces resolution/zoom as a standalone control], (2) gradient rotation speed, (3) lava gravity, (4) lava heat, (5) blob density/size, (6) collision strength (soft mush→hard up-and-out throw), (7) waveform width [added in the Wave R2 eval — narrows the waveform band so the lava fluid has more room to move on loud/busy songs; scales the waveform's amplitude→width mapping and its collision boundary]. NOT a popover or drawer — an in-flow controls container BETWEEN the back link (left) and the lava-lamp toggle (right) on the detail top row (redesigned 2026-06-16, superseding the first realization). The first implementation read "inline collapse/expand" as an
position:absolutebar floating under the lamp (.mix-visualizer-controls-anchor) — it clipped to a vertical sliver and read as a detached popover; Daniel rejected it. The redesign:ReleaseDetailScaffoldgains a new optionalTopRowCenterslot so the top row isback | center-controls | lamp(Track/Cut/Session supply neither → back-link-alone, reusability preserved); the sevenRadialKnobs live in that center slot and expand/collapse in the layout flow (CSS width/opacity transition, container grows horizontally between back and lamp — noposition:absolute, no float, no overlay, no clipping, never overlaps the masthead/hero). On narrow widths the container wraps in-flow to a second line / drops to the scaffold'sTopContentband — never edge-clips. The lava-lamp icon button toggles a bound bool — no MudPopover/MudDrawer. Styled to match the NowPlaying hero aesthetic (the session-detail hero overlay — translucent dark glass, overlay-label typography,Color.Secondary). Also: overflow-clip the visualizer to the dynamic footer height (the player bar changes height minimized/expanded) so visuals stop cleanly above it; the clip line is also the lava rest line.
Plus: redraw the lava-lamp glyph. The current DDIcons.LavaLamp is rejected (Daniel: "form is shit, colors are shit"). Redraw to the classic 1970s silhouette — a wide truncated-cone metal base, a bulbous→roundedly-pointed teardrop glass body, a small cone cap ("offset cones") — with navy fluid + moss blobs (the theme's blue+green, faithful to the reference and on-theme) and a neutral/metallic base+cap. Authored in DeepDrftShared.Client/Common/DDIcons.cs as inner SVG markup (no <svg> wrapper; 24×24 viewBox); body silhouette currentColor, the two accent fills are commented literals traced to their DeepDrftPalettes source (SVG cannot resolve var()).
Heat→intensity and collision soft↔hard transfer functions are staff-engineer tuning tasks (endpoints fixed in the spec — heat 0 = wax rests on floor / heat max = many small turbulent rising bubbles; collision soft = gentle mush / hard = high-elasticity up-and-out throw, wide range, smooth/no jitter — formulas not). Full design, the wax-blob model, the collision model, the three-motion color model, the inline knob-bar, the icon redraw, observable acceptance criteria, and phasing: product-notes/phase-10-mix-visualizer-lava-reframe.md.
Open / undecided (spec §10): (a) pause behavior — whether the lava keeps convecting while audio is paused (lamp "always on") vs. the current freeze-on-pause (rAF gated on isPlaying) is an undecided Daniel call; default to freeze-on-pause until decided, switching is a localized gating change, not a model change. (b) Future enhancement (deferred, NOT in R1–R4 scope): a per-control "auto-modulate" checkbox that slowly oscillates each knob's value via a low-frequency oscillator so the visualizer drifts on its own — Daniel flagged it as a cool future idea; the knob-bar layout leaves room but nothing builds it now.
Sequenced as four reframe waves. Wave R1 → Wave R2 → (Wave R3 ‖ Wave R4). Wave R1 (de-noise + dynamic footer clip + icon redraw) is a cheap unblock for a clean substrate. Wave R2 (wax-blob physics + 2D collision) is the load-bearing prerequisite — prove the lava before the color and the UI. Wave R3 (OKLab three-color gradient, the three motions) and Wave R4 (seven controls — including the new waveform-width knob — + NowPlaying-styled inline knob-bar + widened state to seven properties + extended bridge handle) both depend on Wave R2 but are independent of each other. Both prior open Daniel calls are now decided: controls-UI is an inline collapse/expand knob-bar (not popover/drawer); per-segment color is mix-time-keyed.
Phase 10 — Mix detail Hero + MetaContent overlay (presentation only)
A presentation pass over the Mix detail Hero + MetaContent that mirrors the already-shipped Session detail hero-overlay composition: the cover art becomes a background image with all metadata overlaid on top of a max-medium square cover-art region, replacing the stacked masthead + 220px cover + meta-divider block. Consolidates the view, frees more canvas for the lava-lamp visualizer, and brings Mix into the same design family as Sessions. No renderer/state/bridge change; the Phase 10 reframe top row (TopRowCenter controls + lava-lamp TopRightAction) is preserved unchanged — this touches only the region below it.
DRY recommendation (load-bearing, open for Daniel): extract a shared ReleaseHeroOverlay presentational component (lifted from Sessions' current inline hero) that both pages consume — one source of truth for the overlay, divergence (Mix's visualizer backdrop, the square-medium aspect vs. Sessions' wide hero) riding parameters + a CSS class, per the standing "one source, multiple views" preference. Cost: edits the shipped Sessions page (regression surface — mitigated by a behavior-preserving lift + before/after visual check). Fallback: per-page copy in Mix (fast, zero Sessions risk, takes on duplication debt — recommended only if Sessions is considered too load-bearing to touch). Plain-<div> overlay shell, no MudCard/MudPaper layout wrapper; ::deep required on every class landing on a Mud child's native output.
Full design, the three DRY directions with trade-offs, the square/medium sizing, how the Phase 10 layout is preserved, acceptance criteria, and the open questions for Daniel: product-notes/mix-detail-hero-overlay.md.
Phase 11 — Public Site Enhancements
The next pass over the public listening surface, after Phase 9 + Wave 8 moved the site to release-cardinal browse (/archive) and per-medium detail. The spine of the phase: make the release the cardinal unit of the public site, make every navigation an addressable shareable URL, and make the album a first-class playable object (ordered, queue-able, shareable). Nine Daniel commitments (the original four, plus four added 2026-06-15 when he resolved the open questions and expanded scope, plus a ninth added 2026-06-16): (1) a Cuts detail page /cuts/{id}; (2) the player-bar release-title resolves medium → dedicated detail page; (3) retire the whole track-cardinal stack and normalize release-card rendering into shared components; (4) encode Archive filters in the URL; (5) explicit track ordinal editable from the CMS; (6) release-level Share; (7) a play-queue system (absorbs the queue half of §1.3); (8) a release Description field — multiline free-text on the base release (all media), edited from the CMS add/edit forms and rendered as a text block on every detail page; (9) release GUID identifiers — front the release's transparent sequential int PK with an opaque app-minted GUID handle (the track-EntryKey model), swept across every public addressing site. Full design, framing corrections, wave decomposition, gap analysis: product-notes/phase-11-public-site-enhancements.md.
State it inherits (verified 2026-06-15). /sessions/{id} and /mixes/{id} detail pages exist and are mature (both inherit ReleaseDetailBase's prerender bridge; MixDetail composes ReleaseDetailScaffold, SessionDetail deliberately diverges). /archive is already a release-cardinal searchable browser (search + medium + genre). ReleaseGallery is the shared release-card grid — but only Sessions/Mixes use it; Archive and Cuts re-implement equivalent card markup inline. The real gaps: Cuts have no single-release detail page (/cuts cards open /tracks?album={title}), and /archive holds its filters in component fields, not the URL. The queue/playlist does not exist (single-slot player).
Headline correction — commitment 5 is already built. The brief framed the track ordinal as a new column + EF migration + a Daniel-gated apply step. The read shows it already shipped in Phase 8: TrackEntity.TrackNumber (1-based, non-null), migration 20260611005700 already applied, TrackDto mirror, API write path (validated > 0), CMS reorder (BatchEdit assigns ordinal from list position on submit), and the read already .OrderBy(t => t.TrackNumber). No new schema, no migration to gate. Commitment 5 collapses to verify-and-consume: confirm the public read projects/sorts TrackNumber and that CutDetailViewModel orders by it (a one-line fix if not). See spec §3a.
Mirror-image on commitment 8 — the Description column is genuinely new. Where commitment 5 turned out already-built, commitment 8 is the opposite: no Description member exists on ReleaseEntity or ReleaseDto (greps return nothing; the entity carries Title/Artist/Genre/ReleaseDate/ImagePath/ReleaseType/Medium + the two metadata satellites). So commitment 8 is the real cross-stack schema project — just for a different field: a new base-ReleaseEntity.Description column + EF migration (Daniel-gated apply), ReleaseDto mirror, TrackConverter round-trip, write-path plumbing (UpdateTrackMetadataRequest + upload form + the unified services — threaded wherever Genre is, since there is no dedicated release-update endpoint; release-cardinal fields ride the track update/upload path), the CMS AlbumHeaderFields multiline input, and the detail-page text block. It is a base field (uniform across media) so it lives on the base release, not a per-medium satellite (Phase 9 spine). See spec §3d.
Framing corrections (brief vocabulary vs. live routes). (1) There is no /tracks/{id} route — the track-cardinal detail is /track/{EntryKey}. The brief's "/tracks/{id} becomes a router" is best realized as a medium→route resolver at click sites (the player bar already carries release id + medium — no round-trip), plus a thin /tracks/{id} redirect page for deep links. (2) The new /cuts/{id} album page is the phase's center of gravity — the first multi-track release detail. (3) Requirement 4 is a URL-binding pass over the existing ArchiveView, borrowing the TracksView [SupplyParameterFromQuery] pattern — not a new browser.
Design discipline. The medium→route resolver is one table (ReleaseRoutes.DetailHref) consumed by the player bar, Archive, and Cuts cards. The shared ReleaseGallery becomes the one release-card grid across all four browse surfaces (Archive/Cuts fold in via a new per-card HrefResolver), not three inline copies (memory One source, multiple views). The /cuts/{id} page composes ReleaseDetailScaffold via a generalized Header slot + a BodyContent slot for the track list — not a boolean layout flag (Phase 9 §5.3). The queue is a separate IQueueService orchestrating above the single-slot player (strong steer; final call staff-engineer's). Header Play binds to a single handler that swaps single-track → QueueService.PlayRelease with no page change (memory Design for adaptability up front).
Sequenced as eight waves; the critical path is 11.A → 11.B → 11.C → 11.H, with 11.D / 11.E / 11.F / 11.G hanging off the front and 11.H sitting at the tail (it re-types the public addressing surface that 11.B–11.E build on).
- 11.A —
/cuts/{id}album-detail page. Left header (name, artist, genre, year, Play + Share), right cover with theme border, ordered track list (byTrackNumber) with per-row play, header Play. NewCutDetailViewModel; reusesGetById+ thereleaseId-filtered track page (both exist). Ordinal is a verification (§3a), not a dependency. Header/row Play consume 11.F when present, else degrade to single-track (§3.4 seam). Load-bearing prerequisite for 11.B's Cut resolution. - 11.B —
ReleaseRoutesresolver + repoint. PromoteArchiveView.DetailHrefto a sharedReleaseRoutes.DetailHref; Cut resolves to/cuts/{id}(needs 11.A); repoint player-bar title (→ release), Archive cards,AlbumsViewcards; thin/tracks/{id}redirect page. Depends on 11.A. - 11.C — retire + normalize (the heart). With §2 removing every inbound link: delete the whole track-cardinal stack (
TracksView/TrackDetail/TrackCard/TracksGallery/GalleryViewMode+/tracks,/track/{EntryKey}routes) and fold Archive + Cuts inline cards into the sharedReleaseGallery(newHrefResolver); consolidate the medium-label lookup. Depends on 11.B. (Cut track-row is a separate smallTrackRow, notReleaseGallery.) - 11.D — Archive filters in the URL.
/archive?q=&medium=&genre=, history-driven (§5). Touches onlyArchiveView. Free-floating — but coordinate with 11.C (both editArchiveView). - 11.E — release-level Share.
SharePopovergains a release mode that copiesReleaseRoutes.DetailHref(release); wire the Cut header Share to it. Depends on 11.B (resolver) + a release detail to share. - 11.F — queue model.
IQueueServiceabove the single-slot player + one new playerTrackEndedhook + player-bar skip controls. Free-floating, can start cold day one. Gates the Cuts "play album" affordance (11.A header Play). Preload (§1.3 half b) stays OUT — design the seam, defer the feature. - 11.G — release Description schema slice. New
ReleaseEntity.Descriptioncolumn + EF migration (Daniel-gated apply),ReleaseDtomirror,TrackConverterround-trip, write-path plumbing (UpdateTrackMetadataRequest+ upload form + the unified services, threaded whereverGenreis), CMSAlbumHeaderFieldsmultiline input (§3d). Free-floating, can start cold day one — the only gate is Daniel's migration go-ahead. The detail-page render is NOT in this wave: the Cut text block rides 11.A, the Session/Mix block is a small additive touch to those existing pages. Both degrade cleanly (null Description renders nothing), so render & schema can land in either order. - 11.H — release GUID identifiers (terminal public-site wave). Front the release
longPK with an app-minted GUID-stringEntryKeycolumn — the same pattern tracks use (TrackEntity.EntryKeyisrequired string, app-mintedGuid.NewGuid().ToString(), keeping the int PK private). NewReleaseEntity.EntryKey(string, unique index, minted atFindOrCreateRelease) + EF migration that backfills a GUID-stringEntryKeyfor every existing release row at migration time (Daniel-gated apply);ReleaseDto.EntryKey;TrackConverterround-trip; re-type the public addressing surface fromlongto theEntryKeyhandle — detail routes (:long→{EntryKey}), the/tracks/{id}redirect,ReleaseRoutes.DetailHref,SharePopover.ReleaseId, the public read path, and the public release API params (GET api/release/{id}+ thereleaseIdtrack-page query). Internal FKs (track→release, satellite→release), thelongint PK (unused by the app), and the ApiKey-gated CMS endpoints stay on the int. Depends on 11.B (landed), 11.C, 11.D, 11.E — it sweeps the routes/resolver/share/cards those waves create or edit, so it is the last public-site wave (spec §3e.7). Gating decision (Daniel, spec §3e.5(1)) — RESOLVED (additiveEntryKey, track-pattern): additive app-level GUID-stringEntryKeycolumn matching tracks; thelongPK stays DB-only and unused by the app; existing rows are backfilled at migration time (not a dev reset). Daniel's rationale (2026-06-16): "long at the DB level with an app-level guidEntryKeyfor the releases just like tracks; PK is not used by the app; migrate the existing data to provide the entry key at migration time." The true PK retype is declined (framework fork ofCerebellum.BlazorBlocks.Models—BaseEntity.Idhardwiredlong— plus full FK rewrite; recorded as considered-and-declined per file convention). Still open: raw-GUID URL (recommended) vs. slug, and migration ordering after 11.G's snapshot.
Landed: 11.A (2026-06-16); 11.F (2026-06-16); 11.G (2026-06-16); 11.B (2026-06-16); 11.C (2026-06-16); 11.E (2026-06-16); 11.D (2026-06-16); 11.H (2026-06-16). The §3.4 PlayAlbum→IQueueService seam (deferred in 11.A, awaiting 11.F) is now closed: CutDetail.razor consumes the cascaded IQueueService — header Play calls Queue.PlayRelease(ViewModel.Tracks, 0), per-row play calls Queue.PlayRelease(ViewModel.Tracks, index), currently-playing row toggles play/pause, null-safe fallback to SelectTrackStreaming when the queue cascade is absent (2026-06-16). All Phase 11 tracks (11.A–11.H) are now landed; Phase 11 is complete. Two release-table migrations are authored but not yet applied (Daniel-gated, apply in author order): 20260616035252_AddReleaseDescription (11.G) then 20260616210143_AddReleaseEntryKey (11.H).
Dependency shape: 11.A → 11.B → 11.C → 11.H; 11.B → 11.E; 11.D, 11.F, 11.G parallel (11.D coordinates with 11.C on ArchiveView; 11.F's "play album" is consumed by 11.A; 11.G's Description render rides 11.A + a Session/Mix touch, degrading on null). 11.H is terminal — it re-types the public release-addressing surface (routes, ReleaseRoutes, SharePopover, cards, public API params) that 11.B–11.E create/edit, so it follows all of them; its migration is authored after 11.G's so the EF snapshot stays linear. The cold-start items are 11.A, 11.F, and 11.G — kick 11.A + 11.F off first so "play album" works on first ship of the Cut page; 11.G runs alongside on its own track; 11.H waits for the addressing surface to settle.
Resolved by Daniel (2026-06-15), kept visible per file convention: player-bar title → release detail (was OQ1); track ordinal in scope and already built (was OQ4, reversed then found done); retire the whole track-cardinal stack (was OQ5, full cut chosen); release-level Share in scope; play-queue in scope (queue half of §1.3 absorbed; preload half stays deferred); release Description field in scope (commitment 8 — a real new column, lands as schema slice 11.G with the render on 11.A + a Session/Mix touch). Still open (spec §7.2): /cuts/{id} scaffold strategy (generalized Header slot — recommended — vs. bespoke); Cut header affordance idiom (icon vs. labeled buttons); queue architecture (separate IQueueService — strong steer; staff-engineer's final call); whether release-share keeps "Embed player" (recommend copy-link-only); Description render plain-text vs. markdown (recommend plain text + preserved line breaks for v1) and column max-length (recommend 2000–4000); /genres fate (out of scope, flag as adjacent).
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.