# 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) landed on 2026-06-05 (branch `waveform-w3-cms`, pending merge to dev). ### W3 — CMS PreProcessing panel **Landed 2026-06-05.** Implemented the CMS surface for on-demand waveform profile generation: a new `/tracks/preprocessing` page (table view of all tracks with profile status) and two API endpoints for querying and triggering profile generation. **API endpoints (`DeepDrftAPI`):** - New `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). - New `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/TrackPreProcessing.razor`):** - New page mounted at `/tracks/preprocessing` (guarded by CMS auth). - Table layout: 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. - Nav link added to `TrackList.razor` and `CmsLayout.razor` / `Index.razor` so the page is discoverable from the track management surface. **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. --- ## 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 `