diff --git a/COMPLETED.md b/COMPLETED.md index af1f974..1eeb63a 100644 --- a/COMPLETED.md +++ b/COMPLETED.md @@ -6,6 +6,65 @@ Newest entries at the top. Group by phase/wave header (mirroring `PLAN.md` / `CM --- +## Phase 6 — CMS Enhancements + +### 6.3 Batch Upload Page + +**Landed:** 2026-06-11 on dev. + +- **What:** Replace the single-track form at `/tracks/new` with a two-panel batch upload page that uploads many WAVs in one session under a shared album header. +- **Why:** Uploading an album one track at a time is the current reality — re-entering album, genre, release date, cover art, and artist on every track. Batch upload makes "add a release" a single operation: set the shared header once, queue the tracks, submit. This is the dominant ingestion shape for the collective (releases, not loose singles). +- **Shape:** + - **Route:** New page at **`/tracks/upload`**. Justification: `/tracks/new` reads as "new single track" and the edit route is `/tracks/{id}`; `/tracks/upload` names the operation (batch ingestion) without colliding with the id-parameterised edit route. Repoint the "Add Track" button in `TrackList.razor` (currently `Href="/tracks/new"`) to `/tracks/upload`. Whether `/tracks/new` is retired or left as a redirect is staff-engineer's call; the committed change is that the button goes to the batch page. + - **Data model change — `ReleaseType`:** Add a `ReleaseType` enum to `DeepDrftModels` (`enum ReleaseType { Single, EP, Album }`). Enum over string: three fixed values, and it gates UI (selector) and future grouping logic — a free-text column invites typos. Add a `ReleaseType` property to **`TrackEntity`** and **`TrackDto`**. Decide nullability: recommend **non-null with a default of `Single`** so existing rows backfill cleanly to a sensible value (a release of one track is a single) and the column is never null. This ripples to `TrackConfiguration` (EF mapping — store as string via `HasConversion()` for readable DB values, or as int; recommend string for legibility), `TrackConverter` (assign on round-trip), and the upload/update service signatures. **An EF migration is required** — author it via `dotnet ef migrations add`, never by hand. + - **Data model change — `TrackNumber`:** Add a `TrackNumber` property (type `int`, **1-based, non-null**) to **`TrackEntity`** and **`TrackDto`** to store per-track ordinal position within a release. This ripples through `TrackConfiguration` (EF mapping) and `TrackConverter` (assign on round-trip) the same way `ReleaseType` does. **A second EF migration is required** — author it via `dotnet ef migrations add`, never by hand. May be combined into a single migration with the `ReleaseType` change — staff-engineer's call on whether to combine or keep separate. + - **Shared-vs-per-track field split:** + - *Shared (header strip, applied to every track in the batch):* album name, artist, album cover image (single upload), genre, release date, and `ReleaseType`. One album per batch — the entire batch is one release, and all release-level fields live in the header. + - *Per-track (right detail panel):* track name, the individual WAV file, and that row's upload status. + - **Layout (two-panel under a header strip):** + - **Header strip** (full width, top): album name, artist `MudTextField`, single cover-art `InputFile` (reuse the `MudField` cover-art pattern from `TrackNew`, including the upload-on-submit behaviour), genre `MudTextField`, release-date field, and `ReleaseType` `MudSelect`. These bind to a single batch-header model. + - **Left panel** (track queue): an ordered list of queued tracks; the row order *is* the release track order and reflects each track's `TrackNumber`. Each row shows track name, a reorder affordance (up/down `MudIconButton`s are the low-risk choice; drag-and-drop is a nice-to-have — see open questions), a remove button, and a per-row status indicator (queued / uploading / done / failed). A `+`/`InputFile` (with `multiple`) at the top or bottom of the list adds WAV files; each added file becomes a row with track name defaulted from the filename (sans extension). On submit, each track is assigned its `TrackNumber` (1-based) from its position in the list. + - **Right panel** (selected-track detail): when a row is selected, show its editable fields — track name and the WAV file name/size/status. Selecting a different row swaps the detail. + - **Add-files behaviour:** `InputFile multiple` → append a row per file. Default track name = filename without extension. New rows append to the end of the list, taking the next ordinal position. Keep the 1 GB per-file ceiling and the `.wav` validation from `TrackNew`. + - **Submit behaviour:** Sequential, one request at a time — reuse the existing single-track upload path (`CmsTrackService.UploadTrackAsync`) in a loop. This mirrors the deliberately-sequential waveform backfill in `TrackList.GenerateAllMissing` ("one request at a time so a large backfill does not flood the API"). Per-track progress: each left-panel row reflects its state as the loop advances (`StateHasChanged` between rows). Cover-art upload happens **once** before the loop (upload the image, get the entry key, then pass/link it to every track) — do not re-upload the cover per track. On completion, snackbar a summary (`uploaded N, M failed`) and navigate to `/tracks`. Partial failure: completed tracks stay persisted; failed rows remain visible with their error so the admin can retry just those — do **not** roll back the batch. + - **CmsTrackService surface:** No new method strictly required — the loop calls the existing `UploadTrackAsync` per track and the existing image upload/link path per batch. `UploadTrackAsync`'s signature gains `releaseType` and `trackNumber` parameters (ripples from the data-model change). If the cover-link follow-up (the `UpdateAsync` step `TrackNew` does today) is kept per track, that's existing surface too. + - **API surface:** No new endpoints. Existing `POST api/track/upload` (per track) and `POST api/image/upload` (once per batch) cover it. `api/track/upload` and the metadata update endpoints gain `releaseType` and `trackNumber` in their payloads as a consequence of the entity change. + - **Components:** `BatchUpload.razor` (page + header strip + orchestration), and reasonably a `BatchTrackRow` model class plus left-panel/right-panel as child components or inline sections — staff-engineer's structural call. + - **Constraint — dual-write orphan risk:** Each track inherits the existing dual-write hazard (audio lands in the vault, SQL persist may fail → orphaned audio, no rollback). Batch upload *multiplies the exposure* (N tracks per session instead of one). The mitigation is **Phase 4.3 (dual-write rollback / dead-letter log)** — not a blocker for this feature, but this is the strongest argument yet for landing 4.3. Flag it as a known constraint; do not attempt per-batch transactional rollback (the dual-database split can't give it). +- **Prerequisites:** + - `ReleaseType` enum + `TrackNumber` field + `TrackEntity`/`TrackDto` changes + EF migration(s) must land first (it's the data-model floor for the whole feature, and ripples through `TrackConfiguration`/`TrackConverter`/service signatures). Could be a separate prep commit before the page work. + - **Not blocked by** Phase 4.3, but 4.3 is the right mitigation for the amplified orphan risk and is worth sequencing alongside. +- **Resolved (no longer open):** + - **One album per batch.** The whole batch is one release; album name and all release-level fields (artist, genre, release date, `ReleaseType`, cover art) live in the shared header strip. A batch never mixes albums. + - **Track ordinals are persistent** — `TrackNumber` (int, 1-based, non-null) stores per-track position within a release. The left-panel row order reflects `TrackNumber`, and each track is assigned its ordinal from its list position on submit. + +**Completion note:** `BatchUpload.razor` page implemented at `/tracks/upload`; two-panel layout with header strip (shared album/artist/genre/release-date/cover-art/release-type fields) and left queue + right detail sections for per-track track name and file selection. Sequential upload loop via existing `CmsTrackService.UploadTrackAsync`. Cover-art uploaded once at start; per-track progress reflected in left-panel status indicators. `TrackList.razor` "Add Track" button repointed to `/tracks/upload`. `ReleaseType` enum and `TrackNumber` int field added to `TrackEntity`, `TrackDto`, `TrackConfiguration`, `TrackConverter`, and EF migrations authored. `UploadTrackAsync` signature updated with `releaseType` and `trackNumber` parameters. + +--- + +### 6.1 CMS Home Page — catalogue summary dashboard + +**Landed:** 2026-06-11 on dev. + +- **What:** Replace the redirect-to-`/tracks` at `Index.razor` (route `/`) with a real dashboard showing a grid of summary cards: total tracks, distinct albums, distinct genres. +- **Why:** Quick orientation for the CMS admin — at-a-glance catalogue health on landing, instead of dropping straight into the table. First thing the admin sees, so it carries the bold DeepDrft palette rather than a conservative admin look. +- **Shape:** + - **Route / component:** Keep `Index.razor` at `/`; remove the `OnInitialized` redirect and render the dashboard. The CMS nav lands here; `/tracks` remains reachable from the nav and from the cards. + - **UI:** A responsive `MudGrid` of three `MudCard`s (Tracks / Albums / Genres). Each card: an icon (`LibraryMusic`, `Album`, `Category` or similar), the metric as a large `Typo.h2`/`h3` number, and a label. Cards are clickable (`@onclick` → `Nav.NavigateTo`). Lean into the active MudBlazor palette — `Color.Primary`/`Color.Secondary` fills or accent borders, generous elevation — this is the visual-punch surface, not a muted KPI strip. Loading state: skeleton or per-card `MudProgressCircular` while the three fetches resolve. Each card fetches independently so one slow/failed call doesn't blank the others; a failed card shows a "—" with a retry affordance rather than collapsing the grid. + - **Card navigation (Phase 6 scope):** All three cards navigate to `/tracks` (the track maintenance page). **Per-album / per-genre pre-filtering is deferred** — see 6.2. Ship the cards as plain links to `/tracks` now. + - **Data model:** No entity changes. `AlbumSummaryDto` and `GenreSummaryDto` already exist in `DeepDrftModels`. + - **API surface:** No new API endpoints. The three numbers are already available: + - **Albums count** = length of `GET api/track/albums` (exists, unauthenticated, returns `List`). + - **Genres count** = length of `GET api/track/genres` (exists, unauthenticated, returns `List`). + - **Tracks count** = `TotalCount` from `GET api/track/page` (exists) requested with `pageSize=1` (cheapest paged call that still returns the total). + - **CmsTrackService surface (new methods):** `ICmsTrackService` does not currently expose albums/genres. Add three thin proxy methods mirroring the existing pattern (e.g. `GetAlbumSummariesAsync`, `GetGenreSummariesAsync`, and a `GetTrackCountAsync` that calls `page?pageSize=1` and returns `TotalCount`). These are the only new code on the service. No controller work. + - **Components:** `Index.razor` (dashboard host) plus, optionally, a small `SummaryCard.razor` for the repeated card — worth extracting given three near-identical cards, but staff-engineer's call. +- **Prerequisites:** None. All backing endpoints and DTOs exist. + +**Completion note:** `DeepDrftManager/Components/Pages/Index.razor` redesigned as a 3-card dashboard grid (Tracks / Albums / Genres counts) with independent per-card fetches. Three new `ICmsTrackService` proxy methods (`GetAlbumSummariesAsync`, `GetGenreSummariesAsync`, `GetTrackCountAsync`) wired to existing public API endpoints. Cards navigate to `/tracks` on click. Failed cards show "—" fallback; each card loads independently. + +--- + ## Phase 1.1 — Extended WAV format support **Status:** Fully landed on 2026-06-10 (IEEE Float SubFormat 0x0003 and Padded 24-in-32 container support implemented, tests passing). diff --git a/PLAN.md b/PLAN.md index cec2952..6c816e1 100644 --- a/PLAN.md +++ b/PLAN.md @@ -81,28 +81,11 @@ These follow from `CONTEXT.md §5`. Direction is strongly implied but no specifi --- -## Phase 6 — CMS Enhancements +## Phase 6 — CMS Enhancements (Completed) -Two CMS-side features Daniel has committed to. Both live in `DeepDrftManager` (`InteractiveServer` render mode throughout, MudBlazor UI). All track reads/writes route through `ICmsTrackService` / `CmsTrackService` — no in-process data layer in Blazor components. Follow the existing patterns in `TrackList.razor` / `TrackNew.razor` (MudContainer, MudPaper, MudStack, server-data MudTable). +See `COMPLETED.md` for Phase 6 (§6.1, §6.3) and entity-prep (§6.2 model layer) which landed on dev in June 2026. -### 6.1 CMS Home Page — catalogue summary dashboard - -- **What:** Replace the redirect-to-`/tracks` at `Index.razor` (route `/`) with a real dashboard showing a grid of summary cards: total tracks, distinct albums, distinct genres. -- **Why:** Quick orientation for the CMS admin — at-a-glance catalogue health on landing, instead of dropping straight into the table. First thing the admin sees, so it carries the bold DeepDrft palette rather than a conservative admin look. -- **Shape:** - - **Route / component:** Keep `Index.razor` at `/`; remove the `OnInitialized` redirect and render the dashboard. The CMS nav lands here; `/tracks` remains reachable from the nav and from the cards. - - **UI:** A responsive `MudGrid` of three `MudCard`s (Tracks / Albums / Genres). Each card: an icon (`LibraryMusic`, `Album`, `Category` or similar), the metric as a large `Typo.h2`/`h3` number, and a label. Cards are clickable (`@onclick` → `Nav.NavigateTo`). Lean into the active MudBlazor palette — `Color.Primary`/`Color.Secondary` fills or accent borders, generous elevation — this is the visual-punch surface, not a muted KPI strip. Loading state: skeleton or per-card `MudProgressCircular` while the three fetches resolve. Each card fetches independently so one slow/failed call doesn't blank the others; a failed card shows a "—" with a retry affordance rather than collapsing the grid. - - **Card navigation (Phase 6 scope):** All three cards navigate to `/tracks` (the track maintenance page). **Per-album / per-genre pre-filtering is deferred** — see 6.2. Ship the cards as plain links to `/tracks` now. - - **Data model:** No entity changes. `AlbumSummaryDto` and `GenreSummaryDto` already exist in `DeepDrftModels`. - - **API surface:** No new API endpoints. The three numbers are already available: - - **Albums count** = length of `GET api/track/albums` (exists, unauthenticated, returns `List`). - - **Genres count** = length of `GET api/track/genres` (exists, unauthenticated, returns `List`). - - **Tracks count** = `TotalCount` from `GET api/track/page` (exists) requested with `pageSize=1` (cheapest paged call that still returns the total). - - **CmsTrackService surface (new methods):** `ICmsTrackService` does not currently expose albums/genres. Add three thin proxy methods mirroring the existing pattern (e.g. `GetAlbumSummariesAsync`, `GetGenreSummariesAsync`, and a `GetTrackCountAsync` that calls `page?pageSize=1` and returns `TotalCount`). These are the only new code on the service. No controller work. - - **Components:** `Index.razor` (dashboard host) plus, optionally, a small `SummaryCard.razor` for the repeated card — worth extracting given three near-identical cards, but staff-engineer's call. -- **Prerequisites:** None. All backing endpoints and DTOs exist. -- **Open questions:** - - *(Daniel)* Should the dashboard show only the three counts, or is there appetite for a fourth/fifth metric now (e.g. "tracks missing cover art", "tracks missing waveform profile" — the latter is already computable from `GetWaveformStatusAsync`)? Counts-only is the committed scope; flag any additions before build. +--- ### 6.2 Card-contextual filtering of the Tracks page — `[deferred]` @@ -111,36 +94,6 @@ Two CMS-side features Daniel has committed to. Both live in `DeepDrftManager` (` - **Why deferred:** The dashboard cards aggregate *across all* albums/genres — there is no single album/genre to filter to from a top-level count card. Meaningful per-album/per-genre navigation needs an intermediate browse surface (a list of albums, a list of genres) for the admin to pick from — i.e. it's really a CMS analogue of the public `AlbumsView`/`GenresView`, not a property of the summary cards. That's a larger surface than the dashboard itself and shouldn't be smuggled in. The `GET api/track/page` endpoint already accepts `album=` and `genre=` query filters, so the API substrate is ready; the missing piece is the CMS browse UI and the filter plumbing in `TrackList`. - **Shape (sketch, not committed):** CMS album/genre browse pages (or tabs on `/tracks`) backed by the existing `albums`/`genres` endpoints; rows link to `/tracks?album=…` / `/tracks?genre=…`; `TrackList.LoadServerData` reads the query param and passes it to `GetPagedAsync`. Revisit as its own item when Daniel wants it. -### 6.3 Batch Upload Page - -- **What:** Replace the single-track form at `/tracks/new` with a two-panel batch upload page that uploads many WAVs in one session under a shared album header. -- **Why:** Uploading an album one track at a time is the current reality — re-entering album, genre, release date, cover art, and artist on every track. Batch upload makes "add a release" a single operation: set the shared header once, queue the tracks, submit. This is the dominant ingestion shape for the collective (releases, not loose singles). -- **Shape:** - - **Route:** New page at **`/tracks/upload`**. Justification: `/tracks/new` reads as "new single track" and the edit route is `/tracks/{id}`; `/tracks/upload` names the operation (batch ingestion) without colliding with the id-parameterised edit route. Repoint the "Add Track" button in `TrackList.razor` (currently `Href="/tracks/new"`) to `/tracks/upload`. Whether `/tracks/new` is retired or left as a redirect is staff-engineer's call; the committed change is that the button goes to the batch page. - - **Data model change — `ReleaseType`:** Add a `ReleaseType` enum to `DeepDrftModels` (`enum ReleaseType { Single, EP, Album }`). Enum over string: three fixed values, and it gates UI (selector) and future grouping logic — a free-text column invites typos. Add a `ReleaseType` property to **`TrackEntity`** and **`TrackDto`**. Decide nullability: recommend **non-null with a default of `Single`** so existing rows backfill cleanly to a sensible value (a release of one track is a single) and the column is never null. This ripples to `TrackConfiguration` (EF mapping — store as string via `HasConversion()` for readable DB values, or as int; recommend string for legibility), `TrackConverter` (assign on round-trip), and the upload/update service signatures. **An EF migration is required** — author it via `dotnet ef migrations add`, never by hand. - - **Data model change — `TrackNumber`:** Add a `TrackNumber` property (type `int`, **1-based, non-null**) to **`TrackEntity`** and **`TrackDto`** to store per-track ordinal position within a release. This ripples through `TrackConfiguration` (EF mapping) and `TrackConverter` (assign on round-trip) the same way `ReleaseType` does. **A second EF migration is required** — author it via `dotnet ef migrations add`, never by hand. May be combined into a single migration with the `ReleaseType` change — staff-engineer's call on whether to combine or keep separate. - - **Shared-vs-per-track field split:** - - *Shared (header strip, applied to every track in the batch):* album name, artist, album cover image (single upload), genre, release date, and `ReleaseType`. One album per batch — the entire batch is one release, and all release-level fields live in the header. - - *Per-track (right detail panel):* track name, the individual WAV file, and that row's upload status. - - **Layout (two-panel under a header strip):** - - **Header strip** (full width, top): album name, artist `MudTextField`, single cover-art `InputFile` (reuse the `MudField` cover-art pattern from `TrackNew`, including the upload-on-submit behaviour), genre `MudTextField`, release-date field, and `ReleaseType` `MudSelect`. These bind to a single batch-header model. - - **Left panel** (track queue): an ordered list of queued tracks; the row order *is* the release track order and reflects each track's `TrackNumber`. Each row shows track name, a reorder affordance (up/down `MudIconButton`s are the low-risk choice; drag-and-drop is a nice-to-have — see open questions), a remove button, and a per-row status indicator (queued / uploading / done / failed). A `+`/`InputFile` (with `multiple`) at the top or bottom of the list adds WAV files; each added file becomes a row with track name defaulted from the filename (sans extension). On submit, each track is assigned its `TrackNumber` (1-based) from its position in the list. - - **Right panel** (selected-track detail): when a row is selected, show its editable fields — track name and the WAV file name/size/status. Selecting a different row swaps the detail. - - **Add-files behaviour:** `InputFile multiple` → append a row per file. Default track name = filename without extension. New rows append to the end of the list, taking the next ordinal position. Keep the 1 GB per-file ceiling and the `.wav` validation from `TrackNew`. - - **Submit behaviour:** Sequential, one request at a time — reuse the existing single-track upload path (`CmsTrackService.UploadTrackAsync`) in a loop. This mirrors the deliberately-sequential waveform backfill in `TrackList.GenerateAllMissing` ("one request at a time so a large backfill does not flood the API"). Per-track progress: each left-panel row reflects its state as the loop advances (`StateHasChanged` between rows). Cover-art upload happens **once** before the loop (upload the image, get the entry key, then pass/link it to every track) — do not re-upload the cover per track. On completion, snackbar a summary (`uploaded N, M failed`) and navigate to `/tracks`. Partial failure: completed tracks stay persisted; failed rows remain visible with their error so the admin can retry just those — do **not** roll back the batch. - - **CmsTrackService surface:** No new method strictly required — the loop calls the existing `UploadTrackAsync` per track and the existing image upload/link path per batch. `UploadTrackAsync`'s signature gains `releaseType` and `trackNumber` parameters (ripples from the data-model change). If the cover-link follow-up (the `UpdateAsync` step `TrackNew` does today) is kept per track, that's existing surface too. - - **API surface:** No new endpoints. Existing `POST api/track/upload` (per track) and `POST api/image/upload` (once per batch) cover it. `api/track/upload` and the metadata update endpoints gain `releaseType` and `trackNumber` in their payloads as a consequence of the entity change. - - **Components:** `BatchUpload.razor` (page + header strip + orchestration), and reasonably a `BatchTrackRow` model class plus left-panel/right-panel as child components or inline sections — staff-engineer's structural call. - - **Constraint — dual-write orphan risk:** Each track inherits the existing dual-write hazard (audio lands in the vault, SQL persist may fail → orphaned audio, no rollback). Batch upload *multiplies the exposure* (N tracks per session instead of one). The mitigation is **Phase 4.3 (dual-write rollback / dead-letter log)** — not a blocker for this feature, but this is the strongest argument yet for landing 4.3. Flag it as a known constraint; do not attempt per-batch transactional rollback (the dual-database split can't give it). -- **Prerequisites:** - - `ReleaseType` enum + `TrackNumber` field + `TrackEntity`/`TrackDto` changes + EF migration(s) must land first (it's the data-model floor for the whole feature, and ripples through `TrackConfiguration`/`TrackConverter`/service signatures). Could be a separate prep commit before the page work. - - **Not blocked by** Phase 4.3, but 4.3 is the right mitigation for the amplified orphan risk and is worth sequencing alongside. -- **Resolved (no longer open):** - - **One album per batch.** The whole batch is one release; album name and all release-level fields (artist, genre, release date, `ReleaseType`, cover art) live in the shared header strip. A batch never mixes albums. - - **Track ordinals are persistent** — `TrackNumber` (int, 1-based, non-null) stores per-track position within a release. The left-panel row order reflects `TrackNumber`, and each track is assigned its ordinal from its list position on submit. -- **Open questions (need Daniel before build):** - - *(Daniel)* **Drag-and-drop vs. up/down arrows** for reorder — up/down `MudIconButton`s are the low-risk default and ship first. Drag-and-drop (MudBlazor `MudDropContainer`) is a polish upgrade. Confirm whether arrows are acceptable for v1. - --- ## Phase 3 — New content kinds