From 16f356a760e17d2c8a8f5273aa1f2e043afd4d58 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 11 Jun 2026 11:09:24 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20resolve=20TrackDto=20nesting=20(=C2=A70?= =?UTF-8?q?.3)=20and=20add=20=C2=A78.0=20wave=20sequencing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve Phase 8 open question 0.3 — TrackDto gets a nested Release (ReleaseDto); flat release fields removed, all consumers updated as part of §8.0 (flat read-model rejected). Add §0.6 implementation sequencing: five mergeable waves with Waves 1+2 as a single deployment unit and Waves 3+4 parallelizable. Update PLAN.md §8.0 Shape to match. --- PLAN.md | 4 +- product-notes/phase-8-cms-track-browser.md | 126 ++++++++++++++++++--- 2 files changed, 110 insertions(+), 20 deletions(-) diff --git a/PLAN.md b/PLAN.md index 2dda922..6bd1f95 100644 --- a/PLAN.md +++ b/PLAN.md @@ -138,9 +138,9 @@ Three browse modes for the CMS `/tracks` page — **Track**, **Album**, **Genre* ### 8.0 `TrackEntity` normalization (pre-requisite — must land before §8.1–§8.5) -- **What:** Split the flat `TrackEntity` into two normalized tables. New **`ReleaseEntity`** holds release-cardinal data (`Title`, `Artist`, `Genre?`, `ReleaseDate?`, `ImagePath?`, `ReleaseType`, `CreatedByUserId?`). Slimmed **`TrackEntity`** holds track-cardinal data only (`Id`, `ReleaseId` FK, `Release` nav, `EntryKey`, `TrackName`, `TrackNumber`, `OriginalFileName?`) — the release fields are removed from it. New `ReleaseDto`; `TrackDto` slims and gains `ReleaseId` + a Release access path; `AlbumSummaryDto` is retired in favour of `ReleaseDto`. +- **What:** Split the flat `TrackEntity` into two normalized tables. New **`ReleaseEntity`** holds release-cardinal data (`Title`, `Artist`, `Genre?`, `ReleaseDate?`, `ImagePath?`, `ReleaseType`, `CreatedByUserId?`). Slimmed **`TrackEntity`** holds track-cardinal data only (`Id`, `ReleaseId` FK, `Release` nav, `EntryKey`, `TrackName`, `TrackNumber`, `OriginalFileName?`) — the release fields are removed from it. New `ReleaseDto`; `TrackDto` slims and gains `ReleaseId` + a **nested `Release` (`ReleaseDto`)** (resolved 2026-06-11: nested, not a flat read model — flat fields are removed and every consumer is updated, not denormalized back); `AlbumSummaryDto` is retired in favour of `ReleaseDto`. - **Why:** The flat schema duplicates release-level metadata on every track row — updating an album's cover art or artist means rewriting every track. Phase 8 introduces album-as-a-unit editing (Batch Edit, album-scoped delete), so the model should match the domain: a Release is first-class; Tracks belong to a Release. This collapses several §8 UI open questions (Album-mode parent rows become Release rows directly; no `GROUP BY`-derived summary). -- **Shape:** Breaking migration — create `releases`, populate from distinct `(album, artist)` groups in `tracks`, add + populate `release_id` FK, drop the redundant track columns. Consumer impact reaches the public client (`DeepDrftPublic.Client` reads the flat `TrackDto` fields for the gallery) as well as the CMS — flagged as the highest-risk consumer in the notes. Open questions: nullable release FK for album-less tracks (recommend yes), upload auto-create-or-find Release (recommend yes), flat-vs-nested `TrackDto` read shape for the public API (recommend flat read model). Full spec + migration steps + consumer-impact list: notes §0. +- **Shape:** Sequenced as **five mergeable waves** (notes §0.6): (1) data model — `ReleaseEntity`/config/migration in `DeepDrftData`; (2) DTOs/services/repositories/API — `ReleaseDto`, slimmed `TrackDto`, JOIN-projecting repository, upload find-or-create Release; **Waves 1 + 2 are a single deployment unit** (removing the entity fields breaks compile until the DTO/service layer lands — never merge Wave 1 alone); (3) public-client consumers (`TrackCard`, `TrackDetail`, `TrackMetaLabel`, `NowPlayingCard`) re-point to `track.Release.*`; (4) existing CMS surfaces (`TrackEdit`, `TrackNew`, `BatchUpload`, `TrackList`) minimally updated to compile on the normalized model — Waves 3 + 4 run in parallel; (5) the Phase 8 UI (§8.1–§8.5) begins only after 1–4 are stable. The breaking migration: create `releases`, populate from distinct `(album, artist)` groups, add + populate `release_id` FK, drop redundant track columns. Remaining open questions for Daniel: nullable release FK for album-less tracks (recommend yes), upload auto-create-or-find Release (recommend yes — committed in Wave 2 shape). Full spec, wave breakdown, and per-file consumer list: notes §0 / §0.6. ### 8.1 URL scheme + mode toggle *(depends on §8.0)* diff --git a/product-notes/phase-8-cms-track-browser.md b/product-notes/phase-8-cms-track-browser.md index 71aefab..3c00e6b 100644 --- a/product-notes/phase-8-cms-track-browser.md +++ b/product-notes/phase-8-cms-track-browser.md @@ -2,7 +2,9 @@ Status: spec / one VM, three views. Review decisions folded in 2026-06-11 (waveform-in-grid, DI-scoped VM, BatchEdit extraction confirmed). **A data-model normalization (§0) is now a hard -pre-requisite — it must land before any Phase 8 UI work.** Author: product-designer. +pre-requisite — it must land before any Phase 8 UI work**, sequenced as five mergeable waves +(§0.6). Open Q3 resolved 2026-06-11: `TrackDto` gets a **nested `Release` (`ReleaseDto`)**, flat +release fields removed, all consumers updated as part of §8.0. Author: product-designer. Date: 2026-06-11. **Plan only — no code edits made by this doc.** @@ -83,22 +85,26 @@ The normalization also collapses several open questions in the UI spec below: - New `ReleaseDto` mirroring `ReleaseEntity`. - `TrackDto` slims down: remove `Artist`, `Album`, `Genre`, `ReleaseDate`, `ImagePath`, - `ReleaseType`, `CreatedByUserId`. Add `ReleaseId` (long) and, for read paths that display release - data alongside track data, either a nested `Release` (`ReleaseDto`) or a flat denormalized read - model (open question 3). + `ReleaseType`, `CreatedByUserId`. Add `ReleaseId` (long) and a **nested `Release` (`ReleaseDto`)** + property — populated on reads, ignored on writes where only the FK matters. (Open question 3 + **RESOLVED 2026-06-11**: nested `ReleaseDto`, not a flat read model. See §0.3 below.) - `AlbumSummaryDto` is retired / collapsed into `ReleaseDto` (open question 4). **Consumer-impact list (the most consumer-visible part of this change):** The existing `GET api/track/page` response shape changes. Callers that read `TrackDto.Artist` / -`.Album` / `.Genre` / `.ReleaseDate` / `.ImagePath` must adjust to read those from the Release -(`TrackDto.Release.Artist` / `.Release.Title` / …) or from a flat joined read model. Known -consumers: +`.Album` / `.Genre` / `.ReleaseDate` / `.ImagePath` / `.ReleaseType` must adjust to read those from +the nested Release: `TrackDto.Release.Artist`, `.Release.Title` (was `.Album`), `.Release.Genre`, +`.Release.ReleaseDate`, `.Release.ImagePath`, `.Release.ReleaseType`. **This is not a "keep the flat +fields populated" no-op — the flat fields are removed and every reader is updated.** Known consumers +(full per-file enumeration in §0.6 below): -- **`DeepDrftPublic.Client`** — the public track gallery (`TrackCard` and the views feeding it) reads - `Artist`, `Album`, `Genre`, `ReleaseDate`, `ImagePath` for display. **This is the highest-risk - consumer** — a public-facing surface, a different host, not part of the CMS phase otherwise. -- **`DeepDrftManager`** — the CMS (this phase's surface). +- **`DeepDrftPublic.Client`** — the public track gallery (`TrackCard`, `TrackDetail`, + `TrackMetaLabel`, `NowPlayingCard`) reads `Artist`, `Album`, `Genre`, `ReleaseDate`, `ImagePath` + for display. **This is the highest-risk consumer** — a public-facing surface, a different host, + not part of the CMS phase otherwise. Updated in Wave 3 (§0.6). +- **`DeepDrftManager`** — the CMS (this phase's surface). The existing `TrackEdit` / `TrackNew` / + `BatchUpload` forms read/write the flat fields. Updated in Wave 4 (§0.6). **Service / repository impact (list, not prescription):** @@ -119,11 +125,15 @@ consumers: or does the endpoint auto-create a Release if none exists for the given album name? **Recommend auto-create-or-find** (upsert on `(title, artist)`) — keeps upload-form ergonomics close to today's. -3. **`TrackDto` shape for the public API.** The public gallery reads flat `Artist` / `Album` / - `Genre` / `ReleaseDate` / `ImagePath`. Keep these queryable post-normalization via either a flat - joined read model (no nested object) or a nested `Release` property. **Recommend the flat - read-model for the public API** — no response-shape break if the flat fields stay populated from - the join. Call this out as the most consumer-visible impact. +3. **~~`TrackDto` shape for the public API.~~ — RESOLVED 2026-06-11.** `TrackDto` gets a **nested + `Release` (`ReleaseDto`)** property; the release-cardinal flat fields (`Artist`, `Album`, `Genre`, + `ReleaseDate`, `ImagePath`, `ReleaseType`, `CreatedByUserId`) are **removed** from `TrackDto` and + live only on `ReleaseDto`. The flat read-model alternative is **rejected** — denormalizing the + release fields back onto every `TrackDto` would re-introduce exactly the duplication §0 + normalizes away, just at the contract layer instead of the table layer. All consumers that read + the flat fields are updated as part of §8.0 (Waves 3 + 4, §0.6) — this is mandatory, not + follow-on. This is the most consumer-visible impact of the phase; the consumer enumeration is + §0.6. 4. **`AlbumSummaryDto` fate.** With a Release table, `GetAlbumSummariesAsync` becomes `GetReleasesAsync` returning `List`. **Recommend retiring `AlbumSummaryDto`** in favor of `ReleaseDto` — a direct replacement. @@ -147,6 +157,86 @@ where they mention `AlbumSummaryDto` widening or deriving release fields from ch as resolved by §0** — the Release table supplies those fields directly. Net of §0, those UI sections get *simpler*, not harder. +### 0.6 Implementation sequencing — appropriately sized waves + +§8.0 is a breaking data-model change with a wide consumer blast radius, so it is sequenced into +independently-mergeable waves. The unit of mergeability is: *can this wave land on `dev` without +breaking the next wave's prerequisites?* Two waves (1 + 2) are a forced exception — flagged below. + +**Wave 1 — Data model layer (no app-code changes yet).** *Touches only `DeepDrftData`.* + +- `ReleaseEntity` (new). `TrackEntity` updated: add `ReleaseId` FK + `Release` navigation property, + remove the release-cardinal fields (`Artist`, `Album`, `Genre`, `ReleaseDate`, `ImagePath`, + `ReleaseType`, `CreatedByUserId`). +- `ReleaseConfiguration` (EF Core `IEntityTypeConfiguration`): table name, column names, FK + relationship, indexes. `TrackConfiguration` updated for the FK + removed columns. +- EF Core migration: create `releases` table; add `release_id` FK to `tracks`; **data migration** — + populate `releases` from distinct `(album, artist)` groups in existing `tracks` rows, then assign + the FK back to each track; finally drop the now-redundant columns from `tracks`. +- **CRITICAL — Wave 1 + Wave 2 are a single deployment unit.** Removing the entity fields breaks + every DTO mapper, repository projection, and service that reads them — the solution will not + compile at the end of Wave 1 alone. Wave 1 and Wave 2 must be developed and merged to `dev` + **together** (one PR, or a short-lived branch merged as a unit) before anything is built or run. + Do not attempt to land Wave 1 on its own. + +**Wave 2 — DTOs, services, repositories, API layer.** *`DeepDrftModels`, `DeepDrftData`, +`DeepDrftContent`, `DeepDrftAPI`.* Deploys together with Wave 1. + +- New `ReleaseDto` (mirrors `ReleaseEntity`). +- `TrackDto`: remove flat release fields; add `ReleaseId` (long) + `Release` (`ReleaseDto`, + populated on reads, ignored on writes where only the FK matters). +- `TrackRepository` (`GetPaged` / `GetById` / `Update` / etc.): queries JOIN `releases` and project + into the updated `TrackDto` with nested `Release`. Sort-by-release-field (artist/album/genre/date) + now sorts through the join. +- `TrackManager` / `ITrackService`: signature updates where the changed fields surface. +- `UnifiedTrackService.UploadAsync`: resolve a Release (**find-or-create by title + artist**) before + inserting the Track. The upload form still sends album/artist at the top level; the service layer + resolves the Release FK. (This is open question 2's recommended upsert, now committed.) +- `TrackController` endpoints: request/response shapes updated where they touch the changed fields. +- `AlbumSummaryDto` retired; `GetAlbumSummariesAsync` returns `List` (rename to + `GetReleasesAsync`). `ICmsTrackService.GetAlbumSummariesAsync` replaced (or aliased) accordingly. + +**Wave 3 — Consumer updates: `DeepDrftPublic.Client` (public site).** Can run in parallel with +Wave 4. Read off the nested `Release` everywhere the flat fields were read. Files verified against +source 2026-06-11: + +| File | Flat fields read today | Change | +|---|---|---| +| `Controls/TrackCard.razor` | `Artist`, `Album`, `Genre`, `ReleaseDate`, `ImagePath` (grid + row modes) | → `TrackModel.Release.Artist` / `.Release.Title` / `.Release.Genre` / `.Release.ReleaseDate` / `.Release.ImagePath` | +| `Pages/TrackDetail.razor` | `Artist`, `Album`, `Genre`, `ReleaseDate`, `ImagePath` (masthead, cover, meta block, `hasMeta` guard) | same nested re-pointing | +| `Controls/AudioPlayerBar/TrackMetaLabel.razor` | `Artist`, `Genre`, `ReleaseDate` (player-bar label) | → `Track.Release.*` | +| `Controls/NowPlayingCard.razor` | `CurrentTrack.Artist`, `CurrentTrack.Album` | → `CurrentTrack.Release.Artist` / `.Release.Title` | + +- `TrackCard.razor.cs`, `TracksGallery.razor.cs`, `TrackDetailViewModel.cs`, + `TrackDetail.razor.cs`: pass `TrackDto` through but **do not read the flat fields** — no change + needed beyond recompiling against the new contract. Listed so the implementer confirms rather than + assumes. +- **Not flat-field consumers (do not confuse):** `Pages/AlbumsView.razor` reads + `AlbumSummaryDto.Album` and `Pages/GenresView.razor` reads `GenreSummaryDto.Genre` — these are + *summary* DTOs, affected by the `AlbumSummaryDto` retirement in Wave 2, not by the `TrackDto` + flat-field removal. Reconcile them when Wave 2 settles the summary-DTO shape. + +**Wave 4 — Consumer updates: `DeepDrftManager` (existing CMS surfaces).** Can run in parallel with +Wave 3. These must work against the normalized model *before* the Phase 8 UI work (Wave 5) starts. + +| File | Today | Change | +|---|---|---| +| `Components/Pages/Tracks/TrackEdit.razor` | `TrackEditForm.From(track)` reads `track.Artist/.Album/.Genre/.ImagePath/.ReleaseDate/.ReleaseType`; `SaveAsync` calls `UpdateAsync` with the flat args | read from `track.Release.*`; `UpdateAsync` path follows Wave 2's signature (still passes album/artist, service resolves the Release) | +| `Components/Pages/Tracks/TrackNew.razor` | binds `_album`/`_artist`/`_genre`/`_releaseDate`, calls `UploadTrackAsync` + cover-link `UpdateAsync` | form fields can stay; the upload service now auto-resolves the Release (Wave 2). Binding unchanged externally, resolution changes internally | +| `Components/Pages/Tracks/BatchUpload.razor` | album-header flat fields sent on upload | same — service auto-creates/finds the Release; form fields stay, internal binding adjusts | +| `Components/Pages/Tracks/TrackList.razor` | the `MudTable` columns bind `Artist`/`Album`/`Genre`/`ReleaseDate` | re-point to `.Release.*`. **Note:** this grid is superseded by §8.2 `CmsTrackGrid` in Wave 5 — update it minimally in Wave 4 only to keep `dev` green, since Wave 5 rebuilds it | + +- Most CMS surfaces touched here are *rebuilt* in Wave 5 (Phase 8 UI). Wave 4's job is the minimum + to keep the existing CMS compiling and functional on the normalized model — not to polish surfaces + that Phase 8 replaces. Flag in each PR which edits are throwaway-pending-Wave-5 vs. durable. + +**Wave 5 — Phase 8 UI (the original §8.1–§8.5).** Begins only after Waves 1–4 are merged and the +normalized model is stable on `dev`. `CmsTrackGrid`, `CmsAlbumBrowser`, `CmsGenreBrowser`, +`BatchEdit`, the mode toggle, and routing — all the UI work specced in §1–§13 below. The internal +sequencing *within* Wave 5 is §13. + +**Dependency summary:** `[1 + 2 together] → {3, 4 in parallel} → 5`. + --- ## 1. Summary @@ -662,8 +752,8 @@ the confirm copy unambiguous about the blast radius. | Contract | Change | Why | Risk | |---|---|---|---| | **`TrackEntity` / `ReleaseEntity`** (§0) | **Split** flat `TrackEntity` into `ReleaseEntity` + slim `TrackEntity` | normalization; pre-requisite gate | **High — breaking migration; see §0** | -| **`TrackDto`** (§0) | Slim down (drop release fields, add `ReleaseId` + Release access path) | follows the entity split | High — touches public + CMS consumers (§0.3) | -| **`ReleaseDto`** (§0) | **New**, mirrors `ReleaseEntity`; retires `AlbumSummaryDto` | Album mode reads the Release table directly | Medium | +| **`TrackDto`** (§0) | Slim down (drop release flat fields, add `ReleaseId` + **nested `Release` (`ReleaseDto`)**) | follows the entity split; nested not flat (open Q3 resolved 2026-06-11) | High — touches public + CMS consumers; all updated in Waves 3 + 4 (§0.3, §0.6) | +| **`ReleaseDto`** (§0) | **New**, mirrors `ReleaseEntity`; retires `AlbumSummaryDto` | Album mode reads the Release table directly; also the nested read object on `TrackDto` | Medium | | `ICmsTrackService.GetPagedAsync` | **Add** `string? album = null, string? genre = null` params (or an overload) | All three modes' filtered reads route through here; endpoint already supports the query filters. Post-§0 the filter joins through `releases` | Low — additive, default-null keeps callers compiling | | `CmsTrackService` (impl) | Pass `album`/`genre` through to the `api/track/page` query string | wire the above | Low | | `TrackDto.HasWaveformProfile` (bool) | **Add** (recommended over a second lookup, §8 item 7) | in-grid waveform status column without per-page fan-out | Low — additive; fold into the §0 DTO pass |