docs(plan): add release Description field as commitment 8 / wave 11.G
Verified no Description column exists on ReleaseEntity/ReleaseDto (mirror image of commitment 5, which was already built). Specs the new base-release column + EF migration (Daniel-gated), DTO/converter/write-path plumbing, CMS multiline input, and detail-page text block. Schema lands as 11.G; render rides 11.A plus a Session/Mix touch.
This commit is contained in:
@@ -21,9 +21,10 @@ demoted from the nav (route kept). That work landed and is stable on dev (2026-0
|
||||
|
||||
Phase 11 is the **next coherent pass over the public listening surface**. Daniel's hands-on use
|
||||
surfaced an initial four commitments; on 2026-06-15 he resolved the open questions and expanded the
|
||||
scope to **seven**. They share one spine: **make the release the cardinal unit of the public site,
|
||||
scope to **eight**. They share one spine: **make the release the cardinal unit of the public site,
|
||||
make every navigation an addressable, shareable URL, and make the album a first-class playable
|
||||
object** (ordered, queue-able, shareable). The seven:
|
||||
object** (ordered, queue-able, shareable) — and now, **a release that can describe itself** in prose.
|
||||
The eight:
|
||||
|
||||
1. **Cuts detail page** (`/cuts/{id}`) — structural; the phase's center of gravity.
|
||||
2. **Player-bar release-title → release detail** via a medium→route resolver — structural.
|
||||
@@ -35,12 +36,20 @@ object** (ordered, queue-able, shareable). The seven:
|
||||
6. **Release-level Share** — **new scope**; a Cut/Session/Mix header Share shares the release URL.
|
||||
7. **Play-queue system** — **new scope**; absorbs the deferred `PLAN.md §1.3`. The Cuts "play album"
|
||||
affordance is its first consumer.
|
||||
8. **Release Description field** — **new scope (2026-06-15)**; a multiline free-text field on the
|
||||
base release (all media), edited from the CMS add/edit forms and rendered as a text block on every
|
||||
release detail page. Unlike commitment 5, this **is** a genuinely new column + migration. See §3d.
|
||||
|
||||
This is not a greenfield phase — most of the scaffolding it needs already exists (the medium browse
|
||||
pages already share a `ReleaseGallery` card component; the detail pages already share
|
||||
`ReleaseDetailScaffold`; the Archive already has all filter state). The structural new work is the
|
||||
**Cuts detail page** and the **queue model** (the one real architecture decision). Everything else
|
||||
is closing asymmetries and consolidating rendering that drifted into per-surface copies.
|
||||
is closing asymmetries and consolidating rendering that drifted into per-surface copies. The one
|
||||
piece of genuinely new persisted state the phase introduces is the **release Description column**
|
||||
(commitment 8) — a small, self-contained vertical slice (column → migration → DTO → write path →
|
||||
CMS form input → detail-page text block) that touches no existing wave's surface and follows the
|
||||
**exact channel Genre already uses** (release-cardinal field carried on the track update/upload
|
||||
request, projected onto the linked release row — there is no dedicated release-update endpoint).
|
||||
|
||||
> **Headline correction on commitment 5 (read against live source, 2026-06-15).** Daniel asked for
|
||||
> "an explicit ordinal column, editable from the CMS, with a Daniel-gated migration." **That column
|
||||
@@ -57,6 +66,20 @@ is closing asymmetries and consolidating rendering that drifted into per-surface
|
||||
> use reveals a gap (e.g. the *public* track read does not project `TrackNumber`), that is a small
|
||||
> wiring fix, not the schema project the brief anticipated.
|
||||
|
||||
> **The mirror-image note on commitment 8 (release Description; read against live source, 2026-06-15).**
|
||||
> Where commitment 5 turned out *already built*, commitment 8 is the opposite: **no `Description`
|
||||
> column exists** on `ReleaseEntity` or `ReleaseDto` (greps return nothing; the entity carries
|
||||
> `Title`/`Artist`/`Genre`/`ReleaseDate`/`ImagePath`/`ReleaseType`/`Medium` and the two metadata
|
||||
> satellites, no description). So commitment 8 **is** the real cross-stack schema project the brief's
|
||||
> commitment-5 framing anticipated — just for a different field. The honest scope: a new
|
||||
> `ReleaseEntity.Description` column + EF migration (**Daniel-gated apply**), the `ReleaseDto` mirror,
|
||||
> the `TrackConverter` round-trip (both directions), the write-path plumbing (the
|
||||
> release-cardinal-fields thread through `UpdateTrackMetadataRequest` / the upload form / the
|
||||
> `UnifiedTrackService` and `UnifiedReleaseService` write — the **same path Genre travels**), the CMS
|
||||
> `AlbumHeaderFields` multiline input, and the detail-page text block. §3d carries the full spec. This
|
||||
> is a clean vertical slice that gates the detail-page render but shares no surface with 11.A–F — which
|
||||
> is why it lands as its own wave (11.G), not a graft onto an existing one.
|
||||
|
||||
### What already exists (verified against live source, 2026-06-15)
|
||||
|
||||
| Surface | State | File |
|
||||
@@ -74,6 +97,7 @@ is closing asymmetries and consolidating rendering that drifted into per-surface
|
||||
| `SharePopover` | **Exists, track-keyed.** Takes an `EntryKey`; "Copy link" + "Embed player". No release-level share target. | `Controls/SharePopover.razor` |
|
||||
| Play queue / playlist | **Does not exist.** Player is single-slot (`StreamingAudioPlayerService` holds one `CurrentTrack`). No notion of "next." `PLAN.md §1.3` (preload + queue) is deferred — **now absorbed here**. | — |
|
||||
| `TrackEntity` ordinal | **ALREADY EXISTS — landed in Phase 8.** `TrackEntity.TrackNumber` (int, 1-based, default 1, non-null), column `track_number`, migration `20260611005700_AddReleaseTypeAndTrackNumber` **already applied**. `TrackDto.TrackNumber` mirrors it; `TrackConverter` round-trips it; `UpdateTrackMetadataRequest.TrackNumber` + `TrackController` validate (`> 0`) and persist it; `BatchEdit` already sets it from reorderable list position on submit; `ReleaseRepository.GetTracks` already `.OrderBy(t => t.TrackNumber)`. **Commitment 5 is not new schema — it is verify-and-consume.** | `TrackEntity.cs:17`, `TrackConfiguration.cs:37`, `BatchEdit.razor:192/225` |
|
||||
| `ReleaseEntity` Description | **DOES NOT EXIST.** No `Description` member on `ReleaseEntity` or `ReleaseDto` (greps return nothing). The entity carries `Title`/`Artist`/`Genre`/`ReleaseDate`/`ImagePath`/`ReleaseType`/`Medium` + `SessionMetadata`/`MixMetadata` — no description. **Commitment 8 is the real new-column project** (column + migration + DTO + converter + write path + CMS input + detail block). | `ReleaseEntity.cs:13-30`, `ReleaseDto.cs:10-33` |
|
||||
|
||||
### Three framing corrections (the brief's vocabulary vs. the live routes)
|
||||
|
||||
@@ -103,9 +127,10 @@ these up front so the implementer is not misled:
|
||||
|
||||
---
|
||||
|
||||
## 1. The seven commitments (Daniel, faithful capture; decisions of 2026-06-15 folded in)
|
||||
## 1. The eight commitments (Daniel, faithful capture; decisions of 2026-06-15 folded in)
|
||||
|
||||
The original four (1–4) plus three Daniel added when he resolved the open questions (5–7).
|
||||
The original four (1–4) plus four Daniel added on 2026-06-15: three when he resolved the open
|
||||
questions (5–7), and the release Description field (8) as a focused addition the same day.
|
||||
|
||||
1. **Player-bar release-title → release detail, via a medium→route resolver.** **DECIDED
|
||||
(2026-06-15):** the release-title click resolves the release's `ReleaseMedium` → the correct
|
||||
@@ -152,6 +177,17 @@ The original four (1–4) plus three Daniel added when he resolved the open ques
|
||||
separate orchestrating service) — framed with a recommendation in §3c; the final call is
|
||||
staff-engineer's at implementation. (Was an adjacent gap; promoted to scope. See §3c.)
|
||||
|
||||
8. **Release Description field.** **NEW SCOPE (2026-06-15):** every release medium (Cut, Session, Mix)
|
||||
gains a **Description** — a multiline / paragraph free-text field describing the release. It is a
|
||||
**base `ReleaseEntity` field** (applies to all media uniformly; per the Phase 9 spine it belongs on
|
||||
the base release, **not** a per-medium metadata satellite). Two surfaces: (a) the **CMS add/edit
|
||||
forms** gain a multiline text input (in `AlbumHeaderFields`, alongside Genre/Release Date — base
|
||||
fields, all media); (b) the **release detail pages** (`/cuts/{id}`, `/sessions/{id}`, `/mixes/{id}`)
|
||||
gain a **text block** rendering it. Confirmed against live source: **the column does not exist**, so
|
||||
this carries a real EF migration (Daniel-gated apply), DTO mirror, converter round-trip, and
|
||||
write-path plumbing — the same channel Genre travels. See §3d. (Focused addition; lands as its own
|
||||
schema slice, wave 11.G, with the render folded into 11.A + a small touch to Session/Mix detail.)
|
||||
|
||||
---
|
||||
|
||||
## 2. Requirement 1 reframed — the medium→detail resolver
|
||||
@@ -521,6 +557,123 @@ Phase 11; the preload half remains deferred** there (and gates 1.4/1.5 as before
|
||||
|
||||
---
|
||||
|
||||
## 3d. Commitment 8 — the release Description field (a real new column)
|
||||
|
||||
**Recommendation: add `Description` to the base `ReleaseEntity`, thread it through the existing
|
||||
release-cardinal write channel exactly as `Genre` is threaded, and render it as a text block via the
|
||||
detail-page slots already in scope.** This is a clean vertical slice — no new architecture, no new
|
||||
endpoint — but unlike commitment 5, it **is** a genuine new column with a real migration. The Phase 9
|
||||
spine decides its placement without debate: it applies to **all media uniformly**, so it lives on the
|
||||
**base release**, not a per-medium satellite.
|
||||
|
||||
### 3d.1 Why the base release, not a satellite (the placement is not a judgment call)
|
||||
|
||||
Phase 9 (`ReleaseConfiguration` comment, lines 45–56) is explicit: the default home for medium-varying
|
||||
data is a satellite metadata table; `ReleaseType` is the *one* allowed exception, justified solely by
|
||||
`/cuts` read volume, and **"Future media MUST NOT copy this pattern."** Description is the easy case —
|
||||
it does **not** vary by medium (every Cut, Session, and Mix has the same kind of prose blurb), so the
|
||||
satellite question never arises. A field that is uniform across media belongs on the base table by the
|
||||
same logic that puts `Genre` and `ReleaseDate` there. **No new satellite, no medium conditional, no
|
||||
converter null-for-non-matching-medium dance** (contrast `ReleaseType`, which the converter nulls for
|
||||
Session/Mix — Description needs none of that; it is carried verbatim for all media).
|
||||
|
||||
### 3d.2 What does NOT exist today (verified against live source, 2026-06-15)
|
||||
|
||||
| Layer | State | Evidence |
|
||||
|---|---|---|
|
||||
| Entity | **No `Description`.** `ReleaseEntity` has `Title`, `Artist`, `Genre?`, `ReleaseDate?`, `ImagePath?`, `ReleaseType`, `Medium`, `CreatedByUserId?`, `Tracks`, `SessionMetadata?`, `MixMetadata?`. | `ReleaseEntity.cs:13-30` |
|
||||
| DTO | **No `Description`.** `ReleaseDto` mirrors the above + read-model `TrackCount`. | `ReleaseDto.cs:10-33` |
|
||||
| EF config | No `description` column mapped. | `ReleaseConfiguration.cs:24-72` |
|
||||
| Converter | `TrackConverter.Convert(ReleaseEntity)` / `Convert(ReleaseDto)` map every base field both directions; no `Description` line. | `TrackConverter.cs:19-65` |
|
||||
|
||||
So the column is genuinely new. **A grep for `[Dd]escription` across `ReleaseEntity.cs` returns
|
||||
nothing** — there is no field to verify-and-consume (the commitment-5 outcome); this is the build.
|
||||
|
||||
### 3d.3 The write path — Description rides the Genre channel exactly
|
||||
|
||||
This is the load-bearing realization that keeps the slice small: **there is no dedicated
|
||||
release-update endpoint.** Release-cardinal fields are carried on the *track* update/upload request and
|
||||
projected onto the linked release row by the unified services. The CMS edits the whole release through
|
||||
`BatchEdit`, whose `AlbumHeaderFields` owns `AlbumName`/`Artist`/`Genre`/`ReleaseDate`, and on submit
|
||||
each track's `CmsTrackService.UpdateAsync(...)` / `UploadTrackAsync(...)` carries those
|
||||
release-cardinal fields; `TrackController` then routes them to the linked release (see the
|
||||
`PUT api/track/meta/{id}` contract: *"release-cardinal fields … update the linked release"*).
|
||||
|
||||
So Description travels the **identical thread** as Genre:
|
||||
|
||||
1. **`ReleaseEntity.Description` (string?, nullable)** — base table; EF `description` column, `HasMaxLength`
|
||||
generous for paragraph prose (e.g. 2000–4000; pick a ceiling, mirror the `Genre`/`Title` `HasMaxLength`
|
||||
idiom in `ReleaseConfiguration`). **Nullable** — existing rows migrate with `NULL`, no data migration.
|
||||
2. **EF migration** — `dotnet ef migrations add AddReleaseDescription`. **Daniel-gated apply** (do not
|
||||
auto-run `database update`; the migration is generated and committed, applied on Daniel's go).
|
||||
3. **`ReleaseDto.Description` (string?)** — DTO mirror.
|
||||
4. **`TrackConverter`** — add `Description = entity.Description` to `Convert(ReleaseEntity)` and
|
||||
`Description = dto.Description` to `Convert(ReleaseDto)`. No null-for-medium dance (it is uniform).
|
||||
5. **Write request plumbing** — add `Description` to the release-cardinal field set carried on the
|
||||
track update + upload path: `UpdateTrackMetadataRequest` gains `string? Description`; the upload form
|
||||
gains a `description` field; `TrackController` and `UnifiedTrackService` / `UnifiedReleaseService`
|
||||
thread it onto the linked release **wherever `Genre` is already threaded** (the cleanest diff is
|
||||
"find every `Genre` in the write path, add a sibling `Description`"). *Note the tri-state question:*
|
||||
Genre is passed as a plain nullable today (whitespace → null). Description should follow the **same
|
||||
posent** — empty input → `null`, no special tri-state (it is not the cover-art `ImagePath` case).
|
||||
6. **CMS input** — `AlbumHeaderFields` gains a `MudTextField` with `Lines="4"` (multiline) labeled
|
||||
"Description", bound via a `Description`/`DescriptionChanged` parameter pair, wired through
|
||||
`BatchEdit` (`_description` field, seeded from `release?.Description` on load — mirror `_genre` at
|
||||
`BatchEdit.razor:213`) and the `BatchUpload` create form. It sits in the base-fields block alongside
|
||||
Genre/Release Date, **not** inside `MediumFields` (it is base, not medium-specific).
|
||||
|
||||
> **One honest call to surface (not a blocker):** the write path projects release-cardinal fields from
|
||||
> *each track row* onto the shared release. For a multi-track Cut, every row carries the same
|
||||
> Description, and the last write wins — which is already exactly how Genre/ReleaseDate behave, so
|
||||
> Description inherits the existing semantics with no new edge case. No change wanted; just naming that
|
||||
> the "release field carried per-track" model already in place covers Description for free.
|
||||
|
||||
### 3d.4 The read path — the detail-page text block
|
||||
|
||||
The detail pages already load a `ReleaseDto` (the Cut page via the new `CutDetailViewModel` §3.3;
|
||||
Session/Mix via `ReleaseDetailBase`). Once `ReleaseDto.Description` is populated (3d.3 step 4), the
|
||||
render is a **conditional text block** — show a paragraph when `Description` is non-empty, render
|
||||
nothing when null (most existing rows will be null until re-edited). Placement per surface:
|
||||
|
||||
- **`/cuts/{id}` (11.A):** the Cut page is new, so the text block is part of its first build — a
|
||||
paragraph block in the header column or just below it (Daniel's layout §3.1 has room below the
|
||||
header / above the track list; recommend **below the header masthead, above the track-list divider**
|
||||
so the prose introduces the album before the tracks). Folds into 11.A's `BodyContent`/header
|
||||
composition with no extra wave.
|
||||
- **`/sessions/{id}` and `/mixes/{id}` (existing pages):** a small additive touch — a description
|
||||
text block in each page's `MetaContent` (or equivalent body region). `SessionDetail` is the
|
||||
overlay-diverged page and `MixDetail` composes the scaffold, so the block lands slightly differently
|
||||
in each, but both are a few lines of conditional markup, not a structural change.
|
||||
|
||||
**Styling:** a quiet paragraph — `Typo.body1`/`body2`, muted, respecting `white-space: pre-line` so
|
||||
authored line breaks survive (the field is "multiline / paragraph"). Surface for Daniel only if he
|
||||
wants markdown rendering vs. plain prose; **recommend plain text with preserved line breaks** for v1
|
||||
(no markdown dependency, matches the "free-text field" framing).
|
||||
|
||||
### 3d.5 Wave placement — its own schema slice (11.G), render folded into 11.A
|
||||
|
||||
**Honest call on whether this rides existing waves or warrants its own.** It splits cleanly:
|
||||
|
||||
- The **schema + write path + CMS input** is a self-contained vertical that **shares no surface with
|
||||
11.A–F** (it touches `ReleaseEntity`/`ReleaseDto`/`TrackConverter`/the write request/`AlbumHeaderFields`
|
||||
— none of which the six existing waves modify) and carries the one Daniel-gated migration in the
|
||||
phase. Grafting it onto 11.A (the Cut page) would muddy 11.A's dependency story (11.A depends only on
|
||||
existing data primitives; bolting a migration onto it makes the Cut page wait on a schema apply it
|
||||
doesn't otherwise need). So the schema slice is **its own wave, 11.G** — independent, can start cold,
|
||||
and the *only* thing that gates it is Daniel's migration go-ahead.
|
||||
- The **detail-page render** is a thin consumer that **rides 11.A** (the Cut block is part of the Cut
|
||||
page's first build) **plus a small additive touch to the existing Session/Mix detail pages**. It
|
||||
depends on 11.G having populated `ReleaseDto.Description`, but the render degrades cleanly (a null
|
||||
Description simply renders nothing), so 11.A can ship its Cut page before 11.G lands and gain the
|
||||
description block the moment 11.G does — the same **design-the-seam** discipline used for the queue.
|
||||
|
||||
This is the truthful decomposition: a standalone schema wave is warranted (clean vertical + the one
|
||||
migration), but inventing a *second* wave for the render would be over-decomposition — the render is a
|
||||
few lines folded into work already scoped. **11.G = the schema/write/CMS slice; the render rides 11.A
|
||||
and a Session/Mix touch.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Requirement 3 — full stack retirement + shared release-card normalization
|
||||
|
||||
**DECIDED (2026-06-15):** retire the **whole** track-cardinal stack (not just the `?album` branch),
|
||||
@@ -695,9 +848,10 @@ seed-from-URL step just has to run before the restore decision (as §5.2 specifi
|
||||
## 6. Wave decomposition
|
||||
|
||||
Sequenced so the structural chain (Cuts page → resolver → repoint → retire/normalize) is honored,
|
||||
and the genuinely independent tracks (Archive URL; the queue model) can run in parallel. Seven
|
||||
commitments, six waves. The queue (11.F) is the one work item that can start cold on day one and is
|
||||
the gate for the Cuts "play album" affordance.
|
||||
and the genuinely independent tracks (Archive URL; the queue model; the Description schema slice) can
|
||||
run in parallel. Eight commitments, **seven waves**. Two work items can start cold on day one: the
|
||||
queue (11.F, gate for the Cuts "play album" affordance) and the Description schema slice (11.G, gated
|
||||
only by Daniel's migration go-ahead).
|
||||
|
||||
```
|
||||
┌──────────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
|
||||
@@ -723,13 +877,16 @@ the gate for the Cuts "play album" affordance.
|
||||
│◄────────────────────┤ + a release detail to │
|
||||
▼ │ share (11.A/Session/Mix) │
|
||||
┌──────────────────────────┐ └──────────────────────────┘
|
||||
│ 11.C Retire + normalize │
|
||||
│ • delete track-cardinal │
|
||||
│ stack (Tracks*/Track*) │
|
||||
│ • fold Archive+Cuts cards │
|
||||
│ into shared ReleaseGallery│
|
||||
│ • consolidate medium-label│
|
||||
└──────────────────────────┘
|
||||
│ 11.C Retire + normalize │ ┌──────────────────────────┐
|
||||
│ • delete track-cardinal │ │ 11.G Release Description │
|
||||
│ stack (Tracks*/Track*) │ │ schema slice (col + EF │
|
||||
│ • fold Archive+Cuts cards │ │ migration [Daniel-gated], │
|
||||
│ into shared ReleaseGallery│ │ DTO/converter/write path, │
|
||||
│ • consolidate medium-label│ │ CMS multiline input) │
|
||||
└──────────────────────────┘ │ INDEPENDENT (cold start) │
|
||||
│ render rides 11.A + a │
|
||||
│ Session/Mix detail touch │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- **11.A — `/cuts/{id}` album-detail page.** The page, `CutDetailViewModel`, cover theme border,
|
||||
@@ -764,6 +921,14 @@ the gate for the Cuts "play album" affordance.
|
||||
11.A's header Play calls `QueueService.PlayRelease(tracks)` once 11.F lands (degrading to
|
||||
single-track before then). **Preload (§1.3b) is OUT of this wave** — design the seam, defer the
|
||||
feature (§3c.5).
|
||||
- **11.G — release Description schema slice.** New `ReleaseEntity.Description` column + EF migration
|
||||
(**Daniel-gated apply**), `ReleaseDto` mirror, `TrackConverter` round-trip, write-path plumbing
|
||||
(`UpdateTrackMetadataRequest` + upload form + the unified services — threaded wherever `Genre` is),
|
||||
and the CMS `AlbumHeaderFields` multiline input (§3d). **Independent — can start cold on day one**,
|
||||
in parallel with everything; the only gate is Daniel's migration go-ahead. The **detail-page render
|
||||
is NOT in this wave** — the Cut text block rides 11.A; the Session/Mix text block is a small additive
|
||||
touch to those existing pages. Both degrade cleanly (a null Description renders nothing), so the
|
||||
render can land before or after 11.G applies (§3d.4–3d.5).
|
||||
|
||||
**Dependency shape:**
|
||||
|
||||
@@ -772,12 +937,16 @@ the gate for the Cuts "play album" affordance.
|
||||
└────► 11.E (also needs a release detail to share)
|
||||
11.D (free-floating; coordinate with 11.C on ArchiveView)
|
||||
11.F (free-floating cold start; 11.A's "play album" consumes it)
|
||||
11.G (free-floating cold start; gated only by Daniel's migration go-ahead.
|
||||
Detail-page render rides 11.A + a Session/Mix touch — degrades on null,
|
||||
so render & schema can land in either order)
|
||||
```
|
||||
|
||||
**Critical path:** `11.A → 11.B → 11.C`. **11.D, 11.E, 11.F hang off it** (11.E after 11.B; 11.D and
|
||||
11.F fully parallel). The two "can start immediately" items are **11.A** and **11.F** — kicking both
|
||||
off first shortens the wall-clock to a usable Cut page (page + queue arrive together, so "play album"
|
||||
works on first ship of 11.A rather than as a later retrofit).
|
||||
**Critical path:** `11.A → 11.B → 11.C`. **11.D, 11.E, 11.F, 11.G hang off it** (11.E after 11.B; 11.D,
|
||||
11.F, 11.G fully parallel). The "can start immediately" items are **11.A**, **11.F**, and **11.G** —
|
||||
kicking 11.A + 11.F off first shortens the wall-clock to a usable Cut page (page + queue arrive
|
||||
together, so "play album" works on first ship of 11.A rather than as a later retrofit); 11.G runs
|
||||
alongside on its own track, surfacing the Description on the detail pages as it lands.
|
||||
|
||||
> **Honest dependency notes (the brief asked to keep these straight):**
|
||||
> - **Ordinal does *not* gate a wave.** §3a showed `TrackNumber` already exists end-to-end; the
|
||||
@@ -788,6 +957,12 @@ works on first ship of 11.A rather than as a later retrofit).
|
||||
> - **Shared cards parallel the stack retirement.** Folding Archive/Cuts cards into `ReleaseGallery`
|
||||
> (§4.2) and deleting the track-cardinal stack (§4.1) are both in 11.C and both depend on 11.B's
|
||||
> repoint — they are siblings, not sequential.
|
||||
> - **Description schema (11.G) gates the Description *render*, not the Cut page.** 11.A ships its Cut
|
||||
> page regardless; the Description block (on 11.A and on Session/Mix) renders nothing until 11.G's
|
||||
> column carries data, then lights up with no rework. So 11.G is a cold-start free-floater whose only
|
||||
> hard gate is Daniel's migration go-ahead — *not* a dependency of any other wave. Unlike commitment
|
||||
> 5 (already-built, a verification), commitment 8 is a real migration; unlike the queue, it has no
|
||||
> architecture question — it is a mechanical vertical that rides the existing `Genre` write channel.
|
||||
|
||||
---
|
||||
|
||||
@@ -809,6 +984,11 @@ works on first ship of 11.A rather than as a later retrofit).
|
||||
5. **Play-queue system (§3c).** **DECIDED:** in scope; absorbs the queue half of `PLAN.md §1.3`. The
|
||||
Cuts "play album" affordance is its first consumer. (Was an adjacent gap; promoted.) Preload half
|
||||
stays deferred.
|
||||
6. **Release Description field (§3d).** **DECIDED:** in scope. A multiline base-`ReleaseEntity` field
|
||||
(all media), edited from the CMS add/edit forms and rendered as a text block on every detail page.
|
||||
Confirmed against live source — the column does **not** exist, so this carries a real EF migration
|
||||
(Daniel-gated apply) + DTO/converter/write-path/CMS plumbing. Lands as schema slice 11.G; render
|
||||
rides 11.A + a Session/Mix touch. (Focused addition, 2026-06-15.)
|
||||
|
||||
### 7.2 Still open (need Daniel — recommendations given)
|
||||
|
||||
@@ -824,6 +1004,10 @@ works on first ship of 11.A rather than as a later retrofit).
|
||||
all media; per-track embed stays where a track is the subject.* Trivial either way.
|
||||
5. **`/genres` fate (§4.3).** Already nav-demoted; Archive has genre filtering. Retire `/genres` too?
|
||||
*Out of stated scope — flag as adjacent, low urgency.* **Not in Phase 11** unless Daniel pulls it.
|
||||
6. **Description render: plain text or markdown? (§3d.4).** *Recommend plain text with preserved line
|
||||
breaks (`white-space: pre-line`) for v1 — no markdown dependency, matches "free-text field."* Trivial
|
||||
to revisit if Daniel wants formatting. Also minor: the `HasMaxLength` ceiling for the column (§3d.3
|
||||
step 1) — recommend generous (2000–4000) for paragraph prose; not a decision, just pick and note.
|
||||
|
||||
### 7.3 Small things to get right (not decisions — implementer notes)
|
||||
|
||||
@@ -836,6 +1020,13 @@ works on first ship of 11.A rather than as a later retrofit).
|
||||
player raises a track-ended event or add one (the single new player-side surface — §3c.2).
|
||||
- **Year-only display (§3.1).** The Cut header shows just the year; `MixDetail`/`SessionDetail` show
|
||||
"MMMM yyyy". Don't copy the month-year format into the Cut header.
|
||||
- **Description is a base field, not medium-specific (11.G).** Put the CMS input in
|
||||
`AlbumHeaderFields` (the base-fields block alongside Genre/Release Date), **not** in `MediumFields`.
|
||||
Thread the write wherever `Genre` already threads (entity → DTO → converter → `UpdateTrackMetadataRequest`
|
||||
→ upload form → unified services). No converter null-for-medium dance — it is uniform across media.
|
||||
- **Description render degrades on null (11.G).** Most existing release rows will have `NULL`
|
||||
Description until re-edited; the detail-page block must render nothing (not an empty heading) when
|
||||
null. The Cut block lands in 11.A; the Session/Mix blocks are a small touch to those existing pages.
|
||||
|
||||
---
|
||||
|
||||
@@ -861,6 +1052,12 @@ works on first ship of 11.A rather than as a later retrofit).
|
||||
- **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.
|
||||
- **Uniform release data lives on the base release (§3d).** The Description column lands on the base
|
||||
`ReleaseEntity` because it is uniform across media — the same logic that puts `Genre`/`ReleaseDate`
|
||||
there, and the explicit Phase 9 rule that satellites are for *medium-varying* data only
|
||||
(`ReleaseType` being the one volume-justified exception). Description introduces no satellite, no
|
||||
medium conditional, and rides the existing release-cardinal write channel — schema growth that
|
||||
honors the spine rather than bending it.
|
||||
|
||||
---
|
||||
|
||||
@@ -902,3 +1099,15 @@ works on first ship of 11.A rather than as a later retrofit).
|
||||
- **`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.
|
||||
- **No release `Description` column exists.** `ReleaseEntity` (`ReleaseEntity.cs:13-30`) carries
|
||||
`Title`, `Artist`, `Genre?`, `ReleaseDate?`, `ImagePath?`, `ReleaseType`, `Medium`, `CreatedByUserId?`,
|
||||
`Tracks`, `SessionMetadata?`, `MixMetadata?` — no description; `ReleaseDto` (`ReleaseDto.cs:10-33`)
|
||||
mirrors it + read-model `TrackCount`; `TrackConverter` (`TrackConverter.cs:19-65`) maps every base
|
||||
field both directions with no `Description` line. A grep for `[Dd]escription` across the entity
|
||||
returns nothing. **Commitment 8 is a real new column** (contrast commitment 5, already-built).
|
||||
- **Release-cardinal fields are written through the track update/upload path, not a release endpoint.**
|
||||
There is no `PUT api/release/{id}` metadata endpoint. `BatchEdit`'s `AlbumHeaderFields` owns
|
||||
`Genre`/`ReleaseDate`/etc.; on submit each track's `CmsTrackService.UpdateAsync`/`UploadTrackAsync`
|
||||
carries those fields and `TrackController` (`PUT api/track/meta/{id}`) projects the release-cardinal
|
||||
ones onto the linked release. **Description follows this exact channel** (`UpdateTrackMetadataRequest.cs:14-23`,
|
||||
`BatchEdit.razor:380-488`, `AlbumHeaderFields.razor:16-19/80-81`).
|
||||
|
||||
Reference in New Issue
Block a user