docs: spec Phase 9 Wave 8 remediation + Mix Visualizer interview set
This commit is contained in:
@@ -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 (16–18) are the three
|
||||
things most likely to drive the build's shape and should be settled first.
|
||||
@@ -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<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.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 `<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 1–4 are genuine product calls; 5 has a safe default and should not block.
|
||||
Reference in New Issue
Block a user