77 KiB
COMPLETED.md — DeepDrftHome
Archive of items that have moved out of PLAN.md and CMS-PLAN.md. Per CONTEXT.md §6, completed items are moved here rather than deleted. Each entry preserves the original "What / Why / Shape" body so this file reads as a decision record, not just an outcome list.
Newest entries at the top. Group by phase/wave header (mirroring PLAN.md / CMS-PLAN.md themes) when there are enough entries to warrant it.
Phase 2.1 — Cover art / image vault wired through
Status: Fully landed on 2026-06-07 across three waves (Wave 1: API + vault; Wave 2-A: public proxy + TrackCard; Wave 2-B: CMS upload UI), merged to dev.
- What:
MediaVaultType.Imageis implemented end-to-end and exercised by tests, but the production surface only registers atracksvault of typeAudio.ImagePathonTrackEntityis a free-form URL string today; it should resolve to an entry in an image vault served byDeepDrftContent. - 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 (
imagesorart, typeImage) inStartup.ConfigureDomainServicesand in the CLI. - Add
GET api/image/{entryKey}(unauthenticated, mirrors track read) andPUT api/image/{entryKey}(ApiKey, mirrors track write) onDeepDrftContent. - Change
TrackEntity.ImagePathsemantics from "URL" to "image vault entry key" (column rename optional — could remainimage_pathwith semantic shift, or could becomeimage_entry_keyfor clarity). - Add an image processor sibling of
AudioProcessor.
- Register a second vault (
- Prerequisite: None.
- Constraint: This is a small schema-semantics migration. Existing rows have
nullImagePath in production so there is no data to migrate, but commit before the field has real content to avoid a backfill.
Embeddable iframe player
Status: Feature complete on 2026-06-07 (commit c83b132 feature: Embed Frame Player, merged to dev).
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 implemented:
Layout/EmbedLayout.razor— a minimal layout:MudThemeProvider+AudioPlayerProviderwrapping@Body, with no nav, menu, or marketing chrome. Reuses the dark-modePersistentComponentStateround-trip (CONTEXT.md §3.6) so an embedded player still honours the theme.Pages/FramePlayer.razor— routed at/FramePlayer, usesEmbedLayout, renders a single<AudioPlayerBar Fixed />. Reads aTrackEntryKeyfrom the query string and auto-selects that track on load.Services/ITrackDataService.cs+TrackClientDataService.cs— a new track-metadata fetch seam (GetPage+GetTrack(trackId)) so a component can resolve a single track by key without the gallery VM. 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.
Deferred: CORS for arbitrary external embedders — handle when a concrete external host requires it.
Phase 1.1 — Backward seek
Status: Landed on 2026-06-07 (commits daa334a, 8581103 on seek-fix branch, merged to dev).
- What: Seeking to a position below
playbackOffsetcurrently 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 inWavOffsetService+ 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. - Implementation:
WaveformSeekercontrol supports both forward and backward seeking. The seek logic decides whether to jump within the decoded buffer or tear down and re-request from a byte-aligned offset regardless of direction. Backward seek observes the sameblockAlignrounding-down as forward seek (enforced inWavOffsetService.alignedOffsetandStreamDecoder.calculateByteOffset). Teardown/reinit respects the generation-counter pattern introduced by the concurrent-seek fix.
Phase 6 — Responsive home page (mobile layout)
Status: All six slices landed on 2026-06-07 (branches home-mobile-grid, home-mobile-hero, home-mobile-cta, merged to dev).
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(withxs/sm/mdbreakpoints) for splits where MudBlazor's margin-based gutters are acceptable: hero, section-header, section-split, CTA banner. This is the house pattern already used inDeepDrftShared.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 injectIBreakpointServiceor any breakpoint-observer service into the component.- CSS
@mediaquery 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 overridesgrid-template-columnsis 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@mediaoverrides inHome.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-gridviagap: 1pxover a border-colored background;.features-gridvia per-cardborder-right). MudGrid'sSpacinggutters 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); smrepeat(3, 1fr); xsrepeat(2, 1fr). (Six genres divide cleanly into 3 and 2 — no orphan row.) - Features grid: md+
repeat(4, 1fr)(current); smrepeat(2, 1fr); xs1fr(single column stack).
- Genre grid: md+
- Scoped CSS that must change: Add two
@media (max-width: 960px)and@media (max-width: 600px)blocks overridinggrid-template-columnson.genre-gridand.features-grid. For.features-gridat the stacked/2-col breakpoints, the per-cardborder-rightproduces a dangling right border on the last card in each visual row — switch the hairline strategy at those breakpoints (e.g. applyborder-bottomon cards and dropborder-right, or move togap: 1pxlike 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:
.heroisgrid-template-columns: 1fr 1fratmin-height: 100vh, with.hero-leftpainted white and.hero-rightpainted navy — a full-viewport color split. Migrate the content columns toMudGrid; 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
DeepDrftHeroandNowPlayingin<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 —
DeepDrftHeroon top,NowPlayingbelow. The 100vh constraint should relax tomin-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:
.herokeepsmin-height: 100vhat 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 eachMudItemcarry its own background at the stacked breakpoint).- The white/navy split: at md+ this can stay a CSS background on
.hero(e.g. alinear-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 owndisplay: grid; grid-template-columns: 1fr 1fr(MudGrid now owns column layout). Keepoverflow: 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:
DeepDrftHeroandNowPlayingare 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-headerisgrid-template-columns: 1fr 2fr(label+title left, body paragraph right) withalign-items: end. Migrate toMudGrid. - Stacking behavior: md+ keep the 1fr/2fr asymmetry via
<MudItem md="4">(title) +<MudItem md="8">(body). xs/sm stack toxs="12"each — title block on top, body paragraph below. - Scoped CSS that must change: Remove
display: grid; grid-template-columns: 1fr 2fr; gap: 4remfrom.section-header. Thealign-items: endbaseline-alignment is a desktop nicety that's meaningless when stacked — preserve it at md+ only (MudGridAlign.Endon the row, or a scoped rule)..section-body'salign-self: endsimilarly 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-splitisgrid-template-columns: 1fr 1fratmin-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-leftgreen,.split-rightwhite) 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. SetMudGrid 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::beforedecorative circle stays untouched. Relaxmin-height: 60vhtoautoat 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-bannerisdisplay: flex; justify-content: space-between— headline left, two action buttons right..cta-actionsis 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 flipsflex-direction: columnandalign-items: stretchatmax-width: 600pxachieves 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: addflex-wrap: wrapalways; at xs,width: 100%with the two buttons (.btn-white,.btn-outline-white) goingflex: 1or 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 itsfont-sizeat xs (e.g.clampor 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.mdin-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
DeepDrftHeroorNowPlaying(6.2 constraint). Do not touchDeepDrftPublic/wwwroot/styles/deepdrft-styles.css(shared CSS) — all changes are scoped toHome.razor.css. Preserve every color/font/decoration; this phase changes where columns break, nothing about how the page looks at desktop width.
Play-State Icon Normalization
Status: Phases 1–4 landed on 2026-06-06 (branches track-card-play-state-wave1, track-card-play-state-wave2, merged to dev).
Phase 1 — Fix the gallery bug (correctness, smallest viable change)
Landed 2026-06-06.
Bound TrackCard.IsPlaying to real playback state instead of selection identity. In TracksView/TracksGallery, active track is now computed as PlayerService.IsPlaying && CurrentTrack?.Id == track.Id. Switched the card glyph from MusicNote to the PlayArrow/Pause vocabulary via IsPaused and OnPause parameters. Expanded TracksView.OnPlayerStateChanged to re-render on any state change, not only on !IsLoaded — ensures the gallery correctly reflects pause, play, track-change, and end-of-playback transitions.
Component changes:
TrackCard.razor— added[Parameter] bool IsPaused,[Parameter] EventCallback OnPauseparameters; removedMusicNoteicon; now conditionally rendersPlayArrowwhen not playing orPausewhen playing.TracksView.razor— removed_selectedTrackfield (selection now fully derived from service); removed_clickCount,_lifecycleStatus,TestInteractivitydev scaffolding;OnPlayerStateChangednow callsStateHasChanged()unconditionally instead of only on!IsLoaded.TracksGallery.razor— removed internalSelectedTrackmutation andStateHasChangedcalls on play click; now fully controlled by parent;SelectedTrackparameter is read-only.
Architecture notes:
- Resolves the reported bug: gallery card now shows correct play/pause icon reflecting actual playback state.
- Enabling pause affordance on cards required extending
TrackCardwithIsPaused+OnPause, preserving the component's presentational contract (stays parameter-driven, lives in shared library). TracksView.OnPlayerStateChangedsubscription pattern unchanged; expansion from selective to unconditional re-render ensures high-frequency state changes (like spectrum animation or per-sample progress) do not cause visual lag in the gallery.
Phase 2 — Collapse dual selection state (SRP, prevents regression)
Landed 2026-06-06.
Eliminated divergence between TracksView._selectedTrack and PlayerService.CurrentTrack. TracksGallery is now fully controlled — the parent supplies and owns the active-track identity via parameter binding. Selection state is single-sourced from the player service.
Component changes:
TracksGallery.razor— removed parameter-field write inHandlePlayClick; no longer callsStateHasChanged()on click. RaisesSelectedTrackChangedcallback for the parent to route.TracksView.razor— removed_selectedTrackbacking field and its local mutation.
Architecture notes:
- Resolves the secondary defect: gallery's notion of "active track" can no longer lag the player.
TracksGallerynow a pure presentational component (readsSelectedTrack, raisesSelectedTrackChanged, renders); all state derivation lives in the parent or the service.
Phase 3 — Introduce the single transport-state resolver (DRY)
Landed 2026-06-06.
Introduced a unified glyph-mapping source: PlaybackIcons.Resolve() static method in DeepDrftPublic.Client/Helpers/PlaybackIcons.cs. This is the sole function responsible for mapping (IsPlaying, IsPaused, trackId?, CurrentTrackId?) to the correct transport icon (PlayArrow, Pause, or null). Replaces all hand-rolled ternaries across TrackCard, PlayerControls, and other surfaces.
New code (DeepDrftPublic.Client/Helpers):
PlaybackIcons.cs— staticResolve(bool isPlaying, bool isPaused, long? trackId, long? currentTrackId)method returning(string? Icon, bool IsActive, bool IsPaused)tuple. Icon mapping is the single source of truth.
Component changes:
PlayerControls.razor(.cs)—IsPlayingparameter removed from theAudioPlayerBar → PlayerTransportZone → PlayerControlschain. Instead,PlayerControlsnow subscribes toIPlayerService.StateChangeddirectly and callsPlaybackIcons.Resolve()to determine which icon to render and whether buttons are enabled/disabled.TrackCard.razor— consumes the tuple returned byPlaybackIcons.Resolve()to setIcon,IsActive(CSS class for highlighting), andDisabledstate on the FAB.
Architecture notes:
- Eliminates the three-way duplication of "which icon for this state" logic.
- Icon vocabulary is now standardized across all surfaces (
PlayArrow/Pausepair, noMusicNote). - Future surfaces (queue list, now-playing chip, etc.) call the same
Resolve()function instead of re-implementing the mapping.
Phase 4 (optional, deferred) — Promote to a PlayStateIcon component
Landed 2026-06-06.
Created a new PlayStateIcon.razor component in DeepDrftPublic.Client/Controls/ that encapsulates subscription + icon mapping + rendering. Rather than each surface calling PlaybackIcons.Resolve() and threading icons through parameters, surfaces now drop in <PlayStateIcon /> and the component handles cascading, state subscription, and icon selection in one place.
New component (DeepDrftPublic.Client/Controls/PlayStateIcon.razor):
- Injects
IPlayerServiceand subscribes toStateChangedon mount. - Cascades
[CascadingParameter] DarkModeSettings DarkModefor theming. - Renders an icon button (or FAB) with the correct glyph via
PlaybackIcons.Resolve(). - Forwards
Disabledparameter to the rendered MudIconButton/MudFab. - Raises
OnClickcallback when user clicks.
Component changes:
PlayerControls.razor— refactored to render its play/pause button via<PlayStateIcon />instead of a parameter-driven button.IsPlayingparameter removed from the component signature.- The
AudioPlayerBar → PlayerTransportZone → PlayerControlschain no longer threadsIsPlaying/IsPauseddown; subscription happens insidePlayStateIcon.
Architecture notes:
PlayStateIconhandles the seam betweenIPlayerService(source of truth) and transport-icon rendering (presentation). This was the third surface (afterTrackCardandPlayerControls); Phase 4 was triggered by the appearance of the third call site.- Reduces parameter threading in the component tree (no more passing state flags through intermediate layers).
- New surfaces that need play/pause icons (queue list, hover-row play button, etc.) now have a reusable, off-the-shelf component instead of re-implementing subscription and mapping.
WaveformSeeker Wave 3 — CMS PreProcessing panel
Status: W3 (CMS track-preprocessing panel) refactored on 2026-06-05 (branch waveform-w3-cms, merged to dev).
W3 — CMS PreProcessing panel
Landed 2026-06-05. Refactored 2026-06-05.
Implemented the CMS surface for on-demand waveform profile generation. Initial implementation created a new /tracks/preprocessing page; refactored to fold the preprocessing panel into TrackList.razor as a second MudTabPanel alongside the existing Tracks tab.
API endpoints (DeepDrftAPI):
GET api/track/waveform-status(ApiKey) — returnsWaveformStatusDto[]with per-track profile existence (one entry per track in the database, indicating whether a profile sidecar exists in the vault).POST api/track/{trackId}/waveform(ApiKey) — triggers on-demand profile compute and store for an existing track. Skips if profile already exists; errors surface gracefully (no profile → HTTP 404, track not found → HTTP 400).
Models (DeepDrftModels):
WaveformStatusDto— carriesTrackId,EntryKey,TrackName,HasProfileboolean, and metadata for display/sorting.
CMS service (ICmsTrackService / CmsTrackService in DeepDrftManager):
GetWaveformStatusAsync()— service method wrapping theapi/track/waveform-statuscall; returnsResult<WaveformStatusDto[]>for error handling.GenerateWaveformProfileAsync(entryKey)— service method wrapping the per-track generation endpoint; returnsResult<bool>(success → true, profile already exists → true, error → false with result code).
CMS UI (DeepDrftManager/Components/Pages/Tracks/TrackList.razor):
- Added "Preprocessing"
MudTabPanelas the second tab inTrackList.razor, alongside the existing "Tracks" tab. - Table layout within the panel: track name, artist, "Profile Status" indicator (✓ or ○), with a per-row
Generatebutton. - Sequential "Generate All Missing" bulk action button — iterates tracks with
HasProfile == false, callsGenerateWaveformProfileAsync, shows progress. On completion, refreshes the table. - The standalone
TrackPreProcessing.razorpage at/tracks/preprocessingwas eliminated; the page route is no longer exposed. - Nav link to preprocessing removed from
Index.razordashboard (consolidation makes a separate link unnecessary; the tab is discoverable fromTrackList.razor).
Architecture notes:
- Waveform generation on-demand (not automatic on upload like in W1) is intentional: Wave 1 profiles were computed for all future-uploaded tracks; Wave 3 adds a retroactive tool to populate profiles for existing tracks uploaded before Wave 1. The bulk action supports batching.
- Service calls are fire-and-forget-result, not throw-on-error —
GenerateWaveformProfileAsyncreturns aResultfor the caller to inspect. This matches the FileDatabase philosophy (errors in compute/store are swallowed at the service boundary, callers check return values). - Profile endpoint uses the same
WaveformProfileServicethat computes profiles during upload — no new algorithm or storage path introduced. CMS can only trigger on-demand what the upload path does automatically. - HTTP cache headers are deferred (same as W1-T2). Each
api/track/waveform-statuscall lists all tracks and their current state; this is acceptable for the admin surface where refreshes are infrequent. - Consolidation rationale: Folding the preprocessing panel into
TrackListreduces UI fragmentation — track management (list, add, edit, delete, preprocess) lives in one cohesive view rather than split across separate pages. The tab structure keeps preprocessing distinct from the main track listing without requiring a dedicated route.
WaveformSeeker Wave 2 — DOM seekbar + Interop module
Status: W2 (WaveformSeeker component) landed on 2026-06-05 (branch waveform-w2-seeker, pending merge to dev).
W2 — WaveformSeeker component (seekbar replacement)
Landed 2026-06-05.
Implemented the interactive WaveformSeeker component: a bar-chart-styled seekbar replacing MudSlider in PlayerSeekZone, with DOM-rendered progress split via CSS and lazy-loaded pointer-capture drag interop.
Component changes (DeepDrftPublic.Client/Controls/AudioPlayerBar):
WaveformSeeker.razor(+.cs,.css) — new component consumingWaveformProfile double[]?andDuration, rendering bars as DOM elements with clip-overlay progress. Single CSS variable (--seek-position) changes per seek gesture; no per-bar re-render.- Pointer-capture drag wired via
waveformSeeker.js(ES module, lazy-loaded). Calculates seek target from click/drag position and invokesOnSeekRequestedcallback (delegates toIPlayerService.SeekAsync). - Flat floor-height fallback when profile is unavailable — seek gesture always works, with or without loudness data.
PlayerSeekZone.razor— now hostsWaveformSeekerin place of the removedMudSliderplaceholder.
Interop changes (DeepDrftPublic/Interop/audio/):
- New
waveformSeeker.tsmodule (separate from the TS audio bundle) —PointerCaptureHandlerclass managingpointerdown/pointermove/pointeruplifecycle. Compiled towaveformSeeker.jsinwwwroot/js/audio/. - Module loaded on first use (not bundled with audio stack) to defer its parse cost until the player is expanded and the seekbar is visible.
.gitignore scoping:
- Added scoped negation to track hand-authored
waveformSeeker.jsalongside existing TS-output ignore rule — allows the compiled JS to be committed for fast startup without committing intermediate TS compiler outputs.
Service changes (IPlayerService / AudioPlayerService / StreamingAudioPlayerService):
- New
WaveformProfile double[]?property added to service interface and implementations. - Fetched fire-and-forget on track load via
GetWaveformProfileAsync(trackId, cancellationToken)— existing HTTP call from W1-T2. - Cancellable via the track-reset flow (same cancellation token that stops spectrum animation).
- Cleared on reset with all other track state.
Testing:
- Manual verification: seekbar renders flat when profile unavailable; dragable when profile present; CSS clip-overlay tracks seek position correctly.
Architecture notes:
- WaveformSeeker does not re-fetch the profile — it consumes the same
IPlayerService.WaveformProfilefetched during track load. No additional HTTP round-trip per seek gesture. - Interop module (
waveformSeeker.js) is independent of the audio playback stack — can be updated or replaced without touching audio scheduling logic. - Pointer-capture semantics ensure seek is responsive even when the browser's event queue is saturated by animation frames.
- Flat fallback ensures seek gestures always work, even on tracks with no profile data (uploaded before W1, or on profile-generation failure).
WaveformSeeker Wave 1 — Loudness profile + layout refactor
Status: W1-T1 (backend loudness computation), W1-T2 (HTTP transport), and W1-T3 (player layout refactor) landed on 2026-06-05.
W1-T1 — Backend waveform loudness profiling
Landed 2026-06-05.
Implemented Phase 1 of the WaveformSeeker feature (product-notes/spectrum-seeker.md): loudness-profile computation and storage for preprocessed waveform data.
Backend changes (DeepDrftContent):
- Added
ILoudnessAlgorithmstrategy interface for swappable loudness computation. - Implemented
RmsLoudnessAlgorithm— first loudness algorithm using root-mean-square; future LUFS implementation swaps in via the same interface without touching service, wire format, or storage. WaveformProfileService— computes peak-normalized loudness profile from PCM WAV (one linear buffer pass), buckets by time slice, normalizes to[0,1], stores as byte-quantized sidecar in newprofilesvault (FileDatabaseMediaFileVault).WaveformProfileOptions— config-bound options object carryingBucketCount(default 512) and future algorithm-selection knobs.
Integration changes (DeepDrftAPI):
- Wired
WaveformProfileServiceintoUnifiedTrackService.UploadAsync— profile computed on upload, stored immediately, failure silently swallowed (consistent with FileDatabase philosophy inCLAUDE.md).
Models (DeepDrftModels):
WaveformProfileDto— carries quantized profile data; format independent of algorithm or bucket count.
Testing (DeepDrftTests):
- 4 new unit tests: RMS algorithm correctness against known-good PCM samples, swappable-algorithm contract (two strategies swap cleanly), and integration with
WaveformProfileService.
Architecture notes:
- Profile is derived binary content; stored in FileDatabase vault sidecar per
CLAUDE.mdprinciple ("binary content lives in the vault"). - Loudness measure is an abstraction (not hardwired RMS) — RMS→LUFS future change requires only a new
ILoudnessAlgorithmimplementation, no refactoring of service, component, or wire format. - No external audio-processing dependency pulled in for RMS — reuses existing PCM parser from
AudioProcessor. - Cost: one linear pass over PCM buffer at upload (few hundred ms for typical WAV); never on playback path.
W1-T2 — Waveform profile HTTP transport
Landed 2026-06-05.
Implemented Phase 2 of the WaveformSeeker feature: HTTP transport layer for waveform profile data from backend to client, enabling client-side display of loudness profiles in future seeking UI.
API endpoint (DeepDrftAPI):
- New
GET api/track/{trackId}/waveformendpoint — unauthenticated, returnsWaveformProfileDto(base64-encoded quantized bytes +BucketCount) on success, 404 if track or profile not found. - Leverages existing
WaveformProfileServiceto load profile from vault on demand. - No authentication required — mirrors
GET api/track/{id}streaming policy (public audio access).
Proxy forward (DeepDrftPublic):
- Thin buffered forward in
TrackProxyController— proxies request from client toDeepDrftAPIwaveform endpoint with same path parameters. - Preserves error semantics: 404 from API passes through to client; network errors surface as HTTP errors.
HTTP client (DeepDrftPublic.Client):
- New
TrackMediaClient.GetWaveformProfileAsync(trackId, cancellationToken)method on the content HTTP client. - 404 response maps to
Result.Failure(fail-result signal for WaveformSeeker to render flat fallback). - Network/timeout errors map to separate
Result.Failurewith distinct code. - Callsite can discriminate via result error code whether to retry (transient) or render fallback (not found).
Architecture notes:
- Transport layer is independent of loudness algorithm (W1-T1) — client receives opaque quantized bytes; future algorithm changes on backend do not affect wire format, as long as
BucketCountis included. - HTTP caching via ETag/Last-Modified is deferred to Phase 2 optimization work.
- Profile loading from vault is on-demand (not pre-cached in memory) — load cost amortizes across all requests to the same track.
- 404 handling unambiguous: client renders flat fallback, distinguishing "track has no profile" from "track not found" via error code.
W1-T3 — Player layout refactor (SpectrumVisualizer relocation + VolumeZone rename)
Landed 2026-06-05.
Implemented Phase 3 of the WaveformSeeker feature: architectural layout move separating live-spectrum visualization from loudness-over-time seeking.
Conceptual split:
- Live-spectrum (FFT frequency bars,
SpectrumVisualizer) moved fromPlayerSeekZone→ stacked above the volume slider in newVolumeZone. Conceptually with the output level. - Static loudness-over-time (future
WaveformSeeker) takes over the seek zone. Conceptually with transport position.
Component changes (DeepDrftPublic.Client/Controls/AudioPlayerBar):
VolumeControls.razor→ renamedVolumeZone.razorfor symmetry with transport and seek zones; now a vertical stack hostingSpectrumVisualizerabove the volume slider.SpectrumVisualizer—BucketCountparameter defaulted to 24 buckets (down from 32) to fit the narrow volume cluster; setflex-shrink: 0to pin the spectrum to a fixed footprint above the volume control.PlayerSeekZone.razor—SpectrumVisualizerblock removed; placeholder for futureWaveformSeekercomponent.
CSS changes (AudioPlayerBar.razor.css):
- Adjusted volume cluster width constraints to accommodate the 24-bucket spectrum stacked above.
- Responsive layout unchanged at 600px breakpoint (single-row transport/volume with full-width seek below on narrow; same 3-zone layout on wide).
Scope:
- Pure layout move; zero change to spectrum animation lifecycle, player logic, or seek gesture handling.
- Both
AudioPlayerBarandSpectrumVisualizercomponents affected. - Build clean: 0 errors, 0 new warnings.
Notes for future work:
PlayerSeekZoneis now ready for theWaveformSeekercomponent (W1-T4/Phase 4 onwards).- Volume cluster can comfortably accommodate 24 FFT bars; 32 would cause visual cramping (why the override exists).
- Spectrum visualization lifecycle (subscription to
StateChanged, animation viaAudioInteropService.StartSpectrumAnimationAsync) unchanged — only position in the DOM tree changed.
Phase 2 — Product surface: player and theming
Status: Track card CSS scoping landed on 2026-06-05. Track card glass theming landed on 2026-06-05. AudioPlayerBar responsive unification and SpectrumVisualizer fix landed on 2026-06-05. Track view CSS consolidation landed on 2026-06-05.
Track Card CSS Scoping
Landed 2026-06-05.
Moved track card rules from the global stylesheet into an isolated scoped stylesheet, eliminating style leakage and enabling independent maintenance of the component's appearance.
CSS changes:
DeepDrftPublic/wwwroot/styles/deepdrft-styles.css§8 — removed all track card rules (.deepdrft-track-card-*,.deepdrft-track-title,.deepdrft-track-artist,.deepdrft-track-meta); replaced with a pointer comment directing readers toTrackCard.razor.css.DeepDrftShared.Client/Components/TrackCard.razor.css— created new scoped stylesheet with all card rules: container styling, text-colour hierarchy (title, artist, meta), theme-variant selectors (.deepdrft-theme-dark/.deepdrft-theme-light), and glass background + border styling.- Applied
::deeppseudo-selector to the three MudText text-color rules (deepdrft-track-title,deepdrft-track-artist,deepdrft-track-meta) so CSS isolation doesn't suppress colour overrides on MudBlazor elements. - Eliminated all theme-variant selectors in favour of a single-vocabulary colour scheme: navy-glass fallback,
--deepdrft-whitetitle,--deepdrft-green-accentartist,rgba(250,250,248,0.45)meta. Matches theNowPlayingCardaesthetic. DeepDrftShared.Client/Components/TracksGallery.razor.css— moved.deepdrft-track-gallery-item-centerlayout rule from global stylesheet into scoped CSS alongside the existing gallery container rules.
Scope:
- Affected components:
TrackCard.razor(shared, consumed by public site and CMS) andTracksGallery.razor(shared). - CSS in
DeepDrftPublic/wwwroot/styles/deepdrft-styles.css(global) and two scoped stylesheets. - Build clean: 0 errors, 0 new warnings.
Architecture notes:
- CSS isolation now protects track card rules from accidental mutation by unrelated global changes.
- Light-mode visual is now consistent: single vocabulary eliminates the three-green collision and establishes a stable text hierarchy (off-white title → muted artist → fainter meta).
- Scoped stylesheet pattern mirrors existing usage in other components (
AudioPlayerBar.razor.css,NowPlayingCard.razor.css), establishing a consistent maintenance model.
Track View CSS Consolidation
Landed 2026-06-05.
Implemented CSS consolidation and hierarchy fixes across three components: removed dead layout rules, unified horizontal inset ownership, and resolved the three-green collision in dark mode by demoting artist text and changing the genre chip variant.
Component changes:
DeepDrftPublic.Client/Pages/TracksView.razor— removed deadtracks-page-wrapperclass and associated inert flex/height/padding rules;MudContainernow owns horizontal inset viaMaxWidth.Large.DeepDrftShared.Client/Components/TracksGallery.razor.css— reduced tobox-sizing: border-box; removed redundant padding and inert height constraint.DeepDrftShared.Client/Components/TrackCard.razor— changed genre chip fromVariant.FilledtoVariant.Outlinedto distinguish it from the play FAB.
CSS changes (DeepDrftPublic/wwwroot/styles/deepdrft-styles.css §8):
- Text color rules restructured: base
color: inherit, both dark and light treatments guarded under.deepdrft-theme-dark/.deepdrft-theme-lightancestors at0,2,0specificity. - Artist text demoted from
green-accenttorgba(250,250,248,0.65)in dark mode (leaving green as a purely accent/interactive signal — FAB and chip border). - Meta text (album/year) at
rgba(250,250,248,0.45)in dark mode. - Genre chip treatment now supports outlined styling (borders + text only, no filled ground).
Scope:
- CSS in
deepdrft-styles.cssand scoped stylesheets forTracksView.razorandTracksGallery.razor. - Both
DeepDrftPublic.ClientandDeepDrftShared.Clientcomponents affected. - Build clean: 0 errors, 0 new warnings.
Architecture notes:
- Resolved the three-green visual hierarchy collapse (artist + genre chip + play FAB all rendered the same saturated green). Now: title off-white, artist muted, genre = outlined green tag, FAB = solid green action — a clear three-tier hierarchy matching
NowPlayingCardvocabulary. - Consolidated horizontal inset ownership to
MudContainer(removes duplicate paddings that stacked across three layers). - Removed inert flex-grow and height rules that encoded a sticky-footer intent that was not actually achieved; page layout via normal block flow is cleaner.
Status:
Track Card Glass Theming
Landed 2026-06-05.
Aligned TrackCard component visual language with the NowPlayingCard aesthetic via glass background + text hierarchy. Two coordinated changes:
Razor changes (DeepDrftShared.Client/Components/TrackCard.razor):
- Removed
mud-theme-secondaryclass andColor="Color.Surface"attributes from all fourMudTextelements, handing color control to CSS. - Added semantic class hooks:
deepdrft-track-title(track name),deepdrft-track-artist(artist),deepdrft-track-meta(album and release year). - Changed MudCard
Elevation="4"→Elevation="0"to align with glass-panel vocabulary (no drop shadow).
CSS changes (DeepDrftPublic/wwwroot/styles/deepdrft-styles.css §8):
- Dark theme: navy-glass fallback panel (
color-mix(in srgb, var(--deepdrft-navy) 55%, transparent)+backdrop-filter: blur(8px)+ translucent border), matchingNowPlayingCardglass vocabulary. - Text hierarchy (dark): title in off-white, artist in moss-green accent, meta in muted off-white — mirrors the
NowPlayingCardhierarchy. - Content scrim behind text (dark): dark navy gradient to guarantee legibility over both glass fallback and album art.
- Light theme: subtle navy-tint fallback on off-white, light text inherits body colour for legibility.
- Glass border on card container (dark):
1px solid rgba(250, 250, 248, 0.12)for aesthetic consistency.
Scope:
TrackCardcomponent in sharedDeepDrftShared.Clientconsumed by both public site and CMS.- CSS in
DeepDrftPublic/wwwroot/styles/deepdrft-styles.css(public site only, not loaded by CMS). - Build clean: 0 errors, 0 new warnings.
Notes for future work:
- Genre chip text still uses
Color.Primary(moss-green); it now sits alongside moss-green artist text. Consider a distinct genre-chip treatment (3a) in future polish work.
Status: AudioPlayerBar responsive unification and SpectrumVisualizer fix landed on 2026-06-05.
AudioPlayerBar Responsive Unification
Landed 2026-06-05.
Collapsed the two divergent Razor trees in AudioPlayerBar.razor (@if (_isDesktop) / @else) into a single markup tree where CSS — not a runtime breakpoint flag — drives the responsive layout. Removed IBrowserViewportService, the _isDesktop field, OnAfterRenderAsync, and the viewport subscription/unsubscription from the code-behind.
Structural changes:
- Single
.player-layoutflex container (inAudioPlayerBar.razor.css) replaces the dual-branch conditional. Three children (PlayerTransportZone,VolumeControls,PlayerSeekZone) in source order; media query at 600px (Smbreakpoint) reorders via CSSorderproperty and forcesSeekZoneto full-width below the transport/volume row on narrow viewports. PlayerTransportZoneflips its internal axis (vertical ↔ horizontal) via scoped CSS override ofMudStackflex-directionat the 600px boundary — no parameter added to the component.::deepprefix removed fromMudBlazorcomponent-class selectors inPlayerTransportZone.razor.cssnow that axis is purely CSS-driven and no runtime flag determines structure.- SpectrumVisualizer bars now appear on first expand — fixed by subscribing to the multicast
StateChangedevent (same pattern used byAudioPlayerBar), ensuring animation is initialized after mount.
Scope:
- Unified responsive layout (desktop/mobile branches merged into single tree).
- Both
AudioPlayerBarandSpectrumVisualizercomponents affected. - Build clean: 0 errors, 0 new warnings.
Notes for future work:
- First-render layout flash eliminated by construction (CSS media query evaluates at paint, not async subscription).
Track Card Plain-Shell Refactor
Landed 2026-06-05.
Eliminated !important declarations from track card CSS by replacing MudBlazor surface components with plain HTML. Implemented per product-notes/track-card-css-architecture.md Option A.
Razor changes (DeepDrftShared.Client/Components/TrackCard.razor):
MudCard→<div class="deepdrft-track-card-container">- Fallback
MudPaper→<div class="deepdrft-track-card-fallback"> MudCardContent→<div class="deepdrft-track-card-content">MudText,MudChip,MudFabunchanged.
CSS changes (DeepDrftPublic/wwwroot/styles/deepdrft-styles.css §8):
- Removed four
!importantdeclarations from.deepdrft-track-card-container,.deepdrft-track-card-fallbackbase, and the dark/light theme-scoped variants. - Plain single-class selectors now win by cascade without
!important; theme-scoped rules use normal specificity hierarchy.
Scope:
TrackCardcomponent in sharedDeepDrftShared.Clientconsumed by both public site and CMS.- CSS in
DeepDrftPublic/wwwroot/styles/deepdrft-styles.css(public site only). - Build clean: 0 errors, 0 new warnings.
Notes for future work:
- Plain-div shell re-enables CSS isolation as an option (a
TrackCard.razor.csswould now work against the shell divs). Section 8's public-only scoping remains convenient; isolation is optional for future polish. - Removes the structural mismatch of using a Material surface component (
MudCard/MudPaper) solely as a layout shell. TrackCard now mirrors the construction ofNowPlayingCard(plain divs + themed CSS).
Track Detail Page (/track/{entryKey})
Status: Landed on 2026-06-06 (branch track-detail-page, merged to dev).
A focused, editorial single-track view in DeepDrftPublic.Client. The track gallery answers "what is in the library"; this page answers "tell me about this track" — full metadata, cover art, and a single prominent play affordance, styled to feel like a record-sleeve back-cover rather than a form. Link-only for now (reached from a gallery card / Now Playing), not a top-level nav entry.
Implemented solution
Components (DeepDrftPublic.Client/Pages/):
TrackDetail.razor+TrackDetail.razor.cs— routed at@page "/track/{EntryKey}"with@rendermode InteractiveWebAssembly. Three render states (loading skeleton, loaded layout, 404 not-found) driven byTrackDetailViewModelflags. CascadesIStreamingPlayerServicefor play-affordance wiring. Subscribes toPlayerService.StateChangedto keep the play button label in sync with live transport state.
ViewModel (DeepDrftPublic.Client/ViewModels/):
TrackDetailViewModel— scoped, registered inStartup.ConfigureDomainServices. Depends onITrackDataService(render-mode-agnostic seam, existing). Properties:Track(loaded DTO),IsLoading,NotFound. SingleLoad(entryKey)command idempotent per route, fully resetting all three flags on each call to prevent stale track bleed on navigation.
DI registration (DeepDrftPublic.Client/Startup.cs):
TrackDetailViewModelregistered scoped.
UI layout:
- Subtle back-link
← All tracksto/tracks, muted low-emphasis text affordance. - Large square cover art block — placeholder themed
MudPaperwithAlbumglyph when cover unavailable (default state pending 2.1 image-vault wiring); will display<img src>once 2.1 lands. - Title (TrackName, display-serif h3) / artist (h6, primary accent) masthead.
- Prominent Play button under masthead with state-reactive label ("Play" / "Pause" / "Resume" keyed to current track and playback state via
PlayerServicesubscription). MudDividerseparator.- Optional-field metadata block (Album, Genre, ReleaseDate) — definition-row layout, rendered only if non-null; all three omit silently if unavailable.
- Skeleton loading state matching the loaded layout silhouette.
- 404 messaging on not-found.
CSS classes (DeepDrftPublic/wwwroot/styles/deepdrft-styles.css §14):
deepdrft-track-detail-container— centered single column, max-width, auto-margins, vertical padding.deepdrft-track-detail-cover— square aspect-ratio frame, rounded, subtle shadow/border (light/dark theme-aware),overflow: hiddenfor clean image crop.deepdrft-track-detail-masthead— title/artist spacing, display-serif via existingdeepdrft-font classes.deepdrft-track-detail-meta— metadata block rhythm, small-caps muted labels.deepdrft-track-detail-back— back-link affordance, muted color, hover treatment.
Inbound links wired (DeepDrftShared.Client/Components/TrackCard.razor):
- Cover block and title/artist are now
display:contentsanchors tohref="/track/{track.EntryKey}", making the entire card clickable to the detail page. - Play button on the card untouched (still functions independently for gallery playback).
Architecture notes:
- Render mode
InteractiveWebAssembly(server prerender → WASM hydrate) mirrorsTracksViewconsistency. TrackDetailViewModelis scoped (per-instance), not singleton — navigating between/track/Aand/track/Breuses the same scoped instance, soLoadmust fully reset state to prevent cross-navigation bleed.- Play button implements the same
PlayerService.StateChangedsubscription pattern asTracksView— mandatory for label coherence when the dock bar drives state. - Cover-art default (placeholder) is intentional and designed to be the resting state, not degraded; the page ships immediately without waiting for 2.1 image-vault wiring. Once 2.1 lands and
api/image/{entryKey}exists onDeepDrftContent, the<img src>binding swaps in without further component changes (the placeholder'sonerrorfallback ensures graceful degradation if a vault entry is missing). - Page is link-only navigation (not in the header
MenuPages); reachability depends on inbound links fromTrackCardand Now Playing surfaces, which were wired simultaneously.
Status: Desktop AudioPlayerBar redesign landed on 2026-06-04.
Desktop AudioPlayerBar — migrate to MudBlazor theme system
Landed 2026-06-04.
Desktop branch of AudioPlayerBar.razor migrated off dead CSS palette tokens (--charleston-*, --lowcountry-*, --deepdrft-theme-* — none of which are defined in the live stylesheet) onto the active MudBlazor theme system. This was simultaneously a bug fix (player styling broken against the current palette) and a structural redesign.
Structural changes:
.player-backdropdiv replaced withMudPaper Elevation="8"— surface colour now derives from--mud-palette-surfacevia the live theme, and flips automatically with dark mode (off-white in light, navy in dark).- Three new zone sub-components extracted:
PlayerTransportZone(left transport cluster),PlayerSeekZone(centre seek+spectrum, owns the seek pointer-handler logic),PlayerWindowControls(minimize/close buttons). These remove duplication (seek handlers no longer inline-copied) and name the layout zones explicitly. MudStackreplaces all raw<div class="d-flex gap-*">throughout the desktop branch and sub-components (PlayerControls,VolumeControls,TimestampLabel).SpectrumVisualizerbar colour fixed:var(--mud-palette-primary)replaces the undefined--deepdrft-theme-secondarytoken.- Minimized dock replaced with
MudFab Color="Color.Primary"— rounded button picking up themed primary colour with no hand-rolled gradient. AudioPlayerBar.razor.cssshrunk from ~176 lines (mostly dead-token theming) to ~74 lines (geometry and positioning only).
Scope:
- Desktop branch only (
@if (_isDesktop)). Mobile branch unchanged by design. - Build clean: 0 errors, 0 new warnings.
Notes for future work:
- Mobile branch is also currently broken against the live palette for the same reason (spectrum bars + shared dead-token rules have no colour). A companion migration for mobile is implied but out of scope for this task — marked for future Phase 2 work.
Deployment Infrastructure
Status: CD pipeline infrastructure landed on 2026-06-04.
CD pipeline infrastructure (Gitea workflows + remote host installer)
Landed 2026-06-04.
Continuous deployment infrastructure for DeepDrftHome dual-app deployment. Consists of four Gitea workflows (.gitea/workflows/) — deploy-public.yml, deploy-manager.yml, deploy-api.yml, package-install.yml — all triggered by dev branch (beta) and master branch (prod) pushes, path-filtered to deploy only on changes to the affected service and its dependencies. Five installer scripts (deploy/) — install.sh (one-shot host provisioner), bootstrap.sh (curl-and-run entry point), ssh-wrapper.sh (forced-command dispatcher), three deploy-*.sh per-service deployment scripts — plus systemd service templates (deploy/systemd/) and nginx vhost templates (deploy/nginx/), and credential template files (deploy/credentials/). One auxiliary setup script setup-step10-creds.sh for interactive credential entry on the host. The installer creates users, directories, systemd services, PostgreSQL databases, nginx vhosts, and loads credential files via systemd LoadCredential= into the credential sandbox. The deploy scripts swap binaries in-place, run the EF migrations bundle for the API metadata database, and restart services without touching persistent vault data. Enables hands-off pushes to beta and prod with full CI/CD orchestration.
Two-app split Wave 2 — Phase 4
Status: Phase 4 (project rename) landed on 2026-05-19.
Phase 4 — Two-app split: rename DeepDrftWeb → DeepDrftPublic
Landed 2026-05-19.
Renamed DeepDrftWeb to DeepDrftPublic and DeepDrftWeb.Client to DeepDrftPublic.Client across all project files, .csproj files, namespace declarations, using directives, solution file, and deploy scripts. Updated all references in CLAUDE.md agent guidance to reflect the new names. Also updated prior references to DeepDrftWeb.Services to DeepDrftData to align with the Phase 2 library rename. The solution builds cleanly with all endpoints functional.
CMS Wave 1 — Auth + scaffolding + parity
Status: All sub-items landed on 2026-05-18.
W1.0 DeepDrftContext Postgres migration
Landed 2026-05-18.
Rewrite all existing EF Core migrations from SQLite to PostgreSQL. Update the DeepDrftWeb and DeepDrftCli connection strings in config. Migrate any existing data from ../Database/deepdrft.db to Postgres. Verify the existing api/track/page and api/track/{id} endpoints function against the new backend. This is a prerequisite for W1.2 (which also runs migrations for AuthDbContext against the same Postgres instance).
W1.1 DeepDrftCms RCL skeleton
Landed 2026-05-18.
Project created, added to solution, referenced from DeepDrftWeb. Empty Pages/Cms/Index.razor mounted at /cms returning a "CMS — under construction" placeholder, proving the mount works.
CMS RCL inlined into DeepDrftManager
Landed 2026-05-21.
The DeepDrftCms Razor Class Library has been inlined into DeepDrftManager and the standalone project deleted from the solution. All Razor pages, components, and layouts (CmsLayout, DeleteTrackDialog, TrackList, TrackNew, TrackEdit, and the CMS index page) now live directly in DeepDrftManager/Components/Pages/Cms/, DeepDrftManager/Components/Pages/Tracks/, DeepDrftManager/Components/Layout/, and DeepDrftManager/Components/Shared/. The DeepDrftManager.csproj no longer references the now-deleted DeepDrftCms project. DeepDrftManager/Program.cs no longer calls AddCmsServices() or references the CMS assembly. Solution builds cleanly with all CMS endpoints and pages functional.
W1.2 AuthBlocks integration + login
Landed 2026-05-18.
Reference Cerebellum.AuthBlocks, Cerebellum.AuthBlocks.Web, Cerebellum.AuthBlocks.Models from DeepDrftWeb; reference Cerebellum.AuthBlocks.Web from DeepDrftWeb.Client. Call AddAuthBlocks(...) in Program.cs with JWT secret/issuer/audience, Mailtrap email connection, Postgres connection string, and AdminUserSettings from environment/authblocks.json. Call await app.Services.UseAuthBlocksStartupAsync() post-build. Call app.MapAuthBlocks() to mount /api/auth/* routes. Add the AuthBlocksWeb assembly to AddAdditionalAssemblies so the bundled /account/login and /account/logout pages resolve. In DeepDrftWeb.Client.Startup, call AuthBlocksWeb.Client.Startup.ConfigureServices(builder.Services) for the prerender→WASM auth-state bridge. Add CreatedByUserId : long? column to TrackEntity via a nullable migration. Provision local Postgres (docker-compose) and document the dev setup. Includes CmsStealthRoutingHandler — a custom IAuthorizationMiddlewareResultHandler that returns 404 for any /cms/* hit that fails authorization, honouring the stealth-routing constraint: unauthorized access to admin routes returns 404, not 401 or redirect.
CMS Wave 1 (legacy section header for reference)
Status: All sub-items landed on 2026-05-18.
Goal was: A logged-in collective member can do everything the CLI does today, from a browser.
W1.3 CMS track list
Landed in CMS Wave 3.
/cms/tracks consuming the same GET api/track/page endpoint as the public gallery. Different rendering (table with admin affordances), same VM. No new SQL endpoint.
W1.4 CMS upload endpoint + add page
Landed in CMS Wave 3.
New POST api/cms/track on DeepDrftWeb (auth-gated, see §5 for the transport decision). /cms/tracks/new page wires InputFile to the endpoint. Note: Option B is confirmed — this requires a new POST api/track/upload endpoint on DeepDrftContent (raw WAV in, unpersisted TrackEntity out) in addition to the CMS page and controller.
W1.5 CMS delete endpoint + delete UI
Landed in CMS Wave 3.
New DELETE api/cms/track/{id} on DeepDrftWeb. Removes the SQL row and the vault entry; logs orphans if vault delete fails after SQL delete succeeds. Delete button + confirmation in the list and detail pages.
W1.6 CMS edit endpoint + edit page
Landed in CMS Wave 3.
New PUT api/cms/track/{id} (metadata only — no binary replacement in Wave 1). /cms/tracks/{id} page.
Phase 2 — Product surface: gallery, browsing, ingestion
2.4 Web-side track upload
Landed in CMS Wave 1 (subsumed by CMS-PLAN.md).
The CLI is the only producer of tracks today. A web upload UI would pair with TrackService.AddTrackFromWavAsync and the existing PUT api/track/{id} (already [ApiKeyAuthorize]-protected).
- Why it matters: Lowers the barrier to adding content. The collective can publish without shell access to the host.
- Shape:
- New page or modal on the web client, drag-and-drop file input.
- Upload streams to a
POSTendpoint onDeepDrftWeb(notDeepDrftContent— the web host orchestrates the dual-write, then forwards bytes to content with the API key it already holds). - Authentication: this is the first user-facing action that needs to be gated. A new question — see open question below.
- Prerequisite: Authentication model for the web side. Currently the site has no user concept. Cookie-with-shared-password? OAuth? Per-collective-member account? Decide before building the UI.
- Open question: Same as above. This may also bring forward a wider session/identity decision that other features (favourites, listening history) will need eventually.
- Constraint: Today's dual-write has no compensating rollback — if content-side succeeds and SQL-side fails, the audio is orphaned in the vault. The CLI inherits this; pushing this onto a web upload increases the rate at which orphans can occur. A simple
DeadLetterLogof orphanedentryKeys (suggested in the audit) becomes more pressing once the web upload exists.
Phase 0 — Wireframe-driven home page redesign
Status: All sub-items landed on 2026-05-17.
A design wireframe (deepdrft-wireframe.html at the project root) is the source of truth for a full visual reskin of the public site. The current Home.razor is a MudPaper/MudGrid composition with a generic "purple-tint" feature card aesthetic that doesn't match the collective's intended voice. The wireframe replaces it with a layout-first, editorial design: 50/50 hero, frosted-glass nav, dark feature band, green origin/connect split, navy CTA banner with ghost-watermark, and an italic-serif accent treatment throughout.
Scope here is the home page and the chrome that wraps it (nav, layout container, theme palette, font loading). The track gallery (TracksView.razor), the audio player dock (AudioPlayerBar.razor), and the FileDatabase/streaming substrate are out of scope for Phase 0 — they keep working through the existing MudBlazor theme, which is being recoloured under them. The "Now Playing" card in the hero is a new surface that reads from the existing IPlayerService cascade; it is a view onto the player, not a replacement for the dock.
Phase 0 sub-items decompose into worktree-sized tracks. 0.1 is the foundation everything else inherits — land it first. 0.2–0.4 can proceed in parallel against that foundation. 0.5 is a follow-on tuning pass once the light theme is in.
0.1 Light palette + font system
- What: Replace the "Charleston in the Day"
PaletteLightinDeepDrftWeb.Client/Layout/MainLayout.razorwith the wireframe palette (--white #FAFAF8,--navy #0D1B2A,--green #1A3C34,--green-accent #3D7A68,--muted #8A9BB0), expressed as MudBlazorPaletteLightproperties. Update the corresponding CSS custom properties inDeepDrftWeb/wwwroot/styles/deepdrft-styles.cssso thedeepdrft-*utility classes still resolve. AddGeist Monoto the Google Fonts<link>inDeepDrftWeb/Components/App.razor. Upgrade the existingCormorantlink toCormorant Garamondwith the italic + 300/400/600 weight set used by the wireframe. Remove theBodoni Modalink (and its--font-heroreference) if no remaining surface uses it. - Why it matters: Every other Phase 0 sub-item consumes these tokens. Fonts and palette landing first means 0.2/0.3/0.4 can render at intended fidelity from the moment they're built, not approximate-then-correct. The font swap is also the only Phase 0 change that affects HTML served by the host project (
App.razor), so isolating it cleanly keeps the render-mode seam clear. - Shape:
- MudBlazor palette mapping (light):
Primary = navy,Secondary = green,Tertiary = green-accent,Background = white,Surface = white,AppbarBackground = "rgba(250,250,248,0.88)",AppbarText = navy,TextPrimary = navy,TextSecondary = muted,Divider = "rgba(13,27,42,0.10)",LinesDefault / TableLinesto match. Semantic colours (Info/Success/Warning/Error) stay at MudBlazor defaults. - Typography block (light):
H1–H6and a new wireframe-specific display class useCormorant Garamond;Button/DefaultkeepDM Sans; introduce aSubtitle1/Captionfamily pointing atGeist Monofor label/eyebrow text. - CSS variables: rename or alias the existing
--deepdrft-primary/--deepdrft-secondary/etc.to the wireframe palette in:root. Add--font-mono: "Geist Mono", monospace;and update--font-hero/--font-headersto"Cormorant Garamond", serif. Where the legacy palette has no wireframe equivalent (e.g.--deepdrft-quaternarywarm gold), prefer mapping it to the closest wireframe colour rather than inventing a new one — the goal is convergence on the new vocabulary, not coexistence. - Font loading: a single Google Fonts link, ideally one combined request with
family=Cormorant+Garamond:ital,wght@…&family=Geist+Mono:wght@…&family=DM+Sans:…. One round-trip, three families.
- MudBlazor palette mapping (light):
- Prerequisite: None — this is the foundation.
- Constraint: The dark palette ("Lowcountry Summer Nights") must stay functional after this change even if visually mismatched — 0.5 is the dedicated pass for re-harmonising it. Do not edit the dark palette in 0.1. The dark-mode cookie +
PersistentComponentStateround-trip described inCLAUDE.mdmust be preserved unchanged.
0.2 Frosted-glass top nav
- What: Replace the current MudBlazor
MudAppBar-basedDeepDrftMenu.razorchrome (logo + nav stack + dark-mode toggle, default Material elevation) with the wireframe's fixed frosted-glass nav: 88% opacity off-white background,backdrop-filter: blur(18px), 1px navy-alpha bottom border, no elevation shadow, navy-on-white "Stream Now" CTA pinned right, nav links in Geist Mono uppercase with the muted-to-navy hover transition. - Why it matters: The nav sits across every page, so its visual language sets expectations for the rest of the site. The Material elevation + dropdown menu pattern is the strongest "this is a stock MudBlazor app" tell currently; replacing it is the single largest perceived-quality move of Phase 0.
- Shape:
- Keep
DeepDrftMenu.razoras the file (the existing render-mode wiring and viewport-subscription mobile branch are reused) — rewrite the markup inside it. - Wrap a styled
<nav>element (orMudAppBarwith heavy CSS override) and bind nav links toPages.AllPages. The link text should render via Geist Mono with the wireframe's letter-spacing and uppercase transform. - The "Stream Now" CTA is a new affordance — wire it to
/tracksfor now (it is functionally a "browse the gallery" action since live streaming isn't a Phase 0 surface). - Dark-mode toggle stays — the gas-lamp icon button moves to the right of the CTA. Confirm visual treatment works against both the frosted-white nav (light) and whatever the dark-mode nav becomes after 0.5.
- Mobile branch: the
MudMenudropdown pattern persists, but the activator + items should adopt Geist Mono and the new colour vocabulary. No drawer.
- Keep
- Prerequisite: 0.1 (palette + Geist Mono load).
- Constraint: The nav is rendered through
MainLayout.razorand therefore participates in server prerender.backdrop-filteris CSS-only and renders identically in both passes, so this is safe — but any JS-driven scroll/show behaviour added later must be gated onOnAfterRenderAsync.IBrowserViewportServiceis already used here for breakpoints and must continue to work after the rewrite. Do not regress the dark-mode toggle wiring (DarkModeCookieService.ToggleDarkModeAsync→ cookie →IsDarkModeChangedevent up).
0.3 Split hero with live Now-Playing card
- What: Replace the current centered MudPaper hero in
DeepDrftWeb.Client/Pages/Home.razorwith the wireframe's 50/50 split:- Left: eyebrow ("Charleston, South Carolina"), display title ("Deep / Drft" with italic green emphasis on "Drft"), italic-serif subtitle, body description, and the two CTAs (
Start Streamingfilled /Browse Tracksghost). All entering via the existingfade-upCSS animation pattern with staggered delays. - Right: dark navy panel with three concentric pulsing rings (CSS keyframe
pulse-ring), a frosted "Now Playing" card (label + blinking dot + track title + sub + animated waveform bars), and the stat row (47+ / 2 / ∞).
- Left: eyebrow ("Charleston, South Carolina"), display title ("Deep / Drft" with italic green emphasis on "Drft"), italic-serif subtitle, body description, and the two CTAs (
- Why it matters: This is the page. Hero is what a first-time visitor sees, and it is the only sub-item that wires the new design back into the live audio system — making the design feel inhabited rather than decorative.
- Shape:
- Now-Playing data source:
Home.razorconsumes[CascadingParameter] IPlayerService Player(cascaded byAudioPlayerProviderfromMainLayout). The card binds toPlayer.IsLoaded,Player.IsPlaying,Player.CurrentTime,Player.Duration.IPlayerServicedoes not currently expose the selectedTrackEntityas a public property — addIPlayerService.CurrentTrack { get; }(nullableTrackEntity) and surface the backing field inAudioPlayerService. Additive, no existing consumer is affected — implement it as part of this sub-item without a separate approval gate. - Empty state: when
Player.CurrentTrack is null, render a placeholder ("Nothing playing — pick a track" or similar) inside the card with the same chrome but no waveform animation. The card is permanent layout, not conditional on selection. - Animated waveform bars: Phase 0 uses the wireframe's pure-CSS
wave-dancekeyframe animation with randomised--h-lo/--h-hi/--durper bar — driven by no real audio data. A later phase can wireSpectrumAnalyzerdata throughAudioInteropService.GetSpectrumData()to drive bar heights, but that path is already used bySpectrumVisualizer.razorin the dock and duplicating it here is out of scope. - Stat row: static markup with hard-coded "47+", "2", "∞" and TODO comments. The first two could plausibly become real numbers (track count, member count from a future identity model) — flag those at the markup site for Phase 2/identity work to pick up.
- Pulsing-ring decoration: three absolutely-positioned divs as in the wireframe, with the
pulse-ringkeyframe. These are decorative and live indeepdrft-styles.cssor aHome.razor.cssscoped stylesheet — pick scoped CSS for anything home-page-specific to keep the global stylesheet from accreting. - Render mode:
Home.razorlives inDeepDrftWeb.Client/Pages/, so it is already WASM-interactive end-to-end. The cascadingIPlayerServiceworks in both server prerender (no track loaded → empty state) and post-WASM (live state). NoOnAfterRenderAsyncgymnastics needed.
- Now-Playing data source:
- Prerequisite: 0.1 (fonts + palette for the markup to render correctly).
- Constraint: Do not introduce a second player implementation or a separate state store. The "Now Playing" card is a view onto the same
IPlayerServiceinstance the dock uses (seeuser_one_source_multiple_views). If the dock plays a track, the hero card reflects it; if the hero card eventually grows controls, those calls go through the same cascade. The hero's CTAs route to/tracksand (eventually) triggerPlayer.SelectTrackfrom there — they do not become a parallel selection surface.
0.4 Marketing content sections (sound / features / origin+connect / CTA / footer)
- What: Replace the remainder of
Home.razorwith five wireframe sections in order:- Section divider (
The Soundtag between horizontal rules). - Sound section —
Genres & Moodslabel,Every / Frequency / Exploredtitle with italic green emphasis, body copy, 6-column genre grid (House / Techno / Trance / IDM / Progressive / Ambient) with the scaleX-from-left bottom border hover affordance. - Dark features section — navy background,
What We Offerlabel, 4-card feature grid (Lossless Audio Streaming,Live Sessions Broadcast,Studio Video Content,Growing Archive) with stroked SVG icons. - Split origin + connect — green-panel origin copy on the left with a soft-circle decoration, white-panel "Stay Connected" on the right with Newsletter + Live Alerts option rows and a
Subscribe FreeCTA. - Navy CTA banner with the ghost
DRFTwatermark, headline, sub, and dual CTAs (Explore the Archivefilled-white /View Live Scheduleoutline-white). - Footer with logo, link list, copyright. Replaces nothing today (there is no footer in the current layout) — add it inside
MainLayout.razorso it appears site-wide, or insideHome.razorif Phase 0 wants it on the home page only. Recommend site-wide.
- Section divider (
- Why it matters: These sections are what carries the editorial voice. They are decorative-but-load-bearing — without them, the home page is just a hero floating in whitespace.
- Shape:
- Genre grid: static cards. Each
genre-cardis a Razor markup block (or a small<GenreCard />component if the duplication grates). Phase 2.2 (album/genre views) will wire these to real filtered routes; for Phase 0, anhref="#"placeholder is acceptable, flagged with aTODO: wire to /genres/{slug} in Phase 2.2comment. - Features grid: the four cards mirror the existing copy on the current
Home.razor("High-Quality Streaming", "Live Sessions", "Video Content", "Growing Archive"). Keep the copy intent; reskin to the wireframe. Inline the four SVG icons from the wireframe (they are already 24-boxviewBoxstroked paths and fitDDIcons.csif a static-icon home is preferred — but inline is fine for Phase 0; only promote toDDIconsif reuse appears). - Origin + Connect split: the origin copy is editorial — adapt the existing "Charleston, SC" copy from the current
Home.razorto the new section. The Connect side has two non-functional rows for Phase 0: Newsletter and Live Alerts are decorative pending an identity/subscription system. Flag them. - CTA banner: the
DRFTghost watermark uses::beforewith a22remfont size — verify it doesn't trigger layout overflow on narrow viewports (the wireframe usesoverflow: hiddenon the parent; replicate that). - Footer: new site-wide affordance. Site root
MainLayout.razoris the right home for it (afterMudMainContent, before the closingMudLayout). UsePages.AllPagesfor the link list to keep the source of truth in one place. - Scoped CSS: these sections are home-page-specific decorative styling. Use
Home.razor.css(scoped stylesheet) for anything that doesn't generalise; reservedeepdrft-styles.cssfor things genuinely shared across pages.
- Genre grid: static cards. Each
- Prerequisite: 0.1 (palette + fonts).
- Constraint: The footer added to
MainLayout.razorrenders on every page, including/tracks. The dock is the bottom-fixed surface; the footer must be in the document flow above it. Confirmed: theAudioPlayerBaralready starts minimized (_isMinimized = true) and expands only on track selection — footer coexistence is acceptable as-is. No suppression logic needed.
0.5 Dark theme harmony pass
- What: Review the existing "Lowcountry Summer Nights"
PaletteDarkagainst the Phase 0 light palette and update it so the dark variant feels like a sibling of the new design vocabulary rather than the old one. The current dark palette is coral/sunset/firefly-gold over deep twilight — that may or may not still read as cohesive once the light side has been pulled to navy/green/off-white. - Why it matters: Dark mode is a first-class affordance (cookie-persisted, prerender-aware). If the dark theme reads as a different product after 0.1–0.4 land, the toggle becomes a surprise rather than a preference. This sub-item is the explicit budget for re-harmonising it instead of letting drift accumulate.
- Shape: Confirmed: Option B (mirror). Rebuild the dark palette as a dark-navy ground —
--navyas background, deeper navy as surface,--green-accentas primary accent,--white(#FAFAF8) as text. Visually consistent with the light theme; the "Lowcountry Summer Nights" coral/sunset identity is retired. Adjust contrast values so text and interactive targets meet WCAG thresholds on the darker ground — the light palette's tokens are a starting point, not a direct copy. - Prerequisite: 0.1–0.4 ideally landed so the harmony evaluation has the actual artefact to look at. Can run in a sketch worktree against 0.1 alone if speed matters.
- Constraint: The dark-mode cookie +
PersistentComponentStateround-trip is untouched. Only the palette values inPaletteDarkand the.deepdrft-theme-darkCSS-variable block change. Do not refactor the toggle, the cookie service, or the prerender bridge — those are tested and load-bearing.
Phase 0 deferred (not in scope)
These would naturally appear when scoping a redesign, and are explicitly not Phase 0:
- Real "Now Playing" waveform from
SpectrumAnalyzer. CSS-keyframe waveform is good enough for Phase 0. Wiring real spectrum data into the hero card duplicates work already done in the dock and is better folded into a future "shared spectrum hook" refactor. - Real stat-row numbers. Track count would need a
GET api/track/countendpoint or a count column in the paged response; member count needs an identity model. Hard-coded with TODO is intentional. - Genre-filter routes. Genre cards are decorative in 0.4. Real
/genres/{slug}is Phase 2.2 work. - Subscribe / Live Alerts functionality. Both rows are visual placeholders. Real subscription requires email collection + storage + an identity decision (see "Cross-cutting / not yet themed").
TracksView.razorreskin. The gallery has its own composition (TracksGallery→TrackCard) that deserves its own design pass, not a Phase 0 retrofit. It continues to work under the recoloured MudBlazor theme.AudioPlayerBar.razorreskin. Same logic. The dock works against the new palette via MudBlazor tokens; a dedicated dock redesign is out of scope.- Animation library / scroll-triggered fades. The wireframe's
fade-upis CSS-only with hard-coded delays. Anything richer (IntersectionObserver, framer-motion-equivalent) is post-Phase 0.