# 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. --- ## WaveformSeeker Wave 3 — CMS PreProcessing panel **Status:** W3 (CMS track-preprocessing panel) refactored on 2026-06-05 (branch `waveform-w3-cms`, merged to dev). ### W3 — CMS PreProcessing panel **Landed 2026-06-05. Refactored 2026-06-05.** Implemented the CMS surface for on-demand waveform profile generation. Initial implementation created a new `/tracks/preprocessing` page; refactored to fold the preprocessing panel into `TrackList.razor` as a second `MudTabPanel` alongside the existing Tracks tab. **API endpoints (`DeepDrftAPI`):** - `GET api/track/waveform-status` (ApiKey) — returns `WaveformStatusDto[]` with per-track profile existence (one entry per track in the database, indicating whether a profile sidecar exists in the vault). - `POST api/track/{trackId}/waveform` (ApiKey) — triggers on-demand profile compute and store for an existing track. Skips if profile already exists; errors surface gracefully (no profile → HTTP 404, track not found → HTTP 400). **Models (`DeepDrftModels`):** - `WaveformStatusDto` — carries `TrackId`, `EntryKey`, `TrackName`, `HasProfile` boolean, and metadata for display/sorting. **CMS service (`ICmsTrackService` / `CmsTrackService` in `DeepDrftManager`):** - `GetWaveformStatusAsync()` — service method wrapping the `api/track/waveform-status` call; returns `Result` for error handling. - `GenerateWaveformProfileAsync(entryKey)` — service method wrapping the per-track generation endpoint; returns `Result` (success → true, profile already exists → true, error → false with result code). **CMS UI (`DeepDrftManager/Components/Pages/Tracks/TrackList.razor`):** - Added "Preprocessing" `MudTabPanel` as the second tab in `TrackList.razor`, alongside the existing "Tracks" tab. - Table layout within the panel: track name, artist, "Profile Status" indicator (✓ or ○), with a per-row `Generate` button. - Sequential "Generate All Missing" bulk action button — iterates tracks with `HasProfile == false`, calls `GenerateWaveformProfileAsync`, shows progress. On completion, refreshes the table. - The standalone `TrackPreProcessing.razor` page at `/tracks/preprocessing` was eliminated; the page route is no longer exposed. - Nav link to preprocessing removed from `Index.razor` dashboard (consolidation makes a separate link unnecessary; the tab is discoverable from `TrackList.razor`). **Architecture notes:** - Waveform generation on-demand (not automatic on upload like in W1) is intentional: Wave 1 profiles were computed for all future-uploaded tracks; Wave 3 adds a retroactive tool to populate profiles for existing tracks uploaded before Wave 1. The bulk action supports batching. - Service calls are fire-and-forget-result, not throw-on-error — `GenerateWaveformProfileAsync` returns a `Result` for the caller to inspect. This matches the FileDatabase philosophy (errors in compute/store are swallowed at the service boundary, callers check return values). - Profile endpoint uses the same `WaveformProfileService` that computes profiles during upload — no new algorithm or storage path introduced. CMS can only trigger on-demand what the upload path does automatically. - HTTP cache headers are deferred (same as W1-T2). Each `api/track/waveform-status` call lists all tracks and their current state; this is acceptable for the admin surface where refreshes are infrequent. - **Consolidation rationale:** Folding the preprocessing panel into `TrackList` reduces UI fragmentation — track management (list, add, edit, delete, preprocess) lives in one cohesive view rather than split across separate pages. The tab structure keeps preprocessing distinct from the main track listing without requiring a dedicated route. --- ## WaveformSeeker Wave 2 — DOM seekbar + Interop module **Status:** W2 (WaveformSeeker component) landed on 2026-06-05 (branch `waveform-w2-seeker`, pending merge to dev). ### W2 — WaveformSeeker component (seekbar replacement) **Landed 2026-06-05.** Implemented the interactive WaveformSeeker component: a bar-chart-styled seekbar replacing `MudSlider` in `PlayerSeekZone`, with DOM-rendered progress split via CSS and lazy-loaded pointer-capture drag interop. **Component changes (`DeepDrftPublic.Client/Controls/AudioPlayerBar`):** - `WaveformSeeker.razor` (+ `.cs`, `.css`) — new component consuming `WaveformProfile double[]?` and `Duration`, rendering bars as DOM elements with clip-overlay progress. Single CSS variable (`--seek-position`) changes per seek gesture; no per-bar re-render. - Pointer-capture drag wired via `waveformSeeker.js` (ES module, lazy-loaded). Calculates seek target from click/drag position and invokes `OnSeekRequested` callback (delegates to `IPlayerService.SeekAsync`). - Flat floor-height fallback when profile is unavailable — seek gesture always works, with or without loudness data. - `PlayerSeekZone.razor` — now hosts `WaveformSeeker` in place of the removed `MudSlider` placeholder. **Interop changes (`DeepDrftPublic/Interop/audio/`):** - New `waveformSeeker.ts` module (separate from the TS audio bundle) — `PointerCaptureHandler` class managing `pointerdown` / `pointermove` / `pointerup` lifecycle. Compiled to `waveformSeeker.js` in `wwwroot/js/audio/`. - Module loaded on first use (not bundled with audio stack) to defer its parse cost until the player is expanded and the seekbar is visible. **`.gitignore` scoping:** - Added scoped negation to track hand-authored `waveformSeeker.js` alongside existing TS-output ignore rule — allows the compiled JS to be committed for fast startup without committing intermediate TS compiler outputs. **Service changes (`IPlayerService` / `AudioPlayerService` / `StreamingAudioPlayerService`):** - New `WaveformProfile double[]?` property added to service interface and implementations. - Fetched fire-and-forget on track load via `GetWaveformProfileAsync(trackId, cancellationToken)` — existing HTTP call from W1-T2. - Cancellable via the track-reset flow (same cancellation token that stops spectrum animation). - Cleared on reset with all other track state. **Testing:** - Manual verification: seekbar renders flat when profile unavailable; dragable when profile present; CSS clip-overlay tracks seek position correctly. **Architecture notes:** - WaveformSeeker does not re-fetch the profile — it consumes the same `IPlayerService.WaveformProfile` fetched during track load. No additional HTTP round-trip per seek gesture. - Interop module (`waveformSeeker.js`) is independent of the audio playback stack — can be updated or replaced without touching audio scheduling logic. - Pointer-capture semantics ensure seek is responsive even when the browser's event queue is saturated by animation frames. - Flat fallback ensures seek gestures always work, even on tracks with no profile data (uploaded before W1, or on profile-generation failure). --- ## WaveformSeeker Wave 1 — Loudness profile + layout refactor **Status:** W1-T1 (backend loudness computation), W1-T2 (HTTP transport), and W1-T3 (player layout refactor) landed on 2026-06-05. ### W1-T1 — Backend waveform loudness profiling **Landed 2026-06-05.** Implemented Phase 1 of the WaveformSeeker feature (`product-notes/spectrum-seeker.md`): loudness-profile computation and storage for preprocessed waveform data. **Backend changes (`DeepDrftContent`):** - Added `ILoudnessAlgorithm` strategy interface for swappable loudness computation. - Implemented `RmsLoudnessAlgorithm` — first loudness algorithm using root-mean-square; future LUFS implementation swaps in via the same interface without touching service, wire format, or storage. - `WaveformProfileService` — computes peak-normalized loudness profile from PCM WAV (one linear buffer pass), buckets by time slice, normalizes to `[0,1]`, stores as byte-quantized sidecar in new `profiles` vault (FileDatabase `MediaFileVault`). - `WaveformProfileOptions` — config-bound options object carrying `BucketCount` (default 512) and future algorithm-selection knobs. **Integration changes (`DeepDrftAPI`):** - Wired `WaveformProfileService` into `UnifiedTrackService.UploadAsync` — profile computed on upload, stored immediately, failure silently swallowed (consistent with FileDatabase philosophy in `CLAUDE.md`). **Models (`DeepDrftModels`):** - `WaveformProfileDto` — carries quantized profile data; format independent of algorithm or bucket count. **Testing (`DeepDrftTests`):** - 4 new unit tests: RMS algorithm correctness against known-good PCM samples, swappable-algorithm contract (two strategies swap cleanly), and integration with `WaveformProfileService`. **Architecture notes:** - Profile is derived binary content; stored in FileDatabase vault sidecar per `CLAUDE.md` principle ("binary content lives in the vault"). - Loudness measure is an abstraction (not hardwired RMS) — RMS→LUFS future change requires only a new `ILoudnessAlgorithm` implementation, no refactoring of service, component, or wire format. - No external audio-processing dependency pulled in for RMS — reuses existing PCM parser from `AudioProcessor`. - Cost: one linear pass over PCM buffer at upload (few hundred ms for typical WAV); never on playback path. ### W1-T2 — Waveform profile HTTP transport **Landed 2026-06-05.** Implemented Phase 2 of the WaveformSeeker feature: HTTP transport layer for waveform profile data from backend to client, enabling client-side display of loudness profiles in future seeking UI. **API endpoint (`DeepDrftAPI`):** - New `GET api/track/{trackId}/waveform` endpoint — unauthenticated, returns `WaveformProfileDto` (base64-encoded quantized bytes + `BucketCount`) on success, 404 if track or profile not found. - Leverages existing `WaveformProfileService` to load profile from vault on demand. - No authentication required — mirrors `GET api/track/{id}` streaming policy (public audio access). **Proxy forward (`DeepDrftPublic`):** - Thin buffered forward in `TrackProxyController` — proxies request from client to `DeepDrftAPI` waveform endpoint with same path parameters. - Preserves error semantics: 404 from API passes through to client; network errors surface as HTTP errors. **HTTP client (`DeepDrftPublic.Client`):** - New `TrackMediaClient.GetWaveformProfileAsync(trackId, cancellationToken)` method on the content HTTP client. - 404 response maps to `Result.Failure` (fail-result signal for WaveformSeeker to render flat fallback). - Network/timeout errors map to separate `Result.Failure` with distinct code. - Callsite can discriminate via result error code whether to retry (transient) or render fallback (not found). **Architecture notes:** - Transport layer is independent of loudness algorithm (W1-T1) — client receives opaque quantized bytes; future algorithm changes on backend do not affect wire format, as long as `BucketCount` is included. - HTTP caching via ETag/Last-Modified is deferred to Phase 2 optimization work. - Profile loading from vault is on-demand (not pre-cached in memory) — load cost amortizes across all requests to the same track. - 404 handling unambiguous: client renders flat fallback, distinguishing "track has no profile" from "track not found" via error code. ### W1-T3 — Player layout refactor (SpectrumVisualizer relocation + VolumeZone rename) **Landed 2026-06-05.** Implemented Phase 3 of the WaveformSeeker feature: architectural layout move separating live-spectrum visualization from loudness-over-time seeking. **Conceptual split:** - Live-spectrum (FFT frequency bars, `SpectrumVisualizer`) moved from `PlayerSeekZone` → stacked above the volume slider in new `VolumeZone`. Conceptually with the output level. - Static loudness-over-time (future `WaveformSeeker`) takes over the seek zone. Conceptually with transport position. **Component changes (`DeepDrftPublic.Client/Controls/AudioPlayerBar`):** - `VolumeControls.razor` → renamed **`VolumeZone.razor`** for symmetry with transport and seek zones; now a vertical stack hosting `SpectrumVisualizer` above the volume slider. - `SpectrumVisualizer` — `BucketCount` parameter defaulted to 24 buckets (down from 32) to fit the narrow volume cluster; set `flex-shrink: 0` to pin the spectrum to a fixed footprint above the volume control. - `PlayerSeekZone.razor` — `SpectrumVisualizer` block removed; placeholder for future `WaveformSeeker` component. **CSS changes (`AudioPlayerBar.razor.css`):** - Adjusted volume cluster width constraints to accommodate the 24-bucket spectrum stacked above. - Responsive layout unchanged at 600px breakpoint (single-row transport/volume with full-width seek below on narrow; same 3-zone layout on wide). **Scope:** - Pure layout move; zero change to spectrum animation lifecycle, player logic, or seek gesture handling. - Both `AudioPlayerBar` and `SpectrumVisualizer` components affected. - Build clean: 0 errors, 0 new warnings. **Notes for future work:** - `PlayerSeekZone` is now ready for the `WaveformSeeker` component (W1-T4/Phase 4 onwards). - Volume cluster can comfortably accommodate 24 FFT bars; 32 would cause visual cramping (why the override exists). - Spectrum visualization lifecycle (subscription to `StateChanged`, animation via `AudioInteropService.StartSpectrumAnimationAsync`) unchanged — only position in the DOM tree changed. --- ## Phase 2 — Product surface: player and theming **Status:** Track card CSS scoping landed on 2026-06-05. Track card glass theming landed on 2026-06-05. AudioPlayerBar responsive unification and SpectrumVisualizer fix landed on 2026-06-05. Track view CSS consolidation landed on 2026-06-05. ### Track Card CSS Scoping **Landed 2026-06-05.** Moved track card rules from the global stylesheet into an isolated scoped stylesheet, eliminating style leakage and enabling independent maintenance of the component's appearance. **CSS changes:** - `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` §8 — removed all track card rules (`.deepdrft-track-card-*`, `.deepdrft-track-title`, `.deepdrft-track-artist`, `.deepdrft-track-meta`); replaced with a pointer comment directing readers to `TrackCard.razor.css`. - `DeepDrftShared.Client/Components/TrackCard.razor.css` — created new scoped stylesheet with all card rules: container styling, text-colour hierarchy (title, artist, meta), theme-variant selectors (`.deepdrft-theme-dark` / `.deepdrft-theme-light`), and glass background + border styling. - Applied `::deep` pseudo-selector to the three MudText text-color rules (`deepdrft-track-title`, `deepdrft-track-artist`, `deepdrft-track-meta`) so CSS isolation doesn't suppress colour overrides on MudBlazor elements. - Eliminated all theme-variant selectors in favour of a single-vocabulary colour scheme: navy-glass fallback, `--deepdrft-white` title, `--deepdrft-green-accent` artist, `rgba(250,250,248,0.45)` meta. Matches the `NowPlayingCard` aesthetic. - `DeepDrftShared.Client/Components/TracksGallery.razor.css` — moved `.deepdrft-track-gallery-item-center` layout rule from global stylesheet into scoped CSS alongside the existing gallery container rules. **Scope:** - Affected components: `TrackCard.razor` (shared, consumed by public site and CMS) and `TracksGallery.razor` (shared). - CSS in `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` (global) and two scoped stylesheets. - Build clean: 0 errors, 0 new warnings. **Architecture notes:** - CSS isolation now protects track card rules from accidental mutation by unrelated global changes. - Light-mode visual is now consistent: single vocabulary eliminates the three-green collision and establishes a stable text hierarchy (off-white title → muted artist → fainter meta). - Scoped stylesheet pattern mirrors existing usage in other components (`AudioPlayerBar.razor.css`, `NowPlayingCard.razor.css`), establishing a consistent maintenance model. --- ### Track View CSS Consolidation **Landed 2026-06-05.** Implemented CSS consolidation and hierarchy fixes across three components: removed dead layout rules, unified horizontal inset ownership, and resolved the three-green collision in dark mode by demoting artist text and changing the genre chip variant. **Component changes:** - `DeepDrftPublic.Client/Pages/TracksView.razor` — removed dead `tracks-page-wrapper` class and associated inert flex/height/padding rules; `MudContainer` now owns horizontal inset via `MaxWidth.Large`. - `DeepDrftShared.Client/Components/TracksGallery.razor.css` — reduced to `box-sizing: border-box`; removed redundant padding and inert height constraint. - `DeepDrftShared.Client/Components/TrackCard.razor` — changed genre chip from `Variant.Filled` to `Variant.Outlined` to distinguish it from the play FAB. **CSS changes (`DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` §8):** - Text color rules restructured: base `color: inherit`, both dark and light treatments guarded under `.deepdrft-theme-dark` / `.deepdrft-theme-light` ancestors at `0,2,0` specificity. - Artist text demoted from `green-accent` to `rgba(250,250,248,0.65)` in dark mode (leaving green as a purely accent/interactive signal — FAB and chip border). - Meta text (album/year) at `rgba(250,250,248,0.45)` in dark mode. - Genre chip treatment now supports outlined styling (borders + text only, no filled ground). **Scope:** - CSS in `deepdrft-styles.css` and scoped stylesheets for `TracksView.razor` and `TracksGallery.razor`. - Both `DeepDrftPublic.Client` and `DeepDrftShared.Client` components affected. - Build clean: 0 errors, 0 new warnings. **Architecture notes:** - Resolved the three-green visual hierarchy collapse (artist + genre chip + play FAB all rendered the same saturated green). Now: title off-white, artist muted, genre = outlined green tag, FAB = solid green action — a clear three-tier hierarchy matching `NowPlayingCard` vocabulary. - Consolidated horizontal inset ownership to `MudContainer` (removes duplicate paddings that stacked across three layers). - Removed inert flex-grow and height rules that encoded a sticky-footer intent that was not actually achieved; page layout via normal block flow is cleaner. **Status:** ### Track Card Glass Theming **Landed 2026-06-05.** Aligned `TrackCard` component visual language with the `NowPlayingCard` aesthetic via glass background + text hierarchy. Two coordinated changes: **Razor changes (`DeepDrftShared.Client/Components/TrackCard.razor`):** - Removed `mud-theme-secondary` class and `Color="Color.Surface"` attributes from all four `MudText` elements, handing color control to CSS. - Added semantic class hooks: `deepdrft-track-title` (track name), `deepdrft-track-artist` (artist), `deepdrft-track-meta` (album and release year). - Changed MudCard `Elevation="4"` → `Elevation="0"` to align with glass-panel vocabulary (no drop shadow). **CSS changes (`DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` §8):** - Dark theme: navy-glass fallback panel (`color-mix(in srgb, var(--deepdrft-navy) 55%, transparent)` + `backdrop-filter: blur(8px)` + translucent border), matching `NowPlayingCard` glass vocabulary. - Text hierarchy (dark): title in off-white, artist in moss-green accent, meta in muted off-white — mirrors the `NowPlayingCard` hierarchy. - Content scrim behind text (dark): dark navy gradient to guarantee legibility over both glass fallback and album art. - Light theme: subtle navy-tint fallback on off-white, light text inherits body colour for legibility. - Glass border on card container (dark): `1px solid rgba(250, 250, 248, 0.12)` for aesthetic consistency. **Scope:** - `TrackCard` component in shared `DeepDrftShared.Client` consumed by both public site and CMS. - CSS in `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` (public site only, not loaded by CMS). - Build clean: 0 errors, 0 new warnings. **Notes for future work:** - Genre chip text still uses `Color.Primary` (moss-green); it now sits alongside moss-green artist text. Consider a distinct genre-chip treatment (3a) in future polish work. --- **Status:** AudioPlayerBar responsive unification and SpectrumVisualizer fix landed on 2026-06-05. ### AudioPlayerBar Responsive Unification **Landed 2026-06-05.** Collapsed the two divergent Razor trees in `AudioPlayerBar.razor` (`@if (_isDesktop)` / `@else`) into a single markup tree where CSS — not a runtime breakpoint flag — drives the responsive layout. Removed `IBrowserViewportService`, the `_isDesktop` field, `OnAfterRenderAsync`, and the viewport subscription/unsubscription from the code-behind. **Structural changes:** - Single `.player-layout` flex container (in `AudioPlayerBar.razor.css`) replaces the dual-branch conditional. Three children (`PlayerTransportZone`, `VolumeControls`, `PlayerSeekZone`) in source order; media query at 600px (`Sm` breakpoint) reorders via CSS `order` property and forces `SeekZone` to full-width below the transport/volume row on narrow viewports. - `PlayerTransportZone` flips its internal axis (vertical ↔ horizontal) via scoped CSS override of `MudStack` `flex-direction` at the 600px boundary — no parameter added to the component. - `::deep` prefix removed from `MudBlazor` component-class selectors in `PlayerTransportZone.razor.css` now that axis is purely CSS-driven and no runtime flag determines structure. - **SpectrumVisualizer bars now appear on first expand** — fixed by subscribing to the multicast `StateChanged` event (same pattern used by `AudioPlayerBar`), ensuring animation is initialized after mount. **Scope:** - Unified responsive layout (desktop/mobile branches merged into single tree). - Both `AudioPlayerBar` and `SpectrumVisualizer` components affected. - Build clean: 0 errors, 0 new warnings. **Notes for future work:** - First-render layout flash eliminated by construction (CSS media query evaluates at paint, not async subscription). ### Track Card Plain-Shell Refactor **Landed 2026-06-05.** Eliminated `!important` declarations from track card CSS by replacing MudBlazor surface components with plain HTML. Implemented per `product-notes/track-card-css-architecture.md` Option A. **Razor changes (`DeepDrftShared.Client/Components/TrackCard.razor`):** - `MudCard` → `
` - Fallback `MudPaper` → `
` - `MudCardContent` → `
` - `MudText`, `MudChip`, `MudFab` unchanged. **CSS changes (`DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` §8):** - Removed four `!important` declarations from `.deepdrft-track-card-container`, `.deepdrft-track-card-fallback` base, and the dark/light theme-scoped variants. - Plain single-class selectors now win by cascade without `!important`; theme-scoped rules use normal specificity hierarchy. **Scope:** - `TrackCard` component in shared `DeepDrftShared.Client` consumed by both public site and CMS. - CSS in `DeepDrftPublic/wwwroot/styles/deepdrft-styles.css` (public site only). - Build clean: 0 errors, 0 new warnings. **Notes for future work:** - Plain-div shell re-enables CSS isolation as an option (a `TrackCard.razor.css` would now work against the shell divs). Section 8's public-only scoping remains convenient; isolation is optional for future polish. - Removes the structural mismatch of using a Material surface component (`MudCard`/`MudPaper`) solely as a layout shell. TrackCard now mirrors the construction of `NowPlayingCard` (plain divs + themed CSS). **Status:** Desktop AudioPlayerBar redesign landed on 2026-06-04. ### Desktop AudioPlayerBar — migrate to MudBlazor theme system **Landed 2026-06-04.** Desktop branch of `AudioPlayerBar.razor` migrated off dead CSS palette tokens (`--charleston-*`, `--lowcountry-*`, `--deepdrft-theme-*` — none of which are defined in the live stylesheet) onto the active MudBlazor theme system. This was simultaneously a bug fix (player styling broken against the current palette) and a structural redesign. **Structural changes:** - `.player-backdrop` div replaced with `MudPaper Elevation="8"` — surface colour now derives from `--mud-palette-surface` via the live theme, and flips automatically with dark mode (off-white in light, navy in dark). - Three new zone sub-components extracted: `PlayerTransportZone` (left transport cluster), `PlayerSeekZone` (centre seek+spectrum, owns the seek pointer-handler logic), `PlayerWindowControls` (minimize/close buttons). These remove duplication (seek handlers no longer inline-copied) and name the layout zones explicitly. - `MudStack` replaces all raw `
` throughout the desktop branch and sub-components (`PlayerControls`, `VolumeControls`, `TimestampLabel`). - `SpectrumVisualizer` bar colour fixed: `var(--mud-palette-primary)` replaces the undefined `--deepdrft-theme-secondary` token. - Minimized dock replaced with `MudFab Color="Color.Primary"` — rounded button picking up themed primary colour with no hand-rolled gradient. - `AudioPlayerBar.razor.css` shrunk from ~176 lines (mostly dead-token theming) to ~74 lines (geometry and positioning only). **Scope:** - Desktop branch only (`@if (_isDesktop)`). Mobile branch unchanged by design. - Build clean: 0 errors, 0 new warnings. **Notes for future work:** - Mobile branch is also currently broken against the live palette for the same reason (spectrum bars + shared dead-token rules have no colour). A companion migration for mobile is implied but out of scope for this task — marked for future Phase 2 work. --- ## Deployment Infrastructure **Status:** CD pipeline infrastructure landed on 2026-06-04. ### CD pipeline infrastructure (Gitea workflows + remote host installer) **Landed 2026-06-04.** Continuous deployment infrastructure for DeepDrftHome dual-app deployment. Consists of four Gitea workflows (`.gitea/workflows/`) — `deploy-public.yml`, `deploy-manager.yml`, `deploy-api.yml`, `package-install.yml` — all triggered by `dev` branch (beta) and `master` branch (prod) pushes, path-filtered to deploy only on changes to the affected service and its dependencies. Five installer scripts (`deploy/`) — `install.sh` (one-shot host provisioner), `bootstrap.sh` (curl-and-run entry point), `ssh-wrapper.sh` (forced-command dispatcher), three `deploy-*.sh` per-service deployment scripts — plus systemd service templates (`deploy/systemd/`) and nginx vhost templates (`deploy/nginx/`), and credential template files (`deploy/credentials/`). One auxiliary setup script `setup-step10-creds.sh` for interactive credential entry on the host. The installer creates users, directories, systemd services, PostgreSQL databases, nginx vhosts, and loads credential files via systemd `LoadCredential=` into the credential sandbox. The deploy scripts swap binaries in-place, run the EF migrations bundle for the API metadata database, and restart services without touching persistent vault data. Enables hands-off pushes to beta and prod with full CI/CD orchestration. --- ## Two-app split Wave 2 — Phase 4 **Status:** Phase 4 (project rename) landed on 2026-05-19. ### Phase 4 — Two-app split: rename `DeepDrftWeb` → `DeepDrftPublic` **Landed 2026-05-19.** Renamed `DeepDrftWeb` to `DeepDrftPublic` and `DeepDrftWeb.Client` to `DeepDrftPublic.Client` across all project files, `.csproj` files, namespace declarations, using directives, solution file, and deploy scripts. Updated all references in `CLAUDE.md` agent guidance to reflect the new names. Also updated prior references to `DeepDrftWeb.Services` to `DeepDrftData` to align with the Phase 2 library rename. The solution builds cleanly with all endpoints functional. --- ## CMS Wave 1 — Auth + scaffolding + parity **Status:** All sub-items landed on 2026-05-18. ### W1.0 `DeepDrftContext` Postgres migration **Landed 2026-05-18.** Rewrite all existing EF Core migrations from SQLite to PostgreSQL. Update the `DeepDrftWeb` and `DeepDrftCli` connection strings in config. Migrate any existing data from `../Database/deepdrft.db` to Postgres. Verify the existing `api/track/page` and `api/track/{id}` endpoints function against the new backend. This is a prerequisite for W1.2 (which also runs migrations for AuthDbContext against the same Postgres instance). ### W1.1 `DeepDrftCms` RCL skeleton **Landed 2026-05-18.** Project created, added to solution, referenced from `DeepDrftWeb`. Empty `Pages/Cms/Index.razor` mounted at `/cms` returning a "CMS — under construction" placeholder, proving the mount works. ### CMS RCL inlined into `DeepDrftManager` **Landed 2026-05-21.** The `DeepDrftCms` Razor Class Library has been inlined into `DeepDrftManager` and the standalone project deleted from the solution. All Razor pages, components, and layouts (CmsLayout, DeleteTrackDialog, TrackList, TrackNew, TrackEdit, and the CMS index page) now live directly in `DeepDrftManager/Components/Pages/Cms/`, `DeepDrftManager/Components/Pages/Tracks/`, `DeepDrftManager/Components/Layout/`, and `DeepDrftManager/Components/Shared/`. The `DeepDrftManager.csproj` no longer references the now-deleted `DeepDrftCms` project. `DeepDrftManager/Program.cs` no longer calls `AddCmsServices()` or references the CMS assembly. Solution builds cleanly with all CMS endpoints and pages functional. ### W1.2 AuthBlocks integration + login **Landed 2026-05-18.** Reference `Cerebellum.AuthBlocks`, `Cerebellum.AuthBlocks.Web`, `Cerebellum.AuthBlocks.Models` from `DeepDrftWeb`; reference `Cerebellum.AuthBlocks.Web` from `DeepDrftWeb.Client`. Call `AddAuthBlocks(...)` in `Program.cs` with JWT secret/issuer/audience, Mailtrap email connection, Postgres connection string, and `AdminUserSettings` from `environment/authblocks.json`. Call `await app.Services.UseAuthBlocksStartupAsync()` post-build. Call `app.MapAuthBlocks()` to mount `/api/auth/*` routes. Add the `AuthBlocksWeb` assembly to `AddAdditionalAssemblies` so the bundled `/account/login` and `/account/logout` pages resolve. In `DeepDrftWeb.Client.Startup`, call `AuthBlocksWeb.Client.Startup.ConfigureServices(builder.Services)` for the prerender→WASM auth-state bridge. Add `CreatedByUserId : long?` column to `TrackEntity` via a nullable migration. Provision local Postgres (docker-compose) and document the dev setup. Includes `CmsStealthRoutingHandler` — a custom `IAuthorizationMiddlewareResultHandler` that returns 404 for any `/cms/*` hit that fails authorization, honouring the stealth-routing constraint: unauthorized access to admin routes returns 404, not 401 or redirect. --- ## CMS Wave 1 (legacy section header for reference) **Status:** All sub-items landed on 2026-05-18. Goal was: A logged-in collective member can do everything the CLI does today, from a browser. ### W1.3 CMS track list **Landed in CMS Wave 3.** `/cms/tracks` consuming the same `GET api/track/page` endpoint as the public gallery. Different rendering (table with admin affordances), same VM. No new SQL endpoint. ### W1.4 CMS upload endpoint + add page **Landed in CMS Wave 3.** New `POST api/cms/track` on `DeepDrftWeb` (auth-gated, see §5 for the transport decision). `/cms/tracks/new` page wires `InputFile` to the endpoint. Note: Option B is confirmed — this requires a new `POST api/track/upload` endpoint on `DeepDrftContent` (raw WAV in, unpersisted `TrackEntity` out) in addition to the CMS page and controller. ### W1.5 CMS delete endpoint + delete UI **Landed in CMS Wave 3.** New `DELETE api/cms/track/{id}` on `DeepDrftWeb`. Removes the SQL row and the vault entry; logs orphans if vault delete fails after SQL delete succeeds. Delete button + confirmation in the list and detail pages. ### W1.6 CMS edit endpoint + edit page **Landed in CMS Wave 3.** New `PUT api/cms/track/{id}` (metadata only — no binary replacement in Wave 1). `/cms/tracks/{id}` page. --- ## Phase 2 — Product surface: gallery, browsing, ingestion ### 2.4 Web-side track upload **Landed in CMS Wave 1 (subsumed by `CMS-PLAN.md`).** The CLI is the only producer of tracks today. A web upload UI would pair with `TrackService.AddTrackFromWavAsync` and the existing `PUT api/track/{id}` (already `[ApiKeyAuthorize]`-protected). - **Why it matters:** Lowers the barrier to adding content. The collective can publish without shell access to the host. - **Shape:** - New page or modal on the web client, drag-and-drop file input. - Upload streams to a `POST` endpoint on `DeepDrftWeb` (not `DeepDrftContent` — the web host orchestrates the dual-write, then forwards bytes to content with the API key it already holds). - Authentication: this is the first user-facing action that needs to be gated. A new question — see open question below. - **Prerequisite:** **Authentication model for the web side**. Currently the site has no user concept. Cookie-with-shared-password? OAuth? Per-collective-member account? Decide before building the UI. - **Open question:** Same as above. This may also bring forward a wider session/identity decision that other features (favourites, listening history) will need eventually. - **Constraint:** Today's dual-write has no compensating rollback — if content-side succeeds and SQL-side fails, the audio is orphaned in the vault. The CLI inherits this; pushing this onto a web upload increases the rate at which orphans can occur. A simple `DeadLetterLog` of orphaned `entryKey`s (suggested in the audit) becomes more pressing once the web upload exists. --- ## Phase 0 — Wireframe-driven home page redesign **Status:** All sub-items landed on 2026-05-17. A design wireframe (`deepdrft-wireframe.html` at the project root) is the source of truth for a full visual reskin of the public site. The current `Home.razor` is a MudPaper/MudGrid composition with a generic "purple-tint" feature card aesthetic that doesn't match the collective's intended voice. The wireframe replaces it with a layout-first, editorial design: 50/50 hero, frosted-glass nav, dark feature band, green origin/connect split, navy CTA banner with ghost-watermark, and an italic-serif accent treatment throughout. Scope here is **the home page and the chrome that wraps it** (nav, layout container, theme palette, font loading). The track gallery (`TracksView.razor`), the audio player dock (`AudioPlayerBar.razor`), and the FileDatabase/streaming substrate are **out of scope** for Phase 0 — they keep working through the existing MudBlazor theme, which is being recoloured under them. The "Now Playing" card in the hero is a *new* surface that reads from the existing `IPlayerService` cascade; it is a view onto the player, not a replacement for the dock. Phase 0 sub-items decompose into worktree-sized tracks. 0.1 is the foundation everything else inherits — land it first. 0.2–0.4 can proceed in parallel against that foundation. 0.5 is a follow-on tuning pass once the light theme is in. ### 0.1 Light palette + font system - **What:** Replace the "Charleston in the Day" `PaletteLight` in `DeepDrftWeb.Client/Layout/MainLayout.razor` with the wireframe palette (`--white #FAFAF8`, `--navy #0D1B2A`, `--green #1A3C34`, `--green-accent #3D7A68`, `--muted #8A9BB0`), expressed as MudBlazor `PaletteLight` properties. Update the corresponding CSS custom properties in `DeepDrftWeb/wwwroot/styles/deepdrft-styles.css` so the `deepdrft-*` utility classes still resolve. Add `Geist Mono` to the Google Fonts `` in `DeepDrftWeb/Components/App.razor`. Upgrade the existing `Cormorant` link to `Cormorant Garamond` with the italic + 300/400/600 weight set used by the wireframe. Remove the `Bodoni Moda` link (and its `--font-hero` reference) if no remaining surface uses it. - **Why it matters:** Every other Phase 0 sub-item consumes these tokens. Fonts and palette landing first means 0.2/0.3/0.4 can render at intended fidelity from the moment they're built, not approximate-then-correct. The font swap is also the only Phase 0 change that affects HTML served by the host project (`App.razor`), so isolating it cleanly keeps the render-mode seam clear. - **Shape:** - MudBlazor palette mapping (light): `Primary = navy`, `Secondary = green`, `Tertiary = green-accent`, `Background = white`, `Surface = white`, `AppbarBackground = "rgba(250,250,248,0.88)"`, `AppbarText = navy`, `TextPrimary = navy`, `TextSecondary = muted`, `Divider = "rgba(13,27,42,0.10)"`, `LinesDefault / TableLines` to match. Semantic colours (`Info/Success/Warning/Error`) stay at MudBlazor defaults. - Typography block (light): `H1`–`H6` and a new wireframe-specific display class use `Cormorant Garamond`; `Button` / `Default` keep `DM Sans`; introduce a `Subtitle1` / `Caption` family pointing at `Geist Mono` for label/eyebrow text. - CSS variables: rename or alias the existing `--deepdrft-primary/--deepdrft-secondary/etc.` to the wireframe palette in `:root`. Add `--font-mono: "Geist Mono", monospace;` and update `--font-hero` / `--font-headers` to `"Cormorant Garamond", serif`. Where the legacy palette has no wireframe equivalent (e.g. `--deepdrft-quaternary` warm gold), prefer mapping it to the closest wireframe colour rather than inventing a new one — the goal is convergence on the new vocabulary, not coexistence. - Font loading: a single Google Fonts link, ideally one combined request with `family=Cormorant+Garamond:ital,wght@…&family=Geist+Mono:wght@…&family=DM+Sans:…`. One round-trip, three families. - **Prerequisite:** None — this is the foundation. - **Constraint:** The dark palette ("Lowcountry Summer Nights") must stay functional after this change even if visually mismatched — 0.5 is the dedicated pass for re-harmonising it. Do not edit the dark palette in 0.1. The dark-mode cookie + `PersistentComponentState` round-trip described in `CLAUDE.md` must be preserved unchanged. ### 0.2 Frosted-glass top nav - **What:** Replace the current MudBlazor `MudAppBar`-based `DeepDrftMenu.razor` chrome (logo + nav stack + dark-mode toggle, default Material elevation) with the wireframe's fixed frosted-glass nav: 88% opacity off-white background, `backdrop-filter: blur(18px)`, 1px navy-alpha bottom border, no elevation shadow, navy-on-white "Stream Now" CTA pinned right, nav links in Geist Mono uppercase with the muted-to-navy hover transition. - **Why it matters:** The nav sits across every page, so its visual language sets expectations for the rest of the site. The Material elevation + dropdown menu pattern is the strongest "this is a stock MudBlazor app" tell currently; replacing it is the single largest perceived-quality move of Phase 0. - **Shape:** - Keep `DeepDrftMenu.razor` as the file (the existing render-mode wiring and viewport-subscription mobile branch are reused) — rewrite the markup inside it. - Wrap a styled `