# COMPLETED.md — DeepDrftHome Archive of items that have moved out of `PLAN.md` and `CMS-PLAN.md`. Per `CONTEXT.md §6`, completed items are moved here rather than deleted. Each entry preserves the original "What / Why / Shape" body so this file reads as a decision record, not just an outcome list. Newest entries at the top. Group by phase/wave header (mirroring `PLAN.md` / `CMS-PLAN.md` themes) when there are enough entries to warrant it. --- ## Phase 2.5 — "Stream Now" — random-track instant play **Status:** Fully landed on 2026-06-07 (feature complete, endpoints + service methods + menu wiring, merged to dev). - **What:** The nav-bar "Stream Now ▶" CTA (desktop and mobile, in `DeepDrftMenu.razor`) today just navigates to `/tracks`. Change it to **pick a random track from the library and start playing it immediately**, in place, without forcing the user onto the gallery page. - **Why it matters:** It is the single most prominent call-to-action on the site and currently does the least interesting thing — it dumps the listener on a grid and asks them to choose. "Stream Now" should mean *now*: one click, music plays. It is also the lowest-friction way for a first-time visitor to hear the collective's output, which is the whole point of the public site. Borrowed pattern: the "shuffle play" / "I'm feeling lucky" affordance (Spotify's shuffle, Bandcamp's "play random"). #### UX flow 1. User clicks "Stream Now ▶" (desktop CTA or mobile menu item). 2. Button enters a brief loading affordance (disabled + subtle pulse/spinner) while a track is selected — the selection requires at least one HTTP round-trip, so this is not instantaneous. 3. A random track is chosen from the full library via `GET api/track/random` (server-side `ORDER BY RANDOM() LIMIT 1`). 4. The player begins streaming that track via the existing `AudioPlayerBar` dock at the bottom of the layout. The dock is already cascaded into every page by `AudioPlayerProvider` in `MainLayout`, so it appears/animates in exactly as it does when a gallery card is clicked. 5. The user does **not** navigate. They stay on whatever page they were on (most likely `Home`). Music plays; the dock is the player surface. 6. On mobile, the menu closes (`CloseMobileMenu`) as part of the click, same as the existing nav links. #### Edge cases - **Empty library (`TotalCount == 0`):** No track to play. The button surfaces a non-blocking, transient message ("No tracks yet") and does nothing else. Does not navigate, does not error-toast aggressively. This is a legitimate cold-start state, not a failure. - **Metadata fetch fails (HTTP error):** Surfaces a transient error on the button ("Couldn't reach the library — try again"), re-enables the button, does not navigate. Reuses the existing `ApiResult` failure check pattern (`result is { Success: true, ... }`). - **Track fails to stream (selected track is valid metadata but the audio stream errors):** Already handled downstream by `StreamingAudioPlayerService` / error handlers and surfaced through `IPlayerService.ErrorMessage` and the dock. Stream Now does not duplicate stream-error handling in the menu; it hands off to the same `SelectTrackStreaming` path every other play uses, and inherits that path's error behavior. - **Player already playing something:** Stream Now interrupts it and starts the random track. No confirmation prompt — "Stream Now" is an explicit user command to play something new. - **Repeat clicks / same-track-twice:** Acceptable for v1 to occasionally re-pick the currently-playing track. If it becomes annoying, a cheap "exclude `PlayerService.CurrentTrack?.Id`" filter on the candidate set is a one-line follow-up; noted for future. #### Implementation **API endpoint (`DeepDrftAPI`):** - New `GET api/track/random` (unauthenticated, mirroring `GET api/track/page`) returning a single `TrackDto` via `ORDER BY RANDOM() LIMIT 1` (or the EF-Core equivalent) server-side. **Service methods:** - New method on `ITrackDataService` / `TrackClientDataService`: `Task> GetRandomTrack()`, calling `GET api/track/random` via `TrackClient`. **Menu wiring (`DeepDrftMenu.razor`):** - Injects `ITrackDataService` and cascaded `IStreamingPlayerService`. Click handler: calls `GetRandomTrack()`, on success calls `PlayerService.SelectTrackStreaming(track)`, on empty/failure shows transient message. **AudioContext user-gesture constraint:** - Browsers (Safari most strictly) only allow an `AudioContext` to start inside a user-gesture call stack. `SelectTrackStreaming` starts the context. Stream Now does an `await GetRandomTrack()` (network) before calling `SelectTrackStreaming` — an intervening `await` can lose gesture context on Safari. Mitigation: `IStreamingPlayerService.WarmAudioContext()` method added, called synchronous with the gesture at the start of the click handler, before the network await. #### Acceptance criteria — as implemented - Clicking "Stream Now ▶" (desktop CTA) with a non-empty library selects a track uniformly at random (server-side) and begins streaming it via the existing dock, without navigating away. - Clicking "Stream Now ▶" in the mobile menu does the same and closes the mobile menu. - Selection issues **exactly one** HTTP request (`GET api/track/random`). - With an empty library, the button shows a transient "no tracks" message and does not navigate or throw. - With a failed metadata fetch, the button shows a transient error, re-enables, and does not navigate. - A track that streams-errors after selection surfaces through the *existing* player error path — no new error handling in the menu. - The menu component contains no track-fetch logic inline: selection goes through `ITrackDataService.GetRandomTrack()`; playback goes through `PlayerService.SelectTrackStreaming`. No duplication. - Audio plays on the first click after a cold load on Chrome and Safari — user-gesture/AudioContext constraint satisfied via `WarmAudioContext()` hook. - While selection is in flight, the button is disabled to prevent double-launch. --- ## Phase 2.1 — Cover art / image vault wired through **Status:** Fully landed on 2026-06-07 across three waves (Wave 1: API + vault; Wave 2-A: public proxy + TrackCard; Wave 2-B: CMS upload UI), merged to dev. - **What:** `MediaVaultType.Image` is implemented end-to-end and exercised by tests, but the production surface only registers a `tracks` vault of type `Audio`. `ImagePath` on `TrackEntity` is a free-form URL string today; it should resolve to an entry in an image vault served by `DeepDrftContent`. - **Why it matters:** Prerequisite for any album/release/genre view that wants to look like a music site rather than a list of rows. Also closes a free-form-string surface area that will otherwise calcify. - **Shape:** - Register a second vault (`images` or `art`, type `Image`) in `Startup.ConfigureDomainServices` and in the CLI. - Add `GET api/image/{entryKey}` (unauthenticated, mirrors track read) and `PUT api/image/{entryKey}` (ApiKey, mirrors track write) on `DeepDrftContent`. - Change `TrackEntity.ImagePath` semantics from "URL" to "image vault entry key" (column rename optional — could remain `image_path` with semantic shift, or could become `image_entry_key` for clarity). - Add an image processor sibling of `AudioProcessor`. - **Prerequisite:** None. - **Constraint:** This is a small schema-semantics migration. Existing rows have `null` ImagePath in production so there is no data to migrate, but commit before the field has real content to avoid a backfill. --- ## Embeddable iframe player **Status:** Feature complete on 2026-06-07 (commit `c83b132 feature: Embed Frame Player`, merged to dev). A standalone, chrome-free player surface intended for embedding in an `