docs: gate Phase 8 on TrackEntity normalization (§8.0); fold review decisions
Add §8.0 TrackEntity → Release/Track normalization as a breaking pre-requisite before Phase 8 UI. Fold in review decisions: Waveform tab removed (in-grid status column + per-row/page-level generate), ViewModel is DI-scoped (TracksViewModel pattern), BatchEdit confirmed as a new page sharing extracted sub-components. Dissolve the AlbumSummaryDto widening question (Release table supplies the fields directly).
This commit is contained in:
@@ -1,11 +1,151 @@
|
||||
# Phase 8 — CMS Track Browser
|
||||
|
||||
Status: spec / one VM, three views. Several open decisions flagged in §10 — needs Daniel sign-off
|
||||
before implementation. Author: product-designer. Date: 2026-06-11.
|
||||
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.
|
||||
Date: 2026-06-11.
|
||||
**Plan only — no code edits made by this doc.**
|
||||
|
||||
Cross-references: `PLAN.md §8` (the concise phase entry), `PLAN.md §6.2` (the deferred
|
||||
"card-contextual filtering" item this phase supersedes), memory *One source, multiple views*.
|
||||
Cross-references: `PLAN.md §8` (the concise phase entry, with §8.0 the normalization gate),
|
||||
`PLAN.md §6.2` (the deferred "card-contextual filtering" item this phase supersedes), memory
|
||||
*One source, multiple views*.
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-requisite: Phase 8.0 — `TrackEntity` Normalization
|
||||
|
||||
**This must land before any of §8.1–§8.5 UI work.** It is a breaking data-model change, not a UI
|
||||
change, and every UI section below assumes the normalized schema. Daniel required this gate during
|
||||
review on 2026-06-11.
|
||||
|
||||
### 0.1 What
|
||||
|
||||
Split the current flat `TrackEntity` into two normalized tables.
|
||||
|
||||
**`ReleaseEntity` (new)** — release-cardinal data (the "header" shared across all tracks in a
|
||||
release):
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `Id` | `long` | PK |
|
||||
| `Title` | `string` | the album/release name (currently `TrackEntity.Album`) |
|
||||
| `Artist` | `string` | |
|
||||
| `Genre?` | `string` | |
|
||||
| `ReleaseDate?` | `DateOnly` | |
|
||||
| `ImagePath?` | `string` | cover art |
|
||||
| `ReleaseType` | `ReleaseType` enum | |
|
||||
| `CreatedByUserId?` | `long` | |
|
||||
|
||||
**`TrackEntity` (updated)** — track-cardinal data only:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `Id` | `long` | PK |
|
||||
| `ReleaseId` | `long` (FK → `ReleaseEntity.Id`) | nullable — see open question (1) |
|
||||
| `Release` | navigation property | |
|
||||
| `EntryKey` | `string` | FileDatabase link (immutable) |
|
||||
| `TrackName` | `string` | |
|
||||
| `TrackNumber` | `int` | 1-based |
|
||||
| `OriginalFileName?` | `string` | |
|
||||
|
||||
*Removed from `TrackEntity`:* `Artist`, `Album`, `Genre`, `ReleaseDate`, `ImagePath`,
|
||||
`ReleaseType`, `CreatedByUserId` — these now live on the Release.
|
||||
|
||||
### 0.2 Why
|
||||
|
||||
The current flat schema duplicates release-level metadata (artist, album, genre, release date,
|
||||
cover art) on every track row. This was fine at launch but creates consistency hazards: updating an
|
||||
album's cover art or artist name means rewriting every track row. Phase 8 introduces album-mode
|
||||
editing (Batch Edit, album-scoped delete), so the data model should match the domain — a Release is
|
||||
a first-class entity; Tracks belong to a Release.
|
||||
|
||||
The normalization also collapses several open questions in the UI spec below:
|
||||
|
||||
- `AlbumSummaryDto` becomes a simplified `ReleaseDto` (query the Release table, not `GROUP BY`).
|
||||
- `CmsAlbumBrowser` parent rows map directly to `ReleaseEntity` rows — no derivation needed.
|
||||
- The `AlbumSummaryDto` widening question (old §6 option ii / §12.3) **dissolves entirely** — the
|
||||
Release table just has all the fields.
|
||||
- Batch Edit's "header" = the Release record; "track list" = the Track records.
|
||||
|
||||
### 0.3 Shape (direction, not implementation prescription)
|
||||
|
||||
**Migration path (breaking):**
|
||||
|
||||
1. Create `releases` table (from `ReleaseEntity`).
|
||||
2. Populate `releases` from the distinct albums in `tracks` — one release row per distinct
|
||||
`(album, artist)` identity (see open question 5). Tracks with a null album get a nullable FK or a
|
||||
synthetic "Uncategorized" release (open question 1).
|
||||
3. Add `release_id` FK column to `tracks`; populate from the newly created release rows.
|
||||
4. Drop the now-redundant columns from `tracks` (`artist`, `album`, `genre`, `release_date`,
|
||||
`image_path`, `release_type`, `created_by_user_id`). **This step is the breaking part.**
|
||||
|
||||
**DTO impact:**
|
||||
|
||||
- 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).
|
||||
- `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:
|
||||
|
||||
- **`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).
|
||||
|
||||
**Service / repository impact (list, not prescription):**
|
||||
|
||||
- `TrackRepository` — queries gain joins / `Include`s to load the Release.
|
||||
- `TrackManager.GetPaged` — sorting by release fields (artist, album, genre, date) needs a join.
|
||||
- `UnifiedTrackService.UploadAsync` — must find-or-create the Release before inserting the Track.
|
||||
- `TrackContentService.AddTrackAsync` / the BatchUpload flow — accepts a Release (or release id)
|
||||
rather than embedding album/artist/etc. on the track upload form.
|
||||
- The upload API endpoint form fields split: separate "release" fields from "track" fields, or the
|
||||
upload auto-creates the Release on first track and links subsequent tracks (open question 2).
|
||||
|
||||
### 0.4 Open questions (need Daniel before this lands)
|
||||
|
||||
1. **Tracks with no album.** Nullable `ReleaseId` (a "standalone track" concept), or does every
|
||||
track require a Release (a release with null/empty title)? **Recommend nullable FK** — some tracks
|
||||
genuinely have no album context.
|
||||
2. **Upload flow change.** Does uploading now require the caller to first create/identify a Release,
|
||||
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.
|
||||
4. **`AlbumSummaryDto` fate.** With a Release table, `GetAlbumSummariesAsync` becomes
|
||||
`GetReleasesAsync` returning `List<ReleaseDto>`. **Recommend retiring `AlbumSummaryDto`** in favor
|
||||
of `ReleaseDto` — a direct replacement.
|
||||
5. **Migration release identity.** Existing tracks are flat. The migration derives Release rows from
|
||||
them. If two tracks share an album name but different artists, they'd map to two releases (or one
|
||||
ambiguous one). **Recommend grouping by `(album, artist)`** as the release identity key — flag
|
||||
this edge case explicitly in the migration.
|
||||
|
||||
### 0.5 Impact on the Phase 8 UI sections below
|
||||
|
||||
With normalization landed:
|
||||
|
||||
- `CmsAlbumBrowser` parent rows **are** Release rows — the Release table has all the fields, so the
|
||||
old §6 "derive artist/genre/date/type" problem and the §12(3) "widen the DTO" question both
|
||||
disappear.
|
||||
- Batch Edit's "header" is a `ReleaseDto` form; its "track list" is `List<TrackDto>` (slim).
|
||||
- The `GetPagedAsync` album/genre filter still works — it joins through the `releases` table.
|
||||
|
||||
The UI sections (§1–§13) below are written against the pre-normalization vocabulary in places;
|
||||
where they mention `AlbumSummaryDto` widening or deriving release fields from children, **read that
|
||||
as resolved by §0** — the Release table supplies those fields directly. Net of §0, those UI
|
||||
sections get *simpler*, not harder.
|
||||
|
||||
---
|
||||
|
||||
@@ -17,6 +157,16 @@ The CMS `/tracks` page today is a single `MudTable<TrackDto>` (the "Tracks" tab)
|
||||
toggle, each addressable by a distinct URL so the public home page can deep-link into a given
|
||||
mode.
|
||||
|
||||
**Pre-requisite gate:** all UI work in this phase sits on top of **§0 — the `TrackEntity`
|
||||
normalization** (split into `ReleaseEntity` + slimmed `TrackEntity`). §0 is a breaking data-model
|
||||
change that must land first; it also dissolves several of the open questions this spec originally
|
||||
carried (notably the `AlbumSummaryDto` widening question — the Release table supplies those fields
|
||||
directly). Read the UI sections below as written against the post-normalization schema.
|
||||
|
||||
The Waveform Pre-Processing tab is **removed** (§9): waveform-profile status becomes an in-grid
|
||||
status column on `CmsTrackGrid`, with per-row "Generate" actions and a page-level "Generate All
|
||||
Missing" button replacing the old tab.
|
||||
|
||||
The architectural spine is **one view-model feeding three renderings**. This matches Daniel's
|
||||
standing preference (memory: *One source, multiple views* — "same data, different uses and
|
||||
ergonomics"): the divergence lives in layout, not in data paths. All three modes read from the
|
||||
@@ -37,11 +187,14 @@ existing data preloaded and the submit path swapped from upload to metadata upda
|
||||
### What this is NOT
|
||||
|
||||
- Not a public-site feature. This is CMS-only (`DeepDrftManager`, InteractiveServer). The public
|
||||
home page only *links in*; it renders nothing from this phase.
|
||||
- Not a new data model. `TrackDto`, `AlbumSummaryDto`, `GenreSummaryDto` already exist.
|
||||
home page only *links in*; it renders nothing from this phase. **Caveat:** §0's normalization
|
||||
changes the shared `TrackDto` contract, which *does* touch the public client — see §0.3 consumer
|
||||
impact. The browse-mode UI is CMS-only; the data-model change is not.
|
||||
- **Is** a data-model change — §0 introduces `ReleaseEntity` and slims `TrackEntity`. (The browse
|
||||
*modes* introduce no new model; the pre-requisite does.)
|
||||
- Not a replacement for the existing single-track edit page (`/tracks/{id}`) — that survives,
|
||||
reached from Track Mode's per-row Edit. Batch Edit is a *second*, album-scoped editor.
|
||||
- Not a change to the Waveform Pre-Processing tab (§9 covers how the modes coexist with it).
|
||||
- The Waveform Pre-Processing tab **is removed** — its function moves into the grid (§9).
|
||||
|
||||
---
|
||||
|
||||
@@ -136,9 +289,16 @@ for clarity (each wrapper is two lines; the body owns the VM) — but it's a sma
|
||||
|
||||
## 4. View-model design — `CmsTrackBrowserViewModel`
|
||||
|
||||
One scoped VM, injected into the page (or `@code`-owned if DI-scoping a VM is awkward in
|
||||
InteractiveServer — see §10.5). It owns mode state and the load logic for all three modes. The
|
||||
*views* are dumb; the VM is the single source of truth.
|
||||
**Registered in DI as a scoped service and injected into the page** — matching the established
|
||||
project pattern (`TracksViewModel` in `DeepDrftPublic.Client` is registered scoped in
|
||||
`Startup.ConfigureDomainServices` and injected into `TracksView.razor`). The reason is dependency
|
||||
hygiene: backend service dependencies (`ICmsTrackService` and the waveform service) live on the VM,
|
||||
not in the component's `@inject` chain. The component injects only the VM; the VM holds the
|
||||
services. It owns mode state and the load logic for all three modes. The *views* are dumb; the VM is
|
||||
the single source of truth.
|
||||
|
||||
(This resolves the earlier open question that floated a page-owned `@code` object — see §12.5. The
|
||||
DI-scoped VM is the decided pattern, not the alternative.)
|
||||
|
||||
### 4a. State
|
||||
|
||||
@@ -205,20 +365,23 @@ already-loaded summaries (cache for the page lifetime; a fresh page nav reloads)
|
||||
|
||||
```
|
||||
TrackList.razor (@page "/tracks" + "/tracks/albums" + "/tracks/genres")
|
||||
│ owns: CmsTrackBrowserViewModel, the mode toggle, the existing MudTabs shell
|
||||
│ owns: (injects) CmsTrackBrowserViewModel [DI-scoped], the mode toggle, page header
|
||||
│
|
||||
├── MudTabs
|
||||
│ ├── MudTabPanel "Tracks"
|
||||
│ │ ├── mode toggle (Track | Album | Genre) — MudToggleGroup
|
||||
│ │ ├── [Track mode] CmsTrackGrid (no filter) ← DEFAULT
|
||||
│ │ ├── [Album mode] CmsAlbumBrowser (parent rows + lazy children)
|
||||
│ │ └── [Genre mode] CmsGenreBrowser (card grid + accordion)
|
||||
│ │
|
||||
│ └── MudTabPanel "Waveform Pre-Processing" ← UNCHANGED (§9)
|
||||
├── page header
|
||||
│ ├── mode toggle (Track | Album | Genre) — MudToggleGroup
|
||||
│ └── "Generate All Missing" button (Track mode only) ← §9
|
||||
│
|
||||
└── (navigates to) BatchEdit.razor (/tracks/album/{albumName}/edit)
|
||||
├── [Track mode] CmsTrackGrid (no filter) ← DEFAULT
|
||||
│ └── per-row: status icon + Generate (if no profile) + Edit/Delete/Info ← §9
|
||||
├── [Album mode] CmsAlbumBrowser (parent rows + lazy children)
|
||||
└── [Genre mode] CmsGenreBrowser (card grid + accordion → CmsTrackGrid)
|
||||
|
||||
(navigates to) BatchEdit.razor (/tracks/album/{albumName}/edit)
|
||||
```
|
||||
|
||||
The old `MudTabs` / "Waveform Pre-Processing" `MudTabPanel` is gone (§9). Whether a `MudTabs` shell
|
||||
survives at all depends on whether any other panel needs it — likely not.
|
||||
|
||||
New components:
|
||||
|
||||
### 5a. `CmsTrackGrid.razor` — the single reusable flat track table (THE DRY core)
|
||||
@@ -239,9 +402,9 @@ the component consumed by **both** Track mode (no filter) and Genre mode (`Genre
|
||||
duplicated table markup, exactly the brief's DRY requirement.
|
||||
|
||||
**Columns (new layout, §8 for detail):** Track # | Art (40×40) | Track Name | Artist | Album |
|
||||
Genre | Release Date (`d MMMM, yyyy`) | Actions (Edit, Delete, **Info** tooltip carrying Entry Key
|
||||
+ File Name). Entry Key and File Name columns are *removed* from the grid and moved into the Info
|
||||
tooltip.
|
||||
Genre | Release Date (`d MMMM, yyyy`) | **Waveform Status** | Actions (Edit, Delete, per-row
|
||||
**Generate** when no profile, **Info** tooltip carrying Entry Key + File Name). Entry Key and File
|
||||
Name columns are *removed* from the grid and moved into the Info tooltip.
|
||||
|
||||
### 5b. `CmsAlbumBrowser.razor` — Album mode
|
||||
|
||||
@@ -338,7 +501,8 @@ one-source spine. The only new seam is the lazy-on-expand trigger.
|
||||
Applied in `CmsTrackGrid` (so Genre mode's embedded grid gets them for free):
|
||||
|
||||
1. **Column order (left to right):** Track # → Art thumb → Track Name → Artist → Album → Genre →
|
||||
Release Date → Actions. (Track # is the very first column; thumb second; per the brief.)
|
||||
Release Date → **Waveform Status** → Actions. (Track # is the very first column; thumb second;
|
||||
per the brief. Waveform Status sits just before Actions — see item 7 and §9.)
|
||||
2. **Track # column:** plain `@context.TrackNumber`. Header sortable on `TrackNumber` is optional
|
||||
(default sort stays Track Name asc to match today).
|
||||
3. **Art thumb (40×40):** reuse the public `TrackCard` fallback pattern.
|
||||
@@ -374,21 +538,45 @@ Applied in `CmsTrackGrid` (so Genre mode's embedded grid gets them for free):
|
||||
precisely. The icon is an explicit, discoverable affordance.)
|
||||
6. **Unchanged:** sort labels, paging (`PageSizeOptions { 10, 20, 50 }`), Add Track button (gated
|
||||
by `ShowAddButton`), Edit (→`/tracks/{id}`) and Delete (confirm dialog) actions.
|
||||
7. **Waveform Status column (new — replaces the Waveform tab, see §9).** A status icon showing
|
||||
whether a waveform profile exists for the track: `Icons.Material.Filled.CheckCircle` colored
|
||||
`Color.Success` when present, `Icons.Material.Filled.Cancel` colored `Color.Warning` when absent.
|
||||
This maps to the existing `HasProfile` boolean on `WaveformStatusDto`. **Data-contract decision
|
||||
(flagged):** the grid binds `TrackDto`, which has no waveform status today. Two options —
|
||||
(a) add `HasWaveformProfile` (bool) to `TrackDto`, or (b) the grid does a separate waveform-status
|
||||
lookup and merges by `EntryKey`. **Recommend (a)** — one field, keeps the grid a single data
|
||||
source rather than fanning out a second call per page. When `HasWaveformProfile` is false, the
|
||||
Actions cell also shows a per-row **Generate** action button (only rendered when the profile is
|
||||
missing). See §9.
|
||||
|
||||
---
|
||||
## 9. Waveform processing — tab removed, folded into the grid
|
||||
|
||||
## 9. Coexistence with the Waveform tab (integration detail the brief understates)
|
||||
**Decision (review, 2026-06-11): the "Waveform Pre-Processing" `MudTabPanel` is removed entirely.**
|
||||
Its three responsibilities relocate:
|
||||
|
||||
The mode toggle lives **inside the "Tracks" `MudTabPanel`**, above the rendered mode. The
|
||||
"Waveform Pre-Processing" tab is a sibling `MudTabPanel` and is **untouched** — switching browse
|
||||
modes never affects it; switching to the Waveform tab never affects browse mode.
|
||||
1. **Per-track status** → the **Waveform Status column** in `CmsTrackGrid` (§8 item 7). A
|
||||
success/warning icon driven by the profile-exists boolean. Every track's status is visible inline
|
||||
while browsing, instead of behind a separate tab.
|
||||
2. **Per-track generate** → a per-row **Generate** action in `CmsTrackGrid`, rendered only when the
|
||||
track has no profile (`!HasWaveformProfile`). Generating refreshes the row's status.
|
||||
3. **Bulk generate** → a **"Generate All Missing"** button in the **main tracks-page header area**,
|
||||
visible when the user is in **Track mode**. This replaces the bulk-run button that lived in the
|
||||
Waveform tab.
|
||||
|
||||
One wrinkle: the three browse-mode URLs (`/tracks/albums`, `/tracks/genres`) should land the user
|
||||
on the **Tracks tab** with that mode active. They must not disturb tab selection if the user then
|
||||
clicks over to Waveform. Simplest: the URL drives initial `Mode` and selects the Tracks tab on
|
||||
load; tab and mode are independent state thereafter. Confirm this is the intended behaviour
|
||||
(§10.7) — alternatively the modes could live *outside* the tabs entirely, but that's a bigger
|
||||
restructure and the brief says keep the Waveform tab as-is.
|
||||
With the tab gone, the `MudTabs` shell may collapse to a single surface (the mode toggle + the
|
||||
rendered mode + the page header). Confirm whether `MudTabs` is retained for any other panel or
|
||||
removed outright. The mode toggle and the "Generate All Missing" button both live in the Track-mode
|
||||
page header.
|
||||
|
||||
Because the status + generate affordances live in `CmsTrackGrid`, **Genre mode's embedded grid gets
|
||||
them for free** — a genre's tracks show waveform status and per-row generate identically to Track
|
||||
mode. The "Generate All Missing" page-level button is Track-mode-scoped (it operates over the whole
|
||||
catalogue, not a filtered slice); whether it should also appear scoped-to-filter in Genre/Album mode
|
||||
is a minor follow-up, not required for this phase.
|
||||
|
||||
This depends on the §8 item-7 data-contract decision (recommend `HasWaveformProfile` on `TrackDto`)
|
||||
so the grid has the status without a second per-page lookup — which dovetails cleanly with §0's
|
||||
`TrackDto` rework (the field can be added as part of the normalization DTO pass).
|
||||
|
||||
---
|
||||
|
||||
@@ -396,8 +584,11 @@ restructure and the brief says keep the Waveform tab as-is.
|
||||
|
||||
`@page "/tracks/album/{AlbumName}/edit"` (URL-encoded album name as the route param).
|
||||
|
||||
**Recommendation on the component boundary: a new `BatchEdit.razor` page that shares extracted
|
||||
sub-components with `BatchUpload.razor`, NOT a `BatchUpload` grown with an `isEdit` flag.**
|
||||
**Component boundary — CONFIRMED (review, 2026-06-11): a new `BatchEdit.razor` page that shares
|
||||
extracted sub-components with `BatchUpload.razor`, NOT a `BatchUpload` grown with an `isEdit`
|
||||
flag.** Sub-components to extract: the album-header fields block (post-§0, edits the `ReleaseDto`),
|
||||
the batch track list (master list with move-up/down/remove + status chips), and the track detail
|
||||
pane. Both pages compose these with their own submit logic.
|
||||
|
||||
Why not an `isEdit` parameter on `BatchUpload`:
|
||||
|
||||
@@ -470,14 +661,18 @@ the confirm copy unambiguous about the blast radius.
|
||||
|
||||
| Contract | Change | Why | Risk |
|
||||
|---|---|---|---|
|
||||
| `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 | Low — additive, default-null keeps callers compiling |
|
||||
| **`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 |
|
||||
| `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 |
|
||||
| `AlbumSummaryDto` | **Recommended:** add `Artist?`, `Genre?`, `EarliestReleaseDate?`, `ReleaseType?` (§6 option ii) | Fully-populated parent rows without lazy expansion | Medium — touches the albums-summary query + DTO; alternatively defer (§6 option i) |
|
||||
| albums-summary query (server) | compute the widened fields | as above | Medium — `GROUP BY` aggregation + uniform-value checks |
|
||||
| `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 |
|
||||
| `AlbumSummaryDto` | **Dissolved by §0** — replaced by `ReleaseDto`; `GetAlbumSummariesAsync` → `GetReleasesAsync`. (The old "widen the DTO" question is moot — the Release table has all the fields.) | normalization | — (replaced, not widened) |
|
||||
| New row models (`BatchEditRow`) | new, CMS-internal | edit mode | None (internal) |
|
||||
| Routes | `/tracks/albums`, `/tracks/genres`, `/tracks/album/{albumName}/edit` | mode deep-links + batch edit | Low |
|
||||
|
||||
No new API endpoints. No change to `TrackDto`. No public-site code (it only links in).
|
||||
No new API endpoints (existing endpoints' *shapes* change per §0). The data-model change **does**
|
||||
reach the public client (§0.3) — the browse-mode UI does not, but the `TrackDto` rework does.
|
||||
|
||||
---
|
||||
|
||||
@@ -490,28 +685,34 @@ No new API endpoints. No change to `TrackDto`. No public-site code (it only link
|
||||
(multi-column: thumb, name, artist, count, genre, date, type, actions), which a tree node
|
||||
renders poorly. `MudTreeView` suits hierarchies of like items, not parent-rows-of-different-shape.
|
||||
Confirm.
|
||||
3. **(§6/§11) Widen `AlbumSummaryDto`?** Recommend yes (option ii) so Album-mode parent rows show
|
||||
artist/genre/date/type at rest. Alternative: derive lazily on expand (thin resting row, no DTO
|
||||
change). **This is the biggest call** — it determines whether Album mode needs a server-side
|
||||
query change or is pure CMS UI.
|
||||
4. **(§6) Release-type chip derivation.** Trust first track's stored `ReleaseType`, fall back to a
|
||||
count heuristic (1=Single, 2–5=EP, 6+=LP) only if absent? Recommend yes.
|
||||
5. **(§4/§10.5) VM as DI-scoped service vs. page-owned `@code` object.** A scoped DI VM in
|
||||
InteractiveServer is fine but adds wiring; a plain `@code`-owned object is simpler if the VM is
|
||||
never shared across components. Recommend page-owned unless there's a reason to inject. *Small.*
|
||||
3. **~~(§6/§11) Widen `AlbumSummaryDto`?~~ — DISSOLVED by §0.** The normalization gives Album mode a
|
||||
real `ReleaseEntity`/`ReleaseDto` with artist/genre/date/type on it directly; there is no summary
|
||||
DTO to widen and no derivation from children. The question is moot once §0 lands.
|
||||
4. **(§6) Release-type chip derivation.** *Mostly dissolved by §0* — `ReleaseType` now lives on the
|
||||
Release record, so the chip reads `Release.ReleaseType` directly. The count heuristic is only a
|
||||
migration-time concern (deriving `ReleaseType` for legacy releases that had it per-track); flag in
|
||||
the §0 migration, not the UI.
|
||||
5. **~~(§4/§10.5) VM scoping~~ — RESOLVED.** The VM is **DI-registered as a scoped service** and
|
||||
injected, matching the `TracksViewModel` pattern in `DeepDrftPublic.Client` (keeps backend service
|
||||
deps off the component's inject chain). Not page-owned. See §4.
|
||||
6. **(§8) Shared thumb component.** The 40×40 art+fallback thumb is near-identical to the public
|
||||
`TrackCard` thumb but in a different host. Define locally in `CmsTrackGrid` now; flag a future
|
||||
`DeepDrftShared.Client` extraction (Phase 7 territory) rather than coupling the CMS to the
|
||||
public client's CSS. Confirm "local now, share later."
|
||||
7. **(§9) Mode URL vs. tab selection.** Confirm `/tracks/albums` lands on the Tracks tab with
|
||||
Album mode active, and that tab and mode are independent thereafter (Waveform tab untouched).
|
||||
7. **~~(§9) Mode URL vs. tab selection~~ — DISSOLVED.** The Waveform tab is removed (§9), so there
|
||||
is no tab/mode independence question left. `/tracks/albums` simply lands the page in Album mode.
|
||||
If a `MudTabs` shell survives at all, confirm what (if anything) the second panel is — likely
|
||||
none.
|
||||
8. **(§10) Remove-in-edit semantics.** Does removing an *existing* track row in Batch Edit delete
|
||||
the track (with confirm), or only detach it from the editing session? Recommend delete-with-
|
||||
confirm (a release editor that can't drop a track is half a tool), but it's a destructive
|
||||
action worth Daniel's explicit nod.
|
||||
9. **Batch Edit component boundary (§10).** Confirm the extraction approach (shared sub-components,
|
||||
new `BatchEdit` page) over an `isEdit` flag on `BatchUpload`. Recommend extraction; the flag is
|
||||
the cheaper-diff alternative if Daniel prefers.
|
||||
9. **~~Batch Edit component boundary (§10)~~ — RESOLVED.** Confirmed: a **new `BatchEdit.razor`
|
||||
page** sharing **extracted sub-components** with `BatchUpload.razor` — *not* an `isEdit` flag on
|
||||
`BatchUpload`. Sub-components to extract: the **album-header fields block**, the **batch track
|
||||
list** (master list with move-up/down/remove + status chips), and the **track detail pane**. Both
|
||||
`BatchUpload` and `BatchEdit` compose these with their own submit logic. (Post-§0, the
|
||||
"album-header block" edits the `ReleaseDto`.) See §10.
|
||||
10. **Home-page links.** Out of scope for this CMS phase, but: the public home page will hard-code
|
||||
`/tracks`, `/tracks/albums`, `/tracks/genres` (cross-host links into the Manager app). Confirm
|
||||
the Manager base URL is stable/known to the public site at build time (the URL *paths* are
|
||||
|
||||
Reference in New Issue
Block a user