docs: spec Phase 9 Wave 8 remediation + Mix Visualizer interview set

This commit is contained in:
daniel-c-harvey
2026-06-13 17:02:53 -04:00
parent c83b06aaee
commit fccace1381
3 changed files with 668 additions and 0 deletions
+27
View File
@@ -165,6 +165,33 @@ Sequenced as four waves. Wave 1 is a prerequisite for everything; within Waves 2
Waves 17 are landed (`COMPLETED.md §9`). Wave 6 closes two functional gaps a post-landing smoke-test survey surfaced — surfaces the medium taxonomy did *not* reach, not regressions. Wave 7 hardens the single-track-per-medium rule from a CMS-form convention into a real domain invariant — the one place the medium taxonomy is *declared but not enforced* below the UI. Waves 17 are landed (`COMPLETED.md §9`). Wave 6 closes two functional gaps a post-landing smoke-test survey surfaced — surfaces the medium taxonomy did *not* reach, not regressions. Wave 7 hardens the single-track-per-medium rule from a CMS-form convention into a real domain invariant — the one place the medium taxonomy is *declared but not enforced* below the UI.
### 9.8 Wave 8 — Remediation (from Daniel's Phase-9 testing pass)
Daniel tested the landed Phase 9 surface end-to-end and produced a punch-list of corrections before the phase is called complete. These are **not new features** — they are the gap between what the Wave 17 specs *built* and what hands-on use *wants*. The theme is the same one Phase 9 has carried throughout: the medium taxonomy reaching every surface it should, and the browse surfaces matching the mental model rather than the implementation's first cut.
Two surfaces dominate: the **CMS Release Archive** (the card-grid landing is the wrong shape — Daniel wants medium *tabs*, not navigate-away cards) and the **public Archive** (the three-card overview is dead weight; the searchable all-releases view should *be* the archive). A third item — the **Mix Visualizer redesign** — is explicitly **not specced here**: Daniel wants to be interviewed first. It is carried as a design-pending track with an interview question set in `product-notes/phase-9-mix-visualizer-redesign.md`.
Full track decomposition, acceptance criteria, and parallel/dependent analysis: `product-notes/phase-9-wave-8-remediation.md`. The tracks in brief:
**CMS (`DeepDrftManager`):**
- **8.A — Release Archive as medium tabs, not cards.** Retire the three navigate-away medium cards (`ReleaseArchiveBrowser`); replace with an in-page tab strip (`ALL` + one tab per medium) that swaps the grid below in place. Retire the redundant top-level **Releases** toggle item (the `ALL` tab subsumes it). *(Depends on 8.B for the shared grid contract; 8.C/8.D/8.E layer onto it.)*
- **8.B — `ALL` tab: all-releases grid with edit.** Left-most tab showing the current cross-medium releases grid with working edit buttons — the surface the retired Releases toggle used to show.
- **8.C — Per-medium grids gain working edit affordances.** Cut / Session / Mix tab grids each get an Edit action routing to the correct edit page for that medium. *(Parallel with 8.D, 8.E once 8.A lands.)*
- **8.D — Type chip reads "Session" / "DJ Mix" for non-Cuts.** The cross-medium grid's Type column must not show a Cut-only `ReleaseType` chip for Session/Mix rows. *(Independent.)*
- **8.E — Add-Track buttons in all modes, medium-aware routing.** Every tab surfaces an Add Track button routing to the upload page pre-set to that tab's medium. *(Depends on 8.A.)*
- **8.F — Session hero image in the upload form (retire the two-step).** Compose the hero-image field into the Session upload form so a Session is authored in one pass; remove the "set it later from the browser" alert. *(Independent of the tab work; touches the upload form + the resource-addressed hero endpoint ordering — see note.)*
- **8.G — "Album Name" → "Release Name" label.** Rename the `AlbumHeaderFields` label. *(Independent, trivial.)*
**Public site (`DeepDrftPublic.Client`):**
- **8.H — Archive page becomes the searchable all-releases browser.** Retarget `/archive` from the three-card overview to the searchable all-releases view; this *is* the archive. Resolve the track-vs-release framing (see open question). *(Depends on the framing decision; see note.)*
- **8.I — Nav slimmed: ARCHIVE + three medium modes inline, GENRES removed.** Above the medium breakpoint the appbar carries ARCHIVE (all-releases browser) and the three medium links directly; GENRES is eliminated from the nav. *(Depends on 8.H for the ARCHIVE target.)*
- **8.J — ARCHIVE popover click does not close (bug).** Clicking a popover child leaves the pure-CSS hover dropdown stuck open on SPA navigation. Fix the dismissal. *(Independent bug fix.)*
**Mix Visualizer:**
- **8.K — Mix Visualizer redesign. `[design pending interview]`.** Daniel wants a scrolling high-resolution waveform (bottom-to-top) with a slider coupling scroll-speed to zoom/resolution. He has **explicitly asked to be interviewed before this is designed.** No implementation spec exists or should be written until the interview runs. Question set: `product-notes/phase-9-mix-visualizer-redesign.md`.
**Dependency shape:** 8.B is the foundation for the CMS tab work (8.A consumes the shared grid; 8.C/8.E layer on once 8.A lands). 8.D, 8.F, 8.G are independent and parallelizable immediately. On the public side, 8.J is an independent bug fix; 8.H gates 8.I and rides an open framing question; 8.K is blocked on the interview and must not start until it runs.
## Working with this file ## Working with this file
@@ -0,0 +1,142 @@
# Phase 9 — Wave 8.K: Mix Visualizer Redesign (Interview Question Set)
Status: **design pending interview**. Author: product-designer. Date: 2026-06-13.
**No implementation spec exists or should be written until the interview runs.**
Cross-references: `PLAN.md §9.8` (Wave 8 entry, 8.K), `product-notes/phase-9-wave-8-remediation.md §4`,
`product-notes/phase-9-release-medium-types.md §5.4` (the original `MixWaveformVisualizer` design).
---
## Purpose
Daniel wants the Mix Visualizer **completely redesigned** and has **explicitly asked to be interviewed**
before any design is committed. This document is the structured question set the-boss relays to Daniel
to run that interview. It is **not** a spec. When the interview produces answers, they get captured here
(or in a successor design note), and only then does 8.K become implementable.
---
## Current implementation (grounded, read 2026-06-13)
So the questions are anchored in what exists rather than asked blind:
- **Component:** `MixWaveformVisualizer.razor` + `.razor.cs` in `DeepDrftPublic.Client/Controls/`.
- **What it renders today:** a **static** full-viewport background. It fetches a stored loudness profile
(`WaveformProfileDto`, base64 loudness bytes [0,255]) via `IReleaseDataService.GetMixWaveform(releaseId)`,
and builds **one closed SVG silhouette path** — a vertically mirrored continuous wave around the
horizontal midline, stretched across the full viewport via `preserveAspectRatio="none"`. It is a
single still shape; it does not move.
- **Layout:** rendered as the full-page background behind the Mix detail content
(`MixDetail.razor` places `<MixWaveformVisualizer>` behind a `.mix-detail-foreground` stacking layer).
- **Played-portion wash:** a `<rect>` clipped to the silhouette, width = `PlaybackPosition * width`,
washes the played portion. `PlaybackPosition` is a normalized [0,1] input.
- **Seek seam (inert):** `OnSeek` callback + two-way `PlaybackPosition` binding exist but click-to-seek
is **not wired** — the seam was added for a future wave.
- **Data resolution:** the profile is the **high-resolution** Mix waveform datum computed server-side
(§9.2.B trigger) and stored in the vault; distinct from the player-bar low-res peek.
- **Explicit design boundary (from §5.4):** this component is deliberately **NOT** the player-bar
peak-bar idiom (`SpectrumVisualizer` / `LevelMeterFab`). Those own the player bar; the Mix visualizer
has its own visual language.
**Daniel's seed idea for the redesign:** NOT a static background image. Instead the waveform **scrolls
from the bottom of the screen to the top** in **high resolution**, with a **slider controlling scroll
speed / zoom level** — higher resolution moves faster. That is the entire brief so far; the interview
fills in the rest.
---
## Interview questions
Grouped by theme. Relay to Daniel; capture answers inline or in a successor note.
### A. Motion & scroll behaviour
1. The waveform scrolls **bottom-to-top**. Is it the *whole mix's* waveform scrolling past (like a
scrolling score / piano-roll), or a *windowed* segment around the playback head? I.e. does the
waveform represent the entire track laid out vertically and scroll through it, or a moving window?
2. Is scroll **coupled to playback** (the visualizer scrolls because the track is playing, position =
playhead), or is it a **free ambient motion** independent of playback (scrolls even when paused /
nothing is playing)? Or both modes?
3. If coupled to playback: does the **current playback position** sit at a fixed point on screen (e.g.
always centre, or always at the top "now" line) with the waveform flowing past it? Where is "now"?
4. What happens at the **start and end** of the mix? Does it scroll in from empty / scroll out to empty,
loop, or hold?
5. Direction is bottom-to-top — is that fixed, or is direction itself something to play with (some
visualizers run top-down)? Confirm bottom-to-top is the intent.
### B. Zoom / resolution coupling (the slider)
6. The slider couples **scroll speed and zoom/resolution** ("higher res moves faster"). Unpack the
coupling: does higher zoom mean (a) more waveform detail visible per unit height *and* faster scroll,
or (b) you're "zoomed in" on a shorter time-span so the same playback rate covers more screen, hence
faster apparent motion? These feel different — which is the mental model?
7. Is the slider a **single control** that ties speed and zoom together (one dimension), or do you want
**independent** control of zoom and speed (two sliders / a 2D control)?
8. What's the **range**? At minimum zoom, roughly how much of the mix is visible on screen (the whole
thing? a few minutes?); at maximum zoom, how fine (individual transients? bars/beats)?
9. Does the slider position **persist** across mixes / sessions, or reset each time? Is there a sensible
**default** zoom the page opens at?
10. Should the high-resolution datum support the deepest zoom you want, or is there a resolution ceiling
we should know about? (The stored datum has a fixed bucket count — extreme zoom may exceed its
resolution. Worth knowing the target so the datum resolution can be set to match.)
### C. Colour & aesthetics
11. What's the **visual feel** you're after — is this meant to be hypnotic/ambient (a lava-lamp you can
stare at), informational (read the structure of the mix), or both? What makes it "pleasing" to you?
12. **Colour treatment:** single colour, gradient, theme-aware (light/dark palette — "Charleston in the
Day" / "Lowcountry Summer Nights")? Should it react to anything (frequency, intensity, time)?
13. Does the **played vs. unplayed** distinction matter in the scrolling model the way the wash does
today? Or in a scroll-past model is "played" simply "already scrolled off the top"?
14. **Form of the wave:** keep the mirrored-silhouette filled shape, or something else — lines, bars,
particles, a denser spectral look? You said high-resolution; what does high-res *look* like to you?
15. Does it stay a **full-page background** behind the detail content (as today), or become a more
central/foreground element of the Mix detail page? Does the detail content (title, metadata, play
control) still sit over it?
### D. Interaction model
16. Is the visualizer **interactive**? The current build has an inert click-to-seek seam. Do you want
**click/scrub-to-seek** on the scrolling waveform — and if so, how does seeking interact with a
moving target (click a point as it scrolls past? scrub a position?)?
17. The slider is one control. Any **other controls** on the visualizer surface — play/pause, a
"follow playhead vs. free-scroll" toggle, anything?
18. On **touch / mobile**: does the scroll respond to touch gestures (drag to scrub, pinch to zoom), or
is it display-only on mobile with the slider as the only control?
19. Should the visualizer be **reusable** beyond the Mix detail page (the §5.4 brief made it a named
reusable component — e.g. a mix card preview, an embed)? Does the scrolling behaviour need to work
at small sizes, or is it a full-page-only treatment?
### E. Performance & technical constraints
20. Smooth bottom-to-top scrolling at high resolution is a **continuous animation** — likely Canvas or
WebGL rather than the current static SVG (SVG won't animate a high-res scroll smoothly). Are you
open to that rendering-tech shift, or is there a reason to stay SVG?
21. What's the **target experience** — buttery 60fps on desktop, with a graceful degrade on weaker
devices/mobile? Any device floor we should design to?
22. Does the scroll animation need to **keep running** while audio streams/decodes (the player is a
chunked streaming pipeline), or only animate once enough is buffered? Should it react to buffering
state at all?
23. Is there a **battery / ambient** concern — should it pause/slow when the tab is backgrounded or the
mix is paused, to avoid a CPU-hot idle animation?
### F. Scope & sequencing
24. Is this a **replace-in-place** of the current static visualizer (same data, same page slot, new
rendering), or does it pull in new data needs (e.g. higher-resolution datum, frequency/spectral data
the stored loudness profile doesn't carry)?
25. Is the scrolling visualizer a **must-ship for Phase 9 completion**, or can Phase 9 close with the
current static visualizer and the scroll redesign land as a fast-follow? (Affects whether 8.K blocks
calling Phase 9 done.)
26. Are there **references** — other visualizers, apps, videos — that capture the feel you want? A
concrete "like that, but…" anchors the design far better than abstract description.
---
## After the interview
Capture Daniel's answers (here or in a successor design note), then this track converts from
`[design pending interview]` to a real implementation spec — at which point the rendering-tech decision
(question 20), the data-resolution question (10, 24), and the interaction model (1618) are the three
things most likely to drive the build's shape and should be settled first.
+499
View File
@@ -0,0 +1,499 @@
# Phase 9 — Wave 8: Remediation
Status: spec (CMS + public tracks) / **design-pending-interview** (Mix Visualizer, 8.K).
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 interview question set), memory *One source, multiple views*.
---
## 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.
Three 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)** — compose the Session hero image into the upload form (retire
the two-step), and rename "Album Name" → "Release Name."
3. **Public site (8.H8.J)** — the three-card `/archive` overview is dead weight; the searchable
all-releases view should *be* the archive. Slim the nav, drop GENRES, fix the stuck-open popover.
**Not in this wave:** the **Mix Visualizer redesign (8.K)**. Daniel has explicitly asked to be
interviewed before it is designed. It is carried as `[design pending interview]` with a structured
question set in `phase-9-mix-visualizer-redesign.md`. No implementation spec exists or should be
authored until the interview runs.
---
## 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 pre-selected medium drives the conditional form fields immediately (Session shows the hero field
per 8.F; Cut shows `ReleaseType`; etc.).
**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).
**Open question (minor, recommend a default).** What medium does the `ALL` tab's Add Track default to?
*Recommend Cut* (the existing default and the most common case); the admin can switch the selector. Not
worth blocking.
---
### 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.
**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.
- The per-row hero upload in `CmsSessionBrowser` still works as a replace/correct path.
- A Session uploaded with no hero image still succeeds (hero optional), 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.
**Open question (flag, recommend).** Is the hero image **required** for a Session, or optional? Daniel's
note says "the form should allow uploading all metadata including the hero image" — *allow*, not
*require*. **Recommend optional** (consistent with cover art being optional, and with the existing
per-row set-later path remaining valid). Confirm with Daniel; if required, the form gains a validation
gate.
---
### 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.
---
## 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.
**Shape — and the framing tension that needs a decision.** Daniel's note says the Archive should be
"what the TRACKS page links to now — the searchable view of all releases," and that "naming everything
TRACKS is misleading." There is a real cardinality mismatch to resolve:
- The current `/tracks` (`TracksView`) is **track-cardinal** — it lists individual *tracks* with search
and album/genre filter pills. It is the searchable view that exists today.
- The medium browsers (`/cuts`, `/sessions`, `/mixes`) are **release-cardinal**.
- Daniel says "all releases" — which reads release-cardinal — but points at `TracksView`, which is
track-cardinal.
Two readings, and Daniel should pick:
- **(H1) Archive = the existing track gallery, renamed.** Move/retarget `TracksView`'s searchable
gallery to `/archive`, drop the misleading "Tracks" naming, and treat the flat searchable list as the
archive. Lowest effort — it is the view that exists, relabelled and re-homed. *But* it is
track-cardinal, which sits oddly with "all releases" and with the release-cardinal medium views it
sits beside.
- **(H2) Archive = a new searchable all-*releases* browser.** Build a release-cardinal searchable
browser (search + medium/genre filter) at `/archive`, consistent with the `/cuts` release-cardinal
model and the `api/release` read family. More work (a new browse surface), but coherent: the archive
is releases, the medium tabs filter the archive, search filters within. This matches the *CMS* tab
model (8.A) — the public archive and the CMS archive would share the "all releases, filter by medium,
search within" mental model.
**Recommendation: (H2) if the release is the unit Daniel thinks in; (H1) if speed matters most.** The
CMS side (8.A) is moving to a release-cardinal, medium-filtered archive — symmetry argues for (H2) on
the public side too (*One source, multiple views*: the same browse model, CMS and public). But (H1) is
materially cheaper and may be all Daniel wants. **This is a product decision — flag for Daniel before
building 8.H.** Do not pick by default; the cardinality choice cascades into 8.I (what ARCHIVE links to)
and the fate of `/tracks`.
**Acceptance criteria (conditional on the framing decision).**
- `/archive` renders a searchable browse surface (search + filter), not the three-card overview.
- The surface covers all releases (H2) or all tracks (H1) per the decision.
- The old three-card overview is retired (or repurposed — see 8.I, the mobile question).
- The misleading "Tracks"-as-everything naming is resolved (the route/label reflects "archive").
**Dependencies.** **Gated on the framing decision above.** Gates **8.I** (8.I links ARCHIVE to whatever
8.H produces). Independent of 8.J.
**Open question (carry-over from §5.1 of the medium-types spec).** The original ARCHIVE design used
`/archive` as the *mobile* overview (three cards) since the desktop popover is hover-only. If `/archive`
becomes the searchable browser, **what is the mobile ARCHIVE destination?** Options: ARCHIVE on mobile
goes straight to the searchable browser (the three medium modes are reachable via in-page filter/tabs),
or the mobile hamburger keeps the three medium links indented under ARCHIVE (already does — see 8.I).
Recommend: mobile ARCHIVE → the searchable browser; the medium links live in the hamburger sub-list
(8.I) so the three-card overview is fully retired. Confirm with Daniel.
---
### 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` route and `GenresView` — Daniel says eliminate GENRES from the *nav*. Recommend the same
posture Phase 9 took with CMS genre browse: **drop the nav item, keep the route reachable** (no active
development, no hard removal) unless Daniel says retire it wholesale. Flag.
**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).
- Each medium link navigates to its view page (`/cuts`, `/sessions`, `/mixes`).
- GENRES no longer appears in the nav.
- Below the breakpoint, the nav remains usable (medium links reachable via the hamburger).
**Dependencies.** Depends on **8.H** (ARCHIVE's target). Coordinates with **8.J** (popover fate).
**Open question (flag).** Eliminate GENRES from the **nav only** (keep `/genres` route reachable), or
retire `GenresView` entirely? *Recommend nav-only removal* (consistent with the CMS genre-browse
disposition; the route is built and harmless). Confirm with Daniel.
---
### 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 `[design pending interview]`
**Do not write an implementation spec for this track.** Daniel has explicitly asked to be interviewed
before the Mix Visualizer is redesigned. His seed idea: the waveform **scrolls** bottom-to-top in high
resolution, with a slider controlling scroll speed / zoom level (higher resolution moves faster) —
**not** a static background image (which is what `MixWaveformVisualizer` renders today: a single static
full-viewport mirrored silhouette).
The structured interview question set is in `product-notes/phase-9-mix-visualizer-redesign.md`. It is
grounded in the current implementation (read 2026-06-13): an SVG silhouette built from a stored loudness
profile, full-page background, with an inert click-to-seek seam already present. The questions probe the
motion model, zoom/resolution coupling, aesthetics, interaction, and performance so the eventual design
is built on Daniel's actual intent, not a guess.
**This track stays `[design pending interview]` until the interview runs and a design is captured.** It
must not be dispatched for implementation from this document.
---
## 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) — **independent** of the tab work; land in
parallel any time. 8.F pairs with 8.E (Session Add-Track → hero-capable form).
**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 = searchable browser) — **gated on Daniel's cardinality decision** (H1 vs H2).
- **8.I** (nav slim + GENRES out) — depends on 8.H (ARCHIVE target); coordinates with 8.J.
**Mix Visualizer:**
- **8.K** — **blocked on the interview.** Not implementable from this doc.
**Recommended sequencing.** Land the independent/trivial items first (8.G, 8.D, 8.J, 8.F) — they unblock
nothing and need nothing. Then the CMS spine (8.B → 8.A → 8.C/8.E). On the public side, get Daniel's H1/
H2 decision, then 8.H → 8.I. Run the 8.K interview in parallel with all of it; it gates only itself.
---
## 6. Decisions needed from Daniel before / during build
1. **(8.H) Archive cardinality — H1 vs H2.** Is the public archive the existing **track** gallery
relabelled (H1, cheap), or a new **release**-cardinal searchable browser (H2, coherent with the CMS
archive and the medium views)? *This is the load-bearing product decision of the public cluster.*
Recommend H2 for model symmetry, H1 if speed dominates.
2. **(8.H) Mobile ARCHIVE destination.** With `/archive` becoming the searchable browser, does mobile
ARCHIVE go to the browser (recommended) or keep a card overview? Affects whether the three-card view
is fully retired.
3. **(8.I) GENRES — nav-only removal vs full retirement.** Recommend nav-only (keep `/genres`
reachable). Confirm.
4. **(8.F) Session hero image — optional vs required.** Daniel's wording ("allow") reads optional.
Recommend optional. Confirm.
5. **(8.E, minor) `ALL`-tab Add Track default medium.** Recommend Cut. Not blocking.
Items 14 are genuine product calls; 5 has a safe default and should not block.