Add CONTEXT.md and DOC_PLAN.md

Cold-storage audit: eight projects (net10.0), dual-database, streaming playback.
DOC_PLAN.md briefs doc-keeper on eight folder-level CLAUDE.mds to write or
rewrite; Services libraries are the biggest gap.
This commit is contained in:
Daniel Harvey
2026-05-16 19:33:23 -04:00
parent 51cef436d7
commit de0909f38f
2 changed files with 454 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
# 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.
> **Drift notice.** The root `CLAUDE.md` and every folder-level `CLAUDE.md` currently in the tree describe the project as `.NET 9`. The most recent commit upgraded all projects to `.NET 10` (every `.csproj` now targets `net10.0`, packages pinned at `10.0.1`). Until those docs are refreshed, treat any framework-version claim in them as stale. The other staleness items are listed at the bottom of this file.
---
## 1. What this project is
DeepDrftHome is the home + listening surface for **DeepDrft**, a two-person electronic music collective based in Charleston, SC (per `DeepDrftWeb.Client/Pages/Home.razor`). The product is, at minimum:
- A public-facing site (hero, about, "experience" features).
- 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).
- An admin CLI for adding tracks (Terminal.Gui or scripted), running locally against the same dual-database substrate the site uses.
The interesting engineering bet is the **dual-database split**: structured track metadata in SQLite via EF Core, and binary media + per-vault indexes in a hand-rolled `FileDatabase` that lives on disk. The split is enforced across two ASP.NET Core hosts so that the browser never reaches the database directly.
---
## 2. Solution shape (current)
Eight projects in `DeepDrftHome.sln`, plus an external `NetBlocks` referenced from `C:\lib\NetBlocks\`.
```
DeepDrftWeb ASP.NET Core host. Blazor Web App (Server + WASM render modes).
Owns the SQL-backed API (api/track/page), MudBlazor theme/host,
TypeScript→JS audio interop sources under Interop/.
DeepDrftWeb.Client Blazor WebAssembly assembly. All interactive UI lives here —
pages, controls, player services, dark-mode/theme plumbing,
HTTP clients for both backends.
DeepDrftWeb.Services Class library. EF Core: DeepDrftContext, TrackConfiguration,
Migrations, TrackRepository, TrackService. Sharable between
the web host and the CLI (avoids duplicating data-access).
DeepDrftContent ASP.NET Core host. Binary content API (api/track/{id}).
ApiKey middleware, CORS, ForwardedHeaders. Returns audio bytes
(with optional byte offset) and accepts PUT of AudioBinaryDto.
DeepDrftContent.Services Class library. The FileDatabase implementation in full
(Models, Services, Utils, Abstractions, Constants),
WavOffsetService, AudioProcessor, TrackService (the content-side
orchestrator that processes WAVs and stores them in a vault).
DeepDrftModels Shared contracts: TrackEntity, TrackDto, PagingParameters<T>,
PagedResult<T>. The only project all three layers reference.
DeepDrftCli Console app. Two modes: classic `add` / `list` / `help` and
`gui` (Terminal.Gui). Consumes BOTH service libraries directly
(it's a local admin tool, not a network client).
DeepDrftTests NUnit. Covers the FileDatabase, MediaVault, IndexSystem,
MediaVaultFactory, SimpleMediaTypeRegistry, utility code, and
model behaviour. References DeepDrftContent.Services.
NetBlocks (external) Result patterns: Result, ResultContainer<T>, ApiResult<T>,
ApiResultDto<T>. Referenced via absolute path.
```
Two stray .sln files (`WebAPI.sln`, `WebUI.sln`, `CLI.sln`) exist at the root alongside `DeepDrftHome.sln`. `DeepDrftHome.sln` is the canonical solution; the others appear to be subsets.
---
## 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 1664 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):
- `style simplification and publish upgrades for dotnet 10`
- `Styles & Home Page Content Cleanup Mobile Menu System & Dark Mode Cookie Theme Draft`
- `Theming Draft 2`
- `2026 Deep DRFT Theme Draft 1 WIP`
- `Spectrum Visualizer for player & Layout`
Three observations:
1. **The current arc is presentation, not capability.** The last five commits are framework upgrade, theming, content/layout cleanup, mobile menu, dark-mode persistence, and the spectrum visualiser. The playback substrate, streaming, and seek-beyond-buffer machinery landed earlier and is stable enough to support cosmetic iteration on top.
2. **The "Track Gallery" is the only real page.** `/tracks` is the working surface; `/` is marketing copy. Nav (in `Pages.cs`) defines only `Home` + `Track Gallery`.
3. **Content surface is narrow on purpose.** The DeepDrftContent API exposes exactly two routes: `GET api/track/{id}` (with optional `offset`) and `PUT api/track/{id}` (ApiKey). There is no listing endpoint there; listing lives on DeepDrftWeb because listings are SQL queries.
---
## 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.
- **Upload from the web side, not just the CLI.** The CLI is currently the only producer of tracks. A web-side upload would re-use `DeepDrftContent.Services.TrackService.AddTrackFromWavAsync` and pair it with a `TrackService.Create` on the SQL side. The `[ApiKeyAuthorize]` middleware on `PUT api/track/{id}` is already in place.
- **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)
Captured so the next sweep of folder-level `CLAUDE.md` files can correct in one pass.
- Every folder `CLAUDE.md` says ".NET 9" / "ASP.NET Core 9.0"; reality is `net10.0` across the board.
- `DeepDrftModels/CLAUDE.md` and `DeepDrftContent.Services/FileDatabase/README.md` reference `TrackEntity.MediaPath`; the field is `EntryKey` and the column is `entry_key`.
- `DeepDrftContent/CLAUDE.md` describes a `FileDatabase/` tree inside `DeepDrftContent/`; that tree has moved entirely to `DeepDrftContent.Services/FileDatabase/`. The DeepDrftContent host now contains only `Controllers/`, `Middleware/`, `Models/` (settings POCOs), `environment/`, `Program.cs`, `Startup.cs`.
- `DeepDrftContent/CLAUDE.md` documents only the PUT endpoint; the production API now also has `GET api/track/{id}?offset=` (unauthenticated read, with `WavOffsetService` for offset streaming).
- `DeepDrftWeb/CLAUDE.md` describes EF Core, repositories, services, migrations as living inside `DeepDrftWeb/Data` and `DeepDrftWeb/Services`. They have all moved to `DeepDrftWeb.Services`. The only things still in `DeepDrftWeb` are `Controllers/TrackController.cs`, `Services/DarkModeService.cs`, `Startup.cs`, `Program.cs`, `Components/`, `Interop/`, `wwwroot/`.
- `DeepDrftWeb.Client/CLAUDE.md` lists the `Pages/` directory as containing `Counter.razor` / `Weather.razor` (demo); those are gone. The real client structure is `Pages/Home.razor` + `Pages/TracksView.razor`, plus the `Controls/AudioPlayerBar/` cluster, `Controls/AudioPlayerProvider.razor`, `Services/AudioInteropService.cs` + `AudioPlayerService.cs` + `StreamingAudioPlayerService.cs` + `IPlayerService.cs` + dark-mode services, `Common/DarkModeSettings.cs` + `Common/DDIcons.cs`, and `Layout/Pages.cs` + `Layout/DeepDrftMenu.razor`.
- The `DeepDrftWeb.Services` and `DeepDrftContent.Services` projects have **no** `CLAUDE.md` yet — they are where most of the domain logic actually lives, so this is the biggest gap.
- `DeepDrftCli/CLAUDE.md` references `appsettings.json`; the CLI actually loads `environment/connections.json` into `CliSettings` (with `ConnectionString` and `VaultPath`). The "Available Commands" section is otherwise current, including the `gui` Terminal.Gui mode and interactive `add`.
- `DeepDrftContent.Services/FileDatabase/README.md` (an in-tree dev README, not a CLAUDE.md) refers to `ImageDirectoryVault`; the type is `ImageVault`. It also describes `EntryKey` as removed in favour of strings, which is accurate, but its diagram still says "FileDatabase.csproj (.NET 9.0)" — the FileDatabase no longer has its own csproj at all (it's a subdirectory of `DeepDrftContent.Services`).
+254
View File
@@ -0,0 +1,254 @@
# DOC_PLAN.md — Folder-level CLAUDE.md plan
Brief for doc-keeper. Specifies, folder by folder, what each `CLAUDE.md` should cover and the conventions/patterns to surface. **Authored on a cold-storage read of the current code state on 2026-05-16.** See `CONTEXT.md` for the full architecture orientation.
---
## Ground rules for every folder-level CLAUDE.md
These apply across the whole sweep — do not restate them inside each file unless the local context genuinely differs.
1. **Target framework is `net10.0`** for every project. Do not write ".NET 9" anywhere. Package versions are `10.0.1` family across EF Core, ASP.NET, and Extensions; MudBlazor is `8.15.0`; NUnit is `4.4.0`.
2. **Lead with what's true now, not what's aspirational.** Folder CLAUDE.md files are operational guidance for an agent walking into that directory cold. If something is "potential future usage", omit it.
3. **Cross-reference the root.** Root `CLAUDE.md` already explains the solution shape, dual-database model, and high-level patterns. Folder CLAUDE.mds should pick up *from there* and document what makes that folder specific. No re-running the full architecture overview in every file.
4. **No CLAUDE.md in build output / migrations / leaf static-asset folders.** This plan is deliberately scoped to the subfolders below; do not add files for `obj/`, `bin/`, `Migrations/`, `wwwroot/`, `environment/`, `Properties/`. Migrations are EF-generated and not interesting for agent guidance.
5. **Reference paths, not contents, for code.** If the CLAUDE.md needs to show how something is used, prefer a one-line "see `path/file.cs`" over a copied 30-line snippet. The code is the source of truth — folder CLAUDE.mds drift the moment they replicate it. Small inline examples (a 35 line signature or call) are fine when the call shape itself is the convention being documented.
6. **The eight existing folder CLAUDE.mds need rewrites, not patches.** All eight contain framework-version drift and most contain structural drift (see CONTEXT.md §7). Treat the existing files as raw material, not as a baseline to incrementally edit.
---
## Files to write / rewrite
There are **eight** folder CLAUDE.mds in scope. Five already exist and need rewrites; three are new (the two `*.Services` libraries and a recommendation about the `FileDatabase` README being either retired or replaced with a CLAUDE.md).
| Folder | Status | Priority |
|---|---|---|
| `DeepDrftWeb/` | Rewrite — structural drift | high |
| `DeepDrftWeb.Client/` | Rewrite — major drift (page list, player stack) | high |
| `DeepDrftWeb.Services/` | **New** — no CLAUDE.md exists; domain logic lives here | high |
| `DeepDrftContent/` | Rewrite — FileDatabase tree has moved out | high |
| `DeepDrftContent.Services/` | **New** — no CLAUDE.md; FileDatabase + processors live here | high |
| `DeepDrftModels/` | Rewrite — `MediaPath``EntryKey` rename, framework version | medium |
| `DeepDrftCli/` | Rewrite — config file name, GUI/CLI dual mode is mostly accurate | medium |
| `DeepDrftTests/` | Rewrite — framework version, otherwise mostly accurate | low |
Plus one judgement call:
- `DeepDrftContent.Services/FileDatabase/README.md` is an in-tree developer README, not a CLAUDE.md. It is stale (`ImageDirectoryVault` is `ImageVault`; the `.csproj (.NET 9.0)` line is wrong since FileDatabase no longer has its own csproj). **Recommendation:** doc-keeper should ask Daniel whether to retire it in favour of a `DeepDrftContent.Services/FileDatabase/CLAUDE.md` or leave it as a long-form README and just refresh the inaccuracies. Default to leaving the README and not adding a separate CLAUDE.md inside the FileDatabase subtree — the parent `DeepDrftContent.Services/CLAUDE.md` should cover what an agent needs to know to operate inside `FileDatabase/`.
---
## Per-folder briefs
### `DeepDrftWeb/CLAUDE.md`
**One-line purpose:** The Blazor Web App host. Owns HTTP surface (one controller + render-mode wiring), MudBlazor theme prerender, TypeScript→JS audio interop, and the SQL-side `api/track/page` endpoint. **Domain logic lives elsewhere** (`DeepDrftWeb.Services`).
**Cover:**
- What lives here now (only): `Program.cs`, `Startup.cs`, `Controllers/TrackController.cs`, `Services/DarkModeService.cs`, `Components/App.razor` + `Components/Pages/Error.razor`, `Interop/` (TypeScript sources), `wwwroot/` (compiled JS, CSS, fonts, images).
- What does *not* live here anymore: `DeepDrftContext`, `TrackRepository`, `TrackService`, `Configurations/`, `Migrations/`. All moved to `DeepDrftWeb.Services`. Don't write new repositories or EF code in this project.
- The render-mode model: Blazor Web App with `AddInteractiveServerComponents()` + `AddInteractiveWebAssemblyComponents()`. The root component is `<Routes @rendermode="InteractiveAuto" />` from `Components/App.razor`, and the WASM render-mode loads `DeepDrftWeb.Client._Imports` as an additional assembly. New routable pages should land in `DeepDrftWeb.Client/Pages`, not here.
- The dark-mode prerender bridge: `DarkModeService` reads the `darkMode` cookie via `IHttpContextAccessor` in `App.razor`'s `OnInitialized` and seeds `DarkModeSettings.IsDarkMode` (registered in `DeepDrftWeb.Client.Startup.ConfigureDomainServices`). The setting carries over to WASM via `PersistentComponentState` (`MainLayout.razor`).
- The TypeScript interop pipeline: sources live in `Interop/audio/` (one module per responsibility — `AudioContextManager`, `StreamDecoder`, `PlaybackScheduler`, `SpectrumAnalyzer`, `AudioPlayer`, plus `index.ts` that exposes `window.DeepDrftAudio`). They compile to `wwwroot/js/` via `Microsoft.TypeScript.MSBuild`. `tsconfig.json` must not be copied to output. In development, raw `.ts` is served from `/Interop/` for source-map debugging.
- HTTP client wiring lives mostly in `DeepDrftWeb.Client.Startup`. Server-side `Program.cs` only adds MudBlazor, controllers, render-mode components, SignalR tuning, forwarded-headers config, and calls the client's `Startup.ConfigureApiHttpClient` / `ConfigureContentServices` / `ConfigureDomainServices`.
- Reverse-proxy support: `UseForwardedHeaders` runs first; HTTPS redirect is conditionally disabled via `ForwardedHeaders:DisableHttpsRedirection` so the app can sit behind nginx.
- The one controller (`TrackController`) is a thin wrapper: `GET api/track/page?pageNumber&pageSize&sortColumn&sortDescending``DeepDrftWeb.Services.TrackService.GetPaged``ApiResultDto<PagedResult<TrackEntity>>`. If you're adding new SQL endpoints, this is the file; if you're adding new logic, that goes in `DeepDrftWeb.Services`.
**Do NOT include:**
- Generic Blazor / EF Core tutorials.
- Anything implying repositories or DbContext belong in this project.
- Build/run commands for the whole solution — keep those at the root.
---
### `DeepDrftWeb.Client/CLAUDE.md`
**One-line purpose:** All interactive UI for the site. Blazor WebAssembly. Pages, controls, the streaming audio player stack, theme/dark-mode plumbing, HTTP clients for both backends.
**Cover:**
- Actual structure: `Pages/` (`Home.razor`, `TracksView.razor`), `Layout/` (`MainLayout.razor`, `DeepDrftMenu.razor`, `NavMenu.razor`, `Pages.cs`), `Controls/` (`TrackCard`, `TracksGallery`, `AppNavLink`, `AudioPlayerProvider`, `AudioPlayerBar/` cluster), `Services/` (audio player services + dark-mode services + streaming error handler), `Clients/` (`TrackClient` for SQL API, `TrackMediaClient` for content API), `ViewModels/` (`TracksViewModel`), `Common/` (`DarkModeSettings`, `DDIcons`).
- The "old demo pages" (`Counter.razor`, `Weather.razor`) do not exist. The site has exactly two routable pages today: `/` and `/tracks`. Nav is centralised in `Layout/Pages.cs` (`MenuPages` + `AllPages`).
- The two HTTP clients pattern: `TrackClient` uses named `IHttpClientFactory` client `"DeepDrft.API"`, `TrackMediaClient` uses `"DeepDrft.Content"`. The clients are configured in `Startup.ConfigureApiHttpClient` and `Startup.ConfigureContentServices` — both static methods called from BOTH the server `Program.cs` and the WASM `Program.cs` so prerender and runtime see the same DI.
- The audio player stack (this is the deepest part of the project and most likely to be edited):
- `IPlayerService` / `IStreamingPlayerService` (the contracts exposed to UI).
- `AudioPlayerService` (abstract base: lifecycle, initialise, select track, play/pause/stop/seek/volume).
- `StreamingAudioPlayerService` (the production implementation: chunked stream from `TrackMediaClient`, adaptive 1664 KB buffer, early-playback, **seek-beyond-buffer** via a new offset request).
- `AudioInteropService` (JS interop wrapper over `window.DeepDrftAudio`; manages `DotNetObjectReference` lifetimes for progress, end-of-playback, and spectrum callbacks).
- `AudioPlayerProvider.razor` is the cascading host; everything inside it gets the player via `[CascadingParameter]`. `AudioPlayerBar.razor` is the dock UI; `SpectrumVisualizer.razor` is the bar-graph driven by `getSpectrumData`. `MainLayout.razor` wraps the layout in `AudioPlayerProvider`, so the player survives navigation.
- Dark mode plumbing:
- `DarkModeSettings` (in `Common/`) is `[PersistentState]`-annotated and registered scoped — it is the single source of truth in the client.
- `DarkModeServiceBase` holds the cookie name constant.
- `DarkModeCookieService` writes the cookie via JS `document.cookie` and reads `DarkModeSettings`.
- The server-side `DarkModeService` (in `DeepDrftWeb`, NOT here) reads the cookie during prerender. The settings instance round-trips via `PersistentComponentState`.
- `TracksView.razor` flow: injects `TracksViewModel` + cascaded `IPlayerService`. `SetPage` calls `TrackClient.GetPage` and updates the VM. `PlayTrack` calls `PlayerService.SelectTrack(track)` — which under the hood resolves to `StreamingAudioPlayerService.SelectTrackStreaming(track)` and starts the chunked stream.
- MVVM convention: page state lives in a `ViewModel` (scoped DI), components only render and dispatch. `TracksViewModel` holds the current page, page size, sort. Add new VMs in `ViewModels/` and register in `Startup.ConfigureDomainServices`.
- Theming convention: bespoke `PaletteLight` / `PaletteDark` live inline in `MainLayout.razor`. CSS classes prefixed `deepdrft-` live in `DeepDrftWeb/wwwroot/styles/deepdrft-styles.css`. Custom SVG icons live in `Common/DDIcons.cs`.
**Do NOT include:**
- `Counter.razor` / `Weather.razor` references.
- "ASP.NET Core 9.0".
- Any claim that this project hosts a server or controllers — it is a class-libraryish WASM assembly consumed by `DeepDrftWeb`.
---
### `DeepDrftWeb.Services/CLAUDE.md` (new)
**One-line purpose:** SQL-side domain logic for tracks. EF Core context, configurations, migrations, repository, service, design-time factory. Consumed by both `DeepDrftWeb` and `DeepDrftCli`.
**Cover:**
- Why this project exists: separating domain from host so the CLI can reuse `TrackService` / `TrackRepository` / `DeepDrftContext` without referencing the ASP.NET host. New SQL-side domain code goes here, not in `DeepDrftWeb`.
- Layout: `Data/DeepDrftContext.cs`, `Data/DeepDrftContextFactory.cs` (design-time, hard-codes `../Database/deepdrft.db` for `dotnet ef`), `Data/Configurations/TrackConfiguration.cs`, `Migrations/`, `Repositories/TrackRepository.cs`, `TrackService.cs`.
- The Controller → Service → Repository → DbContext shape. Services return `Result` / `ResultContainer<T>` from NetBlocks; repositories throw / return raw types. Exceptions are caught at the service boundary.
- `TrackEntity` field reality: `Id`, `EntryKey` (required, max 100, the FileDatabase entry id), `TrackName` (max 200), `Artist` (max 200), `Album?` (max 200), `Genre?` (max 100), `ReleaseDate?` (`DateOnly`), `ImagePath?` (max 500). Column names are snake_case. Table is `track` (singular).
- Pagination convention: `TrackService.GetPaged` builds a `PagingParameters<TrackEntity>` with an `OrderBy` `Expression<Func<T, object>>`. Switch in `GetPaged` maps a string sort column to the expression. New sort columns extend this switch. Nulls sort to the end (via padded sentinel strings / `DateOnly.MaxValue`).
- EF migration commands (run from solution root): `dotnet ef migrations add <Name> --project DeepDrftWeb.Services --startup-project DeepDrftWeb` and `dotnet ef database update --project DeepDrftWeb.Services --startup-project DeepDrftWeb`. The design-time factory means you can also run `dotnet ef ... --project DeepDrftWeb.Services` standalone for local development.
- The migrations namespace is `DeepDrftWeb.Migrations` (a legacy name, retained for migration history continuity).
- Connection string source: `appsettings.json``ConnectionStrings:DefaultConnection` on the web side; `environment/connections.json``CliSettings:ConnectionString` on the CLI side. Both point at `../Database/deepdrft.db`.
**Do NOT include:**
- ASP.NET Core lifecycle, controllers, middleware — those are not in scope here.
- HTTP client setup or theming.
---
### `DeepDrftContent/CLAUDE.md`
**One-line purpose:** The binary content API host. ApiKey middleware, CORS, forwarded headers. Returns audio bytes (with optional WAV-aware offset) and accepts authenticated PUTs. **FileDatabase implementation lives in `DeepDrftContent.Services`, not here.**
**Cover:**
- What lives here now (only): `Program.cs`, `Startup.cs`, `Controllers/TrackController.cs`, `Middleware/ApiKeyAuthenticationMiddleware.cs`, `Middleware/ApiKeyAuthorizeAttribute.cs`, `Models/` (settings POCOs: `ApiKeySettings`, `CorsSettings`, `FileDatabaseSettings`), `environment/filedatabase.json`, `environment/apikey.json` (not in repo).
- What does *not* live here anymore: any `FileDatabase/`, `Services/`, `Processors/`, or media model code. All moved to `DeepDrftContent.Services`. Don't add new domain code in this project.
- The endpoint surface (exactly two, do not add a third without product approval):
- `GET api/track/{trackId}?offset=0`**unauthenticated**, returns the WAV bytes from the `tracks` vault. If `offset > 0`, `WavOffsetService.CreateOffsetStream` block-aligns the offset and synthesises a fresh 44-byte WAV header so the response is a valid standalone WAV starting from that byte position.
- `PUT api/track/{trackId}`**`[ApiKeyAuthorize]`**, accepts `AudioBinaryDto` (base64 buffer + size + mime + duration + bitrate), writes via `FileDatabase.RegisterResourceAsync` into the `tracks` vault.
- ApiKey middleware behaviour: only enforces on endpoints with `[ApiKeyAuthorize]` metadata. Reads header `ApiKey`. Returns 401 with body `"API Key was not provided"` or `"Unauthorized client"`. Configured via `environment/apikey.json` (`ApiKeySettings.ApiKey`).
- CORS: `CorsSettings.AllowedOrigins` is required (the app throws on startup if missing). Policy is named `ContentApiPolicy`. AllowCredentials + AllowAnyMethod + AllowAnyHeader.
- Forwarded headers are enabled only in `Production` (this differs from `DeepDrftWeb` which enables them always — be aware when debugging proxy issues).
- Startup wiring (`Startup.ConfigureDomainServices`): adds singleton `WavOffsetService`, loads `environment/filedatabase.json`, awaits `FileDatabase.FromAsync(VaultPath)`, registers it as singleton, and ensures the `tracks` vault exists (`MediaVaultType.Audio`). The vault is created on first boot, then reused.
- OpenAPI is only mapped in `Development`.
**Do NOT include:**
- The internals of how `FileDatabase` works — that's the job of `DeepDrftContent.Services/CLAUDE.md`.
- A directory tree showing `FileDatabase/Services/` inside this project; it is not there.
---
### `DeepDrftContent.Services/CLAUDE.md` (new)
**One-line purpose:** Binary-content domain logic. The FileDatabase implementation, audio processing, WAV stream-with-offset, and the content-side track service. Consumed by `DeepDrftContent` (the host) and `DeepDrftCli`.
**Cover:**
- Layout: `FileDatabase/` (the subsystem — `Abstractions/`, `Models/`, `Services/`, `Utils/`), `Audio/WavOffsetService.cs`, `Processors/AudioProcessor.cs`, `Constants/VaultConstants.cs`, `TrackService.cs`.
- FileDatabase model (refer to `DeepDrftContent.Services/FileDatabase/README.md` for the long-form rationale — it's a port of a TypeScript system):
- `FileDatabase` is the root. Created via `FileDatabase.FromAsync(rootPath)`. Holds a `StructuralMap<string, MediaVault>` and an `IndexWatcher`. Implements `IDisposable`.
- Each `MediaVault` is a subdirectory with its own JSON `index` file. Types: `Media | Image | Audio`. Concrete subclasses are `ImageVault` and `AudioVault` (the README's mention of `ImageDirectoryVault` is stale — the type is `ImageVault`).
- Entry filenames: `{sanitized-key}{extension}`, where sanitisation is `Regex.Replace(entryKey, @"[^a-zA-Z0-9]", "-")`.
- Binary hierarchy: `FileBinary → MediaBinary (+ Extension, MIME via `MimeTypeExtensions`) → AudioBinary (+ Duration, Bitrate) | ImageBinary (+ AspectRatio)`. Each has a matching `*Dto` for base64 JSON transport.
- Index lifecycle: `IndexFactoryService` loads-or-creates `DirectoryIndex` (the root index) and `VaultIndex` (per-vault, records `MediaVaultType`). Saved as JSON via `FileUtils.PutObjectAsync`.
- `IndexWatcher` uses `FileSystemWatcher` to detect external writes to a vault's `index` file (the CLI is the typical writer) and triggers `MediaVault.ReloadIndexAsync` so a long-running web host stays consistent.
- **Error-handling philosophy** (load-bearing): public `Load*` / `Register*` operations swallow exceptions and return `null` / `false` to match the TypeScript original. Callers must check return values. Do not change this without a deliberate design pass.
- WAV offset service: `WavOffsetService.CreateOffsetStream(buffer, byteOffset)` parses the WAV header, block-aligns the offset, synthesises a new 44-byte header for the remaining data, and returns `[new header][data from offset]` as a `MemoryStream`. Used by the content API to serve seek-beyond-buffer requests. Block alignment is *required* for clean audio — do not bypass it.
- Audio processor: `AudioProcessor.ProcessWavFileAsync(filePath)` validates the RIFF/WAVE/PCM structure, parses fmt and data chunks, computes duration + bitrate, falls back to defaults (180s / 1411 kbps / 44.1 kHz / 16-bit stereo) on parse failure with a `Console.WriteLine` warning. PCM-only today; other formats are unsupported.
- Content-side `TrackService` (orchestrator): `AddTrackFromWavAsync` reads a WAV from disk, generates a GUID entry key, ensures the `tracks` vault exists, stores the audio, and returns a populated `TrackEntity` (without saving it to SQL — caller does that). `GetAudioBinaryAsync` reads it back. `InitializeTracksVaultAsync` is a safety call.
- `VaultConstants.Tracks = "tracks"` — the one vault name in production use. New vault names go here.
**Do NOT include:**
- The HTTP surface of `DeepDrftContent` — that belongs in `DeepDrftContent/CLAUDE.md`.
- A reproduction of the FileDatabase README's full design discussion — link to it instead.
---
### `DeepDrftModels/CLAUDE.md`
**One-line purpose:** Shared contracts. Entities, DTOs, pagination types. Every project references this; nothing else references the projects that reference this.
**Cover:**
- Layout: `Entities/TrackEntity.cs`, `DTOs/TrackDto.cs`, `Models/PagingParameters.cs`, `Models/PagedResult.cs`.
- `TrackEntity` fields (verbatim from current code — replace any older field list): `Id` (long PK), `EntryKey` (required string, FileDatabase entry id), `TrackName` (required), `Artist` (required), `Album?`, `Genre?`, `ReleaseDate?` (`DateOnly`), `ImagePath?`. **No `MediaPath` field exists.**
- `TrackDto` mirrors `TrackEntity`. Used only where DTO/entity separation is needed for serialisation; in practice both flow over the wire today.
- `PagingParameters` (base): `Page` (1-based, default 1), `PageSize` (default 20, capped at 100). The cap is a hard ceiling.
- `PagingParameters<T>`: adds `OrderBy: Expression<Func<T, object>>?` and `IsDescending`. `Skip` is computed `(Page - 1) * PageSize`. Type-safe sort expressions — strings only at the API boundary.
- `PagedResult<T>`: `Items`, `TotalCount`, `Page`, `PageSize`. Derived: `TotalPages`, `HasNextPage`, `HasPreviousPage`. Includes `PagedResult<T>.From<TOther>` for cross-type mapping.
- Convention: required reference fields use `required` modifier; optional reference fields are `?`. Don't relax `required` — it's the compile-time guarantee that prevents half-built entities reaching the database.
- This project also references `NetBlocks` only transitively (the actual NetBlocks types — `Result`, `ResultContainer<T>`, `ApiResult<T>`, `ApiResultDto<T>` — come into the services and clients directly).
**Do NOT include:**
- "Potential future usage in DeepDrftContent" — content already uses these.
- Generic C# tutorials on `required` or `Expression<T>`.
---
### `DeepDrftCli/CLAUDE.md`
**One-line purpose:** Local admin tool. Adds and lists tracks. Two modes: classic CLI (`add` / `list` / `help`) and a Terminal.Gui interactive interface (`gui`). Has direct access to both the SQL DB and the FileDatabase (it's not a network client).
**Cover:**
- Why this is allowed to bypass the API: it is a local single-user admin tool, run on the same machine as the databases. Browsers never get to bypass — the API is for them. Don't extend this pattern to network clients.
- Layout: `Program.cs`, `Services/CliService.cs`, `Services/GuiService.cs`, `Models/CliSettings.cs`, `environment/connections.json` (the actual config file — *not* `appsettings.json`).
- `CliSettings`: `ConnectionString` (SQLite path) + `VaultPath` (FileDatabase root). Loaded from `environment/connections.json` resolved against `AppDomain.CurrentDomain.BaseDirectory`. `environment/config.json` is also copied to output but currently unused — leave it as a placeholder.
- DI wiring (`Program.cs`): `DeepDrftContext` (SQLite), `FileDatabase` (singleton via `FromAsync`, blocking on init), `TrackRepository`, `DeepDrftWeb.Services.TrackService`, `AudioProcessor`, `DeepDrftContent.Services.TrackService`, `CliService`, `GuiService`. Logging is console-only.
- Mode dispatch: `gui` / `--gui` runs `GuiService.RunAsync`; everything else runs `CliService.RunAsync(args)`.
- CLI commands:
- `add <wav-file> <track-name> <artist> [album] [genre] [release-date]` — classic positional.
- `add <wav-file> -i|--interactive [...defaults]` — interactive prompts, command-line args become defaults.
- `list` — formatted table.
- `help` / `--help` / `-h`.
- GUI mode: Terminal.Gui application with a custom dark/brand colour scheme, list of tracks, status pane, persistent hotkey legend, and an add-track dialog.
- Dual-database flow for `add` (this is the critical contract — do not skip):
1. `DeepDrftContent.Services.TrackService.AddTrackFromWavAsync` processes the WAV, generates an entry GUID, stores audio in the `tracks` vault, returns a populated `TrackEntity` (not yet saved to SQL).
2. `DeepDrftWeb.Services.TrackService.Create` saves the entity to SQLite and returns the persisted version with `Id` assigned.
3. If step 1 succeeds and step 2 fails, the audio is orphaned in the vault. There is no compensating rollback today — flag this if it comes up in planning.
- Publishing: `PublishSingleFile=true`. `SelfContained` and `IncludeNativeLibrariesForSelfExtract` are commented out — current publish is framework-dependent single file.
- Release date format is `YYYY-MM-DD`. Only `.wav` is accepted.
**Do NOT include:**
- Any claim that `appsettings.json` is the config file.
- Coverage of all GuiService implementation details — just the entry point and that it exists.
---
### `DeepDrftTests/CLAUDE.md`
**One-line purpose:** NUnit test project. Covers the FileDatabase subsystem end-to-end (the only piece of the codebase with non-trivial test coverage). References `DeepDrftContent.Services`.
**Cover:**
- Layout: one test class per subsystem area — `FileDatabaseTests`, `MediaVaultTests`, `MediaVaultFactoryTests`, `IndexSystemTests`, `SimpleMediaTypeRegistryTests`, `UtilityTests`, `ModelTests`. `TestData.cs` holds shared fixtures (a real 16x16 PNG byte buffer; factory helpers for `ImageBinary` / `AudioBinary`; named constants under `TestKeys` / `TestFiles`). `environment/filedatabase.json` is a test-config file copied at build.
- Test isolation pattern (used by `FileDatabaseTests` and others): each test creates a unique directory under `Path.GetTempPath() + "DeepDrftTests/{Guid}"` in `[SetUp]` and best-effort deletes it in `[TearDown]`. New tests touching the filesystem must follow this pattern — do not write into the real `Database/Vaults` path.
- The tests are the load-bearing documentation of the FileDatabase's behaviour. When changing FileDatabase semantics (especially the swallow-and-return-null contract, the entry-key sanitisation regex, the vault-type round-trip on index, or the index-watcher reload), the tests are where intent is anchored. Update them in the same change.
- `TestData.CreateTestAudioBinary` deliberately reuses PNG bytes as a mock audio buffer — the FileDatabase does not parse audio content, so any byte buffer with valid metadata round-trips correctly. Real WAV parsing is exercised by the CLI / content host, not the FileDatabase tests.
- Run commands:
- All: `dotnet test DeepDrftTests/`.
- One class: `dotnet test DeepDrftTests/ --filter "ClassName=FileDatabaseTests"`.
- One method: `dotnet test DeepDrftTests/ --filter "Name=FileDatabase_CanBeCreatedAtSpecifiedLocation"`.
- What is NOT tested (be honest): `TrackService` (Web or Content), `TrackController` (Web or Content), `TrackClient` / `TrackMediaClient`, the audio player services, the dark-mode round-trip, the WAV offset service, the audio processor. Any planned work in those areas should consider whether tests need to land alongside.
**Do NOT include:**
- A claim that the test suite has "comprehensive coverage". It is comprehensive for FileDatabase and not present elsewhere.
- Code coverage instructions if they are not in active use — only document the commands actually run.
---
## Order of operations for doc-keeper
1. Rewrite `DeepDrftWeb/CLAUDE.md` and `DeepDrftWeb.Client/CLAUDE.md` first — they are the most-touched directories during ongoing UI work and have the highest drift.
2. Create `DeepDrftWeb.Services/CLAUDE.md` and `DeepDrftContent.Services/CLAUDE.md` — these are the biggest content gaps (no docs exist; most domain logic lives there).
3. Rewrite `DeepDrftContent/CLAUDE.md` — high drift, but the surface is small.
4. Rewrite `DeepDrftModels/CLAUDE.md` — small file, but the `MediaPath``EntryKey` correction matters everywhere it's wrong.
5. Rewrite `DeepDrftCli/CLAUDE.md` and `DeepDrftTests/CLAUDE.md` — lower priority; mostly polish + framework version.
6. Pause and check in with Daniel about `DeepDrftContent.Services/FileDatabase/README.md` — retire, keep, or replace.