227 lines
21 KiB
Markdown
227 lines
21 KiB
Markdown
# DeepDrftHome — Project Context
|
||
|
||
Living orientation doc for what this repo is, how it is currently shaped, and where it appears headed. Sits alongside the root `CLAUDE.md` (operational guidance) — this file is the product/architecture view.
|
||
|
||
> **Status.** The root `CLAUDE.md` is current — it reflects the post-split ten-project solution, `net10.0`, and the dual-app topology. This file (`CONTEXT.md`) was the lagging document and §2 / §4 / §7 below have been brought back into line with the root `CLAUDE.md` as of 2026-06-06. Folder-level `CLAUDE.md` files are still being swept (`DOC_PLAN.md`); treat framework-version and structural claims in any *folder* `CLAUDE.md` not yet rewritten as potentially stale until that sweep lands.
|
||
|
||
---
|
||
|
||
## 1. What this project is
|
||
|
||
DeepDrftHome is the home + listening surface for **DeepDrft**, a two-person electronic music collective based in Charleston, SC (per `DeepDrftPublic.Client/Pages/Home.razor`). The product is, at minimum:
|
||
|
||
- A public-facing site (hero, about, "experience" features) at `DeepDrftPublic`.
|
||
- A **track gallery** that browses a library of WAV recordings, plays them in-browser with a persistent dock-style player, and supports seek (including seek beyond what's been streamed so far).
|
||
- A browser-based **CMS** (`DeepDrftManager`) for adding, editing, and deleting tracks — gated behind AuthBlocks login and the `Admin` role. This replaced the former `DeepDrftCli` Terminal.Gui admin tool, which has been retired.
|
||
|
||
The interesting engineering bet is the **dual-database split**: structured track metadata in PostgreSQL via EF Core, and binary media + per-vault indexes in a hand-rolled `FileDatabase` that lives on disk. The split is enforced through a dedicated authority host (`DeepDrftAPI`) so that the browser never reaches the database directly.
|
||
|
||
---
|
||
|
||
## 2. Solution shape (current)
|
||
|
||
Ten projects in `DeepDrftHome.sln`, plus an external `NetBlocks` referenced from `C:\lib\NetBlocks\`. The solution is split into **two independent Blazor applications** — the public site (`DeepDrftPublic`) and the CMS (`DeepDrftManager`) — both fronting a single dual-database authority host (`DeepDrftAPI`).
|
||
|
||
```
|
||
── Public application ──────────────────────────────────────────────────────
|
||
DeepDrftPublic ASP.NET Core host. Blazor Web App (Server + WASM render
|
||
modes). Owns the browser-facing proxy controller for
|
||
api/track/* (metadata listing + audio streaming),
|
||
MudBlazor theme prerender, and TypeScript→JS audio interop
|
||
sources under Interop/. The public listening surface.
|
||
DeepDrftPublic.Client Blazor WebAssembly assembly. All interactive public UI —
|
||
pages, the player stack, dark-mode plumbing, HTTP clients
|
||
for the backend. Consumed by DeepDrftPublic.
|
||
|
||
── CMS application ─────────────────────────────────────────────────────────
|
||
DeepDrftManager ASP.NET Core host. Blazor Web App (InteractiveServer).
|
||
Hosts all CMS Razor components/pages (Components/Pages/Cms/,
|
||
Components/Pages/Tracks/, Components/Layout/CmsLayout.razor,
|
||
Components/Shared/ — inlined from the former DeepDrftCms RCL).
|
||
Gated by AuthBlocks login + hierarchical Admin role. All track
|
||
operations proxy via ICmsTrackService / CmsTrackService.
|
||
|
||
── Dual-database authority ─────────────────────────────────────────────────
|
||
DeepDrftAPI ASP.NET Core host. The single authority over both databases
|
||
(SQL metadata + FileDatabase binary). AuthBlocks API host
|
||
(registration, migration/seed, JWT endpoints). Seven track
|
||
endpoints (stream, vault write, upload, delete, paged list,
|
||
single metadata read, metadata update).
|
||
DeepDrftData Class library. EF Core domain logic: DeepDrftContext,
|
||
TrackConfiguration, Migrations, TrackRepository, TrackService,
|
||
TrackManager. Consumed by DeepDrftAPI and tests.
|
||
DeepDrftContent Class library. The FileDatabase implementation in full
|
||
(Models, Services, Utils, Abstractions, Constants),
|
||
WavOffsetService, AudioProcessor, content-side TrackService.
|
||
Consumed by hosts and tests.
|
||
|
||
── Shared ──────────────────────────────────────────────────────────────────
|
||
DeepDrftShared.Client Razor Class Library. Shared Blazor components consumed by
|
||
BOTH DeepDrftPublic and DeepDrftManager (e.g. TrackCard,
|
||
TracksGallery) for consistency across public and admin surfaces.
|
||
DeepDrftModels Shared contracts: TrackEntity, TrackDto, PagingParameters<T>,
|
||
PagedResult<T>, plus waveform DTOs. Every project references this.
|
||
DeepDrftTests NUnit. Covers the FileDatabase, MediaVault, IndexSystem,
|
||
MediaVaultFactory, SimpleMediaTypeRegistry, utility code, model
|
||
behaviour, and the waveform loudness algorithm. References
|
||
DeepDrftContent.
|
||
|
||
NetBlocks (external) Result patterns: Result, ResultContainer<T>, ApiResult<T>,
|
||
ApiResultDto<T>. Referenced via absolute path.
|
||
```
|
||
|
||
**Naming history (for readers of older docs/commits):** `DeepDrftWeb` → `DeepDrftPublic`, `DeepDrftWeb.Client` → `DeepDrftPublic.Client`, `DeepDrftWeb.Services` → `DeepDrftData`, `DeepDrftContent.Services` → `DeepDrftContent` (the host that previously owned the binary API is gone; its proxy duties moved into `DeepDrftPublic`, its authority duties into `DeepDrftAPI`). `DeepDrftCli` and the `DeepDrftCms` RCL have both been removed — the CLI retired in favour of the CMS, and the CMS RCL was inlined into `DeepDrftManager`.
|
||
|
||
**Subdomain topology (deployment):** `deepdrft.com` (public) and `manage.deepdrft.com` (CMS), behind nginx. CD infrastructure (Gitea workflows + installer scripts + systemd/nginx templates) has landed — see `COMPLETED.md` "Deployment Infrastructure."
|
||
|
||
---
|
||
|
||
## 3. Architectural decisions worth remembering
|
||
|
||
### 3.1 Dual-database, two hosts, one client
|
||
|
||
The system is partitioned so the browser never touches storage directly:
|
||
|
||
```
|
||
DeepDrftWeb.Client (WASM)
|
||
│
|
||
├── HttpClient "DeepDrft.API" ──► DeepDrftWeb host ──► EF Core / SQLite
|
||
│ (paged track metadata)
|
||
│
|
||
└── HttpClient "DeepDrft.Content" ──► DeepDrftContent ──► FileDatabase / disk
|
||
(audio bytes + offset stream) (ApiKey required for PUT)
|
||
```
|
||
|
||
The two HttpClients are named, configured in `DeepDrftWeb.Client.Startup`, and base addresses are passed in from server-host configuration (`ApiUrls:ContentApi`). Read endpoints in DeepDrftContent (`GET api/track/{id}`) are **unauthenticated** — only mutating endpoints carry `[ApiKeyAuthorize]`. CORS is enforced per `CorsSettings.AllowedOrigins`.
|
||
|
||
### 3.2 Service projects vs. host projects
|
||
|
||
The split between `DeepDrftWeb` / `DeepDrftWeb.Services` (and the same for Content) is deliberate: the host projects own the HTTP surface, configuration, and DI wiring, while the *Services* projects are plain class libraries holding the domain logic. This is what lets `DeepDrftCli` reuse both `DeepDrftWeb.Services.TrackService` and `DeepDrftContent.Services.TrackService` without taking a dependency on either ASP.NET host. New domain logic should land in the *.Services projects unless it is genuinely host-specific (controllers, middleware, render-mode configuration, theme prerender).
|
||
|
||
### 3.3 FileDatabase as a first-class subsystem
|
||
|
||
The `FileDatabase` (in `DeepDrftContent.Services/FileDatabase/`) is the unusual piece. It is a port of a TypeScript system (see in-tree `FileDatabase/README.md`) and is the only place binary content lives.
|
||
|
||
- A **FileDatabase** is a directory containing typed **MediaVaults**.
|
||
- Each MediaVault has its own JSON `index` file that lists entries and their per-entry metadata.
|
||
- Vaults are typed via `MediaVaultType` (`Media | Image | Audio`). The vault type is recorded in `VaultIndex` and rehydrated on startup.
|
||
- Entry keys are user-supplied strings, sanitised to `[a-zA-Z0-9]` plus `-` and concatenated with the file extension to produce on-disk filenames.
|
||
- Resources implement a binary hierarchy: `FileBinary → MediaBinary (+ Extension/MIME) → AudioBinary (+ Duration, Bitrate) | ImageBinary (+ AspectRatio)`. DTO variants ship as base64-over-JSON.
|
||
- An `IndexWatcher` (FileSystemWatcher) reloads a vault's index when an external process (e.g. the CLI) writes to it, so the web host stays consistent without restart.
|
||
- Error-handling philosophy: load/register operations **swallow exceptions and return `null`/`false`** to match the TypeScript original. Callers must check return values, not catch.
|
||
|
||
The only authorised callers of FileDatabase APIs are the DeepDrftContent host and the CLI — anything that runs in the browser must go through the HTTP endpoints.
|
||
|
||
### 3.4 TrackEntity is a join, not a content blob
|
||
|
||
`TrackEntity` (in `DeepDrftModels`) holds *only* metadata. The link to binary content is the `EntryKey` string, which is the entry id inside the `tracks` audio vault in the FileDatabase. The current schema:
|
||
|
||
```csharp
|
||
public class TrackEntity
|
||
{
|
||
public long Id { get; set; }
|
||
public required string EntryKey { get; set; } // FileDatabase entry id, vault = "tracks"
|
||
public required string TrackName { get; set; }
|
||
public required string Artist { get; set; }
|
||
public string? Album { get; set; }
|
||
public string? Genre { get; set; }
|
||
public DateOnly? ReleaseDate { get; set; }
|
||
public string? ImagePath { get; set; }
|
||
}
|
||
```
|
||
|
||
Note: many existing docs (every folder `CLAUDE.md`, plus the FileDatabase README) still refer to the older `MediaPath` name. The migration `20250904233927_Initial.cs` already ships the `entry_key` column, so `MediaPath` is purely a docs-staleness artefact.
|
||
|
||
### 3.5 Streaming-first audio playback
|
||
|
||
The player is not a fetch-then-play model. The flow is:
|
||
|
||
1. Client calls `GET api/track/{id}` on DeepDrftContent and receives the WAV bytes as a stream (`HttpCompletionOption.ResponseHeadersRead`).
|
||
2. `StreamingAudioPlayerService` reads the stream in adaptive 16–64 KB chunks and pushes each chunk into JS via `AudioInteropService.processStreamingChunk`.
|
||
3. The TypeScript `StreamDecoder` parses the WAV header, decodes chunks to `AudioBuffer`s, and the `PlaybackScheduler` schedules them on a single Web Audio graph.
|
||
4. Playback starts as soon as a configurable minimum number of buffers is queued; UI duration is set from the parsed WAV header (not waiting for the full file).
|
||
5. **Seek beyond buffer**: if a seek target is past what's been decoded, the client issues a new `GET api/track/{id}?offset={byteOffset}` request. The server's `WavOffsetService` block-aligns the offset, synthesises a fresh 44-byte WAV header sized for the remaining data, and streams `[new header][data from offset]`. The player tears down and re-initialises its decoder for the new stream.
|
||
|
||
This is the seam where the FileDatabase's "everything is a complete blob in memory" model meets the browser's "I need to stream and seek" model. Keep this seam clean — it is the most architecturally load-bearing part of the playback path.
|
||
|
||
### 3.6 Theming, dark mode, and prerender
|
||
|
||
- MudBlazor is the UI framework. Light and dark palettes are bespoke ("Charleston in the Day" / "Lowcountry Summer Nights") and defined inline in `MainLayout.razor`.
|
||
- Dark mode toggles via a long-lived cookie (`darkMode`, 365 days), set client-side via JS interop.
|
||
- During server prerender, `DarkModeService` (in `DeepDrftWeb`) reads the cookie and seeds `DarkModeSettings.IsDarkMode`, which carries the value into the WASM render via `PersistentComponentState`. This is what avoids a "wrong theme flash" on initial paint. The `DarkModeSettings` lives in `DeepDrftWeb.Client.Common` because it is consumed by both the server `DarkModeService` and client components.
|
||
- Typography uses three Google Fonts (Bodoni Moda, Cormorant, DM Sans). The hand-rolled gas-lamp icon (lit/unlit) lives in `DDIcons.cs` and is what swaps in the dark-mode toggle.
|
||
|
||
### 3.7 TypeScript interop, not raw JS
|
||
|
||
The audio interop is authored in TypeScript under `DeepDrftWeb/Interop/audio/` and compiled into `wwwroot/js/audio/` via `Microsoft.TypeScript.MSBuild`. The split is intentional: AudioContextManager / StreamDecoder / PlaybackScheduler / SpectrumAnalyzer / AudioPlayer are each their own module, and `index.ts` glues them onto `window.DeepDrftAudio` for Blazor to invoke. The `tsconfig.json` is configured for ES module interop and is **not** copied to output.
|
||
|
||
In dev, the host serves the original `.ts` sources at `/Interop/...` for source-map debugging.
|
||
|
||
---
|
||
|
||
## 4. Where the system is today
|
||
|
||
Recent commits (newest first):
|
||
|
||
- `docs: archive play-state icon normalization; update DeepDrftPublic.Client CLAUDE.md`
|
||
- `Consolidate play/pause icon logic into PlaybackIcons mapper and PlayStateIcon component`
|
||
- `Reflect real playback state on gallery cards and toggle pause/resume`
|
||
- `WASM State Fixes`
|
||
- `CMS Home autoredirect to /tracks`
|
||
- `WaveformSeeker Improvements` / WaveformSeeker waves 1–3
|
||
- (earlier: AudioPlayerBar responsive unification, CMS build-out, the two-app split, deployment infrastructure)
|
||
|
||
Observations:
|
||
|
||
1. **The big structural moves have landed.** Since the last revision of this doc, three large initiatives shipped: the **two-app split** (public/CMS separation with `DeepDrftAPI` as the dual-database authority), the **browser CMS** replacing the CLI (auth via AuthBlocks, stealth-routed `/cms/*`, full add/list/edit/delete parity), and **CD infrastructure** (Gitea workflows + host installer + systemd/nginx templates). The substrate is no longer the frontier — the product and presentation layers are.
|
||
2. **The recent arc is player UX polish.** The latest wave of work is the WaveformSeeker (loudness-profile seekbar), AudioPlayerBar responsive unification, and play-state icon normalization (a single `PlaybackIcons` resolver + `PlayStateIcon` component, gallery cards reflecting real playback state with pause/resume). Presentation iteration on a stable streaming core.
|
||
3. **The "Track Gallery" is still the only real public content page.** `/tracks` is the working listening surface; `/` is the (reskinned) marketing home. Nav (in `Layout/Pages.cs`) is still essentially `Home` + `Track Gallery`. The CMS adds admin surfaces under `/cms` but those are not public.
|
||
4. **The metadata/streaming surface is consolidated on `DeepDrftAPI`.** It exposes seven track endpoints (stream, vault write, upload, delete, paged list, single-metadata read, metadata update) plus waveform endpoints. `DeepDrftPublic` is a thin browser-facing proxy in front of it; the browser never reaches `DeepDrftAPI` or the databases directly.
|
||
5. **In flight (working tree, not yet committed):** an **embeddable iframe player** (`EmbedLayout.razor`, `FramePlayer.razor`, a new `ITrackDataService` seam) — a chrome-free single-track play surface for embedding off-site. Partial and not yet compiling; see `PLAN.md` "In-flight — Embeddable iframe player" for the open questions.
|
||
|
||
---
|
||
|
||
## 5. Likely directions (inferred, not committed)
|
||
|
||
Captured here so the next round of planning has a starting point — none of this is decided.
|
||
|
||
- **More vault types in active use.** `MediaVaultType.Image` exists end-to-end (tests cover it) but the production surface only registers a `tracks` vault of type `Audio`. The path to releases/albums probably runs through images first (cover art via `ImagePath`, which is currently a free-form URL string).
|
||
- **More than one collection view.** The `TrackCard` already conditionally renders `ImagePath`, `Album`, `Genre`, `ReleaseDate` — the data shape supports album-grouped or genre-filtered views without schema work.
|
||
- **Web upload — landed.** *(Historical note: this was a "likely direction" when the CLI was the only producer. It has since shipped.)* The CMS (`DeepDrftManager`) now produces tracks via `POST api/track/upload` on `DeepDrftAPI`, proxied through the auth-gated CMS surface. The CLI has been retired. The dual-write rollback gap (`PLAN.md §4.3`) still stands.
|
||
- **Live/session content.** The home page advertises "Live Sessions" and "Video Content (coming soon)". No data model exists for these yet; they would likely need new vault types (`MediaVaultType.Media` is the obvious home for video) and new entity tables.
|
||
- **Non-WAV formats.** Today the producer side is WAV-only (`AudioProcessor.ProcessWavFileAsync` validates RIFF/WAVE/PCM). `MimeTypeExtensions` already knows mp3/flac/aac/ogg/m4a — the gap is a processor per format and a decoder strategy in the JS player (currently WAV-specific).
|
||
- **Search / filter on the gallery.** `TracksViewModel` exposes `SortBy` / `IsDescending` but no filter. `TrackService.GetPaged` accepts only sort, not filter. Adding filter would be a natural next step on the same pagination contract.
|
||
|
||
---
|
||
|
||
## 6. Conventions that should hold
|
||
|
||
- **Plan docs at the repo root.** `CONTEXT.md` (this file), and when work in flight warrants them, `PLAN.md` / `TODO.md` / `COMPLETED.md`. Completed `PLAN.md` items move to `COMPLETED.md` rather than being deleted.
|
||
- **`*.Services` libraries own domain logic.** Host projects only do HTTP/wiring/render concerns. New repositories, services, processors, and storage code go in `DeepDrftWeb.Services` or `DeepDrftContent.Services`.
|
||
- **Shared contracts in `DeepDrftModels` only.** If a type crosses a project boundary, it lives in `DeepDrftModels`. Project-specific DTOs (e.g. `AudioBinaryDto` for the content API) stay local to where they serialise.
|
||
- **No direct DB from network clients.** The browser must not import EF or speak to SQLite. All access goes through the two API hosts. (The CLI is allowed direct DB access because it is not a network client.)
|
||
- **One source of truth per concept, multiple views over it.** The dark-mode flow (cookie → `DarkModeSettings` → server `DarkModeService` for prerender + client `DarkModeCookieService` for runtime + persistent state across the boundary) is a small example of this: one truth, distinct rendering paths. New view modes for tracks should consume the same `TrackEntity` / `PagedResult<TrackEntity>` and differ only at the rendering layer.
|
||
- **Result types from NetBlocks.** Services return `Result`, `ResultContainer<T>`, or `ApiResult<T>` rather than throwing for expected failure modes. Catch at the service boundary, surface via the result.
|
||
|
||
---
|
||
|
||
## 7. Staleness in existing docs (for doc-keeper to address)
|
||
|
||
Two layers of drift remain. The root `CLAUDE.md` and this `CONTEXT.md` are current; the lag is now in **folder-level `CLAUDE.md` files** and the in-tree `FileDatabase` README. `DOC_PLAN.md` holds the per-folder rewrite briefs, but note that `DOC_PLAN.md` itself was authored against the *pre-split* project names (2026-05-16) and is partly superseded — see the warning at the end of this section.
|
||
|
||
**Project-rename drift (the big one).** The two-app split renamed or removed most projects. Any folder `CLAUDE.md` still using the old names is wrong at the structural level, not just the framework-version level:
|
||
- `DeepDrftWeb` → `DeepDrftPublic`; `DeepDrftWeb.Client` → `DeepDrftPublic.Client`; `DeepDrftWeb.Services` → `DeepDrftData`.
|
||
- `DeepDrftContent.Services` (class library) is now just `DeepDrftContent`; the old `DeepDrftContent` *host* is gone — binary-API duties split between the `DeepDrftPublic` proxy and the `DeepDrftAPI` authority.
|
||
- `DeepDrftCli` and the `DeepDrftCms` RCL are **deleted**. Any `CLAUDE.md` for them should be removed, not rewritten.
|
||
|
||
**Known content drift to correct in the sweep:**
|
||
- Framework version: any folder `CLAUDE.md` still saying ".NET 9" / "ASP.NET Core 9.0" — reality is `net10.0` across the board.
|
||
- `TrackEntity.MediaPath` references (notably the `FileDatabase/README.md`) — the field is `EntryKey`, column `entry_key`.
|
||
- The `FileDatabase/README.md` refers to `ImageDirectoryVault` (the type is `ImageVault`) and a "FileDatabase.csproj (.NET 9.0)" that no longer exists (FileDatabase is a subdirectory of `DeepDrftContent`).
|
||
- `DeepDrftData` and `DeepDrftContent` are where most domain logic lives and are the highest-value targets for accurate `CLAUDE.md` coverage.
|
||
|
||
**Already corrected (no longer stale):**
|
||
- `DeepDrftPublic.Client/CLAUDE.md` was rewritten in commit `9110b4b` and reflects the current player stack, `PlaybackIcons`/`PlayStateIcon`, and the post-split structure.
|
||
|
||
> **`DOC_PLAN.md` caveat.** `DOC_PLAN.md` predates the two-app split — its per-folder briefs reference `DeepDrftWeb*`, `DeepDrftCli`, and a SQLite backend (now PostgreSQL). Treat its *intent* (lead-with-truth, cross-reference root, no docs for build output) as still valid, but its *project list and per-folder details* need reconciling against the current ten-project solution before doc-keeper executes against it. Flag to Daniel whether to refresh `DOC_PLAN.md` first or let doc-keeper work from the root `CLAUDE.md` directly.
|