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

48 KiB
Raw Blame History

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.csGenresView 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.Kout 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.