Files
deepdrft/product-notes/phase-9-wave-8-remediation.md

732 lines
48 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 9 — Wave 8: Remediation
Status: **spec** (CMS + public tracks — open questions resolved 2026-06-13).
Author: product-designer. Date: 2026-06-13.
**Plan only — no code edits made by this doc.**
Cross-references: `PLAN.md §9.8` (the concise Wave 8 entry), `COMPLETED.md §9.19.7`
(the landed Phase 9 waves this remediates), `product-notes/phase-9-release-medium-types.md`
(the originating design — §3 CMS surface, §5 public surface), `product-notes/phase-9-mix-visualizer-redesign.md`
(the 8.K finished design doc — **now a complete spec, pulled out of Phase-9-completion scope**),
memory *One source, multiple views*.
**Open questions are resolved (2026-06-13).** Daniel answered the whole decision list in §6 and ran
the 8.K interview. This doc no longer carries forks: 8.H is decided (H2 — a release-cardinal
all-releases browser at `/archive`), 8.I/8.F/8.E defaults are baked into their acceptance criteria, a
new consolidation track **8.L** is added (with **8.M** split off it for the legacy-form retirement), and
**8.K is moved out of Phase-9-completion scope** to a finished post-Phase-9 design doc. Phase 9 can close
without 8.K or 8.M.
---
## 0. What this wave is, and what it is not
Daniel tested the landed Phase 9 surface (Waves 17, all on `dev`) and produced a punch-list. This
wave is the gap between what the specs *built* and what hands-on use *wants*. It is **remediation, not
new feature work** — every item corrects or reshapes a surface that already exists.
Clusters:
1. **CMS Release Archive (8.A8.E)** — the card-grid landing is the wrong shape. Daniel wants the
medium varieties as **tab modes** that swap the grid in place, with an `ALL` tab on the left, and
working edit + add affordances in every mode. The current `ReleaseArchiveBrowser` (three cards that
navigate to separate `/tracks/sessions`, `/tracks/mixes` pages) is retired.
2. **CMS upload/label polish (8.F8.G, 8.L)** — compose the Session hero image into the upload form
(retire the two-step), rename "Album Name" → "Release Name," and **consolidate the
release-name/track-name pair into a single name for single-track media (Session/Mix)** so the admin
enters one name, not two.
3. **Public site (8.H8.J)** — the three-card `/archive` overview is dead weight; the searchable
all-**releases** view (release-cardinal — decided H2) *is* the archive. Slim the nav, drop GENRES
(nav-only), fix the stuck-open popover.
**Out of Phase-9-completion scope (but documented in full):** the **Mix Visualizer redesign (8.K)**.
The interview has run; `phase-9-mix-visualizer-redesign.md` is now a complete, implementation-ready
design spec. Phase 9 closes without it; it runs as a post-Phase-9 wave.
---
## 1. Verified current state (read against live source 2026-06-13)
**CMS:**
- `TrackList.razor` (`/tracks`) has a `MudToggleGroup` with three items: **Tracks**, **Releases**
(`BrowseMode.Albums`), **Release Archive** (`BrowseMode.Archive`). Routes `/tracks`,
`/tracks/albums`, `/tracks/archive`, `/tracks/genres` all resolve here; Genres has no toggle item
but is reachable by URL.
- `BrowseMode.Archive` renders `ReleaseArchiveBrowser.razor` — a `MudGrid` of **three navigate-away
cards** (Cut → `/tracks/albums`, Session → `/tracks/sessions`, Mix → `/tracks/mixes`), driven off
`Enum.GetValues<ReleaseMedium>()` + a `MediumCards` lookup. The cards leave the page entirely.
- `BrowseMode.Albums` renders `CmsAlbumBrowser` — the cross-medium releases grid with a **Type** column
rendering `<MudChip>@context.Release.ReleaseType</MudChip>` unconditionally, plus a working
batch-edit button (`/tracks/album/{title}/edit`). This grid shows **all** releases regardless of
medium (CUTS, SESSIONS, MIXES together), and the Type chip always shows the Cut-only `ReleaseType`
(Single/EP/Album) — wrong for Session/Mix rows.
- `CmsSessionBrowser` (`/tracks/sessions`) and `CmsMixBrowser` (`/tracks/mixes`) are standalone routable
pages inheriting `CmsMediumBrowserBase`, each with a "Back to Release Archive" button and a per-row
Edit button (added in §9.5.E). They are **not** embedded in `TrackList`; they are navigated to.
- `CmsTrackGrid` (the `Tracks` mode) has an **Add Track** button (`Href="/tracks/upload"`) gated on
`ShowAddButton`. The album/archive modes have no add button.
- `SessionFields.razor` (shown in the upload form for `Medium == Session`) is **only a `MudAlert`**:
"After upload, set the hero image from the **Release Archive → Sessions** browser." No hero upload
input — the hero is set afterward, per-row, in `CmsSessionBrowser`. This is the two-step Daniel wants
collapsed.
- `AlbumHeaderFields.razor` labels the first field **"Album Name"** (`Label="Album Name"`,
`RequiredError="Album Name is required"`).
- Hero-image endpoint is **resource-addressed**: `POST api/release/{id}/session/hero-image` — it needs
a release id, which does not exist until the release is created. This is *why* `SessionFields` punts
to a post-upload step. Composing it into the form means deferring the hero upload to *after* the
create call returns an id, within the same submit handler (see 8.F note).
**Public site:**
- `ArchiveView.razor` (`/archive`) is the **three-card overview** (Cuts / Sessions / Mixes), each an
`<a href>` to the medium view.
- `TracksView.razor` (`/tracks`) is the **flat track gallery** — search field, album/genre filter
pills, grid/list of *tracks* (track-cardinal). Title: "DeepDrft Track Gallery."
- `AlbumsView.razor` (`/cuts`) is the **release-cardinal** Cuts gallery (album cards).
- `Pages.cs` `MenuPages`: **ARCHIVE** (route `/archive`, children Cuts/Sessions/Mixes) + **Tracks**
(`/tracks`) + **Genres** (`/genres`).
- `DeepDrftMenu.razor` desktop renders ARCHIVE as a dual-role node: parent `<a href="/archive">` plus a
**pure-CSS hover dropdown** (`.dd-nav-dropdown`, shown via `:hover`/`:focus-within` in
`DeepDrftMenu.razor.css`). There is **no JS dismissal** — the dropdown hides only when the cursor
leaves the parent region or focus moves out. On SPA navigation (Blazor enhanced nav keeps the DOM),
the cursor often remains over the parent, so the dropdown stays visible: the "stuck open" bug (8.J).
- All three medium views (`/cuts`, `/sessions`, `/mixes`) and the `ReleaseClient`/`ReleaseProxyController`
read family exist and work.
---
## 2. CMS tracks — 8.A through 8.G
### 8.A — Release Archive as medium tabs, not navigate-away cards
**Goal.** Replace the Release Archive card grid with an **in-page tab strip** of medium modes that swap
the grid below in place, instead of navigating to separate pages. Add an `ALL` tab to the left of the
medium tabs. Retire the redundant top-level **Releases** toggle item — the `ALL` tab subsumes it.
**User-visible change.** Opening the Release Archive (or the CMS tracks page in its archive role) shows
a tab strip: **`ALL` · CUTS · SESSIONS · MIXES**. Selecting a tab swaps the grid below without leaving
the page. The old three-card "click to go to another page" interaction is gone, as is the separate
**Releases** toggle button (it is now the `ALL` tab).
**Shape (informing, not prescribing — staff-engineer owns implementation).**
- The cleanest structure folds the medium dimension into `TrackList`'s existing mode model. Today the
top-level toggle is `Tracks / Releases / Release Archive`. Daniel's ask collapses *Releases* and
*Release Archive* into one **release** view with an inner medium tab strip (`ALL / Cut / Session /
Mix`). Whether the medium tabs live as a second toggle group inside the Archive mode, or the whole
top-level toggle is restructured, is an implementation call — the **user-visible** requirement is the
four-tab strip swapping the grid in place.
- `ReleaseArchiveBrowser` (the card grid) is **retired** as the archive landing. The medium→display
lookup it holds (label/descriptor) may survive as tab labels.
- The standalone `/tracks/sessions` and `/tracks/mixes` routes: keep them reachable (a hard 404 on a
previously-working URL is hostile), but they are no longer the *primary* path — the tabs are. The
"Back to Release Archive" buttons in `CmsSessionBrowser`/`CmsMixBrowser` lose their meaning if those
browsers become embedded tab content; resolve their fate as part of 8.C.
**Acceptance criteria.**
- The Release Archive surface shows a tab strip with `ALL`, plus one tab per `ReleaseMedium`, `ALL`
left-most. (Tabs driven off the enum + a label lookup, not a hardcoded three-arm switch — preserve
the Phase 9 extension discipline so a fourth medium surfaces a tab automatically.)
- Selecting a tab swaps the grid below in place; no navigation to a separate page occurs.
- The top-level **Releases** toggle item is removed; its grid is reachable via the `ALL` tab.
- A fourth medium added to the enum surfaces a new tab with no markup change beyond one lookup entry.
**Dependencies.** Consumes the shared grid contract from **8.B** (the `ALL` grid) and the per-medium
grids from **8.C**. Land 8.B first (it defines the grid each tab renders), then 8.A wires the tab strip,
then 8.C/8.E layer the per-tab affordances. 8.A is the structural spine of the CMS cluster.
---
### 8.B — `ALL` tab: all-releases grid with working edit
**Goal.** The left-most `ALL` tab shows the current cross-medium releases grid (every release,
all media) with working edit buttons — the surface the retired **Releases** toggle showed.
**User-visible change.** The `ALL` tab presents the full releases list (CUTS, SESSIONS, MIXES together)
exactly as the current Releases grid does today, with an edit affordance per row.
**Shape.** This is essentially the current `CmsAlbumBrowser` grid (which already lists all releases and
already has a working batch-edit button) re-homed as the `ALL` tab's content. The main *new* work is
making it the default tab and ensuring the Type column behaves (that correctness fix is **8.D**, which
this tab consumes). No new data path — `CmsAlbumBrowser` already loads the cross-medium release list.
**Acceptance criteria.**
- The `ALL` tab lists every release regardless of medium.
- Each row has a working Edit button routing to the release's edit page.
- The grid matches the behaviour of today's Releases toggle grid (no regression in sort, delete,
expand-tracks).
**Dependencies.** Foundation for 8.A (the tab renders this grid). Independent of 8.C/8.D/8.E for build,
though 8.D's Type-chip fix lands *in* this grid. Recommend: 8.B + 8.D land together (same grid), then
8.A wires tabs.
---
### 8.C — Per-medium tab grids gain working edit affordances
**Goal.** The Cut / Session / Mix tab grids each get an Edit action that routes intuitively to the
correct edit page for that medium.
**User-visible change.** In each medium tab, every row has an Edit button. Clicking it opens the edit
form appropriate to that medium (Cut → batch/release edit with `ReleaseType`; Session/Mix → the
single-track edit with the medium-appropriate fields, no `ReleaseType`).
**Shape.** `CmsSessionBrowser` and `CmsMixBrowser` already have per-row Edit buttons (added §9.5.E)
routing to `/tracks/album/{title}/edit` (`BatchEdit`). The Cut tab reuses `CmsAlbumBrowser`'s existing
edit button. The work here is (a) ensuring each tab's grid carries the edit action when embedded as tab
content rather than a standalone page, and (b) confirming the edit destination is the right one per
medium — `BatchEdit` already collapses to single-track for Session/Mix (§9.6.B), so the same route
works for all three; verify it presents correctly.
**Acceptance criteria.**
- Every row in the Cut, Session, and Mix tab grids has a working Edit button.
- Cut edit opens with `ReleaseType` visible; Session/Mix edit opens single-track with no `ReleaseType`
(consistent with the landed §9.6.B collapse).
- Edit from any medium tab loads the correct release and returns to a sensible place after save.
**Dependencies.** Depends on **8.A** (the tabs must exist to host the grids). Parallel with **8.D** and
**8.E** once 8.A lands.
---
### 8.D — Type column chip reads "Session" / "DJ Mix" for non-Cuts
**Goal.** The cross-medium grid's **Type** column must not show a Cut-only `ReleaseType` (Single/EP/
Album) chip for Session/Mix rows. For non-Cut media the chip reads the medium name — **"Session"** or
**"DJ Mix"**.
**User-visible change.** In the `ALL` grid (and anywhere the cross-medium Type column appears), a Cut
row shows its release type (Single/EP/Album) as today; a Session row shows **"Session"**; a Mix row
shows **"DJ Mix"**.
**Shape.** The Type cell currently renders `@context.Release.ReleaseType` unconditionally. Per the
Phase 9 read-model design, `ReleaseDto.ReleaseType` is **nullable** and nulled for non-Cut media at the
mapping point — so for Session/Mix the chip is rendering a default/empty or stale value. The cell
becomes medium-aware: when `Medium == Cut`, show `ReleaseType`; otherwise show the medium's display
name ("Session", "DJ Mix"). Drive the display-name from a medium→label lookup (the same extension
discipline — not an inline `if session ... else if mix`), so a future medium's label comes free. Note
Daniel's exact wording: **"DJ Mix"** for the Mix medium, not "Mix."
**Acceptance criteria.**
- A Cut row's Type chip shows Single/EP/Album as today.
- A Session row's Type chip shows "Session"; a Mix row's shows "DJ Mix".
- No row shows a Cut-only `ReleaseType` value for a non-Cut medium.
**Dependencies.** Independent — a self-contained cell-rendering fix. Lands naturally with 8.B (same
grid). Can be built and tested before the tab restructure.
---
### 8.E — Add-Track buttons in all modes, medium-aware routing
**Goal.** An **Add Track** button appears in every CMS mode/tab, routing to the correct upload page
**pre-set to the selected tab's medium**.
**User-visible change.** Whatever tab the admin is on (`ALL`, Cut, Session, Mix), an Add Track button is
present. Clicking it opens the upload page with the medium already set to that tab (a Session tab's Add
Track opens the upload form in Session mode; a Mix tab's opens it in Mix mode). On the `ALL` tab, Add
Track opens the upload page at its default (Cut) — or whatever Daniel prefers as the neutral default
(flag below).
**Shape.** `CmsTrackGrid` already has an Add Track button (`Href="/tracks/upload"`). The work is (a)
surfacing it in the release/archive modes too, and (b) carrying the medium to the upload page. The
upload page (`TrackNew`/`BatchUpload`) already has a `MediumFields` selector defaulting to Cut; the
cleanest route is a query param (e.g. `/tracks/upload?medium=session`) that pre-selects the medium on
load. This mirrors how `/cuts`-style routes carry a medium filter — a single query-param convention,
not a per-medium route fork.
**Acceptance criteria.**
- Add Track is present on every tab (`ALL`, Cut, Session, Mix).
- Add Track on a medium tab opens the upload form with that medium pre-selected.
- **The `ALL` tab's Add Track defaults to Cut** (Daniel, 2026-06-13) — the existing default and most
common case.
- **The medium selector remains user-changeable after landing on the upload form** (Daniel,
2026-06-13). The pre-selected medium is a *starting point*, not a lock: pre-selecting Session from
the Session tab opens the form in Session mode, but the admin can still switch the selector to Cut/Mix
on the form without going back. The pre-selection seeds; it does not constrain.
- The pre-selected medium drives the conditional form fields immediately (Session shows the hero field
per 8.F; Cut shows `ReleaseType`; etc.), and switching the selector re-drives them live.
**Dependencies.** Depends on **8.A** (tabs exist to host the buttons). Pairs with **8.F** (a Session
Add-Track that pre-selects Session should land the admin on a form that *has* the hero field).
---
### 8.F — Session hero image in the upload form (retire the two-step)
**Goal.** Author a Session — including its hero image — in a **single upload pass**. Remove the
"set the hero image later from the Release Archive → Sessions browser" message; compose the
polymorphic metadata fields so the Session form carries its hero-image input.
**User-visible change.** Uploading a Session no longer shows the "do it in two steps" alert. The Session
upload form has a hero-image file input alongside cover art; on submit, both the release and its hero
image are created in one flow. The post-upload per-row hero step in `CmsSessionBrowser` becomes a
*correction* path (replace/fix), not the *required* authoring path.
**Shape (the ordering subtlety is the crux).** The hero endpoint is resource-addressed —
`POST api/release/{id}/session/hero-image` needs a release id that does not exist until the release is
created. This is precisely why `SessionFields` punts today. Composing it into the form does **not**
require a new endpoint; it requires the submit handler to sequence:
1. create the release via the existing upload path (returns the release id),
2. *then* POST the held hero-image file to `…/{id}/session/hero-image`,
all within one user gesture. The hero file is selected in the form, held client-side, and uploaded
after the create returns. This is the same deferred-upload pattern `AlbumHeaderFields` already uses for
cover art ("Will upload on submit"). The hero input belongs in `SessionFields` (replacing the alert)
or in the `MediumFields` dispatch — staff-engineer's call — but the *user* sees one form, one submit.
`SessionFields.razor`'s current body (a `MudAlert` only) is replaced by a real hero-image input.
**Hero image — optional, but warn if missing (Daniel, 2026-06-13).** The hero image is **not** a hard
validation gate: a Session can be submitted without one and the upload succeeds. **But** the form
**surfaces a warning when a Session is submitted (or about to be submitted) without a hero image** — a
soft nudge, not a block. Rationale: a Session's hero is its primary visual identity on the public detail
page (it is the precedence-first image — see `SessionDetail.razor`), so a missing hero is *usually* an
oversight worth flagging, but Daniel wants the seed-then-correct path (set later via the browser) to
remain valid. So: warn, don't gate.
**Acceptance criteria.**
- The Session upload form presents a hero-image input (in addition to cover art).
- Submitting a Session with a chosen hero image creates the release and sets `HeroImageEntryKey` in one
flow — no separate manual step required.
- The "set hero from the browser later" alert is removed.
- **Submitting a Session with no hero image surfaces a warning** (e.g. an inline `MudAlert` of
`Severity.Warning`, or a confirm-dialog "Submit without a hero image?") — the submit still proceeds;
the warning informs, it does not block. No hard `Required` validation on the hero field.
- The per-row hero upload in `CmsSessionBrowser` still works as a replace/correct path.
- A Session uploaded with no hero image still succeeds and can have one set later via the browser
(back-compat with the existing per-row path).
**Dependencies.** Independent of the tab restructure (8.A8.E). Touches the upload form and the submit
sequencing. Pairs naturally with **8.E** (Session Add-Track should land on this improved form). Can be
built in parallel with the tab work.
---
### 8.G — "Album Name" → "Release Name" label
**Goal.** The `AlbumHeaderFields` first-field label reads **"Release Name"**, not "Album Name."
**User-visible change.** The upload/edit form's first field is labelled "Release Name" (and its
required-error message matches). Since the field now covers Cuts, Sessions, and Mixes — not just albums
— "Release Name" is the accurate noun.
**Shape.** Rename `Label="Album Name"``Label="Release Name"` and the `RequiredError` string in
`AlbumHeaderFields.razor`. Trivial. (Check whether any other surface labels the same field "Album" and
should follow for consistency — e.g. placeholder/help text — but the named change is the label.)
**Acceptance criteria.**
- The first field of the release header form reads "Release Name."
- The required-validation message references "Release Name."
**Dependencies.** Fully independent. Trivial. Can land any time.
---
### 8.L — Consolidate release name + track name for single-track releases
**Goal.** For single-track media (**Session** and **Mix** — the §9.6/§9.7 one-track-per-medium
releases), the UI presents and stores **a single name**. The admin enters/sees one **"Release Name"**;
the track name is **derived from it automatically** — never entered or shown as a separate field. This
is a **consolidation**, not an addition: today these forms surface *two* name inputs (Release Name +
Track Name) for media that conceptually have only one name.
**Why.** A Session or Mix is a single work. There is exactly one thing to name. Surfacing a separate
"Track Name" alongside "Release Name" for these media is a redundant input that invites divergence
(the release titled "Lowcountry Live #3" whose lone track is named "untitled-master-final") and a
confusing authoring experience. Cuts are different — a Cut release legitimately has a release name
distinct from its per-track names ("Charleston EP" → tracks "Battery", "Rainbow Row") — and are
**unaffected** by this track.
**Sync posture — DECIDED (Daniel, 2026-06-13): keep them synced.** On both create and edit of a
single-track release, the underlying track name is **set equal to the Release Name on save, and kept in
sync** so they can never diverge. The admin never sees or touches the track name for Session/Mix. On
edit, changing the Release Name updates the track name with it. The track name is a *derived field*, not
an independent one. (The alternative — let them diverge once set — would reintroduce exactly the
two-name confusion this track removes. Rejected.)
**Discovery audit — every UI surface that surfaces separate Release vs. Track name (read against live
source, 2026-06-13).** This is the full blast radius. Implementation must collapse the name inputs on
the single-track path for each:
*CMS — the surfaces that show BOTH names today (these are the ones to fix):*
- **`BatchUpload.razor` (`/tracks/upload`) — the single-track branch.** When `_medium != Cut`, the form
renders `AlbumHeaderFields` (which carries the Release/Album name — renamed "Release Name" in 8.G)
**plus** a separate `<MudTextField Label="Track Name">` bound to `_tracks[0].TrackName` (lines ~62-65).
This is the primary offender on the create path. For Session/Mix this Track Name input must be
**removed**, and `_tracks[0].TrackName` set equal to the Release Name on submit.
- **`BatchEdit.razor` (`/tracks/album/{AlbumName}/edit`) — the single-track path.** Renders
`AlbumHeaderFields` (Release Name) **plus** `BatchTrackList` + `BatchTrackDetail`. For single-track
media the list is already collapsed to one row (`OnMediumChanged` / load-path trim), but
`BatchTrackDetail` still shows a separate `<MudTextField Label="Track Name">` (its lines ~8-15) for
that one row. On the single-track path this Track Name editor must be **suppressed**, and the row's
`TrackName` kept equal to `_albumName` on save.
- **`BatchTrackDetail.razor`** — the component that renders the "Track Name" field (lines ~8-15). It is
shared by the Cut path (where it stays) and the single-track path (where it must be hidden). Cleanest:
the parent passes a flag (e.g. `ShowTrackName` / `IsSingleTrack`) so `BatchTrackDetail` suppresses the
name field for single-track media while still showing the WAV/Original-File rows.
- **`TrackNew.razor` (`/tracks/new`) and `TrackEdit.razor` (`/tracks/{Id:long}`) — the legacy
single-track forms.** Both surface the separate `Track Name` / `Album` split and a `MediumFields`
selector, so both carry the same two-name redundancy on the single-track path. **These are not patched
in-place for 8.L. Their disposition is consolidation — see 8.M (legacy-form retirement).** Daniel
(2026-06-13): "I would prefer to consolidate the forms and reduce the code surface if possible." The
decision is to fold their responsibility into the batch forms and retire the legacy pair, not to
re-plumb a name-collapse into forms slated for removal. 8.L therefore touches **only** the batch
forms (`BatchUpload` / `BatchEdit` via `BatchTrackDetail`); the legacy forms are out of 8.L's scope
and handled by 8.M.
*CMS — surfaces that already do the right thing (no change needed, listed so implementation knows they
are clean):*
- **`CmsMediumTable.razor`** (the Sessions/Mixes browser grid) shows only `ReleaseAccessor(context).Title`
(the release name) as its title column — no separate track-name column. Clean.
- **`CmsSessionBrowser` / `CmsMixBrowser`** consume `CmsMediumTable` and likewise key off the release
title only. Clean.
*Public — surfaces that already do the right thing (no change needed):*
- **`SessionDetail.razor`** uses only `release.Title` for the masthead and `ViewModel.Track` for
playback — it never renders the track name separately. Clean.
- **`MixDetail.razor`** likewise uses only `release.Title` + `ViewModel.Track`. Clean.
- **`ReleaseGallery.razor`** (the Sessions/Mixes card grid) shows `release.Title` + `release.Artist`
only. Clean.
**Net blast radius (8.L proper):** the name-collapse is **entirely CMS-side** and concentrated in the
two batch forms (`BatchUpload`, `BatchEdit`) via the shared `BatchTrackDetail`. The legacy single-track
forms (`TrackNew`, `TrackEdit`) are **no longer part of 8.L** — their two-name redundancy is resolved by
retiring them outright (8.M), not by patching a collapse into them. The public site already treats a
single-track release as one-named — no public work. This is the discovery step Daniel asked for:
implementation knows the full surface set before touching anything.
**Acceptance criteria.**
- On the **create** path (batch upload, single-track medium): the form presents **one** name field
(Release Name). No separate Track Name input. On save, the single track's `TrackName` is set equal to
the Release Name.
- On the **edit** path (batch edit, single-track medium): the form presents **one** name field (Release
Name). The per-row Track Name editor is suppressed. On save, the track's `TrackName` is set equal to
the (possibly edited) Release Name — they stay synced.
- **Cuts (multi-track) are unaffected**: a Cut release keeps its Release Name distinct from per-track
names, and `BatchTrackDetail` still shows the per-track Track Name field for Cut rows.
- Switching the medium selector mid-form between Cut and a single-track medium re-drives which name
fields are visible (single name for Session/Mix; release + per-track names for Cut) without losing
entered data where it still applies.
- The legacy `TrackNew` / `TrackEdit` forms are **out of 8.L scope** — their consolidation is 8.M
(legacy-form retirement). 8.L lands independently of 8.M; the name-collapse on the batch forms does
not wait on the legacy retirement.
- No public-site change is required (verified: public detail/gallery views already key off the release
title only).
**Dependencies.** Pairs with **8.G** ("Album Name" → "Release Name" — the single name field these forms
present should already read "Release Name" before this collapse lands; sequence 8.G first or together).
Touches the same upload/edit forms as **8.E**/**8.F** — coordinate so the Session form's hero input
(8.F), medium pre-selection (8.E), and name collapse (8.L) land coherently rather than fighting over the
same submit handler. Independent of the tab restructure (8.A8.D), the public cluster, and **8.M** (8.L
lands on the batch forms whether or not the legacy forms are retired).
---
### 8.M — Retire the legacy single-track forms; consolidate onto the batch forms
**Goal.** Retire `TrackNew` (`/tracks/new`) and `TrackEdit` (`/tracks/{Id:long}`) as the single-track
authoring path. Their add/edit responsibility is **absorbed by `BatchUpload` / `BatchEdit`**, whose
single-track branch already handles Session/Mix. This **reduces the duplicate form surface** — Daniel
(2026-06-13): "I would prefer to consolidate the forms and reduce the code surface if possible." The
single-track authoring path becomes the batch form's single-track branch; the legacy routes redirect to
(or are replaced by) the batch routes.
**User-visible change.** Adding or editing a single track no longer opens a distinct single-field form.
The same batch form that handles releases handles the one-track case — one form for both, no second
authoring surface to maintain. (Cuts already author via the batch forms; this brings the Session/Mix and
single-Cut-track cases onto the same path.)
**Feasibility read (against live source, 2026-06-13). Verdict: retirement-with-reconciliation — feasible,
but not a pure delete. One real gap must be closed first.**
What the batch forms already cover that makes this viable:
- `BatchUpload` already *is* the upload path for every medium, with a dedicated single-track branch
(`_medium != Cut` renders a single WAV slot + name field, lines ~5370). `TrackNew` adds nothing the
batch upload doesn't already do — same fields (name, artist, album/release, genre, release date,
medium, cover art), same `UploadTrackAsync` call, same Mix-waveform trigger, same cover-link follow-up.
**`TrackNew` is a clean retirement** (see route note below).
- `BatchEdit` already collapses to a single row for Session/Mix (§9.6.B; `AllowNewTracks` gated on
`_medium == Cut`, `OnMediumChanged` trims to one row) and carries the same edit fields, delete, and
cover handling `TrackEdit` has.
The gap that must reconcile — **addressing model**:
- `TrackEdit` is addressed **by track id** (`/tracks/{Id:long}`). `BatchEdit` is addressed **by release
title** (`/tracks/album/{AlbumName}/edit`) and loads the *whole release*. They key off different
things. For Session/Mix this is harmless (one track = one release; the names are about to be synced by
8.L anyway), but for a **single Cut track** opened from Track mode's per-row Edit, routing to
`BatchEdit` would open the *entire parent release*, not just that track. That is a behaviour change,
not a 1:1 swap.
- **The one live inbound link to a legacy form is `CmsTrackGrid.razor` line ~82** — Track mode's per-row
Edit button: `Href="/tracks/{context.Id}"``TrackEdit`. This is the surface that must be reconciled:
either Track mode's row-edit retargets to a batch-edit-by-release (accepting that editing a track
opens its release), or `BatchEdit` gains an address-by-track-id entry that pre-selects the row. Either
is a real decision, not a mechanical rename.
- `TrackNew` (`/tracks/new`) has **no live inbound nav link** in source — every "Add Track" button
points at `/tracks/upload` (`CmsTrackGrid` line ~16, the 8.E plan). So `TrackNew`'s route can be
dropped or made a redirect to `/tracks/upload` with **zero** caller changes.
Route/redirect implications:
- `/tracks/new` → redirect to (or replace with) `/tracks/upload`. No callers to update.
- `/tracks/{Id:long}` → either redirect to a release-scoped batch edit (and retarget `CmsTrackGrid`'s
row Edit), or retain a thin track-addressed entry into the batch edit. **This is the reconciliation
call** and it is the crux of whether this lands cleanly.
**Verdict, stated plainly:** `TrackNew` is a **clean retirement**. `TrackEdit` is a
**retirement-with-reconciliation** — feasible, but it requires a decision on the single-Cut-track edit
affordance (open the whole release vs. address a single track within the batch edit) and a retarget of
`CmsTrackGrid`'s one inbound link. Not blocked; not free.
**Acceptance criteria.**
- `/tracks/new` no longer renders a distinct form; the add-single-track path is `BatchUpload`'s
single-track branch (route removed or redirected to `/tracks/upload`).
- Editing a single track no longer opens `TrackEdit`'s distinct form; the edit path is `BatchEdit`'s
single-track branch. `CmsTrackGrid`'s per-row Edit routes to the reconciled destination.
- The single-Cut-track edit affordance behaves per the reconciliation decision (documented at
implementation time): either it opens the parent release in `BatchEdit`, or `BatchEdit` accepts a
track-addressed entry that pre-selects the row.
- No dangling references to `TrackNew` / `TrackEdit` routes remain in live navigation.
- Net CMS form-code surface is reduced (two fewer routable forms to maintain).
**Architectural vs. mechanical (assessment).** **Architectural — staff-engineer territory.** This is not
a localized edit: it changes the CMS route map (two routes removed/redirected), removes two routable
components, and — most importantly — **changes the single-track edit addressing model** (track-id vs.
release-title), which carries a navigation-behaviour decision (does editing a Cut track open its whole
release?). That decision touches how Track mode's per-row Edit behaves and may require `BatchEdit` to
accept a new addressing mode. Route-map changes + component removal + a navigation-model decision is
squarely staff-engineer scope, not a maintenance pass.
**Dependencies.** Independent of 8.L (8.L lands the batch-form name-collapse regardless). Coordinates
with **8.E** (the Add-Track buttons already target `/tracks/upload`, so 8.E and 8.M agree on the upload
route). Best sequenced **after** 8.L so the batch single-track branch is already name-consolidated when
it becomes the sole single-track path. Lower priority than the name-collapse — see split rationale in §5.
---
## 3. Public site — 8.H through 8.J
### 8.H — Archive page becomes the searchable all-releases browser
**Goal.** Retarget `/archive` from the dead three-card overview to **the searchable view of all
releases**. The searchable all-releases browser *is* the archive.
**User-visible change.** Visiting `/archive` (or clicking ARCHIVE) lands on a searchable, filterable
browser of **all releases** — not three static cards. The cards are gone; the archive is the browse
surface.
**Decision (H2 — Daniel, 2026-06-13): the archive is release-cardinal.** Build a **new searchable
all-*releases* browser at `/archive`** — search + medium/genre filter, release cards (cover, title,
artist) that link to the right per-medium detail page. This is consistent with the `/cuts`
release-cardinal model and the `api/release` read family, and it mirrors the CMS tab model (8.A): both
the public archive and the CMS archive now share one "all releases, filter by medium, search within"
mental model (*One source, multiple views*). The earlier H1 reading (relabel the track-cardinal
`TracksView` and re-home it) is **dropped** — Daniel thinks in releases, and the archive should match.
**The cascade from H2 (decided):**
- **`/tracks` (`TracksView`, track-cardinal) is no longer the archive.** It is not what ARCHIVE points
at. Its fate: keep the route reachable (no hard 404), but it is demoted from the nav — the archive is
the release-cardinal browser, and the flat track gallery is not the primary browse surface anymore.
Treat `/tracks` the same way 8.I treats `/genres`: **drop it from the nav, keep the route reachable**,
pending a later decision on whether to retire it wholesale. (It is not a Wave 8 deliverable to remove
it; it simply stops being the archive.)
- **8.I follows from H2** — ARCHIVE links to the new release-cardinal browser (not `TracksView`), and
the three medium links sit beside it.
**Shape (informing, not prescribing).** A new public page at `/archive` consuming the `api/release`
paged-list family (which already supports a medium filter — `COMPLETED.md §9`). Reuse the
release-cardinal card idiom (`ReleaseGallery` already renders release cards that link to
`/{detailRoute}/{id}`); the archive adds a search field and a medium filter (ALL + per-medium) above the
grid. Drive the medium filter off `Enum.GetValues<ReleaseMedium>()` + a label lookup (the same Phase 9
extension discipline), so a fourth medium surfaces a filter chip for free. Card→detail routing is
medium-aware (a Cut card → `/cuts`-flavored target, a Session card → `/sessions/{id}`, a Mix card →
`/mixes/{id}`); reuse the existing per-medium detail routes.
**Mobile ARCHIVE → the searchable browser (Daniel, 2026-06-13).** With `/archive` becoming the
searchable browser, **mobile ARCHIVE goes straight to that browser** (the medium modes are reachable
via the in-page medium filter). The medium links also live in the hamburger sub-list under ARCHIVE
(8.I). **The three-card overview is fully retired** — there is no card-overview destination on any
breakpoint.
**Acceptance criteria.**
- `/archive` renders a **release-cardinal** searchable browse surface (search field + medium/genre
filter), not the three-card overview.
- The surface covers **all releases** (every medium), with a medium filter (ALL + per-medium) and search
within.
- Each release card links to the correct per-medium detail page.
- The medium filter is enum-driven (a fourth medium surfaces a filter chip with one lookup entry, no
markup fork).
- The old three-card overview (`ArchiveView`'s current body) is **fully retired** — desktop and mobile.
- `/tracks` (`TracksView`) is no longer the archive and is dropped from the nav (route remains
reachable; see 8.I posture for `/genres`).
**Dependencies.** Gates **8.I** (8.I links ARCHIVE to this browser). Independent of 8.J. No longer gated
on a framing decision — H2 is decided.
---
### 8.I — Nav slimmed: ARCHIVE + three medium modes inline, GENRES removed
**Goal.** Above the medium breakpoint the appbar carries **ARCHIVE** (→ all-releases browser) and the
**three medium modes** (CUTS / SESSIONS / MIXES → their view pages) directly; **GENRES is eliminated**
from the nav.
**User-visible change.** The desktop nav shows ARCHIVE plus the three medium links laid out across the
appbar (Daniel: "The ARCHIVE popover items can fill the appbar above the medium breakpoint"). GENRES no
longer appears in the nav. ARCHIVE links to the all-releases browser (8.H); each medium link goes to its
view page.
**Shape.** `Pages.cs` `MenuPages` currently nests Cuts/Sessions/Mixes as `Children` of ARCHIVE (a hover
popover) and carries Tracks + Genres as siblings. Daniel's ask flattens this above the breakpoint: the
three medium items become top-level appbar links (not hidden in a popover), ARCHIVE is its own link to
the browser, and Genres is dropped. Below the breakpoint (mobile) the existing hamburger indented-child
pattern can keep the medium links under ARCHIVE.
Note this **changes the popover model**: if the three media are inline above the breakpoint, the desktop
ARCHIVE popover may no longer be needed there at all — which also dissolves the 8.J stuck-open bug at
the desktop breakpoint (though 8.J should still be fixed for any breakpoint where a popover survives,
e.g. mobile or a narrow-desktop fallback). Coordinate 8.I and 8.J: 8.I may *reduce* where the popover
exists, 8.J fixes the dismissal wherever it remains.
**GENRES — remove the nav link only, for now (Daniel, 2026-06-13).** Drop the GENRES menu item. **Keep
the `/genres` route and `GenresView` reachable** — no active development, no hard removal. This is the
same posture Phase 9 took with the CMS genre browse and the same posture H2 sets for `/tracks`: demote
from the nav, leave the route intact, defer the wholesale-retire question. So the work here is purely
*remove the menu item from `Pages.cs`*`GenresView` itself is untouched.
**Acceptance criteria.**
- Above the medium breakpoint, ARCHIVE and CUTS / SESSIONS / MIXES appear as appbar links.
- ARCHIVE links to the all-releases browser (8.H's output — the release-cardinal `/archive`).
- Each medium link navigates to its view page (`/cuts`, `/sessions`, `/mixes`).
- **GENRES no longer appears in the nav; `/genres` + `GenresView` remain reachable by URL** (nav-only
removal, not retirement).
- Below the breakpoint, the nav remains usable: the medium links live in the hamburger **sub-list under
ARCHIVE** (8.H confirms mobile ARCHIVE → the searchable browser, with the medium links indented).
**Dependencies.** Depends on **8.H** (ARCHIVE's target). Coordinates with **8.J** (popover fate).
---
### 8.J — ARCHIVE popover click does not close (bug)
**Goal.** Clicking an item in the ARCHIVE popover **closes the popover**. Today it stays stuck open.
**User-visible change.** Clicking a popover child (Cuts/Sessions/Mixes) navigates *and* the dropdown
dismisses, instead of remaining visible over the destination page.
**Shape (root cause, verified).** The desktop dropdown is **pure CSS**`.dd-nav-dropdown` is shown by
`.dd-nav-item-parent:hover` / `:focus-within` in `DeepDrftMenu.razor.css`, with no JS dismissal.
Clicking a child is an `<a href>` SPA navigation (Blazor enhanced nav preserves the DOM), so after
navigation the cursor is often still over the parent region — `:hover` remains true — and the dropdown
stays visible. A pure-CSS hover dropdown has no "I was clicked, now dismiss" state.
Fix direction (informing, not prescribing): give the dropdown a dismissal trigger on child click — e.g.
blur the active element / move focus, or add a small interactivity hook that collapses the dropdown on
navigation, or restructure so a click toggles a closable state. The mobile menu already closes on click
(`@onclick="CloseMobileMenu"`); the desktop popover needs an equivalent. **Note:** if 8.I removes the
desktop popover (medium links inline above the breakpoint), this bug may only remain on whatever
breakpoint still shows a popover — fix it there. Confirm the surviving popover surfaces with 8.I before
implementing.
**Acceptance criteria.**
- Clicking a popover child navigates to the target and the dropdown is no longer visible afterward.
- Hover-to-open still works (no regression to the open behaviour).
- Keyboard focus dismissal still behaves (no trap).
**Dependencies.** Independent bug fix, but **coordinate with 8.I** (which may change where the popover
exists). Can be specced and fixed independently; sequence after 8.I if 8.I reshapes the popover.
---
## 4. Mix Visualizer — 8.K `[post-Phase-9, design-complete]`
**Out of Phase-9-completion scope (Daniel, 2026-06-13).** "Visualizer is outside the scope of phase 9
but we must document it now in complete detail." Phase 9 can close **without** 8.K. The interview has
run, and `product-notes/phase-9-mix-visualizer-redesign.md` is now a **finished, implementation-ready
design spec** — no longer a question set. A future wave can be dispatched straight from it.
Headline of the captured design: the visualizer becomes a **windowed, playback-coupled, bottom-to-top
scrolling waveform** — a musical-score-going-by treatment showing only the currently-playing region.
Zoom couples to apparent scroll speed (Guitar-Hero model: zoomed in = shorter time-span fills the
screen = faster apparent motion), with a hard anchor that **at maximum zoom exactly one quarter note is
visible at 180 BPM (~333 ms of audio)**. It is a **lava-lamp, not test-equipment** aesthetic —
theme-aware gradients, glassy, **strictly read-only** (no seeking), a background/theming element.
Rendering shifts to standard Canvas/WebGL with **no tricks — industry-standard patterns, well
commented**. Full motion/zoom/aesthetic/interaction/performance/data spec, plus the datum-resolution
analysis and recommendation, in the design doc.
**This is the one Wave 8 track that does not gate Phase 9 completion.** It is design-complete and
sequenced as a post-Phase-9 implementation wave.
---
## 5. Dependency and parallelization summary
**CMS cluster:**
- **8.B** (all-releases grid) + **8.D** (Type chip fix) — land together (same grid), foundational.
- **8.A** (tab strip) — consumes 8.B; the structural spine. Land after 8.B.
- **8.C** (per-medium edit), **8.E** (medium-aware Add Track) — layer onto 8.A; parallel with each
other once 8.A lands.
- **8.F** (Session hero in form), **8.G** (label rename), **8.L** (single-track name consolidation) —
**independent** of the tab work; touch the upload/edit forms. Sequence **8.G → 8.L** (the consolidated
field should read "Release Name" first), and **coordinate 8.E/8.F/8.L** — all three touch the Session
upload form and its submit handler.
- **8.M** (legacy single-track form retirement) — **architectural** (route-map change + component
removal + single-track edit addressing decision; staff-engineer scope). Independent of 8.L for build;
best sequenced **after** 8.L so the batch single-track branch is already name-consolidated when it
becomes the sole single-track path. **Lower priority than 8.L** — see split rationale below.
**8.L / 8.M split rationale.** These were one item ("consolidate the names"); they are now two because
they have different cost and risk. **8.L is mechanical-to-moderate, self-contained, and high-value**
collapse two name inputs to one on the batch forms via a `BatchTrackDetail` flag, sync the derived track
name on save. It touches no routes and no component graph. **8.M is architectural** — retiring
`TrackNew`/`TrackEdit` means a route-map change, two component removals, and a real decision about
whether editing a single Cut track opens its whole release (the track-id vs. release-title addressing
gap). Bundling them would gate the cheap, safe name-collapse behind the route/navigation decision. Split
so **8.L lands independently and immediately**, and **8.M follows when the addressing reconciliation is
decided**. They share intent ("one name, fewer forms") but not blast radius.
**Public cluster:**
- **8.J** (popover dismissal bug) — **independent**; can land immediately (but coordinate with 8.I if
8.I reshapes the popover).
- **8.H** (archive = release-cardinal searchable browser) — **decided (H2)**; build the new browser.
- **8.I** (nav slim + GENRES out + `/tracks` demoted) — depends on 8.H (ARCHIVE target); coordinates
with 8.J.
**Mix Visualizer:**
- **8.K** — **out of Phase-9 scope; design-complete.** Sequenced as a post-Phase-9 implementation wave,
dispatchable straight from `phase-9-mix-visualizer-redesign.md`. Does not gate Phase 9 completion.
**Recommended sequencing.** Land the independent/trivial items first (8.G, 8.D, 8.J), then 8.L
(name-collapse, after 8.G). Then the CMS spine (8.B → 8.A → 8.C/8.E), folding 8.F into the Session-form
work alongside 8.E/8.L. On the public side, 8.H (the new release-cardinal archive) → 8.I. **8.M**
(legacy-form retirement) sequences after 8.L and is **not a Phase-9-completion gate** — it is a
code-surface-reduction follow-on, not a taxonomy-reach correction; it can land in Wave 8's tail or just
after. **Phase 9 closes when 8.A8.J + 8.L land; 8.K and 8.M are excluded from the completion gate**
(8.K is a post-Phase-9 design-complete wave; 8.M is a consolidation follow-on that can trail).
---
## 6. Decisions — all resolved (Daniel, 2026-06-13)
The open questions that gated this wave are answered. Recorded here as the decision log; baked into the
acceptance criteria above.
1. **(8.H) Archive cardinality → H2.** The public archive is a **new release-cardinal searchable
browser** at `/archive`, not the relabelled track gallery. Cascade: `/tracks` (`TracksView`) is
demoted from the nav (route kept reachable); 8.I links ARCHIVE to the new browser.
2. **(8.H) Mobile ARCHIVE → the searchable browser.** The three-card overview is fully retired on every
breakpoint; medium links live in the hamburger sub-list under ARCHIVE.
3. **(8.I) GENRES → nav-only removal.** Drop the menu item; keep `/genres` + `GenresView` reachable. No
retirement.
4. **(8.F) Session hero → optional, but warn if missing.** No hard validation gate; the form surfaces a
warning when a Session is submitted without a hero image.
5. **(8.E) `ALL`-tab Add Track default medium → Cut.** The medium selector remains user-changeable after
landing on the upload form.
6. **(8.L) Single-track name sync → CONFIRMED synced.** The derived track name is kept **synced** to
the Release Name for Session/Mix on both create and edit, so they can never diverge. (Daniel,
2026-06-13 — was the one open recommendation; now decided.)
7. **(8.M) Legacy single-track forms → CONSOLIDATE / retire.** Daniel (2026-06-13): "I would prefer to
consolidate the forms and reduce the code surface if possible." `TrackNew`/`TrackEdit` are folded
into the batch forms and retired, not patched in place. Split from 8.L (the name-collapse) because
8.M is architectural (route map + addressing model) while 8.L is self-contained. 8.M is not a
Phase-9-completion gate.
8. **(8.K) Mix Visualizer → out of Phase-9 scope, design-complete.** Documented in full now; built
later. Phase 9 closes without it.