11 KiB
CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository.
Architecture Overview
DeepDrftHome is a net10.0 solution consisting of ten projects implementing a dual-database media management system for the DeepDrft electronic music collective, split into two independent Blazor applications: the public site (DeepDrftPublic) and the CMS (DeepDrftManager).
Core Projects
- DeepDrftPublic: ASP.NET Core host. Blazor Web App with Server + WASM render modes. Owns browser-facing proxy controller for
api/track/*(metadata listing and audio streaming), MudBlazor theme prerender, and TypeScript→JS audio interop. Public-facing site for listeners. - DeepDrftPublic.Client: Blazor WebAssembly assembly. All interactive UI (pages, player stack, dark-mode plumbing, HTTP clients for both backends). Consumed by the public site.
- DeepDrftManager: ASP.NET Core host. Blazor Web App with server-rendered
InteractiveServerrender mode. Hosts all CMS Razor components and pages underComponents/Pages/Cms/,Components/Pages/Tracks/,Components/Layout/CmsLayout.razor, andComponents/Shared/(all inlined from the formerDeepDrftCmsRCL). Gated by AuthBlocks login and hierarchicalAdminrole authorization. All track operations (upload, metadata read/write, delete) are HTTP proxies viaICmsTrackService/CmsTrackServiceinjected directly into Blazor components; no in-process data layer. - DeepDrftShared.Client: Razor Class Library. Shared Blazor components consumed by both
DeepDrftPublicandDeepDrftManagerfor consistency across public and admin surfaces. - DeepDrftData: Class library. EF Core domain logic:
DeepDrftContext,TrackConfiguration,Migrations,TrackRepository,TrackService,TrackManager. Consumed byDeepDrftAPIand tests. - DeepDrftAPI: ASP.NET Core host. Dual-database authority (SQL metadata + FileDatabase binary). AuthBlocks API host (owns registration, migration/seed, JWT endpoints). Seven track endpoints:
GET api/track/{id}unauthenticated streaming;PUT api/track/{id}vault write (ApiKey);POST api/track/uploadupload + SQL persist (ApiKey);DELETE api/track/{id:long}SQL delete + vault remove (ApiKey);GET api/track/pagepaged metadata list (unauthenticated);GET api/track/meta/{id:long}single metadata (ApiKey);PUT api/track/meta/{id:long}metadata update (ApiKey). - DeepDrftContent: Class library. The FileDatabase implementation in full (Models, Services, Utils, Abstractions, Constants),
WavOffsetService,AudioProcessor, content-sideTrackService. Consumed by hosts and tests. - DeepDrftModels: Shared contracts.
TrackEntity,TrackDto,PagingParameters<T>,PagedResult<T>. Every project references this. - 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
Dual-database approach — browser never reaches storage directly:
DeepDrftPublic.Client (WASM)
├── HttpClient "DeepDrft.API" ──► DeepDrftPublic proxy ──► DeepDrftAPI ──► EF Core / PostgreSQL (metadata)
└── HttpClient "DeepDrft.Content" ──► DeepDrftPublic proxy ──► DeepDrftAPI ──► FileDatabase / disk (binary)
Server-side (SSR): Both clients point directly at DeepDrftAPI (server-to-server, no proxy hop).
-
SQL Database (PostgreSQL): Metadata and track info via Entity Framework
- Connection string: Read from
environment/connections.jsonviaCredentialTools.ResolvePathOrThrow("connections")with keyConnectionStrings:DefaultConnection. - Entity:
TrackEntitywithId,EntryKey,TrackName,Artist,Album?,Genre?,ReleaseDate?,ImagePath? - Context:
DeepDrftContextinDeepDrftData
- Connection string: Read from
-
FileDatabase: Custom file-based binary storage system
- Location:
../Database/Vaults(configurable viafiledatabase.json) - Root contains typed MediaVaults (Media, Image, Audio)
- Each vault has a JSON
indexfile 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.
- Location:
Key Architectural Decisions
Service projects vs. host projects
The split between host projects (DeepDrftPublic, DeepDrftManager, DeepDrftContent) and *.Services class libraries (e.g., DeepDrftData, DeepDrftContent.Services) is deliberate: hosts own HTTP surface (endpoints/controllers exposed to network), config, DI wiring, and UI components; *.Services are plain class libraries holding domain logic. This separation allows multiple hosts to consume the same service implementations. Within a host, domain logic like CMS mutations lives in host-internal service classes (e.g., CmsTrackService in DeepDrftManager/Services/), injected directly into Blazor components with no in-process HTTP roundtrip.
New domain logic goes in *.Services (shared class libraries) for logic consumed by multiple hosts, or in host-internal service classes (e.g., Services/) for host-specific logic — not in controllers, which should be thin HTTP boundaries.
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:
DeepDrftContent.TrackService.AddTrackFromWavAsyncprocesses WAV, generates entry GUID, stores audio in vault, returns unpersistedTrackEntity.DeepDrftAPI.Services.UnifiedTrackService.UploadAsyncpersists the entity to SQL viaDeepDrftData.TrackManagerand returns the persisted entity withId.
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:
- Client calls
GET api/track/{id}on DeepDrftContent and receives WAV bytes as a stream (HttpCompletionOption.ResponseHeadersRead). StreamingAudioPlayerServicereads in adaptive 16–64 KB chunks, pushes each viaAudioInteropService.processStreamingChunk.- TypeScript
StreamDecoderparses WAV header, decodes chunks toAudioBuffers.PlaybackSchedulerschedules them on a Web Audio graph. - Playback starts as soon as a min buffer is queued; UI duration from parsed header (not waiting for full file).
- Seek beyond buffer: if seek target is past what's decoded, client issues
GET api/track/{id}?offset={byteOffset}. Server'sWavOffsetServiceblock-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(inDeepDrftPublic) reads the cookie and seedsDarkModeSettings.IsDarkMode, which carries into WASM render viaPersistentComponentState. Avoids "wrong theme flash" on initial paint. DarkModeSettingslives inDeepDrftPublic.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 DeepDrftPublic/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
Build and Test
# Build entire solution
dotnet build DeepDrftHome.sln
# Run all tests
dotnet test DeepDrftTests/
# Run specific test class
dotnet test DeepDrftTests/ --filter "ClassName=FileDatabaseTests"
Running Applications
# Run main web application
dotnet run --project DeepDrftPublic
# Run API (dual-database authority and AuthBlocks host)
dotnet run --project DeepDrftAPI
Entity Framework (SQL Database)
# Add migration (from solution root)
dotnet ef migrations add MigrationName --project DeepDrftData --startup-project DeepDrftPublic
# Update database
dotnet ef database update --project DeepDrftData --startup-project DeepDrftPublic
Key Configuration Files
All projects load secrets via CredentialTools.ResolvePathOrThrow() from gitignored environment/ files:
DeepDrftPublic/appsettings.json: Logging and URL config. Secrets loaded fromenvironment/api.json(DeepDrftAPI base URL viaApi:ContentApiUrl).DeepDrftManager/appsettings.json: Logging and URL config. Secrets loaded fromenvironment/api.json(DeepDrftAPI base URL viaApi:ContentApiUrland API key viaApi:ContentApiKey).DeepDrftAPI/appsettings.json: Logging and hosting config. Secrets loaded fromenvironment/filedatabase.json(FileDatabase vault path),environment/apikey.json(API key),environment/connections.json(SQL and Auth connection strings),environment/authblocks.json(AuthBlocks JWT/email/admin creds).
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
Result Pattern (NetBlocks)
Services return Result, ResultContainer<T>, or ApiResult<T> rather than throwing for expected failure modes. Catch at service boundary, surface via result.
Error Swallowing in FileDatabase
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.
Pagination
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
- Entity Framework Core 10.0.1 (PostgreSQL / Npgsql)
- 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.