Files
deepdrft/PLAN.md
T
2026-06-07 07:27:43 -04:00

32 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.


In-flight — Embeddable iframe player

A standalone, chrome-free player surface intended for embedding in an <iframe> on external pages (e.g. a Bandcamp-style "play this track here" widget on a third-party blog or the collective's socials). Distinct from the dock player, which lives inside the full site chrome.

Shape as it stands in the working tree ([in-progress], code is partial and does not yet compile):

  • Layout/EmbedLayout.razor — a minimal layout: MudThemeProvider + AudioPlayerProvider wrapping @Body, with no nav, menu, or marketing chrome. Reuses the dark-mode PersistentComponentState round-trip (CONTEXT.md §3.6) so an embedded player still honours the theme.
  • Pages/FramePlayer.razor — routed at /FramePlayer, uses EmbedLayout, renders a single <AudioPlayerBar Fixed />. Reads a TrackEntryKey from the query string and is meant to auto-select that track on load.
  • Services/ITrackDataService.cs + TrackClientDataService.cs — a new track-metadata fetch seam (GetPage + a new GetTrack(trackId)) so a component can resolve a single track by key without the gallery VM. Intended to be render-mode-agnostic (one seam, SSR and WASM both served by it).

Why it matters: An embeddable player turns every external mention of a DeepDrft track into a play surface. It is the lightest-weight distribution lever the product has — no app install, no account, just a link that plays. Fits the collective's "get the music in front of people" posture.

Open questions (unresolved — surface to Daniel before this lands):

  • Is this a committed direction or an experiment? The code is a partial spike. Confirm before scoping the rest.
  • Track addressing. FramePlayer keys off TrackEntryKey (the FileDatabase entry key), but ITrackDataService.GetTrack(string trackId) is ambiguous about whether the argument is the SQL Id or the EntryKey. The two ID spaces (CONTEXT.md §3.4) need to be reconciled before the lookup is correct.
  • AudioPlayerBar reuse vs. a dedicated embed control. The FramePlayer TODO comment proposes "an iframe-compatible player using the AudioPlayerControl." Decide whether the embed reuses the full dock bar (current approach) or a stripped single-track control.
  • Embedding security. A public iframe surface implies decisions about X-Frame-Options / CSP frame-ancestors, and whether the unauthenticated GET api/track/{id} stream is acceptable to expose from arbitrary origins (it already is unauthenticated, but embedding makes the exposure explicit).

[in-progress] — capture the design decisions in a product note when Daniel confirms the direction.


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.1 Backward seek

  • What: Seeking to a position below playbackOffset currently clamps silently to the start of the in-memory buffer segment instead of going to the user's chosen time. The forward "seek beyond buffer" path already exists in WavOffsetService + the client's offset-request path; backward seek is the missing mirror.
  • Why it matters: The single highest-impact missing feature in the player. Scrub-bar drags backward feel broken — they appear to seek but land in the wrong place.
  • Shape: Reuse the existing GET api/track/{id}?offset= pathway. The client decision becomes "is the target inside the decoded window?" — if yes, jump within the buffer (existing behaviour); if no (forward or backward), tear down the decoder and re-request from the byte-aligned offset.
  • Prerequisite: None — the substrate exists.
  • Constraint: Backward seek must observe the same blockAlign rounding-down as forward seek (already enforced in WavOffsetService.alignedOffset and StreamDecoder.calculateByteOffset). The teardown/reinit must respect the generation-counter pattern introduced by the concurrent-seek fix.

1.2 Audio format diversity

  • What: Today AudioProcessor, WavOffsetService, and the JS decoder are PCM/WAV-only. MimeTypeExtensions already maps MP3, FLAC, Ogg, AAC, M4A — none are wired.
  • Why it matters: WAV-only is a real ceiling for any non-internal release. Distribution-grade formats (MP3, FLAC at minimum) are table stakes for a music site.
  • Shape: Two seams need a strategy pattern.
    • Server side: replace AudioProcessor.ProcessWavFileAsync with a format-router that selects a per-format processor; replace WavOffsetService with a per-format offset strategy (some formats — MP3, Ogg — have natural frame boundaries; FLAC has block headers; AAC has ADTS).
    • Client side: the JS decoder is currently a WAV byte-walker. For non-WAV, the simplest path is decodeAudioData over the full payload (loses streaming-start). The richer path is per-format chunked decoders. Worth a design pass before committing.
  • Prerequisite: None functionally, but consider settling Phase 4 (HTTP Range) first — native range/cache is much more important for large MP3s than for WAVs.
  • Constraint: Spectrum FFT tap currently relies on raw AudioBuffers through decodeAudioData. If a future path uses MediaElementAudioSourceNode (see 4.1), the FFT tap still works but the early-playback story changes.

1.3 Preload / prefetch of the next track

  • What: No mechanism to begin the next track's stream during the tail of the current. Each play is a cold fetch.
  • Why it matters: Prerequisite for both crossfade (1.4) and gapless (1.5). Also a perceived-latency win on its own — track-change feels instant when the bytes are already in flight.
  • Shape: A second HttpClient request kicked off when the current track passes a configurable threshold (e.g. last 10 seconds). Bytes accumulate into a staged StreamDecoder instance rather than the live one. Promotion to "current" happens at end-of-stream or on user-selected next.
  • Prerequisite: Requires a notion of "next track" — today the player only knows the current one. That implies either a playlist/queue model in IPlayerService or a passive "what was the next row in the gallery" inference.
  • Open question: Does a queue model belong in IPlayerService, or is the player a single-slot device that a future PlaylistService orchestrates above? Worth a design note before implementation. Capture in product notes when picked up.

1.4 Crossfade

  • What: Smooth A→B transition with overlapping fade-out / fade-in.
  • Why it matters: DJ/mix aesthetic that fits the DeepDrft collective's electronic-music context. Distinguishing UX from generic "next track."
  • Shape: Architecturally two simultaneous PlaybackScheduler instances suffice — each owns its own gain node, crossfaded via GainNode.gain.linearRampToValueAtTime. The wiring is the work, not the audio graph itself.
  • Prerequisite: 1.3 (Preload) — there is nothing to fade into without prefetch.

1.5 Gapless playback

  • What: Eliminate the inter-track silence that exists today.
  • Why it matters: Important for live-set rips, mix tapes, anything authored to flow continuously.
  • Shape: The decoder must be able to start the next track's first buffer scheduled exactly at the end of the current one's last buffer (sample-accurate, not wall-clock). With PlaybackScheduler's existing 500 ms lookahead this is mechanically achievable — the next track's first AudioBufferSourceNode.start(t) is set to the previous track's end time.
  • Prerequisite: 1.3 (Preload). Also needs to play nicely with 1.2 because gapless across formats is hard (encoder padding/priming on MP3 in particular).
  • Constraint: Truly sample-accurate gapless requires knowing the priming/padding sample counts of the source format. Out of scope for WAV-only; revisit when format diversity lands.

1.6 Track-skip on error

  • What: A failed processStreamingChunk aborts the entire load with no recovery path.
  • Why it matters: One corrupt frame at byte 4M of a 100 MB stream currently means the listener loses the entire track. Should at minimum surface a clear error and (optionally) skip past the bad region.
  • Shape: Two-level response.
    • Cheap: catch in the streaming loop, surface a user-visible error, advance the gallery to the next track if a queue exists.
    • Richer: byte-scan forward to the next valid frame header for the format and resume. Format-dependent — only worth doing once 1.2 lands.

1.7 Safari compatibility

  • What: Two known Safari edge cases.
    • webkitAudioContext.close() is async-but-not-Promise on older Safari (≤ ~14); await resolves immediately and the next initialize() can run against a not-yet-closed context.
    • iOS Safari < 15 had streaming-fetch quirks; HttpCompletionOption.ResponseHeadersRead behaviour is not guaranteed there.
  • Why it matters: Real listener share. iOS in particular is a primary listening surface for music.
  • Shape: For the close() race — detect webkitAudioContext and poll state === "closed" with a short timeout instead of trusting the await. For the fetch quirks — first decide the minimum supported iOS version; if pre-15 is in scope, fall back to a non-streaming fetch path and accept the latency.
  • Open question: What's the floor? Decide before designing the fallback. iOS 15+ as the floor would let us drop the second concern entirely.

These follow from CONTEXT.md §5. Direction is strongly implied but no specific UI has been committed.

2.1 Cover art / image vault wired through

  • What: MediaVaultType.Image is implemented end-to-end and exercised by tests, but the production surface only registers a tracks vault of type Audio. ImagePath on TrackEntity is a free-form URL string today; it should resolve to an entry in an image vault served by DeepDrftContent.
  • Why it matters: Prerequisite for any album/release/genre view that wants to look like a music site rather than a list of rows. Also closes a free-form-string surface area that will otherwise calcify.
  • Shape:
    • Register a second vault (images or art, type Image) in Startup.ConfigureDomainServices and in the CLI.
    • Add GET api/image/{entryKey} (unauthenticated, mirrors track read) and PUT api/image/{entryKey} (ApiKey, mirrors track write) on DeepDrftContent.
    • Change TrackEntity.ImagePath semantics from "URL" to "image vault entry key" (column rename optional — could remain image_path with semantic shift, or could become image_entry_key for clarity).
    • Add an image processor sibling of AudioProcessor.
  • Prerequisite: None.
  • Constraint: This is a small schema-semantics migration. Existing rows have null ImagePath in production so there is no data to migrate, but commit before the field has real content to avoid a backfill.

2.2 Album and genre views

  • What: TrackCard already renders album/genre/release date; the data is there. Missing are gallery groupings (album view, genre view), filters, and the API-side support for filter expressions in TrackService.GetPaged.
  • Why it matters: The track gallery is the only working content surface. Multiple views over the same library is how it earns the "gallery" name.
  • Shape: Per CONTEXT.md §6, the convention is one source of truth, multiple views over it. New views should consume the same TracksViewModel / PagedResult<TrackEntity> and differ only at the rendering layer.
    • TrackService.GetPaged extended to accept a filter expression (or a simple structured filter DTO).
    • PagingParameters<T> extended with a Where: Expression<Func<T, bool>>? or a parallel FilterParameters<T> — pick one to avoid drift.
    • New routes (/albums, /genres) consume the same VM with different grouping / filter inputs.
  • Prerequisite: 2.1 for any view that prominently features cover art (album view especially is impoverished without it).
  • What: TracksViewModel exposes sort but no filter. TrackService.GetPaged accepts only sort. Simple text search across TrackName / Artist / Album is the obvious first cut.
  • Why it matters: Once the library has more than ~30 entries, sort-only browsing is friction.
  • Shape: Same extension to GetPaged as 2.2. UI is a debounced text input bound to the VM's filter property. EF Core translates Contains to SQLite LIKE.
  • Prerequisite: Fold into 2.2 if both are being done — the same GetPaged extension serves both. Doing them separately doubles the API churn.

Phase 3 — New content kinds

3.1 Live / session content

  • What: The home page advertises "Live Sessions" and "Video Content (coming soon)". No data model exists for these.
  • Why it matters: Honour the home page copy. Also differentiates the site from a generic track gallery — live sessions and video are the collective's authored output.
  • Shape: Speculative; no commitment yet.
    • Likely new entity table(s) sibling to TrackEntity (SessionEntity, VideoEntity?) — or a polymorphic MediaEntity with discriminator. The choice affects how much code in TrackService / TrackController can be reused.
    • New vault type(s). MediaVaultType.Media exists and is the obvious home for video; sessions are probably still Audio.
    • New routes, new UI surfaces, new player considerations (video has its own playback element and does not go through the WAV decoder).
  • Prerequisite: Probably 2.1 (vault wiring proof) and a decision on the entity model before any code lands.
  • [speculative] — direction inferred from home-page copy, not a Daniel-confirmed commitment.

Phase 4 — Infrastructure / delivery

4.1 HTTP Range + CDN caching

  • What: Today's ?offset= query parameter defeats HTTP caching — a CDN sees ?offset=1234567 as a distinct URL from the un-offset request. The architecture re-invents byte-range on top of a custom query param.
  • Why it matters: Material once the site has real listener traffic. Also relevant to non-WAV formats (1.2) where decoder-side seek is cheaper natively.
  • Shape: Two intertwined moves.
    • Server: LoadResourceStreamAsync returning an open FileStream instead of LoadResourceAsync materialising the whole buffer. File(stream, mime, enableRangeProcessing: true). The WavOffsetService synthesised-header path becomes a special-case rather than the default.
    • Client: consider MediaElementAudioSourceNode instead of (or alongside) decodeAudioData-fed AudioBufferSourceNodes. Native seek, native range, native cache; FFT tap on the audio graph still works for the spectrum visualiser.
  • Prerequisite: None functionally, but the audit explicitly flagged this trade-off as architecture-intentional — the current path was chosen because spectrum analysis wants AudioBuffers. Re-deciding the trade-off is itself part of the work.
  • Constraint: A move to MediaElementAudioSourceNode changes the early-playback story (the element handles buffering, not us). Worth a design pass.

4.2 Server-side stream from disk (no buffer materialisation)

  • What: LoadResourceAsync<AudioBinary> reads the entire file into memory before File(file.Buffer, mimeType) returns it. A 100 MB WAV is a 100 MB LOH allocation per request.
  • Why it matters: Scaling ceiling. Currently fine for a small audience and small library; not fine if either grows.
  • Shape: Folds into 4.1 — the same LoadResourceStreamAsync overload solves both. Listed separately because either could land without the other (you could stream from disk while still using the ?offset= query path, or you could move to Range headers while still buffering).

4.3 Dual-write rollback / dead-letter log

  • What: If content-side write succeeds and SQL-side write fails, audio is orphaned in the vault. No compensating mechanism exists.
  • Why it matters: A latent data-integrity issue. Materially riskier once web upload (2.4) exists.
  • Shape: Audit suggested a DeadLetterLog recording orphaned 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 6 — Responsive home page (mobile layout)

The home page (DeepDrftPublic.Client/Pages/Home.razor + Home.razor.css) is built entirely on hand-rolled CSS grids with no responsive breakpoints. Every horizontal split is a fixed column count that holds on desktop and collapses on mobile — six genre cards in one row, four feature cards in one row, two 50/50 splits, and a space-between CTA banner all overflow or squash below ~960px. This phase migrates the layout to be mobile-first while preserving the wireframe-faithful visual styling.

Guiding principle for the whole phase: separate layout from style. The scoped CSS in Home.razor.css does two jobs — it positions columns (the part that breaks on mobile) and it paints the design (colors, fonts, padding, hover states, pseudo-element flourishes). Only the column-positioning job migrates. Colors, typography, padding, ::before/::after decorations, and hover transitions stay in scoped CSS untouched.

Two tools, used deliberately:

  • MudGrid + MudItem (with xs/sm/md breakpoints) for splits where MudBlazor's margin-based gutters are acceptable: hero, section-header, section-split, CTA banner. This is the house pattern already used in DeepDrftShared.Client/Components/TracksGallery.razor (<MudItem xs="12" sm="6" md="4" lg="3">). Match it. Breakpoints: xs=0, sm=600, md=960, lg=1280, xl=1920. MudGrid breakpoint attributes are CSS-only at runtime — do not inject IBreakpointService or any breakpoint-observer service into the component.
  • CSS @media query on the existing scoped grid for the two card blocks (genre grid, features grid). These two are explicitly not MudGrid candidates — see 6.1 for why. Adding a media query that overrides grid-template-columns is the minimal, correct move there.

The one trap to avoid (read before touching the card grids): the genre grid and features grid use gap: 1px (genre) / shared border-right (features) to render the cards as a single block divided by hairline rules — the cards touch, and the 1px gap is the divider line. MudGrid's Spacing parameter produces margin-based gutters (multiples of 4px, with outer margin), which cannot reproduce a shared hairline edge. Porting these two grids to MudGrid would silently destroy the hairline-divider aesthetic. Keep them as CSS grid; only add breakpoints.

6.1 Genre grid + features grid — CSS media queries only

  • What: .genre-grid (repeat(6, 1fr)) and .features-grid (repeat(4, 1fr)) get responsive column counts via @media overrides in Home.razor.css. No markup change to the grid containers themselves.
  • Why MudGrid is wrong here: Both grids render cards as a contiguous block separated by 1px hairline rules (.genre-grid via gap: 1px over a border-colored background; .features-grid via per-card border-right). MudGrid's Spacing gutters are margins, not shared edges — switching would break the visual. Pure CSS keeps the hairline intact while still going responsive.
  • Stacking behavior:
    • Genre grid: md+ repeat(6, 1fr) (current); sm repeat(3, 1fr); xs repeat(2, 1fr). (Six genres divide cleanly into 3 and 2 — no orphan row.)
    • Features grid: md+ repeat(4, 1fr) (current); sm repeat(2, 1fr); xs 1fr (single column stack).
  • Scoped CSS that must change: Add two @media (max-width: 960px) and @media (max-width: 600px) blocks overriding grid-template-columns on .genre-grid and .features-grid. For .features-grid at the stacked/2-col breakpoints, the per-card border-right produces a dangling right border on the last card in each visual row — switch the hairline strategy at those breakpoints (e.g. apply border-bottom on cards and drop border-right, or move to gap: 1px like the genre grid). Specify the exact rule when implementing; the constraint is "no dangling/missing hairlines at any breakpoint."
  • Order of independence: Fully independent. Touches only Home.razor.css, no markup. Can be the first slice landed and verified in isolation.

6.2 Hero — MudGrid for content, CSS for the background color split

  • What: .hero is grid-template-columns: 1fr 1fr at min-height: 100vh, with .hero-left painted white and .hero-right painted navy — a full-viewport color split. Migrate the content columns to MudGrid; keep the background color split in CSS.
  • Why split the treatment: MudGrid rows/items do not carry per-column background colors that bleed to the full viewport height. The white/navy vertical split is a visual property of the section, not of the content columns. Wrap DeepDrftHero and NowPlaying in <MudItem xs="12" md="6"> inside a <MudGrid>, but keep the white/navy backgrounds on the section via CSS.
  • Stacking behavior:
    • md+: 50/50 split — hero copy left (white), NowPlaying right (navy). Current desktop look preserved.
    • xs/sm: stack to single column — DeepDrftHero on top, NowPlaying below. The 100vh constraint should relax to min-height: auto (or a smaller min) when stacked, so the two stacked panels don't each demand a full viewport.
  • Scoped CSS that must change:
    • .hero keeps min-height: 100vh at md+; add @media (max-width: 960px) relaxing it (e.g. min-height: auto) and switching the background from a left/right split to a top/bottom split (or letting each MudItem carry its own background at the stacked breakpoint).
    • The white/navy split: at md+ this can stay a CSS background on .hero (e.g. a linear-gradient(to right, white 50%, navy 50%) on the section, or backgrounds on the two MudItems via scoped classes). At xs/sm the split becomes top/bottom. Implementer picks gradient-on-section vs. background-per-item; the gradient-on-section approach survives the MudGrid gutter cleanly (gutters show the section background, not white margins).
    • Remove .hero's own display: grid; grid-template-columns: 1fr 1fr (MudGrid now owns column layout). Keep overflow: hidden.
  • Order of independence: Independent of all other sections. Has the most CSS nuance (the color split) — schedule it where there's time to verify the split holds at every breakpoint, including the MudGrid gutter not showing a white seam.
  • Constraint: DeepDrftHero and NowPlaying are child components with their own scoped CSS — do not refactor them in this pass. Layout is Home.razor's responsibility only.

6.3 Section header — MudGrid

  • What: .section-header is grid-template-columns: 1fr 2fr (label+title left, body paragraph right) with align-items: end. Migrate to MudGrid.
  • Stacking behavior: md+ keep the 1fr/2fr asymmetry via <MudItem md="4"> (title) + <MudItem md="8"> (body). xs/sm stack to xs="12" each — title block on top, body paragraph below.
  • Scoped CSS that must change: Remove display: grid; grid-template-columns: 1fr 2fr; gap: 4rem from .section-header. The align-items: end baseline-alignment is a desktop nicety that's meaningless when stacked — preserve it at md+ only (MudGrid Align.End on the row, or a scoped rule). .section-body's align-self: end similarly only applies in the side-by-side layout; harmless when stacked but can be dropped from the stacked breakpoint.
  • Order of independence: Independent. Small, low-risk — good warm-up slice.

6.4 Section split (origin + connect) — MudGrid

  • What: .section-split is grid-template-columns: 1fr 1fr at min-height: 60vh — green "Origin" panel left, white "Connect" panel right, each a full-bleed colored column. Same shape as the hero (colored columns) but lower stakes (60vh, not full-viewport, and the colors are per-panel not a single split).
  • Stacking behavior: md+ 50/50. xs/sm stack — Origin (green) on top, Connect (white) below.
  • Scoped CSS that must change: Replace the grid container with <MudGrid> + two <MudItem xs="12" md="6">. Here the per-panel backgrounds (.split-left green, .split-right white) live on the panels themselves, so — unlike the hero — the color survives a MudGrid gutter only if the gutter is removed or the panels fill their items edge-to-edge. Set MudGrid Spacing="0" so the green and white panels meet with no white seam between them, preserving the current flush-color-block look. The .split-left::before decorative circle stays untouched. Relax min-height: 60vh to auto at the stacked breakpoint so each panel sizes to its content.
  • Order of independence: Independent. The Spacing="0" decision here is the same family of problem as the hero seam — landing 6.2 first will surface the seam-handling approach to reuse here.

6.5 CTA banner — MudGrid or flex-wrap

  • What: .cta-banner is display: flex; justify-content: space-between — headline left, two action buttons right. .cta-actions is an inline flex row of two buttons.
  • Stacking behavior: md+ keep headline-left / actions-right. xs/sm stack — headline on top, actions below. At xs the two buttons should go full-width-stacked (or wrap) rather than sitting cramped side by side.
  • Approach — recommend the lighter touch: This one does not need MudGrid. The container is already flex; adding flex-wrap: wrap + a media query that flips flex-direction: column and align-items: stretch at max-width: 600px achieves the stack with the least churn. MudGrid is also fine (<MudItem xs="12" md="6"> × 2) if consistency with the other sections is preferred — but flex-column is fewer moving parts for a two-element banner. Pick flex unless the implementer wants every section uniformly on MudGrid.
  • Scoped CSS that must change:
    • .cta-banner: add @media (max-width: 600px)flex-direction: column; align-items: flex-start; gap: 2rem.
    • .cta-actions: add flex-wrap: wrap always; at xs, width: 100% with the two buttons (.btn-white, .btn-outline-white) going flex: 1 or full-width so they don't crowd.
    • The giant .cta-banner::before "DRFT" watermark (22rem) will overflow badly on mobile — add a media-query rule shrinking its font-size at xs (e.g. clamp or a fixed smaller size) or hiding it, so it doesn't force horizontal scroll. This is a hidden overflow source independent of the flex layout — do not skip it.
  • Order of independence: Independent. The watermark-overflow fix is the non-obvious part; the flex stack itself is trivial.

Phase 6 sequencing summary

All six slices are independent and touch only Home.razor + Home.razor.css (no child components, no shared CSS, no other pages). They can land in any order or in parallel. Recommended order by ascending risk: 6.3 (section header) → 6.1 (card grids) → 6.5 (CTA banner) → 6.4 (section split) → 6.2 (hero) — warm up on the trivial MudGrid swap, get the no-MudGrid card grids done, then tackle the two color-split sections (6.4, 6.2) last since they share the gutter-seam problem and the second reuses the first's solution.

  • Why it matters: The public site is the front door for a music collective whose listeners are disproportionately on phones (social-shared links, live-session discovery). A home page that overflows horizontally on mobile undercuts the entire "get the music in front of people" posture (PLAN.md in-flight iframe item makes the same bet). This is table-stakes polish, not a feature.
  • Prerequisite: None. Pure presentation work on one page.
  • Constraint: Do not refactor DeepDrftHero or NowPlaying (6.2 constraint). Do not touch DeepDrftPublic/wwwroot/styles/deepdrft-styles.css (shared CSS) — all changes are scoped to Home.razor.css. Preserve every color/font/decoration; this phase changes where columns break, nothing about how the page looks at desktop width.

Phase 5 — Documentation backlog

5.1 Folder-level CLAUDE.md sweep

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

Cross-cutting / not yet themed

A small set of items that are real but don't fit a phase yet. Surface them when they become relevant rather than committing now.

  • Identity / accounts. Currently no user concept. Needed before web upload (2.4); also a precondition for favourites, listening history, per-user playlists. Decide the shape before any of those lands. [speculative] until Daniel signals interest.
  • ITrackService interface. Audit-suggested. Low value today (one consumer pair); higher value when the test surface expands beyond FileDatabase.
  • Test coverage outside FileDatabase. Tests today cover the FileDatabase subsystem comprehensively and nothing else. As features in Phases 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.