# CLAUDE.md - DeepDrftPublic.Client Guidance for working in the DeepDrftPublic.Client project (the Blazor WebAssembly assembly). See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project. ## One-line purpose All interactive UI for the site. Blazor WebAssembly. Pages, controls, the streaming audio player stack, theme/dark-mode plumbing, HTTP clients for both backends. ## Actual structure - `Pages/`: Routable components. `Home.razor` (hero/about), `SessionDetail.razor` (session detail — hero-dominant overlay composition rendered via ``: large background hero image with darkening gradient shim, cover thumbnail + title + play button overlaid near the hero's bottom, genre/date/share overlaid at the top; uses `MudContainer MaxWidth="Large"`; **does not compose `ReleaseDetailScaffold`** — `PlayTrack` is wired directly in its own `@code` block; mounts `` ambient engine + `` directly; renders `` below the hero for the release's description blurb), `MixDetail.razor` (mix detail — composes `ReleaseDetailScaffold` with `TopRightAction` lava-lamp ``; hero+meta rendered via `` in the scaffold's `Hero` slot with `ShowHeader="false"` suppressing the duplicate masthead; square ~600px cover-as-background with metadata overlaid; full-bleed `` is the mode-A centerpiece mounted by the page directly; renders `` below the hero for the release's description blurb), `CutDetail.razor` (album detail — composes `ReleaseDetailScaffold` with the `Ambient` slot carrying `` + `` for mode-B ambient layer; renders `` below the hero for the release's description blurb). **No demo pages** (`Counter.razor`, `Weather.razor` do not exist). - `Layout/`: `MainLayout.razor` (root layout, wraps in `AudioPlayerProvider`, hosts theme switcher), `DeepDrftMenu.razor` (branded menu bar), `NavMenu.razor` (nav list), `Pages.cs` (centralised nav index — `MenuPages` for header, `AllPages` for exhaustive list). - `Controls/`: Reusable components. - `TrackCard.razor`: Individual track display (image, name, artist, album, genre, release date). Play/pause icon controlled via `IsPaused` parameter. - `TracksGallery.razor`: Responsive grid of `TrackCard` items (MudBlazor `MudGrid` with breakpoints). Fully controlled by parent; derives active-track state from cascaded player service. - `AppNavLink.razor`: Nav link with active-page highlight. - `AudioPlayerProvider.razor`: Cascading host for `IStreamingPlayerService`. Everything inside it gets the player via `[CascadingParameter]`. - `StreamNowButton.razor`: Reusable streaming-trigger button. Fetches a random track, warms the AudioContext (Safari gesture requirement), and starts streaming via `IStreamingPlayerService`. Accepts `ButtonClass` and `ButtonLabel` for distinct visual presentations; `OnStreamStarted` EventCallback for post-stream side effects (e.g., mobile menu close). - `AudioPlayerBar.razor`: Dock UI at the bottom (play/pause/seek/volume). - `AudioPlayerBar/PlayerControls.razor`: Play/pause/stop buttons in the transport zone. Renders via ``. - `AudioPlayerBar/PlayStateIcon.razor`: Icon button encapsulating service subscription + transport-state icon selection. Injects `IPlayerService`, subscribes to `StateChanged`, calls `PlaybackIcons.Resolve()` to determine icon and active state. - `AudioPlayerBar/LevelMeterFab.razor`: Floating-action button replacing the static FAB in the minimized dock. Renders a continuous vertical fill inside the music-note silhouette that tracks live audio level (0–100%), with fixed three-zone gradient (green 0–60%, yellow 60–85%, orange 85–100%). Note silhouette always visible at 25% opacity; idle when paused/stopped. Reuses spectrum-callback infrastructure. - `SpectrumVisualizer.razor`: Bar-graph spectrum display, driven by `getSpectrumData` JS callback. - `ReleaseHeroOverlay.razor`: Shared presentational overlay shell consumed by both `SessionDetail` and `MixDetail`. Renders a background-image hero region with genre/date/share overlaid at the top and title/artist/play at the bottom. Parameters: `HeroImageKey`, `PlaceholderIcon`, `CoverThumbKey` (optional cover thumb in bottom row), `Title`, `Artist`, `Genre`, `ReleaseDate`, `ShareContent` (slot), `PlayContent` (slot), `Class` (per-page aspect/sizing override). Owns no player logic or data fetch; each consuming page passes its own play and share slots. Overlay shell is plain `
`s; background-image surface is a `
` (no `MudPaper`). - `ReleaseDescription.razor`: Shared presentational blurb block for release detail pages. Renders a header row (mono uppercase eyebrow label "Notes" in `.deepdrft-release-description-label` + thin horizontal divider in `.deepdrft-release-description-rule`, inside `.deepdrft-release-description-header`) above the release's short `Description` paragraph (`.deepdrft-release-description-text`, display-serif weight 300), all wrapped in `.deepdrft-release-description`, placed just below the hero/header. The header row mirrors the home page's `.section-label`/`.divider-line` motif. Purely presentational — owns no data fetch or player wiring. A null/whitespace `Description` renders nothing at all (no block, no divider, no whitespace artifact; the guard lives here so every consumer gets it for free). Parameter: `Description` (`string?`). - `WaveformVisualizer.razor`: The single WebGL2 lava-lamp visualizer engine. Hosts the waveform of whatever track is currently playing/selected. Three hosting modes: mode A (Mix detail — full-bleed centerpiece), mode B (Cut/Session detail — ambient layer behind hero+content via `ReleaseDetailScaffold`'s `Ambient` slot), mode C (NowPlaying hero panel — full-bleed background for the home hero's right side, mounted by `NowPlaying.razor` inside `.np-visualizer-bg`). `[Parameter] bool Fill` switches from fixed-viewport positioning to container-relative sizing (CSS-only; the renderer is identical in both modes). The bridge resolves the current track's `EntryKey` and re-fetches the high-res datum on track change. Subscribes to `WaveformVisualizerControlState.Changed` and pushes each updated dial to the WebGL module via JS interop. Follows the live playing track (keys on host `TrackId` match OR shared host `ReleaseEntryKey`). - `WaveformVisualizerControls.razor`: The waveform visualizer control panel (content hosted by `WaveformVisualizerControlPopover`). Phase 15 re-layout: a deterministic **three-row sectioned layout** encoding the visualizer's two subsystems. Row 1 (MODE, always visible): two iconographic lamp toggles (lava on/off, waveform on/off) left-aligned + collisions knob (conditional — only when both subsystems on) + color knob pinned far-right. Row 2 (LAVA, visible only when `LavaEnabled`): "LAVA:" section label + Gravity / Heat / FluidAmount / FluidViscosity knobs. Row 3 (WAVE, visible only when `WaveformEnabled`): "WAVE:" section label + scroll-speed `MudSlider` (not a knob) + width knob pinned far-right. Total: two lamp toggles, seven `RadialKnob`s, one `MudSlider`. Colour principle: lamp toggles / knob arcs / slider are green (`Color.Primary` — interactive); section labels / knob caption icons are light (static). Each control has a playful `MudTooltip`. `[Parameter] bool PanelChrome` scopes panel chrome (NowPlayingCard look — square corners, lighter-navy, thin border) to the popover mount; chrome classes live in the global `deepdrft-styles.css` (CSS isolation cannot reach portaled overlay content). `[Parameter] bool Visible` gates the rows via `@if` while the container holds reserved min-height. Owns no JS interop: mutates the injected `WaveformVisualizerControlState` and raises `Changed`. No control is a seek surface (read-only contract). - `WaveformVisualizerControlPopover.razor`: Pairs the lava-lamp icon button with `WaveformVisualizerControls` as a **screen-centered tinted modal** (Phase 15). The primitive is `MudOverlay` (`DarkBackground="true"`, `Modal="true"`) — **not** `MudPopover`; `AnchorOrigin`/`TransformOrigin` parameters do not exist (a centered modal has no anchor). Clicking the lava-lamp icon opens the overlay; clicking the scrim closes it (knob-drag-safe: `RadialKnob`'s `position:fixed` capture div sits above the scrim during a drag, so releasing outside the panel never fires the close handler). The panel stops click propagation so an inside click is not a dismissal. `[Parameter] Size IconSize` controls the trigger-icon size (default `Large`). This is the unit every host places — one icon anywhere gives the full control panel centered on screen, regardless of where the icon sits. Placed identically on Mix, Cut, Session, and the NowPlaying hero panel (full parity; in NowPlaying it sits in `.np-visualizer-controls` at the panel's top-right corner, not inside `NowPlayingCard`). - `WaveformZoomMapping.cs`: Maps the `WaveformVisualizerControlState.Resolution` fraction to an integer zoom level for the WebGL renderer. - `NowPlayingCard.razor`: Home-page text panel showing the currently playing track (label, title, sub-line). Renders label/"Now Playing" dot, track name, and artist·release sub-line from the cascaded `IStreamingPlayerService`. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` (reference-guarded, idempotent) and unsubscribes on dispose to re-render on track/state change. No visualizer or popover; those moved to `NowPlaying.razor`. - `NowPlayingStats.razor`: Home hero stat row. Three cards: Studio Cuts (total Cut-medium track count + zero-suppressed per-`ReleaseType` Cut release breakdown), Mixes (`MixReleaseCount` labelled "Sets" + `hh:mm` total mix runtime via `RuntimeFormat`), and Plays (static "XXX / Plays (Coming Soon)" odometer placeholder). Fetches `HomeStatsDto` via `IStatsDataService` on init; bridges the prerender fetch across the WASM seam with `PersistentComponentState` (persists only on a successful load, matching the medium-browse bridge pattern). Implements `IDisposable` to release the `PersistingComponentStateSubscription`. - `NowPlaying.razor`: Owns the home hero's right-side panel (`.now-playing-panel` — the outer wrapper formerly called `.hero-right` in `Home.razor`). Mounts `` as a full-bleed background inside `.np-visualizer-bg`, `` in `.np-visualizer-controls` (top-right corner), the three pulsing `.circle-deco` rings, and the content layer (hosts `` + ``). `Home.razor`'s `MudItem` renders `` directly with no wrapper. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` (reference-guarded, idempotent) and unsubscribes on dispose — needed because the player cascade is `IsFixed` (the provider's own re-render does not reach `NowPlaying`), so the subscription is the only way to re-render and re-propagate `ReleaseEntryKey`/`TrackId`/`TrackEntryKey` into `` when the playing track changes. - `ReleaseDetailScaffold.razor`: Shared scaffold for release detail pages. Gained an optional `Ambient` `RenderFragment` slot (Phase 12) — a full-bleed layer rendered behind the main content. Absent slot = no regression. Cut mounts `` + `` here; Mix uses its own full-bleed mount outside the scaffold. - `Helpers/`: Utilities and mapper functions. - `PlaybackIcons.cs`: Static `Resolve(isPlaying, isPaused, trackId, currentTrackId)` method — the sole glyph-mapping source for transport icons across all surfaces. Returns `(Icon, IsActive, IsPaused)` tuple. - `RuntimeFormat.cs`: Static `ToHoursMinutes(double totalSeconds)` helper. Formats a seconds value as `h:mm` (hours not zero-padded, minutes always two digits). Negative / non-finite inputs return `"0:00"`. Used by `NowPlayingStats` for the mix runtime figure. - `Services/`: Audio player + dark-mode services. - `IPlayerService` / `IStreamingPlayerService`: Contracts exposed to UI. - `AudioPlayerService`: Abstract base (lifecycle, initialise, select track, play/pause/stop/seek/volume). - `StreamingAudioPlayerService`: Production implementation. Chunked stream from `TrackMediaClient`, adaptive 16–64 KB buffer, early-playback, **seek-beyond-buffer** via offset request to the content API. - `AudioInteropService`: JS interop wrapper over `window.DeepDrftAudio`. Manages `DotNetObjectReference` lifetimes for progress, end-of-playback, spectrum callbacks. - Dark-mode services: `DarkModeServiceBase` (cookie name constant), `DarkModeCookieService` (JS cookie read/write). - `WaveformVisualizerControlState`: Scoped session-persistent holder for the visualizer's **eight** continuous control positions plus **two subsystem on/off toggles** (Phase 15): `ScrollSpeed`, `GradientRotationSpeed`, `LavaGravity`, `LavaHeat`, `FluidAmount` (wax count/volume), `FluidViscosity` (cohesion — the second half of the Phase 10 "bubbles" split; `BlobDensity` is gone), `CollisionStrength`, `WaveformWidth`, `LavaEnabled` (bool, default `true`), `WaveformEnabled` (bool, default `true`). Each has a matching `Default*` const. `Changed` event is the decoupling seam — controls mutate state + raise `Changed`; the bridge (`WaveformVisualizer`) subscribes and pushes the affected uniform or subsystem-enable. Scoped DI so state survives SPA nav within a session and resets on fresh page load. - `Clients/`: HTTP API clients (both target DeepDrftAPI). - `TrackClient`: SQL metadata API. Uses named `IHttpClientFactory` client `"DeepDrft.API"`. Sends `page` param (not `pageNumber`). Deserializes response as bare `PagedResult` (not wrapped in ApiResultDto envelope). - `TrackMediaClient`: Content API. Uses named `IHttpClientFactory` client `"DeepDrft.Content"`. Methods like `GetAudioStreamAsync(trackId, byteOffset?)` → `Stream` with optional Range header support for seek-beyond-buffer. - `StatsClient`: Home stats API. Uses named `IHttpClientFactory` client `"DeepDrft.API"`. Single method `GetHomeStats()` → `ApiResult` (calls `GET api/stats/home`; response is a bare DTO, no `ApiResultDto` envelope). Registered scoped; consumed via `IStatsDataService`. - `Services/ITrackDataService`: Contract used by the visualizer bridge and other consumers. Includes `GetTrackWaveform(entryKey)` → high-res `WaveformProfileDto` (calls `GET api/track/{entryKey}/waveform/high-res`); used by `WaveformVisualizer` to re-fetch the datum on track change. - `Services/IStatsDataService` / `StatsClientDataService`: Home-stats read abstraction. `IStatsDataService.GetHomeStats()` → `ApiResult`. `StatsClientDataService` is the single implementation (delegates to `StatsClient`); registered scoped. Components inject `IStatsDataService` so they do not branch on render mode — mirrors `IReleaseDataService`. - `ViewModels/`: Component state. - `TracksViewModel`: Scoped. Holds current page, page size, sort column, descending flag. `SetPage(pageNumber)` calls `TrackClient.GetPageAsync` and updates. Registered in `Startup.ConfigureDomainServices`. - `TrackDetailViewModel`: Scoped. Holds loaded track, loading flag, not-found flag. `Load(entryKey)` fetches via `ITrackDataService` and resets all flags per call (prevents cross-navigation bleed). Registered in `Startup.ConfigureDomainServices`. - `Common/`: Shared utilities. - `DarkModeSettings.cs`: `[PersistentState]`-annotated class (single source of truth for dark mode in the client). Registered scoped. - `ReleaseRoutes.cs`: Static helper. `DetailHref(long id, ReleaseMedium)` returns the canonical public detail route for a release; consumed by Archive, AlbumsView, player bar, and TrackRedirect (11.B). - `Program.cs`: WASM entry point. Calls `Startup.ConfigureApiHttpClient`, `ConfigureContentServices`, `ConfigureDomainServices`. - `_Imports.razor`: Global using statements and component imports. ## Two HTTP clients pattern Both clients are configured in `Startup.cs` (static methods called from **both** server and WASM `Program.cs`): - `TrackClient` uses `"DeepDrft.API"` (base address from `appsettings.json` `ApiUrls:SqlApi`, points to DeepDrftAPI). Fetches paginated metadata. - `TrackMediaClient` uses `"DeepDrft.Content"` (base address from `appsettings.json` `ApiUrls:ContentApi`, points to DeepDrftAPI). Streams audio bytes, optionally with offset. Both are configured with JSON serializer settings (case-insensitive property matching). The dual-client pattern keeps concerns separated: one for structured data, one for binary streaming. Both target DeepDrftAPI (they may share the same host URL in production). ## Audio player stack (deepest part of the codebase) ### Contracts - `IPlayerService`: Initialize, SelectTrack, Play, Pause, Stop, Seek, SetVolume. Sync interface. Owns `EventCallback? OnStateChanged` (single, provider-owned) and `event Action? StateChanged` (multicast, for cascade consumers). - `IStreamingPlayerService`: Extends above. SelectTrackStreaming(track) starts the chunked stream flow. ### Implementation - `AudioPlayerService` (abstract base): Lifecycle. Stores current track, playback state, volume. `SelectTrack` throws `NotSupportedException` (buffered path is dead); derived classes override `SelectTrackStreaming`. - `StreamingAudioPlayerService` (production): Constructor takes `TrackMediaClient`, `AudioInteropService`, logger. `SelectTrackStreaming`: 1. Calls `TrackMediaClient.GetAudioStreamAsync(trackId)`, which returns a response object including `ContentType` (e.g., `audio/wav`, `audio/mpeg`, `audio/flac`). 2. `StreamingAudioPlayerService.StreamAudioAsync` reads chunks (16–64 KB adaptive), pushes each via `AudioInteropService.ProcessStreamingChunkAsync(contentType, chunk)` (JS interop call with format hint). 3. TypeScript `StreamDecoder` is format-agnostic; delegates format-specific header parsing and chunked decoding to the appropriate `IFormatDecoder` implementation (e.g., `WavFormatDecoder` for WAV, TBD MP3/FLAC decoders for other formats). Decoder parses header (first chunk), decodes subsequent chunks to `AudioBuffer`s. 4. `PlaybackScheduler` schedules buffers on Web Audio `AudioContext`. 5. Playback starts as soon as a configurable min buffer count is queued. 6. **Seek beyond buffer**: if seek target is past the decoded range, `Seek(position)` calls `TrackMediaClient.GetAudioStreamAsync(trackId, byteOffset)` with a file-absolute byte offset. Client sends `Range: bytes={offset}-`; server responds 206 with raw bytes (same format as original file); decoder retains the parsed header and feeds the continuation directly into the decode pipeline. ### Interop bridge - `AudioInteropService.CreatePlayerAsync` polls `DeepDrftAudio.isReady()` before proceeding; `index.ts` sets `ready = true` after attaching the API to `window`. This guards against slow WASM boot / cache misses. - `AudioInteropService.ProcessStreamingChunkAsync(contentType, chunk)` calls JS `window.DeepDrftAudio.processStreamingChunk(contentType, chunk)` and awaits the Promise. The `contentType` parameter is passed through to the format-decoder factory. - `AudioInteropService` also manages callback registrations for progress (fired by `PlaybackScheduler`), end-of-playback (fired by `PlaybackScheduler`), and spectrum data (fired by `SpectrumAnalyzer`). Each callback is a `DotNetObjectReference` to a delegate. ### Format decoders (TypeScript) New modules in `DeepDrftPublic/Interop/audio/`: - `IFormatDecoder.ts`: Interface. Defines contract for format-specific decoders: `parseHeader(chunk, offset)` → header metadata; `decodeChunk(chunk, offset)` → `AudioBuffer`; `getAlignedSegmentSize(chunk, offset, rawData?)` → frame-aligned segment boundary (optional `rawData` parameter for format-specific frame-boundary scanning). - `WavFormatDecoder.ts`: Concrete WAV implementation (active). Parses RIFF/WAVE structure, fmt and data chunks. All WAV-specific byte-parsing logic lives here. Exported as the default WAV decoder. - `Mp3FormatDecoder.ts`: Concrete MP3 implementation (implemented, not yet wired). Implements `IFormatDecoder` for MP3: ID3v2 skip, MPEG Layer III frame-sync + header decode (MPEG1/2/2.5), Xing/Info/VBRI VBR-header detection (frame count + 100-entry TOC for seek), CBR frame-aligned segment sizing, VBR TOC-interpolation seek (`calculateByteOffset`), zero-copy `wrapSegment` (raw MP3 frames are self-contained). CBR sub-frame tail guard prevents over-read. - `FlacFormatDecoder.ts`: Concrete FLAC implementation (implemented, not yet wired). Implements `IFormatDecoder` for FLAC: scans all metadata blocks (STREAMINFO mandatory, SEEKTABLE optional), extracts 20-bit sample rate / 3-bit channels / 5-bit bitsPerSample / 36-bit total-samples from bit-packed STREAMINFO, builds 38-byte synthetic STREAMINFO block for per-segment wrapping, binary-search SEEKTABLE for seek. `wrapSegment` prepends `fLaC + STREAMINFO` to each audio segment so `decodeAudioData` sees a valid FLAC stream. `getAlignedSegmentSize` scans backward through peek bytes for the `0xFF/(0xF8|0xF9)` FLAC frame sync so each segment ends on a real frame boundary. `StreamDecoder.ts` remains the orchestrator — it accepts the first chunk, selects the right format decoder via factory (based on `contentType`), peeks candidate bytes before calling `getAlignedSegmentSize` (non-destructive read), passes them as `rawData`, and uses zero-copy `subarray` for the actual segment. It delegates all format-specific work to the decoder and chains subsequent chunks through the same decoder instance. `Mp3FormatDecoder` and `FlacFormatDecoder` are implemented modules but not yet wired into `AudioPlayer.createFormatDecoder` factory (Wave 3 pending). ### Component integration - `AudioPlayerProvider.razor` is the cascading host. It injects `IStreamingPlayerService` (resolved to `StreamingAudioPlayerService` in DI), stores it in a cascade with `IsFixed="true"`, and keeps it alive across navigation. - `AudioPlayerBar.razor` is the dock UI. It cascades the player, binds buttons to `Play()` / `Pause()` / `Seek()` / `SetVolume()`, and displays current time / duration / progress bar. Minimize-state mutations (`Expand`, `ToggleMinimized`, `Close`) all route through a private `SetMinimized(bool value)` mutator, which guards no-ops, fires the `OnMinimized` callback, and calls `StateHasChanged()`. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` (reference-guarded, idempotent) and unsubscribes on dispose to re-render itself when the cascade updates. - `SpectrumVisualizer.razor` calls `AudioInteropService.GetSpectrumData()` on a timer, receives bar heights, renders via MudBlazor `MudChart` or custom canvas. - `TracksView.razor` injects `TracksViewModel` + cascaded `IStreamingPlayerService`. `PlayTrack(track)` calls `PlayerService.SelectTrackStreaming(track)`. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` and calls `StateHasChanged()` unconditionally on any state change, ensuring the gallery correctly reflects play/pause/track-change transitions. Active-track state is derived from `PlayerService.CurrentTrack` and `PlayerService.IsPlaying` (no local `_selectedTrack` field). ## Dark-mode plumbing - `DarkModeSettings` (`Common/`): `[PersistentState]`-annotated class with `IsDarkMode` property. Registered scoped in `Startup.ConfigureDomainServices`. Single source of truth in the client. - `DarkModeServiceBase`: Holds the cookie name constant (`"darkMode"`). - `DarkModeCookieService`: Reads/writes the cookie via JS (`document.cookie` interop). Calls `DarkModeSettings.IsDarkMode = value` when the cookie changes or user toggles the button. - Server-side `DarkModeService` (in `DeepDrftPublic`, **not here**): Reads the cookie during prerender, seeds the `DarkModeSettings` instance, rounds it through `PersistentComponentState` to the client. - `MainLayout.razor`: Wraps entire layout in `CascadingValue` of `DarkModeSettings`, so all children see the current dark-mode state. The dark-mode toggle button (hand-rolled lit/unlit gas-lamp icon from `DeepDrftShared.Client/Common/DDIcons.cs`) calls `DarkModeCookieService.ToggleDarkModeAsync()`. The flow ensures the first paint uses the correct theme (no flash), and toggling the button persists the setting to a 365-day cookie. ## MVVM convention Component state lives in ViewModels (registered scoped in DI). Components render and dispatch only. - `TracksViewModel`: Holds page number, page size, sort column, descending flag. `SetPage(pageNumber)` is the command. `TracksView.razor` injects it and calls `SetPage`. - New VMs go in `ViewModels/` and register in `Startup.ConfigureDomainServices`. ## Theming convention - Bespoke `PaletteLight` / `PaletteDark` defined inline in `MainLayout.razor` (MudBlazor theme objects). - CSS classes prefixed `deepdrft-` live in `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` (shared across server and client). - Custom SVG icons: `DeepDrftShared.Client/Common/DDIcons.cs` (hand-rolled gas-lamp, lava-lamp, etc. — shared across public and CMS surfaces). ## Development commands ```bash # The client runs as part of the DeepDrftPublic host: dotnet run --project DeepDrftPublic # Watch during development (rebuilds WASM as you change .cs/.razor/.ts files): dotnet watch run --project DeepDrftPublic # Build just the client (for verification): dotnet build DeepDrftPublic.Client # Run client-specific tests (if any; currently none exist): dotnet test DeepDrftTests/ ``` ## Configuration - `Program.cs`: Entry point. Calls `Startup.ConfigureApiHttpClient` (registers named clients), `ConfigureContentServices` (same), `ConfigureDomainServices` (registers services like `TracksViewModel`, `DarkModeSettings`, `AudioPlayerService`). - Both `Startup` methods are static and called from **both** the server `DeepDrftPublic/Program.cs` and the client `Program.cs`, ensuring prerender and runtime DI are identical. - No `appsettings.json` in the WASM assembly — config comes from the server `appsettings.json` via HTTP or is hardcoded. ## Important patterns - **Cascading parameters**: `AudioPlayerProvider` cascades `IStreamingPlayerService`. All children (including `MainLayout` and pages) access it via `[CascadingParameter] IStreamingPlayerService Player { get; set; }`. - **Result types**: Clients return `ApiResult` from NetBlocks. UI checks `Success` before using `Value`. - **Async/await**: All operations are async. - **Stream consumption**: `TrackMediaClient.GetAudioStreamAsync` returns a `Stream` (not fully buffered). `StreamingAudioPlayerService` reads it in chunks to avoid memory pressure on large files. - **Detail pages under InteractiveAuto must load in `OnParametersSetAsync`, not `OnInitializedAsync`**: Blazor reuses a scoped component instance across same-template navigations (e.g. `/mixes/5` → `/mixes/8`), firing `OnParametersSet`/`OnParametersSetAsync` rather than re-running `OnInitialized`. If load logic is in `OnInitialized` only, the ViewModel retains the prior track and Play will stream the wrong item. Capture the route parameter (id/key) synchronously at the top of `OnParametersSetAsync` before any await — after an await the route state may have advanced. Guard `PersistentComponentState` restores on id/key equality to prevent cross-item bleed when the prerender and WASM-boot passes disagree on which item is current. When working with this project, maintain the separation between presentation (Razor components) and logic (ViewModels/Clients), follow the established audio player architecture, and respect the dark-mode round-trip (cookie → DarkModeSettings → PersistentComponentState → client).