From df7acd9e80f8847fb2d6a36d5eb39722725f7782 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 18 Jun 2026 13:14:52 -0400 Subject: [PATCH] docs: reflect live home-hero stats (duration column, stats endpoint, backfill, NowPlayingStats wiring) --- CLAUDE.md | 10 +++++----- COMPLETED.md | 18 ++++++++++++++++++ DeepDrftAPI/CLAUDE.md | 24 ++++++++++++++++++++++++ DeepDrftData/CLAUDE.md | 9 +++++++-- DeepDrftModels/CLAUDE.md | 15 ++++++++++++++- DeepDrftPublic.Client/CLAUDE.md | 4 ++++ WASM_SEAMS.md | 3 ++- 7 files changed, 74 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 58ad998..6a6fc6b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,11 +9,11 @@ DeepDrftHome is a **net10.0** solution consisting of ten projects implementing a ### Core Projects - **DeepDrftPublic**: ASP.NET Core host. Blazor Web App with Server + WASM render modes. Owns browser-facing proxy controller for `api/track/*` (metadata listing and audio streaming), MudBlazor theme prerender, and TypeScript→JS audio interop. Public-facing site for listeners. -- **DeepDrftPublic.Client**: Blazor WebAssembly assembly. All interactive UI (pages, player stack, dark-mode plumbing, HTTP clients for both backends). Pages include the public `/about` editorial page (`Pages/About.razor` — three-movement **"Liner Notes"** editorial treatment: numbered left-rail (oversized Bodoni numerals + vertical hairline spine + mono marginalia captions), asymmetric content column, pull-quotes breaking into the margin, hand-authored SVG waveform movement dividers (self-contained motif, not the live `WaveformVisualizer`), and stacked editorial definition list for CUTS/SESSIONS/MIXES; active-movement highlight via `about-rail.ts` IntersectionObserver interop; registered in `Layout/Pages.cs`). Consumed by the public site. +- **DeepDrftPublic.Client**: Blazor WebAssembly assembly. All interactive UI (pages, player stack, dark-mode plumbing, HTTP clients for both backends). Pages include the public `/about` editorial page (`Pages/About.razor` — three-movement **"Liner Notes"** editorial treatment: numbered left-rail (oversized Bodoni numerals + vertical hairline spine + mono marginalia captions), asymmetric content column, pull-quotes breaking into the margin, hand-authored SVG waveform movement dividers (self-contained motif, not the live `WaveformVisualizer`), and stacked editorial definition list for CUTS/SESSIONS/MIXES; active-movement highlight via `about-rail.ts` IntersectionObserver interop; registered in `Layout/Pages.cs`). Home hero stat row (`NowPlayingStats.razor`) is live-data-backed via `IStatsDataService` / `StatsClient` (named `"DeepDrft.API"` client) with a `PersistentComponentState` prerender bridge; `RuntimeFormat` helper converts mix runtime seconds to `hh:mm`. Consumed by the public site. - **DeepDrftManager**: ASP.NET Core host. Blazor Web App with server-rendered `InteractiveServer` render mode. Hosts all CMS Razor components and pages under `Components/Pages/Cms/`, `Components/Pages/Tracks/`, `Components/Layout/CmsLayout.razor`, and `Components/Shared/` (all inlined from the former `DeepDrftCms` RCL). Public entry point: `Components/Pages/Home.razor` (`@page "/"`, no `[Authorize]`, uses lean `CmsHomeLayout`) — unauthenticated visitors see a DeepDrft-branded splash with a Login CTA; authenticated admins are redirected to `/catalogue` via `RedirectToCatalogue`. The catalogue dashboard (`Components/Pages/Index.razor`) lives at `@page "/catalogue"` and remains `[Authorize]`-gated with `CmsLayout`; its cards are **CUTS / SESSIONS / MIXES**, each deep-linking to `/releases?medium=` with the matching tab pre-selected. The consolidated browse surface is `Components/Pages/Tracks/Releases.razor` (`@page "/releases"`): bulk-action buttons (Generate All Profiles / Backfill High-res) → medium tab strip (ALL / CUTS / SESSIONS / MIXES) → the active tab's grid; waveform columns (Profile / High-res) — each showing a status icon when a datum is present and an always-visible generate/regenerate button — and per-track info tooltip live in `CmsAlbumBrowser`'s expanded child-row track table. Old list routes `/tracks`, `/tracks/albums`, `/tracks/archive` are kept as aliases on `Releases.razor` so bookmarks don't 404; operational sub-routes (`/tracks/upload`, edit routes, etc.) remain at `/tracks/*`. Gated by AuthBlocks login and hierarchical `Admin` role authorization. All track operations (upload, metadata read/write, delete) are HTTP proxies via `ICmsTrackService` / `CmsTrackService` injected directly into Blazor components; no in-process data layer. Two named HttpClients: `DeepDrft.Content.Cms` (bounded 100 s default, for all non-upload calls) and `DeepDrft.Content.Cms.Upload` (`InfiniteTimeSpan`, for large WAV uploads). Upload progress and idle/heartbeat timeout are driven by a single `ProgressStreamContent` wrapper (`Services/ProgressStreamContent.cs`); `CmsTrackService.UploadTrackAsync` adds a two-phase cancellation (idle window resets per progress tick; separate response-wait budget arms when the body completes). - **DeepDrftShared.Client**: Razor Class Library. Shared Blazor components consumed by both `DeepDrftPublic` and `DeepDrftManager` for consistency across public and admin surfaces. - **DeepDrftData**: Class library. EF Core domain logic: `DeepDrftContext`, `TrackConfiguration`, `Migrations`, `TrackRepository`, `TrackService`, `TrackManager`. Consumed by `DeepDrftAPI` and tests. -- **DeepDrftAPI**: ASP.NET Core host. Dual-database authority (SQL metadata + FileDatabase binary). AuthBlocks API host (owns registration, migration/seed, JWT endpoints). Track endpoints: streaming, vault write, upload+persist, delete+cleanup, paged list with filters, single metadata (ApiKey-gated operations), metadata update, waveform profiles (512-bucket seeker + per-track high-res visualizer datum in the `track-waveforms` vault), release-track join operations. Release endpoints: paged list with medium filter, single read, session hero-image upload (all unauthenticated reads; authenticated writes via ApiKey). Image endpoints: authenticated upload, unauthenticated streaming. +- **DeepDrftAPI**: ASP.NET Core host. Dual-database authority (SQL metadata + FileDatabase binary). AuthBlocks API host (owns registration, migration/seed, JWT endpoints). Track endpoints: streaming, vault write, upload+persist, delete+cleanup, paged list with filters, single metadata (ApiKey-gated operations), metadata update, waveform profiles (512-bucket seeker + per-track high-res visualizer datum in the `track-waveforms` vault), release-track join operations, `POST api/track/duration/backfill` (ApiKey-gated one-time backfill of `DurationSeconds` for existing rows from vault audio). Stats endpoints: `GET api/stats/home` (unauthenticated; returns `HomeStatsDto` with cut track count, per-`ReleaseType` cut release counts, mix release count, and total mix runtime seconds). Release endpoints: paged list with medium filter, single read, session hero-image upload (all unauthenticated reads; authenticated writes via ApiKey). Image endpoints: authenticated upload, unauthenticated streaming. - **DeepDrftContent**: Class library. The FileDatabase implementation in full (Models, Services, Utils, Abstractions, Constants), `AudioProcessor`, content-side `TrackService`. Consumed by hosts and tests. - **DeepDrftModels**: Shared contracts. `TrackEntity`, `TrackDto`, `PagingParameters`, `PagedResult`. Every project references this. - **DeepDrftTests**: NUnit test suite. Comprehensive FileDatabase tests (vault creation, media storage, indexing, factory patterns, utilities). Integration-focused with temp-directory test isolation. @@ -34,7 +34,7 @@ Server-side (SSR): Both clients point directly at DeepDrftAPI (server-to-server, 1. **SQL Database (PostgreSQL)**: Metadata and track info via Entity Framework - Connection string: Read from `environment/connections.json` via `CredentialTools.ResolvePathOrThrow("connections")` with key `ConnectionStrings:DefaultConnection`. - - Entity: `TrackEntity` with `Id`, `EntryKey`, `TrackName`, `Artist`, `Album?`, `Genre?`, `ReleaseDate?`, `ImagePath?` + - Entity: `TrackEntity` with `Id`, `EntryKey`, `TrackName`, `Artist`, `Album?`, `Genre?`, `ReleaseDate?`, `ImagePath?`, `DurationSeconds?` - Context: `DeepDrftContext` in `DeepDrftData` 2. **FileDatabase**: Custom file-based binary storage system @@ -114,10 +114,10 @@ dotnet run --project DeepDrftAPI ### Entity Framework (SQL Database) ```bash # Add migration (from solution root) -dotnet ef migrations add MigrationName --project DeepDrftData --startup-project DeepDrftPublic +dotnet ef migrations add MigrationName --project DeepDrftData --startup-project DeepDrftAPI # Update database -dotnet ef database update --project DeepDrftData --startup-project DeepDrftPublic +dotnet ef database update --project DeepDrftData --startup-project DeepDrftAPI ``` ## Key Configuration Files diff --git a/COMPLETED.md b/COMPLETED.md index 99c9bec..f8b3f91 100644 --- a/COMPLETED.md +++ b/COMPLETED.md @@ -6,6 +6,24 @@ Newest entries at the top. Group by phase/wave header (mirroring `PLAN.md` / `CM --- +## Home Hero Stats — Live data wiring (landed 2026-06-18) + +**Landed:** 2026-06-18 on dev (commits `5f0422a` + `8fa330f`, merged `e9e6b60`). + +- **What:** Replaced the hard-coded placeholder figures in the public home hero stat row (`NowPlayingStats`) with real SQL-backed aggregates. Resolves the "Real stat-row numbers" deferred item from Phase 0 §0.3. + +- **Why:** The stat row ("47+ / 2 / ∞") was intentionally hard-coded at Phase 0 with a TODO; the data model now has enough shape (releases, medium discriminator, track–release join) to serve real numbers in a single efficient query. + +- **Shape:** + - **New SQL column:** `DurationSeconds` (`double?`, column `duration_seconds`) on `TrackEntity` and `TrackDto`. Populated at upload via the existing dual-database add flow (`TrackContentService` extracts duration from vault audio; `UnifiedTrackService` persists it to SQL). Migration `20260618155002_AddTrackDuration`. Configured in `TrackConfiguration`. + - **New aggregate query:** `TrackRepository.GetHomeStatsAsync` → `HomeStatsDto` (new DTO in `DeepDrftModels/DTOs/`). Returns cut track count, per-`ReleaseType` cut release counts (zero-count types suppressed), mix release count, and total mix runtime seconds (null durations counted as 0; tracks under soft-deleted releases excluded). Surfaced via `ITrackService.GetHomeStats` on `TrackManager`. + - **New API endpoints:** `GET api/stats/home` (`StatsController`, unauthenticated; returns `HomeStatsDto` bare) and `POST api/track/duration/backfill` (ApiKey-gated; one-time backfill of `DurationSeconds` for pre-existing rows from vault audio, delegated to `UnifiedTrackService.BackfillDurationsAsync`). + - **New public proxy:** `StatsProxyController` in `DeepDrftPublic` mirrors `ReleaseProxyController`; forwards `GET api/stats/home` from the browser to DeepDrftAPI. + - **New client surface:** `StatsClient` (`Clients/`, named `"DeepDrft.API"` client) + `IStatsDataService` / `StatsClientDataService` (`Services/`) registered scoped in `Startup.ConfigureDomainServices`. `RuntimeFormat` static helper (`Helpers/`) converts seconds to `hh:mm`. + - **`NowPlayingStats.razor`:** now renders live data — Studio Cuts card (cut track count + zero-suppressed Single/EP/Album breakdown), Mixes card (`MixReleaseCount` "Sets" + `hh:mm` runtime), Plays card (static "XXX / Coming Soon" odometer placeholder). Uses `PersistentComponentState` to bridge the SSR prerender fetch across the WASM seam (only persists on a successful load). + +--- + ## Phase 12 — About Page (public site editorial) (landed 2026-06-17) **Landed:** 2026-06-17 on dev. diff --git a/DeepDrftAPI/CLAUDE.md b/DeepDrftAPI/CLAUDE.md index 6fd1dcd..b133149 100644 --- a/DeepDrftAPI/CLAUDE.md +++ b/DeepDrftAPI/CLAUDE.md @@ -110,6 +110,16 @@ Admin backfill view: returns every track with flags indicating whether each wave - **Response**: `List` with `TrackId`, `EntryKey`, `TrackName`, `HasProfile` (bool — 512-bucket player-bar seeker profile in `waveform-profiles` vault), and `HasHighRes` (bool — duration-derived high-res visualizer datum in `track-waveforms` vault). - Returns 200 on success. Returns 500 on query error. +### POST api/track/duration/backfill ([ApiKeyAuthorize]) + +Admin backfill: for every track whose `DurationSeconds` SQL column is still null, reads the `AudioBinary.Duration` from the vault and writes it to SQL. Idempotent — a re-run only touches still-null rows; rows that already have a value are skipped. + +- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`. +- No request body. +- Calls `UnifiedTrackService.BackfillDurationsAsync`. Lives on `TrackController` in the literal-route block (before `{trackId}` routes, so the segment is never treated as a trackId). +- **Response**: `{ updated: int, skipped: int }` — counts of rows written vs. already-populated rows bypassed. +- Returns 200 on success. Returns 500 if the backfill operation fails. + ### DELETE api/track/release/{id:long} ([ApiKeyAuthorize]) Soft-delete a release row. Used by the albums browser to remove an orphaned release (one with no live tracks). @@ -272,6 +282,20 @@ Stores a hero image in the `images` vault and links it via `SessionMetadata.Hero - Validates MIME type (rejects unsupported types with `.bin` sentinel). Calls `UnifiedReleaseService.SetHeroImageAsync`. - Returns 200 on success. Returns 400 for missing file or unsupported MIME type. Returns 404 if release not found. Returns 500 on processing or vault failure. +## The stats endpoints + +### GET api/stats/home (unauthenticated) + +Aggregate figures behind the public home hero stat row (`NowPlayingStats`). A single read returns everything the three cards need so the client makes one round-trip. Public, same auth posture as `GET api/track/page`. + +- **Response**: `HomeStatsDto` with: + - `CutTrackCount` (int): total non-deleted tracks on Cut-medium releases. + - `CutReleaseTypeCounts` (`List`): per-`ReleaseType` Cut release counts; zero-count types are absent (zero-suppressed server-side). + - `MixReleaseCount` (int): total non-deleted Mix-medium releases. + - `MixRuntimeSeconds` (double): sum of `DurationSeconds` across all non-deleted tracks on Mix releases (null durations count as 0). +- Aggregated in `TrackRepository.GetHomeStatsAsync`, surfaced via `ITrackService`/`TrackManager`. Controller is `StatsController` — a thin HTTP boundary; no domain logic lives there. +- Returns 200 on success. Returns 500 on query error. + ## ApiKey middleware behaviour `ApiKeyAuthenticationMiddleware` runs on every request but only enforces on endpoints with `[ApiKeyAuthorize]` metadata. diff --git a/DeepDrftData/CLAUDE.md b/DeepDrftData/CLAUDE.md index b9fc324..a4db6cb 100644 --- a/DeepDrftData/CLAUDE.md +++ b/DeepDrftData/CLAUDE.md @@ -42,6 +42,7 @@ DeepDrftData/ - `Album`, `Genre`: optional, max 200 / 100 - `ReleaseDate`: optional `DateOnly` - `ImagePath`: optional, max 500 (currently a free-form URL string; points to images vault in future) +- `DurationSeconds`: optional `double?` (nullable; populated at upload from vault audio; backfillable via `POST api/track/duration/backfill`; used for aggregate mix-runtime queries). Column: `duration_seconds`. Migration: `20260618155002_AddTrackDuration`. ## Service → Repository → DbContext shape @@ -49,6 +50,10 @@ DeepDrftData/ - **Repository** (`TrackRepository`): Internal data access. Queries the DbContext. Throws on error (service catches). - **DbContext** (`DeepDrftContext`): EF Core. Directly accessed by repository, never by service (pattern isolation). +Notable repository / service methods beyond the standard CRUD: + +- `TrackRepository.GetHomeStatsAsync` / `ITrackService.GetHomeStats`: Returns `HomeStatsDto` — cut track count, per-`ReleaseType` cut release counts (zero-suppressed), mix release count, total mix runtime seconds (null durations counted as 0; tracks under a soft-deleted release excluded). Used by `StatsController`. + Example: ```csharp @@ -117,10 +122,10 @@ Run from the solution root: ```bash # Add a migration -dotnet ef migrations add MigrationName --project DeepDrftData --startup-project DeepDrftPublic +dotnet ef migrations add MigrationName --project DeepDrftData --startup-project DeepDrftAPI # Apply to database -dotnet ef database update --project DeepDrftData --startup-project DeepDrftPublic +dotnet ef database update --project DeepDrftData --startup-project DeepDrftAPI ``` The design-time factory means you can also run `dotnet ef ... --project DeepDrftData` standalone for local development (it doesn't need the startup project). diff --git a/DeepDrftModels/CLAUDE.md b/DeepDrftModels/CLAUDE.md index f92e661..151fe95 100644 --- a/DeepDrftModels/CLAUDE.md +++ b/DeepDrftModels/CLAUDE.md @@ -15,7 +15,8 @@ DeepDrftModels/ ├── Entities/ │ └── TrackEntity.cs # Database entity for tracks ├── DTOs/ -│ └── TrackDto.cs # DTO mirror of TrackEntity +│ ├── TrackDto.cs # DTO mirror of TrackEntity +│ └── HomeStatsDto.cs # Aggregate figures for the public home hero stat row ├── Models/ │ ├── PagingParameters.cs # Pagination configuration (base + generic) │ └── PagedResult.cs # Paginated result wrapper @@ -35,6 +36,7 @@ public string? Album { get; set; } // Optional album (max 200) public string? Genre { get; set; } // Optional genre (max 100) public DateOnly? ReleaseDate { get; set; } // Optional release date public string? ImagePath { get; set; } // Optional image URL (max 500) +public double? DurationSeconds { get; set; } // Audio runtime in seconds (nullable; populated at upload, backfilled for older rows) ``` **No `MediaPath` field exists.** That was a legacy name. The field is `EntryKey`. @@ -47,6 +49,17 @@ Convention: required reference fields use `required` modifier; optional referenc Mirrors `TrackEntity` structure (identical fields, same nullability). Used where DTO/entity separation is needed for serialisation. In practice, both flow over the wire today, but the separation is available if APIs need to diverge (e.g., hide `Id` in responses). +## HomeStatsDto + +Aggregate figures behind the public home hero stat row (`NowPlayingStats`). A single round-trip returns everything the three cards need. Fields: + +- `CutTrackCount` (int): total non-deleted tracks on Cut-medium releases. +- `CutReleaseTypeCounts` (`List`): per-`ReleaseType` Cut release counts; zero-count types are absent (suppressed server-side). +- `MixReleaseCount` (int): total non-deleted Mix-medium releases. +- `MixRuntimeSeconds` (double): sum of `DurationSeconds` across all non-deleted tracks on Mix releases (null durations count as 0). Rendered as `hh:mm` by `RuntimeFormat` on the client. + +`CutReleaseTypeCount` is a nested type (`ReleaseType`, `Count` int) defined in the same file. + ## Pagination system ### PagingParameters (base) diff --git a/DeepDrftPublic.Client/CLAUDE.md b/DeepDrftPublic.Client/CLAUDE.md index 99f49fa..f77e03e 100644 --- a/DeepDrftPublic.Client/CLAUDE.md +++ b/DeepDrftPublic.Client/CLAUDE.md @@ -30,10 +30,12 @@ All interactive UI for the site. Blazor WebAssembly. Pages, controls, the stream - `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). @@ -44,7 +46,9 @@ All interactive UI for the site. Blazor WebAssembly. Pages, controls, the stream - `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`. diff --git a/WASM_SEAMS.md b/WASM_SEAMS.md index bd93cd6..f70bfa2 100644 --- a/WASM_SEAMS.md +++ b/WASM_SEAMS.md @@ -89,7 +89,8 @@ Addresses **G2**. No code change. The remediation is a one-line code comment (st - **`DeepDrftManager` (CMS).** Render mode is `InteractiveServer` (server-rendered, single lifecycle — no SSR→WASM handoff). The whole class of bug does not exist there. Not audited beyond confirming the render mode. - **`DeepDrftData`, `DeepDrftContent`, `DeepDrftAPI`.** Server-side only; never reach WASM. No client lifecycle. - **`AudioPlayerProvider` / `StreamingAudioPlayerService` / `AudioPlayerBar` / `SpectrumVisualizer` / `PlayStateIcon` / `WaveformSeeker`.** These subscribe to the player's `StateChanged` multicast and re-render off live runtime state. They hold no prerender-fetched data to persist — the player cannot be live during prerender (gesture-gated). Their `OnParametersSet`/`OnAfterRender` subscription logic is correct fencing, not a missing persist. Leave them. -- **`NowPlayingStats`, `DeepDrftHero` stat row, genre cards, feature cards on `Home.razor`.** Fully static markup (hard-coded copy, no fetch). Same content on both passes → no flip. The only animated one is the hero (G1/R1); the rest have no entrance animation (`Home.razor.css` has only steady-state `transition:` rules on hover/theme, lines 127/145/199/368/489/509 — those fire on interaction, not mount, and are correct). +- **`NowPlayingStats` stat row on `Home.razor`.** Live data — fetches `HomeStatsDto` via `IStatsDataService`/`StatsClient` (`GET api/stats/home`) and bridges the prerender→WASM fetch through `PersistentComponentState` (same Mode A seam as S1/S2). On the WASM pass the persisted `HomeStatsDto` is restored before any fetch, so there is no skeleton-to-content swap or re-fetch. No entrance animation, no flip. Already correct; do not touch. +- **`DeepDrftHero` genre cards, feature cards on `Home.razor`.** Fully static markup (hard-coded copy, no fetch). Same content on both passes → no flip. No entrance animation (`Home.razor.css` has only steady-state `transition:` rules on hover/theme, lines 127/145/199/368/489/509 — those fire on interaction, not mount, and are correct). - **Infinite/steady-state CSS animations** — `circle-deco` pulse-ring (`NowPlaying.razor.css`), waveform `wave-dance`/`blink` (`NowPlayingCard.razor.css`), spectrum bars. These loop continuously by design; a remount restarting their loop is imperceptible (they never "settle"). Not entrance animations, not a regression. - **CSS / JS asset staleness across deploys.** Separate concern, already handled: `@Assets[...]` fingerprints CSS (`App.razor`), `` fingerprints the audio JS module graph. Do not conflate with the seam work.