# Phase 11 — Public Site Enhancements Status: spec / design. Author: product-designer. Date: 2026-06-15. **Plan only — no code edits made by this doc.** Cross-references: `PLAN.md §11` (the concise phase entry), `product-notes/phase-9-release-medium-types.md` (the medium model — `ReleaseMedium` enum, the per-medium detail-page strategy, the `ReleaseDetailScaffold` contract), `COMPLETED.md §9.8` (Wave 8 remediation — the `/archive` release-cardinal browser, the inline nav, the `TracksView` demotion), memory *One source, multiple views* and *Design for adaptability up front*. --- ## 0. Why this phase exists, and the state it inherits Phase 9 made the medium taxonomy real end-to-end and gave each medium a browse + detail surface. Wave 8 then remediated the public side: `/archive` became a release-cardinal searchable browser, the nav flattened to ARCHIVE + Cuts/Sessions/Mixes, and the track-cardinal `/tracks` gallery was demoted from the nav (route kept). That work landed and is stable on dev (2026-06-13/14). Phase 11 is the **next coherent pass over the public listening surface**. Daniel's hands-on use surfaced four commitments. They share one spine: **make the release the cardinal unit of the public site, and make every navigation an addressable, shareable URL.** Two of the four are structural (a Cuts detail page; `/tracks` collapses to a router); two are normalization/polish (retire the `/tracks?album` view; encode Archive filters in the URL). This is not a greenfield phase — most of the scaffolding it needs already exists. The job is **closing the one asymmetry Phase 9 left** (Cuts are the only medium with no single-release detail page) and **finishing the URL-addressability story** the Archive browser started. ### What already exists (verified against live source, 2026-06-15) | Surface | State | File | |---|---|---| | `/sessions/{id}` detail | **Exists, mature.** Hero-dominant overlay; bridged prerender; play wiring. | `Pages/SessionDetail.razor` | | `/mixes/{id}` detail | **Exists, mature.** Full-page WebGL waveform background + scaffold; bridged prerender. | `Pages/MixDetail.razor` | | `ReleaseDetailScaffold` | **Exists.** Invariant trio (back link, masthead, play/share) + `Hero`/`MetaContent` slots. Composed by `TrackDetail` and `MixDetail`. | `Controls/ReleaseDetailScaffold.razor` | | `ReleaseDetailBase` | **Exists.** Shared load + prerender-bridge for single-release detail pages (id-addressed, resolves the playable track via `releaseId`-filtered track page). | `Pages/ReleaseDetailBase.cs` | | `/cuts` gallery | **Exists** as `AlbumsView` (medium-parameterized card grid). Cards open `/tracks?album={title}`. | `Pages/AlbumsView.razor` | | `/archive` browser | **Exists, release-cardinal.** Debounced search + medium chips + genre filter. Cards route per-medium. **Filters held in component fields, NOT in the URL.** | `Pages/ArchiveView.razor(.cs)` | | `/track/{EntryKey}` detail | **Exists** as `TrackDetail` — the *track-cardinal* detail the player-bar title links to. | `Pages/TrackDetail.razor` | | `/albums` | **Exists** as a permanent redirect → `/cuts`. | `Pages/AlbumsRedirect.razor` | ### Three framing corrections (the brief's vocabulary vs. the live routes) The brief's framing is directionally right but uses route names that do not match the code. Naming these up front so the implementer is not misled: 1. **There is no `/tracks/{id}` route.** The track-cardinal detail route is **`/track/{EntryKey}`** (`TrackDetail`), keyed by the FileDatabase entry key (a string), not a numeric id. The player-bar title links to `/track/{Track.EntryKey}` (`TrackMetaLabel.razor` line 9). When the brief says "`/tracks/{id}` becomes a pure router," the *intent* is: **the thing a release-title click lands on should resolve medium → the correct dedicated detail page.** See §2 for what this actually means against the live routes — the brief's "router" is better realized as a small **medium→route resolver** than as a literal `/tracks/{id}` page, because the player bar carries a track, not a release id. 2. **Cuts genuinely have no single-release detail page.** This is the real asymmetry. `/cuts` (`AlbumsView`) cards and `/archive` Cut cards both open `/tracks?album={title}` — the track gallery filtered to the album title. Sessions and Mixes route to dedicated `/sessions/{id}` / `/mixes/{id}` pages. The new `/cuts/{id}` page (§3) closes this gap and is the heart of the phase. 3. **`/archive` filters are already in component state but not the URL.** `ArchiveView` holds `_selectedMedium`, `_selectedGenre`, `SearchText` as private fields with no `[SupplyParameterFromQuery]` and no `NavigateTo` on filter change. Requirement 4 is therefore a **URL-binding pass over an existing browser**, not a new feature. The pattern to borrow already lives next door: `TracksView` reads `?album=`/`?genre=`/`?q=` via `[SupplyParameterFromQuery]` (`TracksView.razor.cs` lines 21–23). --- ## 1. The four commitments (Daniel, faithful capture) 1. **`/tracks/{id}` becomes a pure router** → resolve a release's `ReleaseMedium` to the correct dedicated detail page (`/cuts/{id}` | `/sessions/{id}` | `/mixes/{id}`). Hit only when a user clicks the release title in the player bar; its sole job is medium → page resolution. 2. **New `/cuts/{id}` page — album view.** - Header section, **left-aligned**: release name, artist, genre, release year, plus **Play** and **Share** buttons. - Cover art **large-ish, right**, with a **theme border** around the image. - Below: an **ordered track list**, each row with a play button. - Header Play starts the **first track**. 3. **Code normalization / reduction.** Eliminate the now-redundant `/tracks?album` page view (the Archive provides all filtering/searching, so `/tracks` collapses to a pure router). Identify and normalize other UI redundancy. 4. **Archive search params in the URL.** Search term, medium filter, genre filter all encoded in `/archive?…` so filter actions create navigable history anchors (back/forward, shareable links). --- ## 2. Requirement 1 reframed — the medium→detail resolver **Recommendation: do not build a literal `/tracks/{id}` page. Build a `medium → detail route` resolver and apply it at the two click sites that need it.** Here is why, and the three shapes considered. ### What the player-bar title actually carries The player bar holds a **`TrackDto`** (`TrackMetaLabel.razor`). A `TrackDto` carries a nested `Release` (with `Id` and `Medium`). So at the click site we already know the release id *and* the medium — there is no need for a round-trip "router page" that fetches the release to discover its medium. The resolver is a pure function: ``` Release { Id, Medium } → "/cuts/{Id}" | "/sessions/{Id}" | "/mixes/{Id}" ``` ### Three shapes - **(i) Literal `/tracks/{id}` router page.** A routable component that fetches the release by id, reads `Medium`, and `NavigateTo`s the dedicated page (`replace: true` so the router never sits in history). *Cost:* a network round-trip and a flash of an empty page on every release-title click, to rediscover a medium the caller already knew. Justified **only** if some entry point has *only* a release id and not the medium (e.g. an external deep link to `/tracks/{id}`). *Keep this as a thin fallback, not the primary path.* - **(ii) Pure resolver helper, applied at click sites (RECOMMENDED).** A single `ReleaseRoutes.DetailHref(ReleaseDto)` (or `(long id, ReleaseMedium medium)`) helper — **one table, one location** — that every release-title / release-card click consumes. `ArchiveView` already has a private `DetailHref` switch (lines 121–126); **promote it to the shared helper** so the Archive, the player bar, and the new Cuts cards all route through one source. No round-trip, no flash. This is the *One source, multiple views* discipline applied to routing. - **(iii) Both.** The resolver helper (ii) is the primary path; a thin `/tracks/{id}` redirect page (i) exists as the addressable fallback for bare-release-id deep links and for honoring the brief's literal route. The redirect page consumes the *same* resolver helper. **Recommend (iii): resolver helper as the spine, plus a thin `/tracks/{id}` redirect page that reuses it.** This satisfies the brief's literal "`/tracks/{id}` is a pure router" wording *and* gives the common case (player-bar click, where the medium is already in hand) a zero-round-trip path. The redirect page is ~15 lines and shares the resolver, so it is not a second source of truth. > **Open question (Daniel):** the player-bar title currently links to **`/track/{EntryKey}`** (the > track-cardinal `TrackDetail`). The brief says the title click should resolve medium → dedicated > page. **Confirm the player-bar title should now point at the release detail (via the resolver), > not the track detail.** If yes, `TrackMetaLabel`'s `` changes from `/track/{EntryKey}` to > `ReleaseRoutes.DetailHref(Track.Release)` — and `TrackDetail`'s role shrinks (see §4, the > normalization question on whether `TrackDetail` survives at all). ### What "resolver" means for Cut `DetailHref` for a Cut today returns `/tracks?album={title}`. After §3 lands, a Cut resolves to **`/cuts/{Id}`** (id-addressed, consistent with Session/Mix). This repoint is the hinge between requirements 1, 2, and 3 — see the dependency note in §6. --- ## 3. Requirement 2 — the `/cuts/{id}` album-detail page This is the phase's center of gravity: the first **multi-track** release detail page. Sessions and Mixes are single-track (their detail pages show one play affordance); a Cut is an album/EP/single with an ordered track list. ### 3.1 Layout (Daniel's spec, literal) ``` ┌─────────────────────────────────────────────────────────────┐ │ ← All cuts │ │ │ │ ┌─────────────────────────────┐ ┌──────────────────────┐ │ │ │ RELEASE NAME (h3) │ │ │ │ │ │ Artist (h6, primary) │ │ COVER ART │ │ │ │ Genre · 2025 │ │ (large-ish, │ │ │ │ │ │ theme border) │ │ │ │ [ ▶ Play ] [ ⤴ Share ] │ │ │ │ │ └─────────────────────────────┘ └──────────────────────┘ │ │ ↑ header content LEFT ↑ cover RIGHT │ │ │ │ ───────────────────────────────────────────────────────────│ │ 1. ▶ Track One 3:42 │ │ 2. ▶ Track Two 4:18 │ │ 3. ▶ Track Three 2:55 │ │ … │ └─────────────────────────────────────────────────────────────┘ ``` - **Header left:** release name, artist, genre, release year, **Play** + **Share** buttons. - **Cover right:** large, **theme border** around the image (a `deepdrft-`-prefixed border using a palette token — mirror the existing cover treatments but with an explicit framed border, which is the new visual element). - **Track list below:** ordered rows, each with a play button. Row click / row play streams that track. - **Header Play** starts **track 1**. ### 3.2 Compose `ReleaseDetailScaffold`, or not? — the load-bearing design call Phase 9 §5.3 established `ReleaseDetailScaffold` as the shared detail scaffold and committed to "refactor `TrackDetail` onto it; per-medium variance rides slots." The scaffold owns the **invariant trio**: back link, masthead (title + artist), play/share affordance. `MixDetail` composes it; `SessionDetail` deliberately diverges (overlay layout). The question for Cuts: - **(i) Compose the scaffold.** The Cut header *is* the invariant trio (title, artist, play, share) — almost exactly what the scaffold provides. The cover goes in the `Hero` slot; the genre/year go in `MetaContent`; the **track list** rides a new `BodyContent` slot (the scaffold has no body slot today — adding one is the cheap, correct extension Phase 9 §5.3 anticipated: *"named slots are fine where genuinely needed, e.g. `BodyContent` for the Cut/Album multi-track listing"*). - **(ii) Bespoke page** (like `SessionDetail`). Full control over the left/right header split, but duplicates the play/share wiring and the back-link/masthead the scaffold already owns. **Recommend (i): compose the scaffold, add one `BodyContent` slot.** Two caveats that decide whether this is clean or a fight: 1. **The header layout is left-content / right-cover; the scaffold's masthead is a top row.** The scaffold today renders masthead-then-Hero vertically (`MixDetail` stacks cover below the masthead). The Cut layout wants masthead and cover **side by side**. This is a **layout variance**, and Phase 9 §5.3 is explicit: *"a boolean layout parameter on the scaffold is a design failure — that variance belongs in a slot."* So the right answer is **not** a `HeroBesideMeta` flag on the scaffold. Two clean options: - **(a)** The Cut page supplies its whole left+right header as the page content and uses the scaffold only for the back link + play/share wiring + body slot — i.e. the scaffold's masthead is *one* arrangement and the Cut wants a different one, so the Cut composes a richer header into a slot. Risk: the scaffold's built-in masthead then competes with the Cut's own header. - **(b)** Generalize the scaffold's header region into a `Header` slot with the current masthead+play row as the default content, so `MixDetail`/`TrackDetail` are unchanged (default) and `CutDetail` supplies a left/right `Header`. This is the cleaner *One source* move but touches the scaffold's shared contract. - **Recommend (b)**, but flag it: it is a scaffold-contract change that ripples to every composer. If Wave pressure makes (b) risky, **(ii) bespoke page** is the honest fallback — record it as deliberate divergence (as `SessionDetail` already is) rather than bending the scaffold with a boolean. 2. **The play/share affordance differs.** The scaffold renders a `PlayStateIcon` (icon toggle) + `SharePopover`. Daniel's Cut spec says **Play and Share buttons** (labeled buttons, per the mockup). If the Cut wants text buttons rather than the icon idiom, that is a **slot for the affordance row**, not a scaffold edit. Minor — flag for Daniel whether the Cut header keeps the icon idiom (consistency with Session/Mix) or uses labeled buttons (the literal spec wording). ### 3.3 Data path — the track list The Cut page needs the release **and its ordered tracks**. Both primitives exist: - **Release:** `IReleaseDataService.GetById(id)` → `ReleaseDto` (with `Title`, `Artist`, `Genre`, `ReleaseDate`, `ImagePath`, `Medium`, `ReleaseType`, `TrackCount`). - **Tracks:** the track-data service already supports a **`releaseId`-filtered** track page — `ReleaseDetailViewModel.Load` uses `GetPage(pageNumber: 1, pageSize: 1, releaseId: …)` to resolve the single track for Session/Mix. The Cut page issues the same call with a **larger page size** (cover the whole album — `pageSize: 100` matches the gallery convention) to get the ordered list. **Ordering.** The track list must render "in order." Verify the track page's default sort yields a stable, meaningful order for an album (track-number if it exists; otherwise insertion/`Id` order). *Open question:* does `TrackEntity` carry a track-number / ordinal field? From the current schema it does **not** (`Id, EntryKey, TrackName, Artist, Album, Genre, ReleaseDate, ImagePath`). If album track order matters beyond insertion order, that is a **data-model gap** (a `TrackNumber` column) — flagged in §7 as a Daniel decision, *not* assumed into this phase. **New `CutDetailViewModel` vs. extend `ReleaseDetailViewModel`.** `ReleaseDetailViewModel` resolves *one* track. The Cut page needs *many*. Two options: - Extend `ReleaseDetailViewModel` with an optional `Tracks` collection populated when the medium is Cut. Risk: the VM grows a medium conditional — the smell Phase 9 fought. - A dedicated `CutDetailViewModel` (loads release + full track list). Cleaner SRP; the Cut detail is genuinely a different shape (multi-track) from the single-release VM. **Recommend a dedicated `CutDetailViewModel`** + a `CutDetail` page deriving the same prerender-bridge discipline `ReleaseDetailBase` encodes (persist release + tracks across the prerender→WASM seam, guard restore on id). If the bridge logic is substantial, consider generalizing `ReleaseDetailBase`'s bridge into a shared base both single- and multi-track details use — but only if it doesn't force a medium conditional into the base. Flag the choice; don't pre-commit the implementer. ### 3.4 Play wiring - **Row play:** stream the row's track (the `TracksView.PlayTrack` idiom — toggle if already current, else `SelectTrackStreaming`). - **Header Play:** stream **track 1** (`Tracks.FirstOrDefault()`). If a queue/playlist model existed (`PLAN.md §1.3`), header Play would enqueue the whole album — **it does not today**, so header Play starts track 1 and the rest is manual. *Adjacent opportunity, not in scope* — see §7. --- ## 4. Requirement 3 — normalization and reduction The brief: eliminate the `/tracks?album` page view; `/tracks` collapses to a pure router; identify other redundancy. ### 4.1 The `/tracks?album` retirement is gated on §2 and §3 `/tracks?album={title}` has **two live consumers** today: - `AlbumsView` (`/cuts`) cards → `OpenAlbum` → `/tracks?album={title}` (line 62). - `ArchiveView` Cut cards → `DetailHref` → `/tracks?album={title}` (line 125). Both must be **repointed to `/cuts/{id}`** before the `?album=` view can be retired. So the ordering is: §3 (`/cuts/{id}` exists) → §2 (resolver returns `/cuts/{Id}` for Cut) → §4.1 (repoint both consumers) → **then** the `?album=` filter branch in `TracksView` can be removed. > **Scope subtlety.** "Eliminate the `/tracks?album` page view" means remove the **album-filtered > *mode*** of the track gallery, not necessarily the whole `/tracks` route. `TracksView` also > supports `?genre=` and `?q=` and a plain unfiltered gallery. Decide (Daniel) whether: > - **(A)** remove only the `album` query branch (keep `/tracks` reachable with genre/search), or > - **(B)** retire `TracksView` **entirely** — the Archive (§release-cardinal search) subsumes it, > and Phase 9 §8.I already demoted `/tracks` from the nav. Cuts now have a proper detail page; > the track-cardinal gallery may have no remaining job. > > **Recommend (B) as the eventual target, (A) as the safe first step.** If `/cuts/{id}` + `/archive` > cover every browse/play path, `TracksView`, `TrackDetail`, `TrackCard`, `TracksGallery`, and > `GalleryViewMode` become removable surface — a meaningful reduction. But that is a bigger cut than > the brief literally asks; confirm before deleting. See §4.3. ### 4.2 What `TrackDetail` becomes If §2's resolver repoints the player-bar title to the **release** detail (the recommended direction), `TrackDetail` (`/track/{EntryKey}`) loses its only inbound link from the player bar. `TrackCard` still links to it (`/track/{EntryKey}`). If `TracksView`/`TrackCard` are retired (4.1 option B), `TrackDetail` has **no inbound links** and is dead surface — retire it too. If `/tracks` survives (option A), `TrackDetail` stays as the track-cardinal detail. **This is the cleanest reduction lever in the phase:** the track-cardinal stack (`TracksView`, `TrackDetail`, `TrackCard`, `TracksGallery`) exists from the pre-medium era. Phase 9 made the release cardinal; Phase 11's Cuts page removes the last reason a Cut needed the track gallery. Whether to pull the whole stack is a Daniel call (§7), but the phase should **name the dead surface explicitly** so the reduction is deliberate, not accidental. ### 4.3 Candidate redundancy inventory (for the normalization pass) Surfaced from the read; each is a *candidate*, not a commitment: 1. **`/tracks?album` filter branch** — retire (gated, §4.1). **High confidence.** 2. **`TracksView` + `TrackCard` + `TracksGallery` + `GalleryViewMode`** — track-cardinal gallery stack. Removable iff Cuts page + Archive cover all paths (§4.1 B). **Daniel decision.** 3. **`TrackDetail` (`/track/{EntryKey}`)** — removable iff its inbound links go (§4.2). **Follows 2.** 4. **`GenresView` (`/genres`)** — already demoted from nav (§8.I), route kept. The Archive has a genre filter. Is `/genres` still needed? Probably retire-able, but **out of this phase's stated scope** — flag as adjacent. **Daniel decision, low urgency.** 5. **`AlbumsView` `OpenAlbum`** — repointed from `/tracks?album` to `/cuts/{id}` (§4.1). **Part of the work, not removable.** 6. **`ArchiveView.DetailHref` + `ArchiveView.MediumLabel`** — `DetailHref` promotes to the shared `ReleaseRoutes` resolver (§2). `MediumLabel` is a label lookup; check whether it duplicates the CMS `MediumTypeLabels` (§8.D) or the Archive's own medium-chip labels — if the public side has two medium-label lookups, **consolidate to one**. **Medium confidence.** > The phase should land **(1)** for sure, **decide (2)+(3) as a pair** with Daniel, and treat > **(4)+(6)** as opportunistic tidy-ups. Do not let the reduction sprawl — normalize what the four > commitments *touch*, surface the rest as adjacent. --- ## 5. Requirement 4 — Archive filters in the URL **This is a URL-binding pass over the existing `ArchiveView`, borrowing the `TracksView` pattern verbatim.** No new browser, no new data path — the filter state already drives `LoadReleases`; the change is making that state **enter and leave via the query string**. ### 5.1 Target URL scheme ``` /archive?q={search}&medium={cut|session|mix}&genre={genre} ``` - All three params optional; omitting one means "no filter on that axis" (matches the current null-means-all semantics). - `medium` uses the same lowercase enum token the data service already speaks (`Medium.ToString().ToLowerInvariant()`), parsed back with `Enum.TryParse(ignoreCase:true)` + `Enum.IsDefined` — the exact posture `BatchUpload` (§8.E) and the API already use. - Plain `/archive` (no params) = the unfiltered first page, which is the bridged/prerendered state. ### 5.2 The binding mechanics (borrow `TracksView`) `TracksView.razor.cs` is the template: - **In (URL → state):** add `[SupplyParameterFromQuery]` for `q`, `medium`, `genre`. Seed `SearchText` / `_selectedMedium` / `_selectedGenre` from them in `OnInitializedAsync` **before** the restore/fetch decision (so a direct nav to a filtered URL fetches filtered, and the bridge restore is skipped when a filter is active — `ArchiveView` already has `HasActiveFilter` gating exactly this). - **Out (state → URL):** each of `OnSearchInput` / `OnMediumSelected` / `OnGenreSelected` calls `Navigation.NavigateTo($"/archive?{composed query}")` **instead of** (or before) calling `LoadReleases` directly. The query-param change drives the re-fetch. ### 5.3 The one real subtlety — same-route query change does not re-run `OnInitialized` Blazor reuses the component on a same-route query-string change and fires `OnParametersSet`, **not** `OnInitializedAsync` (the `TracksView.ClearFilter` comment, lines 117–120, documents exactly this trap). So the filter→fetch reaction must live where it sees the change: - **Option A (history-driven):** filter handlers only `NavigateTo` the new URL; move the state-seeding + `LoadReleases` into `OnParametersSet`/`OnParametersSetAsync` keyed off the query params. Cleanest — the URL is the single source of truth; back/forward "just works" because each nav re-runs the same seed-and-fetch. **Recommended.** - **Option B (dual-write):** handlers both `NavigateTo` *and* `LoadReleases` directly. Simpler diff but the URL and the fetch are two writes that can drift, and back/forward needs separate handling. **Recommend Option A.** It makes the URL the source of truth (which is the whole point of the requirement) and gets shareable links + back/forward correctness as a structural consequence rather than as bolted-on handling. Guard against the debounce/nav interplay: the search field debounces (400ms) before firing; ensure a debounced search nav doesn't fight a rapid medium-chip nav (the `OnParametersSet` reaction should be idempotent on identical param sets — mirror the `_loadedEntryKey` guard idiom). ### 5.4 Persistence interaction The bridged unfiltered first page (`PersistKey = "archive-releases"`) must keep restoring **only** when no filter is active — `ArchiveView` already gates persist + restore on `HasActiveFilter`. The URL-binding pass must preserve that gate: a `/archive?medium=mix` direct load must **fetch**, not restore the unfiltered bridge. The existing `HasActiveFilter` check already expresses this; the seed-from-URL step just has to run before the restore decision (as §5.2 specifies). --- ## 6. Wave decomposition Sequenced so the structural dependency (Cuts page → resolver → repoint → retire) is honored, and the two independent tracks (Archive URL; Cuts page) can run in parallel. ``` Wave A (parallel start) Wave B (parallel start) ┌──────────────────────────┐ ┌────────────────────────────┐ │ 11.A /cuts/{id} page │ │ 11.D Archive filters →URL │ │ + CutDetailViewModel │ │ (TracksView-pattern bind) │ │ + cover theme border │ └────────────────────────────┘ │ + ordered track list │ (fully independent — └──────────┬───────────────┘ touches only ArchiveView) │ ▼ ┌──────────────────────────┐ │ 11.B medium→route │ │ resolver (ReleaseRoutes) │ │ + thin /tracks/{id} │ │ redirect page │ │ + repoint player-bar │ │ title + Archive + │ │ AlbumsView cards │ └──────────┬───────────────┘ │ ▼ ┌──────────────────────────┐ │ 11.C Normalization: │ │ retire /tracks?album │ │ branch; decide + (maybe) │ │ retire track-cardinal │ │ stack; consolidate │ │ medium-label lookups │ └──────────────────────────┘ ``` - **11.A — `/cuts/{id}` detail page.** The new page, its VM, the cover theme border, the ordered track list, header Play→track 1, row Play. Independent of everything except the existing `GetById` + `releaseId`-filtered track page (both exist). **Load-bearing prerequisite for 11.B's Cut resolution.** - **11.B — medium→route resolver + repoint.** Promote `ArchiveView.DetailHref` to a shared `ReleaseRoutes` helper; Cut now resolves to `/cuts/{id}` (needs 11.A); repoint player-bar title, Archive cards, and `AlbumsView` cards through it; add the thin `/tracks/{id}` redirect page. **Depends on 11.A.** - **11.C — normalization.** Retire the `/tracks?album` filter branch (safe once 11.B repoints both consumers); execute the agreed reduction from §4.3 (decide 4.3#2/#3 with Daniel first). **Depends on 11.B.** - **11.D — Archive filters in the URL.** Fully independent — touches only `ArchiveView`. Can land first, last, or alongside Wave A. **No dependency.** **Critical path:** 11.A → 11.B → 11.C. **11.D is free-floating.** --- ## 7. Open questions and adjacent gaps (need Daniel) **Decisions inside the four commitments:** 1. **Player-bar title target (§2).** Confirm the title click should resolve **release** detail (via the resolver), replacing the current `/track/{EntryKey}` link. *Recommend yes* — it is the premise of requirement 1. 2. **`/cuts/{id}` scaffold strategy (§3.2).** Compose `ReleaseDetailScaffold` with a generalized `Header` slot (recommended) vs. bespoke page like `SessionDetail`. Sets whether the scaffold contract changes. *Recommend the `Header`-slot generalization; bespoke as fallback.* 3. **Cut header affordance idiom (§3.2.2).** Keep the icon `PlayStateIcon`+`SharePopover` (consistent with Session/Mix) vs. labeled **Play/Share buttons** (the literal spec wording). *Minor — flag.* 4. **Track ordering / `TrackNumber` (§3.3).** Does an album need explicit track ordering beyond insertion order? If yes, that is a **`TrackEntity.TrackNumber` data-model addition** (new column + migration + CMS field + ordering in the read) — a real schema gap. *Recommend: ship 11.A on insertion/`Id` order; raise `TrackNumber` as its own small phase if hands-on use shows albums landing out of order.* **Daniel call — do not assume into Phase 11.** 5. **`/tracks` retirement scope (§4.1).** Remove only the `album` branch (A, safe) vs. retire the whole track-cardinal stack — `TracksView`/`TrackDetail`/`TrackCard`/`TracksGallery` (B, bigger reduction). *Recommend B as the target, A as the floor.* This is the phase's biggest reduction decision. 6. **`/genres` fate (§4.3#4).** Already nav-demoted; Archive has genre filtering. Retire `/genres` too? *Out of stated scope — flag as adjacent, low urgency.* **Adjacent gaps surfaced by the read (not in the four commitments — for the roadmap, not this phase):** - **No album-play queue.** Header Play starts track 1 only; there is no "play the whole album" because the player is single-slot (no queue — `PLAN.md §1.3` preload/queue is still deferred). A Cuts album page is the **strongest product argument yet for a queue model** — "play album" is the expected affordance on an album page. *Recommend surfacing this as a renewed case for §1.3, not building it in Phase 11.* The Cut page's Play button is the natural future entry point; design the header Play so a future "enqueue album" is a swap of the handler, not a rewrite (the *Design for adaptability up front* seam). - **Share on a Cut shares a track, not the release.** `SharePopover` takes an `EntryKey` (a track). A Cut's header Share should arguably share the **release URL** (`/cuts/{id}`), not a single track's embed. *Flag:* the existing `SharePopover` is track-keyed; sharing a release is a new share target. Decide whether the Cut header Share shares the release page or punts to track-share. *Recommend release-URL share for the Cut header* — but it is a `SharePopover` extension, so flag it. - **`TrackDto.Release` nullability on the player bar.** `TrackMetaLabel` guards `Track.Release?` — if a track ever loads without its release populated, the resolver (§2) has no medium to route on. Verify the player's `TrackDto` always carries `Release` (the streaming select path). *Low risk; worth a verification line in 11.B.* - **No `ReleaseDate`-only-year display helper.** The Cut header shows "release year"; `MixDetail`/ `SessionDetail` show "MMMM yyyy". Minor inconsistency — the Cut header wants just the year per the spec. *Trivial; note it so the implementer doesn't copy the month-year format.* --- ## 8. Why this is consistent with the system's grain - **Release-cardinal everywhere.** Phase 9 + Wave 8 moved the public site to release-cardinal browse (`/archive`) and per-medium detail. Phase 11 closes the one hole (Cuts had no detail page) and makes the player-bar→detail path release-cardinal too. After this, the track-cardinal stack is vestigial — which is *why* the reduction (§4) is available. - **One source, multiple views.** The medium→route resolver (§2) is one table consumed by the player bar, Archive, and Cuts cards — not three `DetailHref` switches. The Cut detail reuses the scaffold's invariant trio and the existing `GetById` + `releaseId`-filtered track page — no new data path. (Memory: *One source, multiple views*.) - **URL as source of truth (§5).** Borrowing the `TracksView` `[SupplyParameterFromQuery]` pattern makes the Archive's filters addressable, which is the same shareable-link discipline the embed player and `/tracks?album` deep links already established. - **Extension, not modification.** The resolver and the `ReleaseDetailScaffold` `Header`/`BodyContent` slots are additive; a future medium's detail page composes the same scaffold and the resolver gains one entry — the Phase 9 Open/Closed discipline, unchanged. --- ## 9. Verified facts (read against live source 2026-06-15) - **No `/tracks/{id}` route exists.** Track-cardinal detail is `/track/{EntryKey}` (`TrackDetail.razor` line 1). Player-bar title links to `/track/{Track.EntryKey}` (`TrackMetaLabel.razor` line 9). - **`/sessions/{id}` and `/mixes/{id}` exist and are mature** (`SessionDetail.razor`, `MixDetail.razor`, both id-addressed, both inherit `ReleaseDetailBase`'s prerender bridge; `MixDetail` composes `ReleaseDetailScaffold`, `SessionDetail` deliberately diverges). - **Cuts have no single-release detail page.** `/cuts` is `AlbumsView` (medium-parameterized card grid); cards open `/tracks?album={title}` (`AlbumsView.razor.cs` line 62). `/albums` → `/cuts` redirect exists (`AlbumsRedirect.razor`). - **`/archive` is release-cardinal, filters held in component fields not the URL.** `ArchiveView` has `_selectedMedium`, `_selectedGenre`, `SearchText` as private state; no `[SupplyParameterFromQuery]`, no `NavigateTo` on filter change (`ArchiveView.razor.cs`). It has a private `DetailHref` switch (lines 121–126) routing Session→`/sessions/{id}`, Mix→`/mixes/{id}`, Cut→`/tracks?album={title}`. - **`TracksView` already reads `?album=`/`?genre=`/`?q=` from the URL** via `[SupplyParameterFromQuery]` (`TracksView.razor.cs` lines 21–23) — the pattern requirement 4 borrows. It documents the same-route-query-change trap (`OnParametersSet` not `OnInitialized`, lines 117–120). - **`ReleaseDetailScaffold`** owns the invariant trio (back link, masthead, play/share) + `Hero` and `MetaContent` slots; **no body/track-list slot today** (`ReleaseDetailScaffold.razor`). - **Data primitives for the Cut page both exist:** `IReleaseDataService.GetById(id)` returns a full `ReleaseDto`; the track-data service supports `releaseId`-filtered paging (`ReleaseDetailViewModel.Load` uses `GetPage(…, releaseId: …)`). `ReleaseDto` carries `TrackCount` but the **track list needs the filtered track page** (the DTO has no nested track collection). - **`TrackEntity` has no track-number / ordinal field** (`Id, EntryKey, TrackName, Artist, Album, Genre, ReleaseDate, ImagePath`). Album track ordering beyond insertion order is a data-model gap. - **`Pages.cs` `MenuPages`** = ARCHIVE (→ `/archive`) with Cuts/Sessions/Mixes children; `/tracks` and `/genres` are absent from nav, routes reachable (§8.I). - **`SharePopover` is track-keyed** (takes `EntryKey`) — sharing a release is a new target.