diff --git a/PLAN.md b/PLAN.md index 4f53afb..09a3a0e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -165,6 +165,33 @@ Sequenced as four waves. Wave 1 is a prerequisite for everything; within Waves 2 Waves 1–7 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 1–7 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 diff --git a/product-notes/phase-9-mix-visualizer-redesign.md b/product-notes/phase-9-mix-visualizer-redesign.md new file mode 100644 index 0000000..5c46c52 --- /dev/null +++ b/product-notes/phase-9-mix-visualizer-redesign.md @@ -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 `` behind a `.mix-detail-foreground` stacking layer). +- **Played-portion wash:** a `` 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 (16–18) are the three +things most likely to drive the build's shape and should be settled first. diff --git a/product-notes/phase-9-wave-8-remediation.md b/product-notes/phase-9-wave-8-remediation.md new file mode 100644 index 0000000..fa71e79 --- /dev/null +++ b/product-notes/phase-9-wave-8-remediation.md @@ -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.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 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 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. + +Three clusters: + +1. **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 `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.F–8.G)** — compose the Session hero image into the upload form (retire + the two-step), and rename "Album Name" → "Release Name." +3. **Public site (8.H–8.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()` + a `MediumCards` lookup. The cards leave the page entirely. +- `BrowseMode.Albums` renders `CmsAlbumBrowser` — the cross-medium releases grid with a **Type** column + rendering `@context.Release.ReleaseType` 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 + `` 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 `` 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.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. + +**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 `` 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 1–4 are genuine product calls; 5 has a safe default and should not block.