docs: resolve TrackDto nesting (§0.3) and add §8.0 wave sequencing
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.
This commit is contained in:
@@ -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)*
|
||||
|
||||
|
||||
@@ -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<ReleaseDto>`. **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<ReleaseDto>` (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<TrackDto>` 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 |
|
||||
|
||||
Reference in New Issue
Block a user