docs: spec Phase 9 Wave 5 — gap cleanup
This commit is contained in:
@@ -165,6 +165,72 @@ Sequenced as four waves. Wave 1 is a prerequisite for everything; within Waves 2
|
||||
|
||||
---
|
||||
|
||||
### 9.5 Wave 5 — Gap Cleanup
|
||||
|
||||
Waves 1–4 are on `dev`. This wave fixes functional gaps discovered in the landed code: one disclosed by the Wave 3 engineer (medium is never written through the upload path), two structural issues flagged by review (fragile track resolution in the detail VM, browser duplication), and one nav gap (`/tracks` is unreachable from the public menu). Items are ordered: **A–B are blockers** (data correctness); **C–E are correctness/nav gaps**; **F is a structural debt item** worth landing when the browsers next need editing.
|
||||
|
||||
**9.5.A — Medium write path: `POST api/track/upload`**
|
||||
|
||||
- **What:** The `POST api/track/upload` endpoint accepts no `medium` form field. `CmsTrackService.UploadTrackAsync` already sends `medium` in the multipart body (a forward-compatible no-op left by the Wave 3 engineer), but the API ignores it. Every uploaded release is created with `Medium = Cut` regardless of the CMS form selection. Sessions and Mixes uploaded through the CMS are silently mis-typed at the database level.
|
||||
- **Why:** This is the primary functional gap of the phase. A mix uploaded as `Cut` does not appear in the `/mixes` browser, does not trigger waveform generation on the correct release, and the public `/mixes/{id}` detail page will never find it. The bug is silent — no error surfaces; the track uploads cleanly into the wrong category.
|
||||
- **Shape:** Three layers, each minimal:
|
||||
1. **`TrackController.UploadTrack`** — add `[FromForm] string? medium` parameter. Parse it with `Enum.TryParse<ReleaseMedium>` (same defensive pattern as `releaseType`, defaulting to `Cut` with a logged warning on unrecognised values). Pass the parsed value into `UnifiedTrackService.UploadAsync`.
|
||||
2. **`UnifiedTrackService.UploadAsync`** — add `ReleaseMedium medium` parameter. Include it in the `ReleaseDto` passed to `FindOrCreateRelease` (the DTO already has the `Medium` field; it is simply not populated today).
|
||||
3. **`FindOrCreateRelease` find-path:** When the release *already exists*, the returned row's `Medium` is not updated to match the upload's intent. This is correct behaviour for the first track — the release was created with the right medium. It is potentially wrong for subsequent tracks uploaded to the same release with a corrected medium. No change required here: medium is a release-level property, and the first upload is authoritative. Document this explicitly in the service comment so future engineers do not try to "fix" it.
|
||||
- **Acceptance criteria:** A Session upload from the CMS creates (or links to) a release with `Medium == Session`; a Mix upload creates a release with `Medium == Mix`; a Cut upload is unchanged. The `GET api/release?medium=session` endpoint returns the Session release immediately after upload with no manual migration.
|
||||
- **Open question:** Should the upload path *update* an existing release's medium when it differs? Recommend no — a release's medium is set on creation and should not silently change on a subsequent track add. If an admin needs to change a release's medium, that is an edit operation (9.5.B). Capture this as a comment in the service, not a policy decision to re-open here.
|
||||
|
||||
**9.5.B — Medium write path: `PUT api/track/meta`**
|
||||
|
||||
- **What:** `UpdateTrackMetadataRequest` carries no `Medium` field. `PUT api/track/meta/{id}` can update `ReleaseType` on a release but cannot change `Medium`. `CmsTrackService.UpdateAsync` sends no `medium` field. An admin who uploads a Session as `Cut` (due to the pre-9.5.A bug, or a future form mistake) has no way to correct the medium through the CMS after the fact.
|
||||
- **Why:** Without an edit path, the only remediation is a direct DB update or a delete-and-re-upload. Both are bad. The edit path should be complete.
|
||||
- **Shape:**
|
||||
1. **`UpdateTrackMetadataRequest`** — add `ReleaseMedium? Medium` (nullable: null = no change, matching the `ReleaseType?` pattern already on the request).
|
||||
2. **`TrackController.UpdateMeta`** — apply `request.Medium` to `release.Medium` when non-null, alongside the existing `ReleaseType` conditional (the same six-line pattern at line 394–395 of the controller).
|
||||
3. **`CmsTrackService.UpdateAsync`** — add `ReleaseMedium? medium = null` parameter, include in the JSON body.
|
||||
4. **`ICmsTrackService`** — update the interface signature to match.
|
||||
5. **`TrackEdit.razor` / `BatchEdit.razor`** — wire the `MediumFields` selector (already present for upload via `BatchUpload`) into the edit submit path, passing the selected medium.
|
||||
- **Acceptance criteria:** An admin can open an existing release in `TrackEdit` or `BatchEdit`, change the medium selector, submit, and the release's `Medium` column updates in the DB. The browsers (`CmsAlbumBrowser`, `CmsSessionBrowser`, `CmsMixBrowser`) reflect the new medium after the edit.
|
||||
- **Constraint:** The `ReleaseType`-only-for-`Cut` invariant: when medium changes away from `Cut`, the controller should null (or ignore) `ReleaseType` on the release — the same enforcement the `TrackConverter` already applies on the read path. Mirror that logic on the write path: if `request.Medium` is non-null and not `Cut`, reset `release.ReleaseType = ReleaseType.Single` (the DB-level default) rather than leaving a stale studio-format value.
|
||||
|
||||
**9.5.C — `ReleaseDetailViewModel`: replace fragile album-title track resolution**
|
||||
|
||||
- **What:** `ReleaseDetailViewModel.Load` resolves the playable track for a Session or Mix detail page by calling `_trackData.GetPage(pageNumber: 1, pageSize: 1, album: release.Title)`. This is a string join on album title. If two releases share the same title (different artists — e.g., both have an untitled mix), the wrong track is returned. More fundamentally, filtering by album title relies on the `Release.Title` matching what was stored as the album string at upload time — a join that is fragile once releases can be renamed via the edit path (9.5.B).
|
||||
- **Why:** The correct join is by `releaseId`, not album title. The track-page endpoint already supports `album=` filtering; it needs an additional `releaseId=` filter, or the public API needs a `GET api/track/by-release/{releaseId}` endpoint. This is a correctness issue, not a cosmetic one — a collision silently plays the wrong track.
|
||||
- **Shape (recommended):** Add a `releaseId` query parameter to `GET api/track/page` in `TrackController` and thread it through `ITrackService.GetPaged` → `TrackRepository.GetPagedFilteredAsync` as an additional `WHERE release_id = @releaseId` predicate. `TrackFilter` gains a `long? ReleaseId` field. `ReleaseDetailViewModel.Load` then calls `GetPage(pageNumber: 1, pageSize: 1, releaseId: release.Id)` — an exact join, no title string. The public `IReleaseDataService` and `ReleaseClientDataService` do not need changes if the track page is called directly via `ITrackDataService`.
|
||||
- **Acceptance criteria:** `/sessions/{id}` and `/mixes/{id}` resolve their playable track by `releaseId`, not by album title string. Two releases with identical titles return their own correct tracks on their respective detail pages.
|
||||
- **Open question:** Should `TrackFilter.ReleaseId` be exposed on the public unauthenticated `GET api/track/page` endpoint? Yes — it is a read-only filter on public data, same posture as `album=` and `genre=`. No auth change.
|
||||
|
||||
**9.5.D — Public nav: `/tracks` route unreachable**
|
||||
|
||||
- **What:** `Pages.MenuPages` (the public nav model) contains ARCHIVE (with sub-items /cuts, /sessions, /mixes) and Genres. `/tracks` (the original track gallery at `TracksView.razor`) is not in the nav. The route is still live — typing `/tracks` in the address bar works — but there is no menu entry, no link from any existing page, and no redirect from any of the new medium surfaces.
|
||||
- **Why it matters:** The track gallery is a useful surface (flat cross-medium search, grid/list toggle, genre/album filter). Removing it from the nav without a replacement or deliberate deprecation is a nav gap. A listener who does not know about `/cuts` has no way to discover the flat track list.
|
||||
- **Shape (three options — pick one):**
|
||||
- **Option A (recommended): Add `/tracks` back to the nav.** Add a "Tracks" entry (flat, no children) to `Pages.MenuPages` alongside ARCHIVE and Genres. Zero risk; the page exists and works. Honest about what the site offers.
|
||||
- **Option B: Retire `/tracks` explicitly.** Add a redirect from `/tracks` → `/cuts` (or `/archive`) and remove `TracksView.razor`. Requires confirming that `/cuts` is a complete replacement (it is not — `/cuts` shows only Cut releases; `/tracks` is a flat cross-medium list). Not recommended unless Daniel confirms the gallery is intentionally retired.
|
||||
- **Option C: Make ARCHIVE the gallery.** Repurpose `/archive` from the current three-card overview to the flat track gallery. Feels wrong — `/archive` is already a meaningful overview page, not a gallery.
|
||||
- **Recommendation:** Option A. The track gallery is valuable and distinct from the medium-specific browsers. Add "Tracks" to `Pages.MenuPages`. If Daniel later wants to retire the gallery, that is a separate explicit decision with a redirect. Do not silently leave a useful route off the nav.
|
||||
- **Acceptance criteria:** `/tracks` appears in the public navigation menu. Desktop and mobile nav both link to it. Existing functionality of `TracksView` is unchanged.
|
||||
|
||||
**9.5.E — `CmsSessionBrowser` and `CmsMixBrowser`: missing Edit row action**
|
||||
|
||||
- **What:** The Wave 3 spec for 9.3.B says "row Edit + hero-image management" for the Session browser, and the Mix browser should similarly have an edit affordance. The landed `CmsSessionBrowser` and `CmsMixBrowser` provide the medium-specific action (hero upload / waveform generate) but no Edit button linking to the standard release edit page (`/tracks/album/{name}/edit` via `BatchEdit`).
|
||||
- **Why:** Without the Edit button, an admin cannot rename a session, change its artist, update its genre, or swap its cover art from the browser. The only path is navigating to `/tracks`, finding the session track, and editing it from there — which itself is now off the nav (9.5.D).
|
||||
- **Shape:** Add a `MudButton` (or `MudIconButton`) per row linking to `/tracks/album/@Uri.EscapeDataString(context.Release.Title)/edit` in both browsers, matching the `CmsAlbumBrowser` pattern. No new components or endpoints.
|
||||
- **Acceptance criteria:** Each row in `CmsSessionBrowser` and `CmsMixBrowser` has an Edit button that navigates to `BatchEdit` for that release. The edit page loads the release's tracks and release-level fields correctly.
|
||||
|
||||
**9.5.F — `CmsSessionBrowser` / `CmsMixBrowser` structural duplication (DRY debt)**
|
||||
|
||||
- **What:** Both browsers share an identical structural skeleton: a `LoadAsync` method with `_loading` / `_rows` fields, an `OnInitializedAsync` → `LoadAsync` call, a `ThumbUrl` static helper, snackbar error handling, and a `MudTable` with cover-thumbnail + title + artist columns. Only the per-row action column and the row model differ. This is copy-paste, not composition. The Phase 9 intro promises "a new medium is one entry, one file" — with this structure, a new medium browser is instead two files of boilerplate plus one file of new logic.
|
||||
- **Why:** Manageable now at three media, but violates the open/closed discipline the phase established. The right fix is a `MediumBrowserBase` abstract base (or a parameterized `CmsMediumBrowser` component with an action-column slot), reducing each browser to its medium-specific action markup only.
|
||||
- **Shape:** Extract a `CmsMediumBrowserBase` class (analogous to `MediumBrowseBase` on the public site) carrying: `_loading`, `_rows`, `OnInitializedAsync`, `LoadAsync`, `ThumbUrl`. Subclasses supply the `ReleaseMedium` and the per-row action column. The table structure (cover, title, artist, actions) is rendered in the base or via a shared `CmsMediumTable` Razor component with an `ActionContent` `RenderFragment` parameter. A new medium browser is then a subclass that overrides the medium enum and implements the action fragment.
|
||||
- **Acceptance criteria:** `CmsSessionBrowser` and `CmsMixBrowser` no longer duplicate `LoadAsync` / `ThumbUrl` / the error-snackbar pattern. A third medium browser (hypothetical) would require only the medium-specific action markup, with zero structural boilerplate.
|
||||
- **Note:** This is structural debt, not a functional gap. Mark `[nice-to-have]` if Wave 5 is time-boxed. The functional items (A–E) are the priority; F can defer to Wave 6 if needed.
|
||||
|
||||
**Dependency summary for Wave 5:** A and B are independent of each other (parallel tracks) and are the highest priority — both are data-correctness blockers for Session/Mix releases created since Wave 3 landed. C depends on A and B being stable (so the detail VM resolves tracks for correctly-typed releases). D and E are independent nav/UI fixes. F is independent structural debt.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Working with this file
|
||||
|
||||
Reference in New Issue
Block a user