48 KiB
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.1–9.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 1–7, 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:
- CMS Release Archive (8.A–8.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
ALLtab on the left, and working edit + add affordances in every mode. The currentReleaseArchiveBrowser(three cards that navigate to separate/tracks/sessions,/tracks/mixespages) is retired. - CMS upload/label polish (8.F–8.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.
- Public site (8.H–8.J) — the three-card
/archiveoverview 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 aMudToggleGroupwith three items: Tracks, Releases (BrowseMode.Albums), Release Archive (BrowseMode.Archive). Routes/tracks,/tracks/albums,/tracks/archive,/tracks/genresall resolve here; Genres has no toggle item but is reachable by URL.BrowseMode.ArchiverendersReleaseArchiveBrowser.razor— aMudGridof three navigate-away cards (Cut →/tracks/albums, Session →/tracks/sessions, Mix →/tracks/mixes), driven offEnum.GetValues<ReleaseMedium>()+ aMediumCardslookup. The cards leave the page entirely.BrowseMode.AlbumsrendersCmsAlbumBrowser— 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-onlyReleaseType(Single/EP/Album) — wrong for Session/Mix rows.CmsSessionBrowser(/tracks/sessions) andCmsMixBrowser(/tracks/mixes) are standalone routable pages inheritingCmsMediumBrowserBase, each with a "Back to Release Archive" button and a per-row Edit button (added in §9.5.E). They are not embedded inTrackList; they are navigated to.CmsTrackGrid(theTracksmode) has an Add Track button (Href="/tracks/upload") gated onShowAddButton. The album/archive modes have no add button.SessionFields.razor(shown in the upload form forMedium == Session) is only aMudAlert: "After upload, set the hero image from the Release Archive → Sessions browser." No hero upload input — the hero is set afterward, per-row, inCmsSessionBrowser. This is the two-step Daniel wants collapsed.AlbumHeaderFields.razorlabels 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 whySessionFieldspunts 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.csMenuPages: ARCHIVE (route/archive, children Cuts/Sessions/Mixes) + Tracks (/tracks) + Genres (/genres).DeepDrftMenu.razordesktop renders ARCHIVE as a dual-role node: parent<a href="/archive">plus a pure-CSS hover dropdown (.dd-nav-dropdown, shown via:hover/:focus-withininDeepDrftMenu.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 theReleaseClient/ReleaseProxyControllerread 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 isTracks / 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/sessionsand/tracks/mixesroutes: 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 inCmsSessionBrowser/CmsMixBrowserlose 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 perReleaseMedium,ALLleft-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
ALLtab. - 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
ALLtab 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
ReleaseTypevisible; Session/Mix edit opens single-track with noReleaseType(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
ReleaseTypevalue 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
ALLtab'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:
- create the release via the existing upload path (returns the release id),
- 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 patternAlbumHeaderFieldsalready uses for cover art ("Will upload on submit"). The hero input belongs inSessionFields(replacing the alert) or in theMediumFieldsdispatch — 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
HeroImageEntryKeyin 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
MudAlertofSeverity.Warning, or a confirm-dialog "Submit without a hero image?") — the submit still proceeds; the warning informs, it does not block. No hardRequiredvalidation on the hero field. - The per-row hero upload in
CmsSessionBrowserstill 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.A–8.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 rendersAlbumHeaderFields(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].TrackNameset equal to the Release Name on submit.BatchEdit.razor(/tracks/album/{AlbumName}/edit) — the single-track path. RendersAlbumHeaderFields(Release Name) plusBatchTrackList+BatchTrackDetail. For single-track media the list is already collapsed to one row (OnMediumChanged/ load-path trim), butBatchTrackDetailstill 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'sTrackNamekept equal to_albumNameon 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) soBatchTrackDetailsuppresses the name field for single-track media while still showing the WAV/Original-File rows.TrackNew.razor(/tracks/new) andTrackEdit.razor(/tracks/{Id:long}) — the legacy single-track forms. Both surface the separateTrack Name/Albumsplit and aMediumFieldsselector, 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/BatchEditviaBatchTrackDetail); 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 onlyReleaseAccessor(context).Title(the release name) as its title column — no separate track-name column. Clean.CmsSessionBrowser/CmsMixBrowserconsumeCmsMediumTableand likewise key off the release title only. Clean.
Public — surfaces that already do the right thing (no change needed):
SessionDetail.razoruses onlyrelease.Titlefor the masthead andViewModel.Trackfor playback — it never renders the track name separately. Clean.MixDetail.razorlikewise uses onlyrelease.Title+ViewModel.Track. Clean.ReleaseGallery.razor(the Sessions/Mixes card grid) showsrelease.Title+release.Artistonly. 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
TrackNameis 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
TrackNameis 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
BatchTrackDetailstill 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/TrackEditforms 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.A–8.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:
BatchUploadalready is the upload path for every medium, with a dedicated single-track branch (_medium != Cutrenders a single WAV slot + name field, lines ~53–70).TrackNewadds nothing the batch upload doesn't already do — same fields (name, artist, album/release, genre, release date, medium, cover art), sameUploadTrackAsynccall, same Mix-waveform trigger, same cover-link follow-up.TrackNewis a clean retirement (see route note below).BatchEditalready collapses to a single row for Session/Mix (§9.6.B;AllowNewTracksgated on_medium == Cut,OnMediumChangedtrims to one row) and carries the same edit fields, delete, and cover handlingTrackEdithas.
The gap that must reconcile — addressing model:
TrackEditis addressed by track id (/tracks/{Id:long}).BatchEditis 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 toBatchEditwould 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.razorline ~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), orBatchEditgains 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(CmsTrackGridline ~16, the 8.E plan). SoTrackNew's route can be dropped or made a redirect to/tracks/uploadwith 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 retargetCmsTrackGrid'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/newno longer renders a distinct form; the add-single-track path isBatchUpload'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 isBatchEdit'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, orBatchEditaccepts a track-addressed entry that pre-selects the row. - No dangling references to
TrackNew/TrackEditroutes 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/tracksthe 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.
/archiverenders 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+GenresViewremain 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 +
/tracksdemoted) — 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.A–8.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.
- (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. - (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.
- (8.I) GENRES → nav-only removal. Drop the menu item; keep
/genres+GenresViewreachable. No retirement. - (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.
- (8.E)
ALL-tab Add Track default medium → Cut. The medium selector remains user-changeable after landing on the upload form. - (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.)
- (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/TrackEditare 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.K) Mix Visualizer → out of Phase-9 scope, design-complete. Documented in full now; built later. Phase 9 closes without it.