Rewrite all folder-level CLAUDE.md files; update root

net10.0 throughout; corrected EntryKey field name; documented *.Services split.
Two new files (DeepDrftWeb.Services, DeepDrftContent.Services). FileDatabase
README inaccuracies fixed (ImageVault, project target).
This commit is contained in:
Daniel Harvey
2026-05-16 21:45:56 -04:00
parent de0909f38f
commit 7cf3d965b6
10 changed files with 1233 additions and 869 deletions
+102 -58
View File
@@ -1,45 +1,87 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This file provides guidance to Claude Code when working with code in this repository.
## Architecture Overview
DeepDrftHome is a .NET 9 solution consisting of multiple projects that implement a dual-database media management system:
DeepDrftHome is a **net10.0** solution consisting of eight projects implementing a dual-database media management system for the DeepDrft electronic music collective.
### Core Projects
- **DeepDrftWeb**: Blazor Server/WebAssembly hybrid web application using MudBlazor UI framework
- **DeepDrftWeb.Client**: Blazor WebAssembly client components
- **DeepDrftContent**: Web API project providing content management endpoints with API key authentication
- **DeepDrftModels**: Shared data models and entities
- **DeepDrftTests**: NUnit test project with comprehensive FileDatabase tests
- **NetBlocks**: External dependency located at `C:\lib\NetBlocks\`
- **DeepDrftWeb**: ASP.NET Core host. Blazor Web App with Server + WASM render modes. Owns the SQL-backed `api/track/page` endpoint, MudBlazor theme prerender, and TypeScript→JS audio interop.
- **DeepDrftWeb.Client**: Blazor WebAssembly assembly. All interactive UI (pages, player stack, dark-mode plumbing, HTTP clients for both backends).
- **DeepDrftWeb.Services**: Class library. EF Core domain logic: `DeepDrftContext`, `TrackConfiguration`, `Migrations`, `TrackRepository`, `TrackService`. Sharable between web host and CLI.
- **DeepDrftContent**: ASP.NET Core host. Binary content API (`GET api/track/{id}` unauthenticated, `PUT api/track/{id}` ApiKey-protected). Returns audio bytes with optional WAV-aware offset streaming.
- **DeepDrftContent.Services**: Class library. The FileDatabase implementation in full (Models, Services, Utils, Abstractions, Constants), `WavOffsetService`, `AudioProcessor`, content-side `TrackService`. Consumed by host and CLI.
- **DeepDrftModels**: Shared contracts. `TrackEntity`, `TrackDto`, `PagingParameters<T>`, `PagedResult<T>`. Every project references this.
- **DeepDrftCli**: Console app. Two modes: classic CLI (`add` / `list` / `help`) and Terminal.Gui (`gui`). Direct access to both databases (local admin tool, not a network client).
- **DeepDrftTests**: NUnit test suite. Comprehensive FileDatabase tests (vault creation, media storage, indexing, factory patterns, utilities). Integration-focused with temp-directory test isolation.
External: **NetBlocks** (absolute path `C:\lib\NetBlocks\`). Provides `Result`, `ResultContainer<T>`, `ApiResult<T>`, `ApiResultDto<T>`.
### Database Architecture
The application uses a **dual-database approach**:
**Dual-database approach** — browser never reaches storage directly:
1. **SQL Database (SQLite)**: Stores metadata and track information via Entity Framework
- Located: `../Database/deepdrft.db`
- Entity: `TrackEntity` with fields for MediaPath, TrackName, Artist, Album, Genre, etc.
- Context: `DeepDrftContext` with SQLite provider
```
DeepDrftWeb.Client (WASM)
├── HttpClient "DeepDrft.API" ──► DeepDrftWeb host ──► EF Core / SQLite (metadata)
└── HttpClient "DeepDrft.Content" ──► DeepDrftContent ──► FileDatabase / disk (binary)
```
2. **FileDatabase**: Custom file-based storage system for binary media content
- Located: `../Database/Vaults` (configurable via `filedatabase.json`)
- Manages **MediaVaults** with different types: Media, Image, Audio
- Supports structured binary storage with metadata (duration, bitrate, aspect ratio)
1. **SQL Database (SQLite)**: Metadata and track info via Entity Framework
- Location: `../Database/deepdrft.db`
- Entity: `TrackEntity` with `Id`, `EntryKey`, `TrackName`, `Artist`, `Album?`, `Genre?`, `ReleaseDate?`, `ImagePath?`
- Context: `DeepDrftContext` in `DeepDrftWeb.Services`
### FileDatabase System Details
2. **FileDatabase**: Custom file-based binary storage system
- Location: `../Database/Vaults` (configurable via `filedatabase.json`)
- Root contains typed **MediaVaults** (Media, Image, Audio)
- Each vault has a JSON `index` file listing entries + per-entry metadata
- Entries are user-supplied strings sanitized to `[a-zA-Z0-9-]` + file extension
- Binary hierarchy: `FileBinary``MediaBinary` (+ Extension/MIME) → `AudioBinary` (+ Duration/Bitrate) | `ImageBinary` (+ AspectRatio)
- **Error-handling philosophy**: public operations swallow exceptions and return `null`/`false` — callers must check return values, not catch.
The FileDatabase is the core innovation of this project:
## Key Architectural Decisions
- **Vault-based Organization**: Content organized into typed vaults (MediaVaultType: Media, Image, Audio)
- **Index System**: Uses JSON-based indexing with DirectoryIndex and VaultIndex structures
- **Media Types**:
- `AudioBinary`: Buffer + Duration + Bitrate + Extension
- `ImageBinary`: Buffer + AspectRatio + Extension
- `MediaBinary`: Base buffer + Extension + MIME type support
- **Factory Pattern**: `MediaVaultFactory` creates appropriate vault types
- **Async Operations**: All database operations are async with error swallowing
### Service projects vs. host projects
The split between `DeepDrftWeb` / `DeepDrftWeb.Services` (and the same for Content) is deliberate: hosts own HTTP surface, config, DI wiring; `*.Services` are plain class libraries holding domain logic. This lets `DeepDrftCli` reuse both `TrackService` implementations without taking ASP.NET dependencies.
**New domain logic goes in `*.Services` unless genuinely host-specific** (controllers, middleware, render-mode config, theme prerender).
### TrackEntity is a join, not a content blob
`TrackEntity` holds *only* metadata. The link to binary content is `EntryKey` (string) — the entry id inside the `tracks` vault in FileDatabase. Dual-database add flow:
1. `DeepDrftContent.Services.TrackService.AddTrackFromWavAsync` processes WAV, generates entry GUID, stores audio in vault, returns unpersisted `TrackEntity`.
2. `DeepDrftWeb.Services.TrackService.Create` saves to SQLite and returns the persisted entity with `Id`.
If step 1 succeeds and step 2 fails, audio is orphaned in the vault (no rollback today).
### Streaming-first audio playback
The player is not fetch-then-play:
1. Client calls `GET api/track/{id}` on DeepDrftContent and receives WAV bytes as a stream (`HttpCompletionOption.ResponseHeadersRead`).
2. `StreamingAudioPlayerService` reads in adaptive 1664 KB chunks, pushes each via `AudioInteropService.processStreamingChunk`.
3. TypeScript `StreamDecoder` parses WAV header, decodes chunks to `AudioBuffer`s. `PlaybackScheduler` schedules them on a Web Audio graph.
4. Playback starts as soon as a min buffer is queued; UI duration from parsed header (not waiting for full file).
5. **Seek beyond buffer**: if seek target is past what's decoded, client issues `GET api/track/{id}?offset={byteOffset}`. Server's `WavOffsetService` block-aligns offset, synthesises a fresh 44-byte WAV header, streams `[new header][data from offset]`. Player tears down and re-initialises decoder for the new stream.
Keep this seam clean — it is the most architecturally load-bearing part of the playback path.
### Theming and dark mode
- MudBlazor is the UI framework. Light and dark palettes (bespoke "Charleston in the Day" / "Lowcountry Summer Nights") defined inline in `MainLayout.razor`.
- Dark mode toggles via cookie (`darkMode`, 365 days). Client-side via JS interop.
- During server prerender, `DarkModeService` (in `DeepDrftWeb`) reads the cookie and seeds `DarkModeSettings.IsDarkMode`, which carries into WASM render via `PersistentComponentState`. Avoids "wrong theme flash" on initial paint.
- `DarkModeSettings` lives in `DeepDrftWeb.Client.Common` (consumed by both server prerender and client components).
- Typography: Google Fonts (Bodoni Moda, Cormorant, DM Sans). Hand-rolled gas-lamp icon (lit/unlit) lives in `DDIcons.cs`.
### TypeScript interop, not raw JS
Audio interop authored in TypeScript under `DeepDrftWeb/Interop/audio/`, compiled to `wwwroot/js/audio/` via `Microsoft.TypeScript.MSBuild`. One module per responsibility (AudioContextManager, StreamDecoder, PlaybackScheduler, SpectrumAnalyzer, AudioPlayer), plus `index.ts` exposing `window.DeepDrftAudio`. `tsconfig.json` is **not** copied to output. In dev, raw `.ts` served from `/Interop/` for source-map debugging.
## Development Commands
@@ -52,11 +94,7 @@ dotnet build DeepDrftHome.sln
dotnet test DeepDrftTests/
# Run specific test class
dotnet test DeepDrftTests/ --filter "FileDatabaseTests"
# Build specific project
dotnet build DeepDrftWeb/
dotnet build DeepDrftContent/
dotnet test DeepDrftTests/ --filter "ClassName=FileDatabaseTests"
```
### Running Applications
@@ -66,48 +104,54 @@ dotnet run --project DeepDrftWeb
# Run content API
dotnet run --project DeepDrftContent
# Run CLI (classic mode)
dotnet run --project DeepDrftCli -- list
# Run CLI (GUI mode)
dotnet run --project DeepDrftCli -- gui
```
### Entity Framework (SQL Database)
```bash
# Add migration
dotnet ef migrations add MigrationName --project DeepDrftWeb
# Add migration (from solution root)
dotnet ef migrations add MigrationName --project DeepDrftWeb.Services --startup-project DeepDrftWeb
# Update database
dotnet ef database update --project DeepDrftWeb
dotnet ef database update --project DeepDrftWeb.Services --startup-project DeepDrftWeb
```
## Key Configuration Files
- `DeepDrftWeb/appsettings.json`: SQL connection string, logging configuration
- `DeepDrftContent/environment/filedatabase.json`: FileDatabase vault path configuration
- `DeepDrftContent/environment/apikey.json`: API authentication (not in repo)
- `DeepDrftWeb/appsettings.json`: SQL connection string, logging
- `DeepDrftContent/environment/filedatabase.json`: FileDatabase vault path
- `DeepDrftContent/environment/apikey.json`: API key (not in repo)
- `DeepDrftCli/environment/connections.json`: CLI config (`ConnectionString`, `VaultPath`)
## Folder-Level Guidance
Folder-level `CLAUDE.md` files provide specifics on structure, patterns, and commands for each project. Start with the project's folder `CLAUDE.md` when entering that directory. The root CLAUDE.md here sets the architectural context; folder files answer "what's in this folder and how do I work here?"
## Important Patterns
### FileDatabase Usage
```csharp
// Initialize FileDatabase
var fileDatabase = await FileDatabase.FromAsync(rootPath);
### Result Pattern (NetBlocks)
// Register audio content
await fileDatabase.RegisterResourceAsync("tracks", trackId, audioBinary);
Services return `Result`, `ResultContainer<T>`, or `ApiResult<T>` rather than throwing for expected failure modes. Catch at service boundary, surface via result.
// Load audio content
var audio = await fileDatabase.LoadResourceAsync<AudioBinary>("tracks", trackId);
```
### Error Swallowing in FileDatabase
### Dependency Injection
- FileDatabase service registration occurs in startup configuration
- SQL context registered via `AddDbContext<DeepDrftContext>`
- Repository pattern with `TrackRepository` and `TrackService`
Public `Load*` / `Register*` operations in FileDatabase swallow exceptions and return `null` / `false`. Callers must check return values. This matches the TypeScript original and is load-bearing — do not change without a design pass.
### Testing Strategy
- Comprehensive FileDatabase integration tests with temporary directories
- Uses NUnit framework with async test patterns
- Test isolation via unique temp directories per test
- Covers vault creation, media storage, and retrieval scenarios
### Pagination
## Project Dependencies
Services build `PagingParameters<T>` with an `OrderBy` expression. Switch in the service maps a string sort column to the expression. New sort columns extend this switch. Nulls sort to end (padded sentinel strings / `DateOnly.MaxValue`).
External dependencies include Entity Framework Core (SQLite), MudBlazor, and the custom NetBlocks library. The FileDatabase system is entirely custom-built and forms the backbone of the media storage architecture.
## External Dependencies
- Entity Framework Core 10.0.1 (SQLite)
- MudBlazor 8.15.0
- NUnit 4.4.0
- NetBlocks (Result patterns)
- Microsoft.TypeScript.MSBuild (TS compilation)
See individual project files for detailed dependency lists and versions.
+187 -166
View File
@@ -1,204 +1,225 @@
# CLAUDE.md - DeepDrftCli
This file provides guidance to Claude Code (claude.ai/code) when working with the DeepDrftCli project.
Guidance for working in the DeepDrftCli project (the admin CLI tool).
## Project Overview
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
DeepDrftCli is a **console application** for managing audio tracks in the DeepDrft system. It provides command-line interface for adding WAV files to both the SQL database and FileDatabase, leveraging services from both DeepDrftWeb and DeepDrftContent projects.
## One-line purpose
## Architecture
Local admin tool. Adds and lists tracks. Two modes: classic CLI (`add` / `list` / `help`) and Terminal.Gui interactive interface (`gui`). Has direct access to both the SQL DB and the FileDatabase (it's not a network client — it runs on the same machine as the databases).
### Technology Stack
- **.NET 9.0 Console Application**: Command-line interface
- **Microsoft.Extensions.Hosting**: Dependency injection and configuration
- **Entity Framework Core**: SQL database operations via DeepDrftWeb
- **FileDatabase**: Binary storage via DeepDrftContent
## Why this is allowed to bypass the API
CLI is a **local single-user admin tool**, run on the same machine as the databases by an admin. It consumes `DeepDrftWeb.Services` and `DeepDrftContent.Services` directly without going through HTTP. Browsers never get to bypass — the APIs are for them. **Don't extend this pattern to network clients.**
## Layout
### Project Structure
```
DeepDrftCli/
├── Services/
│ ├── CliService.cs # Main CLI command handler
│ └── [Processors moved to DeepDrftContent]
├── Program.cs # Application entry point with DI setup
├── appsettings.json # Configuration
── DeepDrftCli.csproj # Project file with references
```
### Service Dependencies
- **DeepDrftWeb**: SQL database operations, TrackService, TrackRepository
- **DeepDrftContent**: FileDatabase, AudioProcessor (in /Processors)
- **DeepDrftModels**: Shared entities and DTOs
- **NetBlocks**: Result pattern (ResultContainer<T>)
## Audio Processing Architecture
### AudioProcessor (DeepDrftContent/Processors/)
Handles WAV file processing with full metadata extraction:
```csharp
public async Task<AudioBinary?> ProcessWavFileAsync(string filePath)
{
// WAV header parsing for duration, bitrate, sample rate
// Creates AudioBinary with extracted metadata
}
```
### TrackService (DeepDrftContent/Services/)
Orchestrates dual-database operations:
```csharp
public async Task<TrackEntity?> AddTrackFromWavAsync(
string wavFilePath, string trackName, string artist, ...)
{
// 1. Process WAV file → AudioBinary
// 2. Store in FileDatabase → get trackId
// 3. Create TrackEntity with MediaPath = trackId
// 4. Return entity for SQL storage
}
```
## Command-Line Interface
### Available Commands
#### GUI Mode (Interactive Terminal Interface)
```bash
DeepDrftCli gui
```
Launches the interactive Terminal.Gui interface with:
- **DeepDrft brand color theme** (Magenta/Purple/Pink)
- **Color-coded track list** with navigation
- **Persistent hotkey legend** showing shortcuts
- **Interactive add track dialog** with file browser
- **Real-time status updates** and feedback
- **Full keyboard shortcuts** for all operations
#### Add Track
```bash
DeepDrftCli add <wav-file-path> <track-name> <artist> [album] [genre] [release-date]
```
**Example:**
```bash
DeepDrftCli add "mysong.wav" "My Song" "Artist Name" "Album" "Rock" "2024-01-01"
```
#### List Tracks
```bash
DeepDrftCli list
```
Shows all tracks from SQL database with formatted table output.
#### Help
```bash
DeepDrftCli help
# or
DeepDrftCli --help
# or
DeepDrftCli -h
```
## Development Commands
### Building
```bash
# Build CLI project
dotnet build DeepDrftCli/
# Build entire solution
dotnet build DeepDrftHome.sln
```
### Running
```bash
# Run from project directory
cd DeepDrftCli
dotnet run -- add "song.wav" "Track Name" "Artist"
# Run from solution root
dotnet run --project DeepDrftCli -- list
# Run built executable
./DeepDrftCli/bin/Debug/net9.0/DeepDrftCli.exe add "song.wav" "Track" "Artist"
```
### Database Requirements
Ensure databases exist and are accessible:
```bash
# SQL Database: ../Database/deepdrft.db
# FileDatabase: ../Database/Vaults
│ ├── CliService.cs # Main CLI command dispatcher
│ └── GuiService.cs # Terminal.Gui interface
├── Models/
│ └── CliSettings.cs # Config POCO (ConnectionString, VaultPath)
── Program.cs # Entry point, DI setup
├── environment/
│ ├── connections.json # The actual config file (not appsettings.json)
│ └── config.json # Placeholder, currently unused
└── DeepDrftCli.csproj
```
## Configuration
### appsettings.json
`CliSettings` (loaded from `environment/connections.json`):
```csharp
public class CliSettings
{
public required string ConnectionString { get; set; } // SQLite path
public required string VaultPath { get; set; } // FileDatabase root
}
```
**The config file is `environment/connections.json`, not `appsettings.json`.** Example:
```json
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=../Database/deepdrft.db"
},
"FileDatabaseSettings": {
"CliSettings": {
"ConnectionString": "Data Source=../Database/deepdrft.db",
"VaultPath": "../Database/Vaults"
}
}
```
### Dependency Injection Setup
Program.cs configures full DI container:
- **SQL Context**: DeepDrftContext with SQLite
- **FileDatabase**: Singleton initialized from vault path
- **Services**: TrackRepository, TrackServices (both Web and Content), AudioProcessor
- **Logging**: Console logging for development
Paths are resolved relative to `AppDomain.CurrentDomain.BaseDirectory`.
## Important Patterns
## DI wiring (Program.cs)
Registers:
- `DeepDrftContext` with SQLite (from connection string)
- `FileDatabase` singleton (awaited on init via `FileDatabase.FromAsync`)
- `TrackRepository`, `DeepDrftWeb.Services.TrackService` (SQL side)
- `AudioProcessor`, `DeepDrftContent.Services.TrackService` (content side)
- `CliService`, `GuiService`
- Console logging
All services are scoped or singletons. The app waits for `FileDatabase.FromAsync` on startup (blocking), so the vault is ready before any command runs.
## Mode dispatch
`Program.cs` checks the first argument:
- `gui` or `--gui`: runs `GuiService.RunAsync()` → Terminal.Gui interactive interface.
- Anything else: runs `CliService.RunAsync(args)` → classic CLI command parsing.
## CLI commands (classic mode)
### add <wav-file> <track-name> <artist> [album] [genre] [release-date]
Positional arguments. Adds a track:
```bash
DeepDrftCli add "/path/to/song.wav" "My Song" "Artist Name" "Album Title" "Rock" "2024-01-15"
```
Release date format: `YYYY-MM-DD`. Optional: album, genre, release date.
### add <wav-file> -i|--interactive [defaults...]
Interactive mode. Prompts for each field; command-line args become defaults:
```bash
DeepDrftCli add "/path/to/song.wav" -i "Artist Name"
```
Prompts: "Track Name? [default] → ", "Artist? [default] → ", etc.
### list
Formats and displays all tracks from SQL:
```bash
DeepDrftCli list
```
Table output: ID, Name, Artist, Album, Genre, ReleaseDate, EntryKey, ImagePath.
### help, --help, -h
Shows command list.
### gui (or --gui)
Launches the Terminal.Gui interactive interface (separate mode).
## GUI mode (Terminal.Gui)
Launches a full interactive terminal UI with:
- **DeepDrft brand color scheme** (Magenta/Purple/Pink).
- **Track list** (scrollable, selectable, deletable).
- **Status pane** showing feedback and errors.
- **Persistent hotkey legend** (F1=Help, A=Add, D=Delete, Q=Quit, etc.).
- **Add dialog** with file browser and track metadata entry.
- **Keyboard navigation**: arrow keys to navigate, Enter to select, escape to cancel.
`GuiService.RunAsync()` creates a `Window`, populates controls, and runs the main loop.
## Dual-database add flow (critical contract)
This is the seam where both databases are written — **must be idempotent and crash-safe**:
1. `DeepDrftContent.Services.TrackService.AddTrackFromWavAsync(filePath)`:
- Validates file is `.wav` and readable.
- Calls `AudioProcessor.ProcessWavFileAsync(filePath)``AudioBinary` (duration, bitrate extracted).
- Generates a GUID entry key.
- Ensures the `tracks` vault exists.
- Calls `FileDatabase.RegisterResourceAsync("tracks", entryKey, audioBinary)`.
- Returns a populated `TrackEntity` (with `Id = 0` since not yet in SQL).
2. `DeepDrftWeb.Services.TrackService.Create(trackEntity)`:
- Saves the entity to SQLite.
- Returns the persisted entity with `Id` assigned.
**If step 1 succeeds and step 2 fails, the audio is orphaned in the vault.** There is no compensating rollback today. The CLI logs both results and exits with status code indicating overall success/failure.
## File validation
- **Extension**: must be `.wav` (only WAV files are supported).
- **Existence**: checked before processing.
- **Readability**: attempted during processing.
- **WAV structure**: validated by `AudioProcessor` (RIFF/WAVE/PCM header parsing).
Non-WAV files are rejected with a clear error message.
## Release date format
Only `YYYY-MM-DD` is accepted. Parsing is strict:
```csharp
DateOnly.ParseExact(releaseDateStr, "yyyy-MM-dd")
```
Invalid dates result in an error. Optional fields (if not provided or invalid) are set to `null`.
## Publishing
Build configuration in `.csproj`:
- `PublishSingleFile=true`: Produces a single executable.
- `SelfContained` and `IncludeNativeLibrariesForSelfExtract` are **commented out** — publish is framework-dependent (requires .NET runtime installed).
To publish:
```bash
dotnet publish DeepDrftCli -c Release -o ./bin/Release/publish
```
Result is a single `.exe` (Windows) or binary (Linux) that consumes the host framework.
## Service registration patterns
All service calls return `Result` or `ResultContainer<T>`. CLI checks `Success` and `Messages` to display feedback:
### Result Pattern (NetBlocks)
All service operations return ResultContainer<T>:
```csharp
var result = await _webTrackService.Create(trackEntity);
if (result.Success && result.Value != null)
{
// Success handling
Console.WriteLine($"✓ Saved to SQL with ID {result.Value.Id}");
}
else
{
var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
// Error handling
Console.WriteLine($"✗ SQL save failed: {string.Join("; ", result.Messages.Select(m => m.Message))}");
}
```
### Dual Database Strategy
1. **Process WAV** → AudioBinary (metadata + buffer)
2. **Store in FileDatabase** → generates unique trackId
3. **Create TrackEntity** with MediaPath = trackId
4. **Store in SQL** → gets database ID and full entity
## Development commands
### WAV File Processing
- **RIFF/WAVE format parsing**: Header validation and chunk parsing
- **Metadata extraction**: Duration, bitrate, sample rate, channels
- **Fallback handling**: Default values if parsing fails
- **Binary preservation**: Full WAV file stored as-is
### Error Handling
- **File validation**: Existence and .wav extension checking
- **Graceful degradation**: Default metadata if parsing fails
- **User-friendly messages**: Clear error reporting for CLI users
- **Logging integration**: Structured logging for debugging
## CLI Usage Notes
### File Path Handling
- Supports absolute and relative paths
- Arguments with spaces must be quoted
- Only .wav files are supported currently
### Date Format
Release dates must be in YYYY-MM-DD format:
```bash
DeepDrftCli add "song.wav" "Title" "Artist" "Album" "Genre" "2024-01-01"
# Build
dotnet build DeepDrftCli
# Run classic CLI mode
dotnet run --project DeepDrftCli -- add "test.wav" "Test Track" "Test Artist"
dotnet run --project DeepDrftCli -- list
dotnet run --project DeepDrftCli -- help
# Run GUI mode
dotnet run --project DeepDrftCli -- gui
# Build for single-file publish
dotnet publish DeepDrftCli -c Release -o ./bin/Release/publish
```
### Output Format
- Success: Detailed track information display
- List: Formatted table with ID, Name, Artist, Album, Genre
- Errors: Clear, actionable error messages
## Important patterns
When working with this project, focus on maintaining the dual-database consistency and the established patterns for CLI argument handling and user feedback.
- **No HTTP**: CLI talks directly to databases. No HTTP clients needed.
- **Async/await**: All database operations are async. Even CLI mode runs in an async context.
- **Error swallowing in FileDatabase**: Vault operations return `null` / `false`. CLI must check return values.
- **Local-only**: CLI has no authentication or CORS. It runs locally as an admin tool.
- **Rollback**: Adding a track is not transactional across the two databases. If one fails, the other may have partially succeeded (orphaned audio in the vault). Plan accordingly.
## Configuration
- `environment/connections.json`: **required** at runtime. Must contain `CliSettings` with `ConnectionString` and `VaultPath`.
- `environment/config.json`: Placeholder, currently unused. Keep it as a placeholder for future expansion.
- No `appsettings.json` — don't add one.
When working with this project, focus on the dual-database consistency of the add flow, CLI argument parsing, and maintaining the interactive Terminal.Gui mode as a first-class interface (not a second-class citizen).
+168
View File
@@ -0,0 +1,168 @@
# CLAUDE.md - DeepDrftContent.Services
Guidance for working in the DeepDrftContent.Services project (the binary-content domain logic).
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
## One-line purpose
Binary-content domain logic. The FileDatabase implementation in full (Models, Services, Utils, Abstractions, Constants), WAV stream-with-offset, audio processing, and the content-side track service. Consumed by `DeepDrftContent` (the host) and `DeepDrftCli` (the admin CLI).
## Layout
```
DeepDrftContent.Services/
├── FileDatabase/ # The subsystem (port of TypeScript system)
│ ├── Abstractions/ # Interfaces
│ ├── Models/ # Data models, DTOs, enums
│ ├── Services/ # FileDatabase, MediaVault, IndexSystem, IndexWatcher
│ └── Utils/ # StructuralMap, StructuralSet, FileUtils
├── Audio/
│ └── WavOffsetService.cs # Byte offset → valid WAV stream
├── Processors/
│ └── AudioProcessor.cs # WAV file parsing, metadata extraction
├── Constants/
│ └── VaultConstants.cs # Vault name definitions
├── TrackService.cs # Content-side orchestrator
└── DeepDrftContent.Services.csproj
```
## FileDatabase model (high-level)
See `FileDatabase/README.md` for the long-form design discussion — it's a port of a TypeScript system and has deep rationale. This section covers the essentials for an agent walking in cold.
### Core structure
- **FileDatabase**: Root object. Created via `FileDatabase.FromAsync(rootPath)`. Holds a collection of `MediaVault` instances and an `IndexWatcher`. Implements `IDisposable`. Singleton in the host.
- **MediaVault**: A subdirectory under the FileDatabase root. Each vault has its own JSON `index` file listing entries and per-entry metadata. Typed via `MediaVaultType` enum (`Media | Image | Audio`).
- Concrete implementations: `ImageVault` (for images), `AudioVault` (for audio). Do not use `ImageDirectoryVault` (that's stale docs) — the type is `ImageVault`.
- **Entry filenames**: `{sanitized-key}{extension}`, where sanitisation is `Regex.Replace(entryKey, @"[^a-zA-Z0-9]", "-")`. So entry id `"my-song"` with extension `.wav` → filename `my-song.wav`.
### Binary hierarchy
```
FileBinary (base: byte buffer)
└── MediaBinary (+ Extension: string, MIME type inferred via MimeTypeExtensions)
├── AudioBinary (+ Duration: double, Bitrate: int)
└── ImageBinary (+ AspectRatio: double)
```
Each has a matching `*Dto` variant for base64 JSON transport (e.g., `AudioBinaryDto` with buffer encoded as base64).
### Index lifecycle
- **DirectoryIndex**: Root index file (at `{rootPath}/index`). Tracks which vaults exist.
- **VaultIndex**: Per-vault index (at `{vaultPath}/index`). Records `MediaVaultType` and lists all entries in that vault.
- Both are JSON files. Created/loaded via `IndexFactoryService`.
- When a file is written externally (e.g., the CLI calls `FileDatabase.RegisterResourceAsync` directly), the **IndexWatcher** detects the write to the vault's `index` file and triggers `MediaVault.ReloadIndexAsync`, so a long-running web host stays consistent without restart.
## Error-handling philosophy (load-bearing)
Public `Load*` / `Register*` operations **swallow exceptions and return `null` / `false`** to match the TypeScript original.
```csharp
public async Task<T?> LoadResourceAsync<T>(string vaultId, string entryId) where T : FileBinary
{
try { /* load and deserialize */ }
catch { return null; } // Swallow, return null
}
public async Task<bool> RegisterResourceAsync(string vaultId, string entryId, FileBinary media)
{
try { /* store and update index */ }
catch { return false; } // Swallow, return false
}
```
**Callers must check return values.** Do not change this without a deliberate design pass — it's embedded in all FileDatabase tests and client code.
## WAV offset service
`WavOffsetService.CreateOffsetStream(buffer, byteOffset)`:
1. Parses the WAV header from the buffer.
2. Block-aligns the byte offset to the nearest block boundary (required for clean audio — misalignment causes clicks).
3. Synthesises a new 44-byte WAV header sized for the remaining data (from offset to EOF).
4. Returns a `MemoryStream` containing `[new header][data from offset]`.
Used by the content API to serve seek-beyond-buffer requests. The player asks for a new stream at the byte offset it wants to seek to; the server returns a valid WAV that starts there.
**Block alignment is critical.** Do not bypass it. The WAV fmt chunk tells you the block size; use it.
## Audio processor
`AudioProcessor.ProcessWavFileAsync(filePath)`:
1. Validates the RIFF/WAVE/PCM structure.
2. Parses the fmt and data chunks.
3. Extracts duration (sample count / sample rate) and bitrate (file size / duration).
4. Returns `AudioBinary` with all metadata.
5. **Fallback**: If parsing fails, logs a warning and returns defaults (180s / 1411 kbps / 44.1 kHz / 16-bit stereo).
PCM-only today. Other formats (mp3, flac, aac, ogg, m4a) are listed in `MimeTypeExtensions` but not implemented. The processor validates RIFF/WAVE/PCM format — anything else is rejected.
## Content-side TrackService (orchestrator)
### AddTrackFromWavAsync(filePath)
1. Reads a WAV file from disk.
2. Calls `AudioProcessor.ProcessWavFileAsync``AudioBinary`.
3. Generates a GUID entry key (via `Guid.NewGuid().ToString()`).
4. Ensures the `tracks` vault exists (creates if missing).
5. Calls `FileDatabase.RegisterResourceAsync("tracks", entryKey, audioBinary)`.
6. Returns a populated `TrackEntity` (with `Id = 0` since it's not yet in SQL).
**Note**: The caller (CLI or web) is responsible for then saving this entity to SQL via `DeepDrftWeb.Services.TrackService.Create`. If the vault write succeeds and SQL write fails, audio is orphaned (no compensating rollback).
### GetAudioBinaryAsync(entryKey)
Reads audio from the `tracks` vault via `FileDatabase.LoadResourceAsync<AudioBinary>("tracks", entryKey)`. Returns `null` if not found or read fails.
### InitializeTracksVaultAsync()
Safety call to ensure the `tracks` vault exists (creates if missing). Called on host startup.
## Vault constants
`VaultConstants.Tracks = "tracks"` — the one vault name in production use. New vault names go here when adding new vault types (e.g., `VaultConstants.Images = "images"` if image uploads are added).
## Service registration
In `DeepDrftContent/Startup.ConfigureDomainServices()` and `DeepDrftCli/Program.cs`:
```csharp
services.AddSingleton<WavOffsetService>();
services.AddSingleton<FileDatabase>(/* from FileDatabase.FromAsync */);
services.AddScoped<AudioProcessor>();
services.AddScoped<TrackService>(); // DeepDrftContent.Services.TrackService
```
## Development commands
```bash
# Build
dotnet build DeepDrftContent.Services
# Run tests (FileDatabase tests cover vault/index/factory/utilities thoroughly)
dotnet test DeepDrftTests/
# Run CLI (which consumes this service)
dotnet run --project DeepDrftCli -- add myfile.wav "Track Name" "Artist"
```
## Important patterns
- **Async/await**: All FileDatabase operations are async. No sync methods.
- **Type safety**: Generic `LoadResourceAsync<T>` ensures callers know what they're loading.
- **Vault lifecycle**: Vaults are created on first boot, then reused. The `FileDatabase` singleton holds them in memory with live `IndexWatcher`es.
- **Entry sanitisation**: Keys are sanitised client-side (in the CLI and web host) *and* by `MediaVault` (defensive). Always sanitise before registering — it's the only way to ensure safe filenames.
- **Metadata hierarchy**: Use the appropriate media type (`AudioBinary`, `ImageBinary`) so downstream code can rely on the metadata. Don't store an audio file as a generic `MediaBinary` — use `AudioBinary` with duration/bitrate.
## What does NOT live here
- HTTP controllers or middleware — that's `DeepDrftContent`.
- SQL database code — that's `DeepDrftWeb.Services`.
- Blazor components or UI logic — that's `DeepDrftWeb.Client`.
- Configuration files (`appsettings.json`, `filedatabase.json`) — those are in the host project.
When working with this project, focus on the FileDatabase subsystem (the most complex piece of the codebase), audio processing, and the orchestration logic that bridges binary and SQL databases. The tests (in `DeepDrftTests/`) are the load-bearing documentation of FileDatabase behaviour — consult them when FileDatabase semantics are unclear.
@@ -18,7 +18,8 @@ The C# port preserves the original three-layer architecture:
- **Location**: `Services/MediaVault.cs`
- **Components**:
- `MediaVault` (Abstract base class)
- `ImageDirectoryVault` (Concrete implementation for images)
- `ImageVault` (Concrete implementation for images)
- `AudioVault` (Concrete implementation for audio)
- **Key Features**:
- File path normalization and media key generation
- Generic type-safe operations using `MediaVaultType` enum
@@ -107,7 +108,7 @@ FileDatabase/
│ ├── StructuralMap.cs # Structural equality map
│ ├── StructuralSet.cs # Structural equality set
│ └── FileUtils.cs # File I/O utilities
└── FileDatabase.csproj # Project file (.NET 9.0)
└── [FileDatabase is now part of DeepDrftContent.Services.csproj]
```
## Key Architectural Decisions
+107 -151
View File
@@ -1,187 +1,143 @@
# CLAUDE.md - DeepDrftContent
This file provides guidance to Claude Code (claude.ai/code) when working with the DeepDrftContent project.
Guidance for working in the DeepDrftContent project (the binary content API host).
## Project Overview
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
DeepDrftContent is a **Web API project** that serves as the content management backend for the DeepDrft system. It provides secure API endpoints for managing the FileDatabase system and handles binary media content storage and retrieval.
## One-line purpose
## Architecture
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.**
### Technology Stack
- **ASP.NET Core Web API 9.0**: RESTful API framework
- **Custom FileDatabase System**: Binary file storage with vault management
- **API Key Authentication**: Secure endpoint protection
- **OpenAPI/Swagger**: Development API documentation
## What lives here now (only)
### Project Structure
```
DeepDrftContent/
├── Controllers/ # API endpoint controllers
│ ├── TrackController.cs # Track media management
│ └── WeatherForecastController.cs # Demo endpoint
├── FileDatabase/ # Core FileDatabase implementation
│ ├── Services/ # Database and vault services
│ ├── Models/ # Data models and DTOs
│ ├── Utils/ # Utility classes
│ └── Abstractions/ # Interfaces and contracts
├── Middleware/ # Custom middleware
│ ├── ApiKeyAuthenticationMiddleware.cs
│ └── ApiKeyAuthorizeAttribute.cs
├── Models/ # Application models
├── environment/ # Configuration files
│ ├── filedatabase.json # FileDatabase settings
│ └── apikey.json # API authentication (not in repo)
└── Program.cs # Application entry point
```
- `Program.cs`, `Startup.cs`: HTTP host config, DI wiring, middleware setup, port binding.
- `Controllers/TrackController.cs`: Two endpoints (see below).
- `Middleware/ApiKeyAuthenticationMiddleware.cs`, `Middleware/ApiKeyAuthorizeAttribute.cs`: ApiKey validation logic.
- `Models/`: Settings POCOs only (`ApiKeySettings`, `CorsSettings`, `FileDatabaseSettings`). No domain code.
- `environment/filedatabase.json`: FileDatabase vault path config (required).
- `environment/apikey.json`: API key (not in repo, must be created locally or at deployment).
## Core FileDatabase System
## What does NOT live here anymore
### Key Components
- Any `FileDatabase/`, `Services/`, or `Processors/` code — all moved to `DeepDrftContent.Services`.
- Media models (`AudioBinary`, `ImageBinary`, etc.) — in `DeepDrftContent.Services`.
- `WavOffsetService` — in `DeepDrftContent.Services`.
- Don't add new domain code to this project.
#### FileDatabase Service
Main orchestration class managing multiple MediaVaults:
```csharp
public class FileDatabase : DirectoryIndexDirectory
{
// Factory creation
public static async Task<FileDatabase?> FromAsync(string rootPath)
## The endpoint surface (exactly two endpoints)
// Vault management
public async Task CreateVaultAsync(string vaultId, MediaVaultType vaultType)
public MediaVault? GetVault(string vaultId)
### GET api/track/{trackId}?offset=0 (unauthenticated)
// Resource operations
public async Task<T?> LoadResourceAsync<T>(string vaultId, string entryId) where T : FileBinary
public async Task<bool> RegisterResourceAsync(string vaultId, string entryId, FileBinary media)
}
```
Returns the WAV bytes from the `tracks` vault.
#### MediaVault Types
- **MediaVaultType.Media**: General binary media storage
- **MediaVaultType.Audio**: Audio-specific storage with duration/bitrate
- **MediaVaultType.Image**: Image storage with aspect ratio
- **Query parameter `offset`** (optional, default 0): byte position to start streaming from.
- If `offset == 0`: streams the entire file from the beginning.
- 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. This is load-bearing for seek-beyond-buffer — the player asks for a new stream at the offset it wants to seek to, gets back a valid WAV that starts there, and tears down/re-initialises the decoder.
- Returns 404 if track not found. Returns 500 if vault operations fail (with error swallowing — the vault returns `null`).
#### Media Models Hierarchy
```
FileBinary (base)
├── MediaBinary (+ Extension, MIME type)
├── AudioBinary (+ Duration, Bitrate)
└── ImageBinary (+ AspectRatio)
```
### PUT api/track/{trackId} ([ApiKeyAuthorize])
### Index System
- **DirectoryIndex**: Manages vault organization
- **VaultIndex**: Individual vault metadata and type information
- **JSON-based storage**: All indexes stored as JSON files
**Authenticated endpoint.** Writes audio bytes to the `tracks` vault.
## API Authentication
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
- **Body**: `AudioBinaryDto` (base64 buffer + size + mime + duration + bitrate).
- `TrackService.AddTrackFromWavAsync` (via DI) is NOT called here — the endpoint just validates and writes to the vault. The caller (CLI) is responsible for then saving the returned `TrackEntity` to SQL.
- Actually: the endpoint is rarely used in production (the CLI calls `FileDatabase.RegisterResourceAsync` directly). But the endpoint exists for potential web-side uploads in future.
- Returns 200 on success, 401 if ApiKey invalid, 400 if body invalid.
### Custom API Key Middleware
```csharp
// Usage in controllers
[ApiKeyAuthorize]
[HttpPut("{trackId}")]
public async Task<ActionResult> PutTrack([FromQuery] string trackId, [FromBody] AudioBinaryDto track)
```
**Do not add a third endpoint without product approval.** The surface is intentionally minimal.
### Authentication Flow
1. Client sends request with `ApiKey` header
2. `ApiKeyAuthenticationMiddleware` validates against configured key
3. Endpoints marked with `[ApiKeyAuthorize]` require valid API key
4. Returns 401 for missing/invalid keys
## ApiKey middleware behaviour
## Development Commands
`ApiKeyAuthenticationMiddleware` runs on every request but only enforces on endpoints with `[ApiKeyAuthorize]` metadata.
### Running the API
```bash
# Run development server
dotnet run --project DeepDrftContent
- Reads header `ApiKey`.
- Compares against `ApiKeySettings.ApiKey` from `environment/apikey.json`.
- Returns 401 with body `"API Key was not provided"` or `"Unauthorized client"` if validation fails.
- Endpoints without `[ApiKeyAuthorize]` skip the check entirely (e.g., `GET api/track/{id}` is unauthenticated).
# Run with specific environment
dotnet run --project DeepDrftContent --environment Development
```
## CORS configuration
### Building
```bash
# Build project
dotnet build DeepDrftContent
`CorsSettings.AllowedOrigins` is **required** — the app throws on startup if missing. Policy is named `ContentApiPolicy`:
# Publish for deployment
dotnet publish DeepDrftContent -c Release
```
- `AllowCredentials()`
- `AllowAnyMethod()`
- `AllowAnyHeader()`
### Testing FileDatabase
```bash
# Run FileDatabase-specific tests
dotnet test DeepDrftTests/ --filter "FileDatabaseTests"
```
Configured in `Startup.ConfigureDomainServices()`, applied to all endpoints via `UseCors()`.
## Configuration
## Forwarded headers
### FileDatabase Settings
`environment/filedatabase.json`:
```json
{
**Enabled only in `Production` mode** (via `if (app.Environment.IsProduction())`). This differs from `DeepDrftWeb`, which enables them always. Be aware when debugging proxy issues.
`UseForwardedHeaders()` processes `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host` so the app knows its real client IP and scheme when sitting behind nginx.
## Startup wiring (Startup.ConfigureDomainServices)
1. Load `environment/filedatabase.json` and bind `FileDatabaseSettings`.
2. Await `FileDatabase.FromAsync(VaultPath)` to load or create the database.
3. Register `FileDatabase` as singleton.
4. Ensure the `tracks` vault exists (type `MediaVaultType.Audio`, created on first boot if missing).
5. Register singleton `WavOffsetService`.
6. Register named `IHttpClientFactory` client (for future use, e.g., fetching from external APIs).
7. Add MudBlazor (if shared components need it in future).
8. Add CORS policy.
The singleton `FileDatabase` is thread-safe for reads. Writes are atomic at the vault level (index updates are serialised). The `IndexWatcher` reloads the vault index if an external process (e.g., CLI) writes to it, so a long-running web host stays consistent.
## OpenAPI
Mapped in `Development` only. Swagger UI at `/swagger` for testing endpoints locally.
## Configuration files
- `appsettings.json`: Logging, hosting config. **Does not contain secrets.**
- `environment/filedatabase.json` (required):
```json
{
"FileDatabaseSettings": {
"VaultPath": "../Database/Vaults"
}
}
```
### API Key Configuration
`environment/apikey.json` (not in repository):
```json
{
"ApiKeySettings": {
"ApiKey": "your-secret-api-key"
}
}
```
- `environment/apikey.json` (required at runtime, not in repo):
```json
{
"ApiKeySettings": {
"ApiKey": "your-secret-key"
}
}
```
## Development commands
```bash
# Run the content API (default https://localhost:5002)
dotnet run --project DeepDrftContent
# Watch during development
dotnet watch run --project DeepDrftContent
# Build
dotnet build DeepDrftContent
# Test endpoints (requires API key in environment/apikey.json)
curl -H "ApiKey: your-secret-key" -X PUT https://localhost:5002/api/track/test-id \
-H "Content-Type: application/json" \
-d '{"buffer":"base64-encoded-audio","size":1000,"mime":"audio/wav"}'
curl https://localhost:5002/api/track/test-id?offset=0
```
## Key Patterns
## Important patterns
### Factory Pattern
MediaVault creation uses factory pattern:
```csharp
var directoryVault = await MediaVaultFactory.From(path, vaultType);
```
- **Result types**: Controllers return `ActionResult<T>`. Service calls return `Result` or `ResultContainer<T>` from NetBlocks. The controller checks `Success` and returns 200/4xx/5xx accordingly.
- **Error swallowing**: FileDatabase operations return `null` or `false` on failure. The controller surfaces this as 500. Never throw — check return values.
- **Async/await**: All operations are async.
- **Vault operations**: Always use the injected `FileDatabase` singleton. Never construct a new one — it has the `IndexWatcher` and is the source of truth.
### Async/Await Throughout
All FileDatabase operations are async with consistent error handling:
```csharp
// Swallow exceptions and return null/false for failed operations
try { /* operation */ }
catch { return null; } // or return false;
```
## The FileDatabase import
### Type-Safe Media Handling
Generic methods ensure type safety for media operations:
```csharp
var audio = await fileDatabase.LoadResourceAsync<AudioBinary>("tracks", trackId);
```
See `DeepDrftContent.Services/CLAUDE.md` for the FileDatabase API and semantics. This host only provides the HTTP surface over it.
### MIME Type Management
Automatic extension/MIME type conversion via `MimeTypeExtensions`:
```csharp
// Supported audio formats: mp3, wav, flac, aac, ogg, m4a
// Supported image formats: jpg, png, gif, webp, svg, bmp
```
## Important Notes
### Vault Organization
- Each vault is a directory with its own index
- Entry IDs are sanitized to create safe filenames
- Media keys generated via regex: `[^a-zA-Z0-9]``-`
### Error Handling Philosophy
FileDatabase uses "swallow and return null" pattern to match TypeScript behavior:
- Failed operations return `null` or `false`
- No exceptions propagated to callers
- Consistent behavior across all async operations
### Binary Data Handling
All media stored as `byte[]` buffers with associated metadata (size, extension, duration, etc.)
When working with this project, focus on the FileDatabase system architecture and maintain the established patterns for vault management, async operations, and API security.
When working with this project, focus on the HTTP surface (controllers, middleware, CORS, forwarded headers) and the wiring that connects the host to the FileDatabase. New domain logic goes in `DeepDrftContent.Services`.
+106 -103
View File
@@ -1,163 +1,166 @@
# CLAUDE.md - DeepDrftModels
This file provides guidance to Claude Code (claude.ai/code) when working with the DeepDrftModels project.
Guidance for working in the DeepDrftModels project (shared contracts across all projects).
## Project Overview
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
DeepDrftModels is a **shared models library** that defines common data structures, entities, DTOs, and model classes used across the entire DeepDrft solution. It serves as the data contract layer between all projects.
## One-line purpose
## Architecture
Shared contracts. Entities, DTOs, pagination types. Every project references this; nothing else references the projects that reference this.
### Technology Stack
- **.NET 9.0 Class Library**: Shared library targeting .NET 9
- **Entity Framework Compatible**: Entities work with EF Core
- **JSON Serializable**: Models support JSON serialization for APIs
## Layout
### Project Structure
```
DeepDrftModels/
├── Entities/ # Database entities
│ └── TrackEntity.cs # Core track data model
├── DTOs/ # Data Transfer Objects
│ └── TrackDto.cs # Track DTO for API transfers
├── Models/ # Shared model classes
│ ├── PagingParameters.cs # Pagination configuration
├── Entities/
│ └── TrackEntity.cs # Database entity for tracks
├── DTOs/
│ └── TrackDto.cs # DTO mirror of TrackEntity
├── Models/
│ ├── PagingParameters.cs # Pagination configuration (base + generic)
│ └── PagedResult.cs # Paginated result wrapper
└── DeepDrftModels.csproj # Project file
└── DeepDrftModels.csproj
```
## Core Models
## TrackEntity (the core entity)
The single source of truth for track metadata. Fields:
### TrackEntity
Primary database entity for track metadata:
```csharp
public class TrackEntity
{
public long Id { get; set; } // Primary key
public required string MediaPath { get; set; } // FileDatabase vault reference
public required string TrackName { get; set; } // Track title
public required string Artist { get; set; } // Artist name
public string? Album { get; set; } // Optional album
public string? Genre { get; set; } // Optional genre
public DateOnly? ReleaseDate { get; set; } // Optional release date
public string? ImagePath { get; set; } // Optional cover image path
}
public long Id { get; set; } // Primary key (auto-assigned by SQLite)
public required string EntryKey { get; set; } // FileDatabase entry id (max 100)
public required string TrackName { get; set; } // Track title (max 200)
public required string Artist { get; set; } // Artist name (max 200)
public string? Album { get; set; } // Optional album (max 200)
public string? Genre { get; set; } // Optional genre (max 100)
public DateOnly? ReleaseDate { get; set; } // Optional release date
public string? ImagePath { get; set; } // Optional image URL (max 500)
```
### TrackDto
Data transfer object matching TrackEntity structure for API operations:
- Mirrors all TrackEntity properties
- Used for JSON serialization/deserialization
- Client-server data exchange
**No `MediaPath` field exists.** That was a legacy name. The field is `EntryKey`.
## Pagination System
The `EntryKey` is the link to binary content — it's the entry id in the `tracks` vault inside FileDatabase. The database column is `entry_key` (snake_case).
### PagingParameters<T>
Generic pagination configuration with LINQ expression support:
```csharp
public class PagingParameters<T> : PagingParameters
{
public Expression<Func<T, object>>? OrderBy { get; set; } // Sorting expression
public bool IsDescending { get; set; } = false; // Sort direction
public int Skip => (Page - 1) * PageSize; // Calculated skip count
}
```
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 from reaching the database.
## TrackDto
Mirrors `TrackEntity` structure (identical fields, same nullability). Used where DTO/entity separation is needed for serialisation. In practice, both flow over the wire today, but the separation is available if APIs need to diverge (e.g., hide `Id` in responses).
## Pagination system
### PagingParameters (base)
### PagingParameters
Base pagination class with size constraints:
```csharp
public class PagingParameters
{
public int Page { get; set; } = 1; // Current page (1-based)
public int PageSize { get; set; } = 20; // Items per page (max 100)
public int PageSize { get; set; } = 20; // Items per page (default 20, max 100)
}
```
`PageSize` setter enforces a hard ceiling at 100 — prevents clients from requesting huge pages that would tank performance.
### PagingParameters<T> (generic)
```csharp
public class PagingParameters<T> : PagingParameters
{
public Expression<Func<T, object>>? OrderBy { get; set; } // Type-safe sort expression
public bool IsDescending { get; set; } // Sort direction
public int Skip => (Page - 1) * PageSize; // Calculated (0-based)
}
```
Used by services to build type-safe LINQ queries. The service takes a string sort column from the API, maps it to an `Expression<Func<T, object>>`, and passes it in `OrderBy`. The repository applies the expression to the DbSet.
### PagedResult<T>
Container for paginated data with metadata:
```csharp
public class PagedResult<T>
{
public IEnumerable<T> Items { get; set; } // Page items
public IEnumerable<T> Items { get; set; } // Current page items
public int TotalCount { get; set; } // Total items available
public int Page { get; set; } // Current page
public int PageSize { get; set; } // Items per page
// Calculated properties
public int TotalPages { get; } // Total pages available
public bool HasNextPage { get; } // Can navigate forward
public bool HasPreviousPage { get; } // Can navigate backward
public int TotalPages => (TotalCount + PageSize - 1) / PageSize;
public bool HasNextPage => Page < TotalPages;
public bool HasPreviousPage => Page > 1;
}
```
## Key Patterns
Returned by service `GetPaged` methods. Includes a static factory `From<TOther>` for cross-type mapping (e.g., `PagedResult<TrackEntity>``PagedResult<TrackDto>`).
## Key patterns
### Required fields
Essential fields use the `required` modifier. This is a compile-time guarantee that prevents half-built entities from being instantiated.
### Required Properties
Uses C# required modifier for essential properties:
```csharp
public required string MediaPath { get; set; } // Compile-time requirement
public required string EntryKey { get; set; }
public required string Artist { get; set; }
```
### Nullable Reference Types
### Nullable reference types
Explicit nullability throughout:
```csharp
public string? Album { get; set; } // Optional nullable
public required string Artist { get; set; } // Required non-null
public string? Album { get; set; } // Optional, can be null
public required string Artist { get; set; } // Required, must be assigned
```
### Generic Type Conversion
PagedResult supports type transformation:
```csharp
public static PagedResult<T> From<TOther>(PagedResult<TOther> other, IEnumerable<T> items)
```
### Expression-based sorting
Type-safe LINQ expressions for dynamic sorting. No string-based `OrderBy` at runtime:
### Expression-Based Sorting
Type-safe LINQ expressions for dynamic sorting:
```csharp
parameters.OrderBy = entity => entity.TrackName; // Compile-time checked
```
## Integration Points
Services map string column names to expressions in a switch, so only valid sorts are possible.
### Entity Framework
- `TrackEntity` configured for EF Core in `DeepDrftWeb.Data.Configurations`
- Long Id as primary key for SQLite compatibility
- DateOnly support for release dates
### Pagination defaults
### API Serialization
- All models JSON-serializable for web API usage
- DTO pattern separates data transfer from domain models
- `Page`: 1 (1-based)
- `PageSize`: 20 (per-item default)
- `PageSize` max: 100 (hard cap, enforced by setter)
### Cross-Project Usage
Referenced by:
- **DeepDrftWeb**: Entity Framework, services, repositories
- **DeepDrftWeb.Client**: API client communication
- **DeepDrftContent**: API DTOs (potential future usage)
- **DeepDrftTests**: Test data and assertions
Client requests outside these bounds are clamped by the API controller before being passed to the service.
## External Dependencies
## Integration points
### NetBlocks Library
Some projects reference `NetBlocks.Models` for:
- `ResultContainer<T>`: Consistent result handling pattern
- `ApiResult<T>`: API response wrapper
- `Result`: Simple operation result
- **EF Core**: `TrackEntity` is configured in `DeepDrftWeb.Services.Data.Configurations.TrackConfiguration`.
- **API serialisation**: All models JSON-serializable. Controllers return `ActionResult<ApiResultDto<PagedResult<TrackEntity>>>`.
- **Cross-project usage**:
- `DeepDrftWeb`: Server render preload, controller responses.
- `DeepDrftWeb.Client`: HTTP client deserialization, UI display.
- `DeepDrftContent.Services`: `TrackService.AddTrackFromWavAsync` returns populated `TrackEntity`.
- `DeepDrftCli`: Entity display, table formatting.
- `DeepDrftTests`: Test assertions.
## Development Notes
## NetBlocks transitivity
### Page Size Limits
Maximum page size enforced at 100 items to prevent performance issues:
```csharp
set => _pageSize = value > _maxPageSize ? _maxPageSize : value;
This project does not directly reference NetBlocks. The result types (`Result`, `ResultContainer<T>`, `ApiResult<T>`, `ApiResultDto<T>`) come into services and clients directly via their own NetBlocks references. Models stay independent of result wrappers — they are serializable POCOs.
## Important conventions
- **Column naming**: Entity property `EntryKey` maps to table column `entry_key` (snake_case, configured in EF).
- **Table name**: `track` (singular, configured in EF).
- **Required fields**: Properties with `required` modifier cannot be left unassigned.
- **Page numbers**: 1-based (page 1 is the first page). The `Skip` calculation converts to 0-based for LINQ (`(Page - 1) * PageSize`).
## Development commands
```bash
# Build
dotnet build DeepDrftModels
# No tests live here (models are simple POCOs)
# Tests that use these models live in DeepDrftTests/
```
### 1-Based Pagination
Page numbers start at 1 (not 0) following common UI patterns:
```csharp
public int Skip => (Page - 1) * PageSize; // Convert to 0-based for queries
```
### Immutable Design
Models favor immutability where possible, using init-only properties and required fields.
When working with this project, maintain consistency in data models across the solution and preserve the established patterns for pagination, nullability, and type safety.
When working with this project, maintain the required/optional distinction rigorously, keep the pagination contract tight, and remember that this project is the interface between all other projects — changes here ripple everywhere.
+180 -132
View File
@@ -1,20 +1,15 @@
# CLAUDE.md - DeepDrftTests
This file provides guidance to Claude Code (claude.ai/code) when working with the DeepDrftTests project.
Guidance for working in the DeepDrftTests project (the test suite).
## Project Overview
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
DeepDrftTests is a **comprehensive test suite** using **NUnit framework** that validates the FileDatabase system and related components. The tests follow SOLID principles and provide extensive coverage of the custom FileDatabase functionality.
## One-line purpose
## Architecture
NUnit test project. Covers the FileDatabase subsystem end-to-end (the only piece of the codebase with non-trivial test coverage). References `DeepDrftContent.Services`.
### Technology Stack
- **NUnit 4.2.2**: Testing framework with modern async support
- **NUnit3TestAdapter**: Visual Studio test adapter
- **.NET 9.0**: Latest framework features
- **Coverlet**: Code coverage collection
## Layout
### Project Structure
```
DeepDrftTests/
├── FileDatabaseTests.cs # Main FileDatabase integration tests
@@ -22,82 +17,109 @@ DeepDrftTests/
├── MediaVaultFactoryTests.cs # Factory pattern tests
├── IndexSystemTests.cs # Index management tests
├── SimpleMediaTypeRegistryTests.cs # Media type registry tests
├── UtilityTests.cs # Utility class tests
├── UtilityTests.cs # Utility class tests (StructuralMap, FileUtils, etc.)
├── ModelTests.cs # Model validation tests
├── TestData.cs # Shared test data and helpers
├── environment/ # Test configuration files
│ └── filedatabase.json # FileDatabase test settings
└── DeepDrftTests.csproj # Project configuration
├── TestData.cs # Shared test fixtures and helpers
├── environment/
│ └── filedatabase.json # Test-config FileDatabase settings
└── DeepDrftTests.csproj
```
## Test Organization
## Test isolation strategy
### SOLID Principles Implementation
Tests follow SOLID design principles:
Each test creates a unique directory under `Path.GetTempPath() + "DeepDrftTests/{Guid}"` in `[SetUp]` and best-effort deletes it in `[TearDown]`.
- **Single Responsibility**: Each test class focuses on one component
- **Open/Closed**: Base test classes allow extension without modification
- **Liskov Substitution**: All vault implementations tested consistently
- **Interface Segregation**: Tests through abstractions where possible
- **Dependency Inversion**: Tests depend on abstractions, not concretions
### DRY Pattern with Base Classes
```csharp
// Base class eliminates test setup duplication
public abstract class MediaVaultTestBase
[SetUp]
public void SetUp()
{
protected string TestDirectory { get; private set; }
_testDatabasePath = Path.Combine(
Path.GetTempPath(),
"DeepDrftTests",
Guid.NewGuid().ToString());
Directory.CreateDirectory(_testDatabasePath);
}
[SetUp] public virtual void SetUp() { /* Common setup */ }
[TearDown] public virtual void TearDown() { /* Common cleanup */ }
[TearDown]
public void TearDown()
{
try { Directory.Delete(_testDatabasePath, recursive: true); }
catch { /* Best-effort cleanup — ignore failures */ }
}
```
## Key Test Classes
**New tests touching the filesystem must follow this pattern.** Do not write into the real `Database/Vaults` path. Do not use `[SetUp]` in base classes without allowing derived classes to override — some tests need custom setup.
## Core test classes
### FileDatabaseTests
**Core integration tests** for the main FileDatabase functionality:
- Database creation and initialization
- Vault management (create, retrieve, check existence)
- Resource operations (register, load, type safety)
- Multi-vault scenarios and error handling
Integration tests for the main FileDatabase functionality:
- Database creation and initialization at a specified path.
- Vault management (create, retrieve, check existence).
- Resource operations (register, load, type safety, edge cases).
- Multi-vault scenarios.
- Error handling (e.g., load from non-existent vault returns null).
These are the load-bearing tests. When FileDatabase semantics change, **these tests anchor the intent**. Update them in the same commit as the code change.
### MediaVaultTests
**Component tests** for individual MediaVault implementations:
- Entry storage and retrieval
- Media type handling (Audio, Image, Media)
- File path sanitization
- Index maintenance
Component tests for individual vault implementations:
- Entry storage and retrieval.
- Media type handling (Audio, Image).
- File path sanitization (regex: `[^a-zA-Z0-9]``-`).
- Index maintenance (entries added/removed from vault index).
### IndexSystemTests
**Index management** validation:
- DirectoryIndex creation and persistence
- VaultIndex type management
- JSON serialization/deserialization
- Index factory service operations
Index management validation:
- DirectoryIndex creation and persistence (JSON round-trip).
- VaultIndex type management (MediaVaultType recorded and rehydrated).
- IndexFactoryService operations.
### MediaVaultFactoryTests
**Factory pattern** validation:
- Vault creation for different MediaVaultTypes
- Path handling and directory creation
- Type-specific vault instantiation
## Test Data Management
Factory pattern validation:
- Vault creation for different `MediaVaultType` values.
- Path handling and directory creation.
- Type-specific vault instantiation.
### TestData Class
Centralized test data and helper methods:
### SimpleMediaTypeRegistryTests
Media type registry tests:
- Extension → MIME type mapping.
- Supported formats (wav, mp3, jpg, png, gif, etc.).
### UtilityTests
Utility class tests:
- `StructuralMap<TKey, TValue>` (JSON-based equality).
- `StructuralSet<T>` (set semantics with structural equality).
- `FileUtils` (async file I/O, chunked reading/writing).
### ModelTests
Model validation:
- Binary models can be created and serialized.
- DTOs round-trip correctly (serialize → deserialize).
- Metadata hierarchy is correct.
## TestData (shared fixtures)
Centralised test data and helper methods:
```csharp
public static class TestData
{
// Real PNG bytes for authentic image testing
public static readonly byte[] TestPngBytes = [...];
// Real PNG byte buffer (16x16) — reused for mock audio testing
public static readonly byte[] TestPngBytes = [/* 144 bytes */];
// Factory methods for test objects
public static ImageBinary CreateTestImageBinary(double aspectRatio = 1.0)
public static AudioBinary CreateTestAudioBinary(double duration = 120.0, int bitrate = 320)
// Factory methods
public static ImageBinary CreateTestImageBinary(double aspectRatio = 1.0);
public static AudioBinary CreateTestAudioBinary(double duration = 120.0, int bitrate = 320);
// Consistent test keys
// Named constants
public static class TestKeys
{
public const string TestImageEntry = "test";
@@ -107,111 +129,137 @@ public static class TestData
}
```
## Development Commands
**Important**: `TestData.CreateTestAudioBinary` deliberately reuses PNG bytes as a mock audio buffer. The FileDatabase does not parse audio content — it stores any bytes with the metadata. Real WAV parsing is exercised by the CLI and content host, not the FileDatabase tests.
## Run commands
### Running All Tests
```bash
# Run entire test suite
# Run all tests
dotnet test DeepDrftTests/
# Run with detailed output
dotnet test DeepDrftTests/ --verbosity normal
# Run with code coverage
# Run specific test class
dotnet test DeepDrftTests/ --filter "ClassName=FileDatabaseTests"
# Run specific test method
dotnet test DeepDrftTests/ --filter "Name=FileDatabase_CanBeCreatedAtSpecifiedLocation"
# Run with code coverage (if tooling is installed)
dotnet test DeepDrftTests/ --collect:"XPlat Code Coverage"
```
### Running Specific Tests
```bash
# Run FileDatabase tests only
dotnet test DeepDrftTests/ --filter "FileDatabaseTests"
## Testing patterns
# Run specific test method
dotnet test DeepDrftTests/ --filter "FileDatabase_CanBeCreatedAtSpecifiedLocation"
### Async test methods
# Run tests by category
dotnet test DeepDrftTests/ --filter "Category=Integration"
```
### Build and Clean
```bash
# Build test project
dotnet build DeepDrftTests/
# Clean test outputs
dotnet clean DeepDrftTests/
```
## Test Isolation Strategy
### Temporary Directory Management
Each test uses isolated temporary directories:
```csharp
_testDatabasePath = Path.Combine(Path.GetTempPath(), "DeepDrftTests", Guid.NewGuid().ToString());
```
### Setup and Cleanup Pattern
```csharp
[SetUp]
public void SetUp()
{
// Create unique test directory
Directory.CreateDirectory(_testDatabasePath);
}
[TearDown]
public void TearDown()
{
// Clean up test directory (with error tolerance)
try { Directory.Delete(_testDatabasePath, true); }
catch { /* Ignore cleanup errors */ }
}
```
## Testing Patterns
### Async Test Methods
All database operations tested with async/await:
```csharp
[Test]
public async Task FileDatabase_CanRegisterAndRetrieveAudio()
{
var fileDatabase = await FileDatabase.FromAsync(_testDatabasePath);
// ... async operations
var audio = new AudioBinary(/* params */);
var registered = await fileDatabase.RegisterResourceAsync("tracks", "test-entry", audio);
Assert.That(registered, Is.True);
var loaded = await fileDatabase.LoadResourceAsync<AudioBinary>("tracks", "test-entry");
Assert.That(loaded, Is.Not.Null);
}
```
### Type-Safe Media Testing
### Type-safe media testing
Generic type parameters ensure type safety:
```csharp
var audio = await fileDatabase.LoadResourceAsync<AudioBinary>("tracks", trackId);
Assert.That(audio, Is.Not.Null);
Assert.That(audio.Duration, Is.EqualTo(120.0));
Assert.That(audio.Bitrate, Is.EqualTo(320));
```
### Error Scenario Coverage
Tests validate error handling patterns:
- Non-existent vaults return null
- Invalid entry IDs return null
- File system errors are handled gracefully
### Error scenario coverage
## Important Testing Principles
Tests validate the error-swallowing contract:
### Authentic Test Data
Uses real PNG bytes instead of mock data for realistic testing scenarios.
```csharp
// Load from non-existent vault returns null
var result = await fileDatabase.LoadResourceAsync<AudioBinary>("non-existent", "entry");
Assert.That(result, Is.Null);
// Register to non-existent vault returns false (after creating it? depends on semantics — check code)
```
## Important testing principles
### The tests are the load-bearing documentation
When FileDatabase semantics change, especially:
- The swallow-and-return-null contract
- Entry-key sanitisation regex
- Vault-type round-trip on index
- Index-watcher reload
...the tests are where intent is anchored. Update them in the same commit as the code change.
### Authentic test data
Uses real PNG bytes instead of mock data for realistic testing scenarios. The FileDatabase doesn't care about actual audio/image content — it stores opaque buffers. But using real bytes makes the tests less misleading.
### Comprehensive coverage
### Comprehensive Coverage
Tests cover:
- Happy path scenarios
- Edge cases and error conditions
- Type safety and generics
- Async operation patterns
- File system interactions
- Happy path scenarios (create, register, load, list).
- Edge cases (empty vault, non-existent entry, sanitisation edge cases).
- Type safety and generics.
- Async operation patterns.
- File system interactions (directory creation, file I/O, index JSON round-trip).
### Performance
### Performance Considerations
Tests are designed for speed:
- Minimal test data sizes
- Parallel test execution support
- Efficient cleanup strategies
- Minimal test data sizes.
- Parallel test execution support (via temp directory isolation).
- Efficient cleanup.
When working with this test project, maintain the established patterns for test isolation, async operations, and comprehensive coverage of the FileDatabase system.
## What is NOT tested
Be honest about coverage gaps:
- `TrackService` (Web or Content).
- `TrackController` (Web or Content).
- `TrackClient` / `TrackMediaClient` (HTTP clients).
- The audio player services (streaming, seek, interop).
- Dark-mode round-trip (cookie → settings → persistent state).
- `WavOffsetService` (byte offset → new WAV stream).
- `AudioProcessor` (WAV parsing, metadata extraction).
Any planned work in those areas should consider whether tests need to land alongside. **Testing the FileDatabase thoroughly does not mean testing everything** — it means testing the part that is most likely to break.
## Development commands
```bash
# Build
dotnet build DeepDrftTests/
# Run all tests
dotnet test DeepDrftTests/
# Run with live output
dotnet test DeepDrftTests/ -v normal
# Run a single test class
dotnet test DeepDrftTests/ --filter "ClassName=FileDatabaseTests"
# Clean test artifacts
dotnet clean DeepDrftTests/
```
## Configuration
- `environment/filedatabase.json`: Test-config FileDatabase settings (copied to output at build).
- `TestData.cs`: Centralised fixtures and helpers. Use these rather than duplicating test data.
When working with this project, treat the tests as the load-bearing specification of FileDatabase behaviour. When in doubt, read the tests first — they are clearer than the code.
+99 -119
View File
@@ -1,146 +1,126 @@
# CLAUDE.md - DeepDrftWeb.Client
This file provides guidance to Claude Code (claude.ai/code) when working with the DeepDrftWeb.Client project.
Guidance for working in the DeepDrftWeb.Client project (the Blazor WebAssembly assembly).
## Project Overview
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
DeepDrftWeb.Client is a **Blazor WebAssembly** client project that provides interactive UI components for the DeepDrft music management system. It runs in the browser and communicates with the server-side DeepDrftWeb application.
## One-line purpose
## Architecture
All interactive UI for the site. Blazor WebAssembly. Pages, controls, the streaming audio player stack, theme/dark-mode plumbing, HTTP clients for both backends.
### Technology Stack
- **Blazor WebAssembly**: Client-side .NET runtime in browser
- **MudBlazor**: Material Design UI components
- **HttpClient**: API communication with server
- **ASP.NET Core 9.0**: Framework components
## Actual structure
### Project Structure
```
DeepDrftWeb.Client/
├── Pages/ # Routable page components
│ ├── Home.razor # Home page
│ ├── Counter.razor # Demo counter page
├── Weather.razor # Demo weather page
│ └── TracksView.razor # Main tracks interface
├── Controls/ # Reusable UI components
│ ├── TracksGallery.razor # Grid layout for tracks
│ └── TrackPlayer.razor # Individual track player
├── Layout/ # Layout components
│ ├── MainLayout.razor # Primary layout
│ └── NavMenu.razor # Navigation menu
├── Clients/ # HTTP API clients
├── ViewModels/ # Component view models
├── wwwroot/ # Static web assets
└── Program.cs # WebAssembly entry point
```
- `Pages/`: Routable components. `Home.razor` (hero/about), `TracksView.razor` (track gallery with pagination/sorting). **No demo pages** (`Counter.razor`, `Weather.razor` do not exist).
- `Layout/`: `MainLayout.razor` (root layout, wraps in `AudioPlayerProvider`, hosts theme switcher), `DeepDrftMenu.razor` (branded menu bar), `NavMenu.razor` (nav list), `Pages.cs` (centralised nav index — `MenuPages` for header, `AllPages` for exhaustive list).
- `Controls/`: Reusable components.
- `TrackCard.razor`: Individual track display (image, name, artist, album, genre, release date).
- `TracksGallery.razor`: Responsive grid of `TrackCard` items (MudBlazor `MudGrid` with breakpoints).
- `AppNavLink.razor`: Nav link with active-page highlight.
- `AudioPlayerProvider.razor`: Cascading host for `IPlayerService`. Everything inside it gets the player via `[CascadingParameter]`.
- `AudioPlayerBar.razor`: Dock UI at the bottom (play/pause/seek/volume).
- `SpectrumVisualizer.razor`: Bar-graph spectrum display, driven by `getSpectrumData` JS callback.
- `Services/`: Audio player + dark-mode services.
- `IPlayerService` / `IStreamingPlayerService`: Contracts exposed to UI.
- `AudioPlayerService`: Abstract base (lifecycle, initialise, select track, play/pause/stop/seek/volume).
- `StreamingAudioPlayerService`: Production implementation. Chunked stream from `TrackMediaClient`, adaptive 1664 KB buffer, early-playback, **seek-beyond-buffer** via offset request to the content API.
- `AudioInteropService`: JS interop wrapper over `window.DeepDrftAudio`. Manages `DotNetObjectReference` lifetimes for progress, end-of-playback, spectrum callbacks.
- Dark-mode services: `DarkModeServiceBase` (cookie name constant), `DarkModeCookieService` (JS cookie read/write).
- `Clients/`: HTTP API clients.
- `TrackClient`: SQL metadata API. Uses named `IHttpClientFactory` client `"DeepDrft.API"`. Async methods like `GetPageAsync(pageNumber, pageSize, sortColumn, sortDescending)``ApiResult<PagedResult<TrackEntity>>`.
- `TrackMediaClient`: Content API. Uses named `IHttpClientFactory` client `"DeepDrft.Content"`. Methods like `GetAudioStreamAsync(trackId, offset)``Stream`.
- `ViewModels/`: Component state.
- `TracksViewModel`: Scoped. Holds current page, page size, sort column, descending flag. `SetPage(pageNumber)` calls `TrackClient.GetPageAsync` and updates. Registered in `Startup.ConfigureDomainServices`.
- `Common/`: Shared utilities.
- `DarkModeSettings.cs`: `[PersistentState]`-annotated class (single source of truth for dark mode in the client). Registered scoped.
- `DDIcons.cs`: Hand-rolled SVG icons (gas-lamp lit/unlit for dark mode toggle).
- `Program.cs`: WASM entry point. Calls `Startup.ConfigureApiHttpClient`, `ConfigureContentServices`, `ConfigureDomainServices`.
- `_Imports.razor`: Global using statements and component imports.
## Key Patterns
## Two HTTP clients pattern
### MVVM Pattern
Components use ViewModels for data management and business logic separation:
```csharp
// TracksViewModel.cs - Manages tracks data and pagination
public class TracksViewModel
{
public PagedResult<TrackEntity>? Page { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; }
public string SortBy { get; set; }
public bool IsDescending { get; set; }
}
```
Both clients are configured in `Startup.cs` (static methods called from **both** server and WASM `Program.cs`):
### HTTP Client Pattern
API communication through dedicated client classes:
```csharp
// TrackClient.cs - Handles track-related API calls
public async Task<ApiResult<PagedResult<TrackEntity>>> GetPage(
int pageNumber, int pageSize, string? sortColumn = null, bool sortDescending = false)
```
- `TrackClient` uses `"DeepDrft.API"` (base address from `appsettings.json` `ApiUrls:SqlApi`). Fetches paginated metadata.
- `TrackMediaClient` uses `"DeepDrft.Content"` (base address from `appsettings.json` `ApiUrls:ContentApi`). Streams audio bytes, optionally with offset.
### Component Architecture
- **Pages**: Routable components (URL endpoints)
- **Controls**: Reusable components with parameters
- **Layout**: Shared layout structures
Both are configured with JSON serializer settings (case-insensitive property matching). The dual-client pattern keeps concerns separated: one for structured data, one for binary streaming.
## Key Components
## Audio player stack (deepest part of the codebase)
### TracksGallery.razor
Grid-based track display using MudBlazor responsive grid:
```razor
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var track in Tracks)
{
<MudItem xs="12" sm="6" md="4" lg="2" xl="2">
<TrackPlayer Track="@track" />
</MudItem>
}
</MudGrid>
```
### Contracts
- `IPlayerService`: Initialize, SelectTrack, Play, Pause, Stop, Seek, SetVolume. Sync interface.
- `IStreamingPlayerService`: Extends above. SelectTrackStreaming(track) starts the chunked stream flow.
### TrackPlayer.razor
Individual track player component with audio controls and track information display.
### Implementation
- `AudioPlayerService` (abstract base): Lifecycle. Stores current track, playback state, volume. Derived classes implement `SelectTrackStreaming` / `SelectTrackImmediate`.
- `StreamingAudioPlayerService` (production): Constructor takes `TrackMediaClient`, `AudioInteropService`, logger. `SelectTrackStreaming`:
1. Calls `TrackMediaClient.GetAudioStreamAsync(trackId, offset: 0)`.
2. `StreamingAudioPlayerService.StreamAudioAsync` reads chunks (1664 KB adaptive), pushes each via `AudioInteropService.ProcessStreamingChunkAsync` (JS interop call).
3. TypeScript `StreamDecoder` parses WAV header (first chunk), decodes subsequent chunks to `AudioBuffer`s.
4. `PlaybackScheduler` schedules buffers on Web Audio `AudioContext`.
5. Playback starts as soon as a configurable min buffer count is queued.
6. **Seek beyond buffer**: if seek target is past the decoded range, `Seek(position)` calls `TrackMediaClient.GetAudioStreamAsync(trackId, offset: byteOffset)`. Server's `WavOffsetService` synthesises a new 44-byte WAV header and streams from the offset. Player tears down and re-initialises decoder for the new stream.
### TracksView.razor
Main tracks management interface combining gallery view with pagination and sorting controls.
### Interop bridge
- `AudioInteropService.ProcessStreamingChunkAsync(chunk)` calls JS `window.DeepDrftAudio.processStreamingChunk(chunk)` and awaits the Promise.
- `AudioInteropService` also manages callback registrations for progress (fired by `PlaybackScheduler`), end-of-playback (fired by `PlaybackScheduler`), and spectrum data (fired by `SpectrumAnalyzer`). Each callback is a `DotNetObjectReference` to a delegate.
## Development Commands
### Component integration
- `AudioPlayerProvider.razor` is the cascading host. It injects `IPlayerService` (resolved to `StreamingAudioPlayerService` in DI), stores it in a cascade, and keeps it alive across navigation.
- `AudioPlayerBar.razor` is the dock UI. It cascades the player, binds buttons to `Play()` / `Pause()` / `Seek()` / `SetVolume()`, and displays current time / duration / progress bar.
- `SpectrumVisualizer.razor` calls `AudioInteropService.GetSpectrumData()` on a timer, receives bar heights, renders via MudBlazor `MudChart` or custom canvas.
- `TracksView.razor` injects `TracksViewModel` + cascaded `IPlayerService`. `PlayTrack(track)` calls `PlayerService.SelectTrack(track)` (which resolves to `StreamingAudioPlayerService.SelectTrackStreaming(track)`).
## Dark-mode plumbing
- `DarkModeSettings` (`Common/`): `[PersistentState]`-annotated class with `IsDarkMode` property. Registered scoped in `Startup.ConfigureDomainServices`. Single source of truth in the client.
- `DarkModeServiceBase`: Holds the cookie name constant (`"darkMode"`).
- `DarkModeCookieService`: Reads/writes the cookie via JS (`document.cookie` interop). Calls `DarkModeSettings.IsDarkMode = value` when the cookie changes or user toggles the button.
- Server-side `DarkModeService` (in `DeepDrftWeb`, **not here**): Reads the cookie during prerender, seeds the `DarkModeSettings` instance, rounds it through `PersistentComponentState` to the client.
- `MainLayout.razor`: Wraps entire layout in `CascadingValue` of `DarkModeSettings`, so all children see the current dark-mode state. The dark-mode toggle button (hand-rolled lit/unlit gas-lamp icon from `DDIcons.cs`) calls `DarkModeCookieService.ToggleDarkModeAsync()`.
The flow ensures the first paint uses the correct theme (no flash), and toggling the button persists the setting to a 365-day cookie.
## MVVM convention
Component state lives in ViewModels (registered scoped in DI). Components render and dispatch only.
- `TracksViewModel`: Holds page number, page size, sort column, descending flag. `SetPage(pageNumber)` is the command. `TracksView.razor` injects it and calls `SetPage`.
- New VMs go in `ViewModels/` and register in `Startup.ConfigureDomainServices`.
## Theming convention
- Bespoke `PaletteLight` / `PaletteDark` defined inline in `MainLayout.razor` (MudBlazor theme objects).
- CSS classes prefixed `deepdrft-` live in `DeepDrftWeb/wwwroot/styles/deepdrft-styles.css` (shared across server and client).
- Custom SVG icons: `Common/DDIcons.cs` (hand-rolled gas-lamp, etc.).
## Development commands
### Building
```bash
# Build WebAssembly project
# The client runs as part of the DeepDrftWeb host:
dotnet run --project DeepDrftWeb
# Watch during development (rebuilds WASM as you change .cs/.razor/.ts files):
dotnet watch run --project DeepDrftWeb
# Build just the client (for verification):
dotnet build DeepDrftWeb.Client
# Publish for production
dotnet publish DeepDrftWeb.Client -c Release
```
### Running
The client runs as part of the DeepDrftWeb host application:
```bash
# Run from DeepDrftWeb (hosts the client)
dotnet run --project DeepDrftWeb
# Run client-specific tests (if any; currently none exist):
dotnet test DeepDrftTests/
```
## Configuration
### Service Registration
Services registered in `Startup.ConfigureDomainServices()`:
```csharp
// Clients for API communication
builder.Services.AddTransient<TrackClient>();
- `Program.cs`: Entry point. Calls `Startup.ConfigureApiHttpClient` (registers named clients), `ConfigureContentServices` (same), `ConfigureDomainServices` (registers services like `TracksViewModel`, `DarkModeSettings`, `AudioPlayerService`).
- Both `Startup` methods are static and called from **both** the server `DeepDrftWeb/Program.cs` and the client `Program.cs`, ensuring prerender and runtime DI are identical.
- No `appsettings.json` in the WASM assembly — config comes from the server `appsettings.json` via HTTP or is hardcoded.
// ViewModels for component state management
builder.Services.AddTransient<TracksViewModel>();
```
## Important patterns
### HTTP Client Setup
Configured to communicate with the hosting DeepDrftWeb server:
- Uses dependency injection for HttpClient
- JSON serialization with case-insensitive property matching
- Query string building for API parameters
- **Cascading parameters**: `AudioPlayerProvider` cascades `IPlayerService`. All children (including `MainLayout` and pages) access it via `[CascadingParameter] IPlayerService Player { get; set; }`.
- **Result types**: Clients return `ApiResult<T>` from NetBlocks. UI checks `Success` before using `Value`.
- **Async/await**: All operations are async.
- **Stream consumption**: `TrackMediaClient.GetAudioStreamAsync` returns a `Stream` (not fully buffered). `StreamingAudioPlayerService` reads it in chunks to avoid memory pressure on large files.
## Important Patterns
### API Communication
All API calls use the established result pattern:
- `ApiResult<T>` for typed responses
- JSON deserialization with `JsonSerializer`
- Query string construction for GET parameters
### MudBlazor Integration
- Responsive grid system (`MudGrid`, `MudItem`)
- Breakpoint-aware layout (xs, sm, md, lg, xl)
- Material Design components throughout
### Component Parameters
Components accept parameters for data binding:
```razor
[Parameter] public List<TrackEntity> Tracks { get; set; } = [];
[Parameter] public TrackEntity Track { get; set; } = null!;
```
### State Management
ViewModels handle component state, pagination, and sorting logic, keeping components focused on presentation.
When working with this project, maintain the separation between presentation (Razor components) and logic (ViewModels/Clients), and follow the established MudBlazor patterns for responsive UI design.
When working with this project, maintain the separation between presentation (Razor components) and logic (ViewModels/Clients), follow the established audio player architecture, and respect the dark-mode round-trip (cookie → DarkModeSettings → PersistentComponentState → client).
+173
View File
@@ -0,0 +1,173 @@
# CLAUDE.md - DeepDrftWeb.Services
Guidance for working in the DeepDrftWeb.Services project (the SQL-side domain logic).
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
## One-line purpose
SQL-side domain logic for tracks. EF Core context, configurations, migrations, repository, service, design-time factory. Consumed by both `DeepDrftWeb` (the host) and `DeepDrftCli` (the admin CLI).
## Why this project exists
Separating domain logic from the host so the CLI can reuse `TrackService` / `TrackRepository` / `DeepDrftContext` without referencing the ASP.NET host. The CLI is a local admin tool, not a network client — it needs direct DB access.
**New SQL-side domain code goes here, not in `DeepDrftWeb`.**
## Layout
```
DeepDrftWeb.Services/
├── Data/
│ ├── DeepDrftContext.cs # EF DbContext
│ ├── DeepDrftContextFactory.cs # Design-time factory (hard-codes ../Database/deepdrft.db)
│ └── Configurations/
│ └── TrackConfiguration.cs # EF fluent configuration for TrackEntity
├── Migrations/ # EF-generated migrations (namespace DeepDrftWeb.Migrations)
├── Repositories/
│ └── TrackRepository.cs # Data access layer
├── TrackService.cs # Service orchestrator
└── DeepDrftWeb.Services.csproj
```
## EF DbContext and configuration
`DeepDrftContext` targets SQLite, connection string from `appsettings.json` (`ConnectionStrings:DefaultConnection`). The design-time factory (`DeepDrftContextFactory`) hard-codes `../Database/deepdrft.db` for `dotnet ef` commands, so you can run migrations locally without a full app context.
`TrackConfiguration` uses EF fluent API:
- Table name: `track` (singular)
- Columns: snake_case (`entry_key`, `track_name`, `artist`, `album`, `genre`, `release_date`, `image_path`)
- `EntryKey`: required, max 100 (the FileDatabase vault entry id)
- `TrackName`, `Artist`: required, max 200
- `Album`, `Genre`: optional, max 200 / 100
- `ReleaseDate`: optional `DateOnly`
- `ImagePath`: optional, max 500 (currently a free-form URL string; points to images vault in future)
## Controller → Service → Repository → DbContext shape
- **Service** (public contract): Takes `TrackRepository`, catches exceptions at service boundary, returns `Result` / `ResultContainer<T>`.
- **Repository** (internal): Queries the DbContext. Throws on error (service catches).
- **DbContext** (EF): Directly accessed by repository, never by service (pattern isolation).
Example:
```csharp
// TrackService.GetPaged (public)
public async Task<ResultContainer<PagedResult<TrackEntity>>> GetPaged(
int pageNumber = 1,
int pageSize = 20,
string? sortColumn = null,
bool sortDescending = false)
{
try
{
var parameters = new PagingParameters<TrackEntity>
{
Page = pageNumber,
PageSize = pageSize,
OrderBy = GetOrderExpression(sortColumn), // Maps string to LINQ expression
IsDescending = sortDescending
};
var result = await _repository.GetPagedAsync(parameters);
return ResultContainer<PagedResult<TrackEntity>>.CreatePassResult(result);
}
catch (Exception e)
{
return ResultContainer<PagedResult<TrackEntity>>.CreateFailResult(e.Message);
}
}
```
## Pagination convention
`PagingParameters<T>` holds:
- `Page` (1-based, default 1)
- `PageSize` (default 20, capped at 100)
- `OrderBy: Expression<Func<T, object>>?` (LINQ expression for sorting)
- `IsDescending`
`TrackService.GetPaged` maps a string `sortColumn` (from the API query) to an expression via a switch:
```csharp
private static Expression<Func<TrackEntity, object>> GetOrderExpression(string? sortColumn)
=> sortColumn switch
{
"TrackName" => e => e.TrackName,
"Artist" => e => e.Artist,
"Album" => e => e.Album ?? "", // Nulls sort to end
"Genre" => e => e.Genre ?? "",
"ReleaseDate" => e => e.ReleaseDate ?? DateOnly.MaxValue,
_ => e => e.Id // Default to ID
};
```
Add new sort columns by extending this switch and the corresponding column names in the API.
## EF Migration commands
Run from the solution root:
```bash
# Add a migration
dotnet ef migrations add MigrationName --project DeepDrftWeb.Services --startup-project DeepDrftWeb
# Apply to database
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 (it doesn't need the startup project).
## Migrations namespace
Migrations live in the `DeepDrftWeb.Migrations` namespace (a legacy name retained for history continuity). Migration files are auto-generated and rarely edited by hand.
## Connection string
- **Web side**: `appsettings.json``ConnectionStrings:DefaultConnection`
- **CLI side**: `environment/connections.json``CliSettings:ConnectionString`
- Both point at `../Database/deepdrft.db`
The design-time factory hard-codes the path for `dotnet ef` commands.
## Service registration
In `DeepDrftWeb/Program.cs` and `DeepDrftCli/Program.cs`:
```csharp
services.AddDbContext<DeepDrftContext>(options =>
options.UseSqlite(configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<TrackRepository>();
services.AddScoped<TrackService>();
```
## Important patterns
- **Required properties**: `EntryKey`, `TrackName`, `Artist` are `required` strings. This compile-time guarantee prevents half-built entities from reaching the database.
- **Optional properties**: `Album?`, `Genre?`, `ReleaseDate?`, `ImagePath?` are nullable. Queries must handle nulls (e.g., `Album ?? ""` in the sort expression to push nulls to end).
- **Result types**: Services return `ResultContainer<T>` or `Result` from NetBlocks. No exceptions propagate to callers — the service catches and wraps.
- **Async operations**: All database methods are async (`GetPagedAsync`, `GetByIdAsync`, etc.). Sync is not available.
## Development commands
```bash
# Build
dotnet build DeepDrftWeb.Services
# Add migration (from solution root)
dotnet ef migrations add MigrationName --project DeepDrftWeb.Services --startup-project DeepDrftWeb
# Apply migration
dotnet ef database update --project DeepDrftWeb.Services --startup-project DeepDrftWeb
# Run from CLI (which consumes this service)
dotnet run --project DeepDrftCli -- list
```
## What does NOT live here
- HTTP controllers or middleware
- Blazor components or rendering logic
- FileDatabase or binary content code (that's in `DeepDrftContent.Services`)
- Configuration for the web host (`appsettings.json` stays in `DeepDrftWeb`)
When working with this project, focus on the data layer (repository, service, EF configuration) and ensure all new SQL logic is testable and reusable by both the web host and the CLI.
+87 -117
View File
@@ -1,145 +1,115 @@
# CLAUDE.md - DeepDrftWeb
This file provides guidance to Claude Code (claude.ai/code) when working with the DeepDrftWeb project.
Guidance for working in the DeepDrftWeb project (the Blazor Web App host).
## Project Overview
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
DeepDrftWeb is the main web application using **Blazor Server/WebAssembly hybrid** architecture with **MudBlazor** UI framework. It serves as the front-end interface for the DeepDrft music management system.
## One-line purpose
## Architecture
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 in `DeepDrftWeb.Services`.**
### Technology Stack
- **Blazor Hybrid**: Interactive Server + WebAssembly components
- **MudBlazor**: Material Design UI framework
- **Entity Framework Core**: SQLite database access
- **ASP.NET Core 9.0**: Web framework
## What lives here now (only)
### Project Structure
```
DeepDrftWeb/
├── Components/ # Blazor components (.razor files)
│ ├── App.razor # Root application component
│ ├── Pages/ # Page components
│ └── _Imports.razor # Global using statements
├── Controllers/ # MVC API controllers
├── Data/ # Database layer
│ ├── DeepDrftContext.cs # EF DbContext
│ ├── Configurations/ # EF configurations
│ ├── Migrations/ # EF migrations
│ └── Repositories/ # Data access layer
├── Services/ # Business logic layer
└── Program.cs # Application entry point
```
- `Program.cs`, `Startup.cs`: HTTP host config, DI wiring, port binding.
- `Controllers/TrackController.cs`: Single controller. `GET api/track/page?pageNumber&pageSize&sortColumn&sortDescending` → service call → `ApiResultDto<PagedResult<TrackEntity>>`.
- `Services/DarkModeService.cs`: Server-side dark-mode prerender (reads `darkMode` cookie, seeds `DarkModeSettings.IsDarkMode` via `IHttpContextAccessor`, carries to WASM via `PersistentComponentState`).
- `Components/App.razor`: Root component with `@rendermode="InteractiveAuto"`. Calls `DarkModeService.InitializeAsync()` in `OnInitialized`.
- `Components/Pages/Error.razor`: Error fallback.
- `Interop/audio/`: TypeScript sources (one module per responsibility: `AudioContextManager.ts`, `StreamDecoder.ts`, `PlaybackScheduler.ts`, `SpectrumAnalyzer.ts`, `AudioPlayer.ts`, `index.ts`). Compiled to `wwwroot/js/audio/` via `Microsoft.TypeScript.MSBuild`. `tsconfig.json` **must not** be copied to output. In dev, raw `.ts` served from `/Interop/` for source-map debugging.
- `wwwroot/`: Static assets (compiled JS, CSS, fonts, images, favicons).
## Key Patterns
## What does NOT live here anymore
- `DeepDrftContext`, `TrackRepository`, `TrackService`, `Configurations/`, `Migrations/` — all moved to `DeepDrftWeb.Services`. Do not add new repositories or EF code to this project.
- Any FileDatabase code — that lives in `DeepDrftContent.Services`.
## Blazor Web App render modes
Hybrid Blazor with `AddInteractiveServerComponents()` + `AddInteractiveWebAssemblyComponents()`.
- Root component is `<Routes @rendermode="InteractiveAuto" />` from `Components/App.razor`.
- WASM render-mode loads `DeepDrftWeb.Client._Imports` as an additional assembly.
- **New routable pages go in `DeepDrftWeb.Client/Pages`, not here** — the client project owns the interactive UI.
Server-side prerender happens before WASM kicks in. Dark mode, CORS, forwarded headers, and MudBlazor setup must all tolerate this split.
## Dark-mode prerender bridge
`DarkModeService` in this project reads the `darkMode` cookie via `IHttpContextAccessor` in `App.razor`'s `OnInitialized` and seeds `DarkModeSettings.IsDarkMode`. This setting is registered in `DeepDrftWeb.Client.Startup.ConfigureDomainServices`. The setting carries over to WASM via `PersistentComponentState` in `MainLayout.razor`.
The flow ensures the first paint uses the correct theme (no flash).
## TypeScript interop pipeline
Audio interop is TypeScript, not raw JS:
- Sources live in `Interop/audio/` with one module per responsibility.
- Compiled to `wwwroot/js/` via `Microsoft.TypeScript.MSBuild`.
- `index.ts` exposes all modules onto `window.DeepDrftAudio` for Blazor to invoke.
- `tsconfig.json` configured for ES module interop and must **not** be copied to output.
- In development, raw `.ts` is served from `/Interop/` for source-map debugging.
Blazor calls TypeScript via `AudioInteropService.ts` (a JS interop wrapper in `DeepDrftWeb.Client`), which manages `DotNetObjectReference` lifetimes for progress, end-of-playback, and spectrum callbacks.
## HTTP client wiring
Mostly in `DeepDrftWeb.Client.Startup`:
- Named clients `"DeepDrft.API"` (SQL metadata) and `"DeepDrft.Content"` (binary audio).
- Base addresses passed in from `appsettings.json` (`ApiUrls:ContentApi`, `ApiUrls:SqlApi`).
- `Startup.ConfigureApiHttpClient` and `Startup.ConfigureContentServices` are static methods called from **both** the server `Program.cs` and the WASM `Program.cs` so prerender and runtime see the same DI.
Server-side `Program.cs` adds:
- MudBlazor (`AddMudServices`)
- Controllers
- Render-mode components
- SignalR tuning (if needed)
- Forwarded headers
- Calls to `Startup.ConfigureApiHttpClient` / `ConfigureContentServices` / `ConfigureDomainServices`
## Reverse-proxy support
`UseForwardedHeaders()` runs first in the pipeline. HTTPS redirect is conditionally disabled via `ForwardedHeaders:DisableHttpsRedirection` so the app can sit behind nginx without forcing HTTPS at the host level.
## The one controller
`TrackController` is thin — it just deserializes query parameters, calls `DeepDrftWeb.Services.TrackService.GetPaged`, and wraps the result:
### Service Architecture
```csharp
// Three-layer pattern: Controller → Service → Repository
TrackController TrackService TrackRepository DeepDrftContext
[HttpGet("api/track/page")]
public async Task<ActionResult<ApiResultDto<PagedResult<TrackEntity>>>> GetPage(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? sortColumn = null,
[FromQuery] bool sortDescending = false)
```
### Result Pattern
All service methods return `ResultContainer<T>` or `Result` for consistent error handling:
```csharp
public async Task<ResultContainer<TrackEntity?>> GetById(long id)
{
try
{
var track = await _repository.GetById(id);
return ResultContainer<TrackEntity?>.CreatePassResult(track);
}
catch (Exception e)
{
return ResultContainer<TrackEntity?>.CreateFailResult(e.Message);
}
}
```
If you're adding new SQL endpoints, this is the file. If you're adding new logic, that goes in `DeepDrftWeb.Services/TrackService.cs`.
### Paging Support
Uses `PagingParameters<T>` and `PagedResult<T>` from DeepDrftModels for consistent pagination:
```csharp
var parameters = new PagingParameters<TrackEntity>()
{
Page = pageNumber,
PageSize = pageSize,
OrderBy = entity => entity.TrackName,
IsDescending = sortDescending
};
```
## Development commands
## Development Commands
### Running the Application
```bash
# Run web application
# Run the web host (includes WASM from DeepDrftWeb.Client)
dotnet run --project DeepDrftWeb
# Watch for changes during development
# Watch during development
dotnet watch run --project DeepDrftWeb
```
### Entity Framework
```bash
# Add migration
dotnet ef migrations add MigrationName --project DeepDrftWeb
# Update database
dotnet ef database update --project DeepDrftWeb
# Drop database
dotnet ef database drop --project DeepDrftWeb
```
### Building
```bash
# Build project
# Build
dotnet build DeepDrftWeb
# Clean build
dotnet clean DeepDrftWeb && dotnet build DeepDrftWeb
# Add migration (run from solution root; creates in DeepDrftWeb.Services)
dotnet ef migrations add MigrationName --project DeepDrftWeb.Services --startup-project DeepDrftWeb
```
## Configuration
### Database Connection
- **Connection String**: `appsettings.json``ConnectionStrings.DefaultConnection`
- **Database Location**: `../Database/deepdrft.db` (SQLite)
- **Context**: `DeepDrftContext` with `TrackEntity` DbSet
- `appsettings.json`: `ConnectionStrings:DefaultConnection` (SQLite path), `ApiUrls:*` (backend base addresses), logging, logging config. Port binding via `Kestrel:Endpoints` or `ASPNETCORE_URLS`.
- MudBlazor theme (`MainLayout.razor` in client): bespoke light ("Charleston in the Day") and dark ("Lowcountry Summer Nights") palettes.
- No `wwwroot/` changes during normal development — TS → JS compilation is automatic.
### Service Registration
Key services registered in `Startup.ConfigureDomainServices()`:
- `DeepDrftContext` (EF DbContext)
- `TrackRepository` (Scoped)
- `TrackService` (Scoped)
## Important patterns
### HttpClient Setup
Configured for API communication with DeepDrftContent:
```csharp
builder.Services.AddHttpClient("DeepDrft.API", client =>
client.BaseAddress = new Uri(Startup.GetKestrelUrl(builder)));
```
All service calls in the controller return `ResultContainer<T>` or `Result`. The controller doesn't catch — it checks `Success` and returns 200/4xx/5xx accordingly. See `DeepDrftWeb.Services` for the contract.
## Important Notes
### Blazor Hybrid Architecture
- Server-side rendering with interactive components
- WebAssembly components from `DeepDrftWeb.Client`
- Shared assemblies for interactive modes
### MudBlazor Integration
- UI framework providing Material Design components
- Registered via `builder.Services.AddMudServices()`
### Repository Pattern
- Clean separation of concerns
- Async operations throughout
- CRUD operations for TrackEntity
### URL Configuration
Dynamic URL resolution via `Startup.GetKestrelUrl()` supporting:
- `ASPNETCORE_URLS` environment variable
- `Kestrel:Endpoints` configuration
- Development defaults (https://localhost:5001)
When working with this project, focus on maintaining the established patterns for service layer interaction, result handling, and Blazor component structure.
When working with this project, focus on the host surface (controllers, middleware, config) and prerender coordination. New domain logic goes in `DeepDrftWeb.Services`.