docs(plan): spec Phase 2 CMS enhancements — home dashboard and batch upload
This commit is contained in:
@@ -93,6 +93,67 @@ These follow from `CONTEXT.md §5`. Direction is strongly implied but no specifi
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — CMS Enhancements
|
||||
|
||||
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).
|
||||
|
||||
### 2A.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 2 scope):** All three cards navigate to `/tracks` (the track maintenance page). **Per-album / per-genre pre-filtering is deferred** — see 2A.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<AlbumSummaryDto>`).
|
||||
- **Genres count** = length of `GET api/track/genres` (exists, unauthenticated, returns `List<GenreSummaryDto>`).
|
||||
- **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.
|
||||
|
||||
### 2A.2 Card-contextual filtering of the Tracks page — `[deferred]`
|
||||
|
||||
- **What:** Make the Album and Genre dashboard cards navigate into a *filtered* `/tracks` view (e.g. clicking an album card shows only that album's tracks), rather than the unfiltered table.
|
||||
- **Why:** Turns the dashboard from a read-only summary into a navigation hub — the natural next step once the cards exist.
|
||||
- **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.
|
||||
|
||||
### 2B.1 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<string>()` 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.
|
||||
- **Shared-vs-per-track field split:**
|
||||
- *Shared (header strip, applied to every track in the batch):* album cover image (single upload), genre, release date, `ReleaseType`, and a **shared artist** field.
|
||||
- *Per-track (right detail panel):* track name, artist (pre-filled from shared artist on add, overridable per row), the individual WAV file, and that row's upload status. Album name itself is shared too (it's a release) — recommend the album *name* live in the header strip alongside cover/genre/date, not per-track. Confirm in open questions.
|
||||
- **Layout (two-panel under a header strip):**
|
||||
- **Header strip** (full width, top): album name, single cover-art `InputFile` (reuse the `MudField` cover-art pattern from `TrackNew`, including the upload-on-submit behaviour), genre `MudTextField`, release-date field, `ReleaseType` `MudSelect`, and the shared-artist `MudTextField`. These bind to a single batch-header model.
|
||||
- **Left panel** (track queue): an ordered list of queued tracks. Each row shows track name, artist, 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) and artist defaulted from the shared-artist field.
|
||||
- **Right panel** (selected-track detail): when a row is selected, show its editable fields — track name, artist (per-track override), 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; default artist = current shared-artist value (captured at add time; later edits to the shared-artist field do **not** retroactively rewrite rows already added — confirm in open questions). 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 a `releaseType` parameter (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` 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 + `TrackEntity`/`TrackDto` change + EF migration 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.
|
||||
- **Open questions (need Daniel before build):**
|
||||
- *(Daniel)* **Album name placement** — confirm album name lives in the shared header strip (treating the batch as one release), not per-track. Recommended: shared. If any batch can mix albums, this collapses and the whole "shared header" model needs rethinking.
|
||||
- *(Daniel)* **Reorder semantics** — does track order need to *persist* (i.e. a track-number / ordinal stored on `TrackEntity`), or is the left-panel ordering purely an upload-session convenience? `TrackEntity` has no track-number field today. If persisted ordering matters, that's a second entity field + migration and should be decided now, not retrofitted. Recommended: start with session-only ordering; add a persisted `TrackNumber` only if Daniel wants album track order preserved for playback/display.
|
||||
- *(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.
|
||||
- *(Daniel)* **Shared-artist retroactivity** — when the shared-artist field changes after rows are already queued, should already-added rows update, or only rows added afterward? Recommended: only-afterward (rows are independent once added), which is simpler and avoids clobbering per-track overrides.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — New content kinds
|
||||
|
||||
### 3.1 Live / session content
|
||||
|
||||
Reference in New Issue
Block a user