Files
daniel-c-harvey 1bda2b7bea
Deploy DeepDrftManager / Build & Publish (push) Successful in 1m29s
Deploy DeepDrftPublic / Build & Publish (push) Successful in 4m7s
Deploy DeepDrftManager / Deploy (push) Successful in 1m23s
Deploy DeepDrftPublic / Deploy (push) Successful in 1m28s
docs: reflect Phase 23 SEO crawl directives as landed
2026-06-23 07:40:57 -04:00

9.9 KiB

CLAUDE.md - DeepDrftPublic

Guidance for working in the DeepDrftPublic project (the Blazor Web App host).

See the root CLAUDE.md for full architecture overview. This file covers what is specific to this project.

One-line purpose

The Blazor Web App host. Owns a browser-facing proxy controller for api/track/* (metadata and audio streaming), crawl-directive endpoints (/robots.txt + /sitemap.xml), MudBlazor theme prerender, and TypeScript→JS audio interop.

What lives here now (only)

  • Program.cs, Startup.cs: HTTP host config, DI wiring, port binding.
  • Controllers/TrackProxyController.cs: Thin proxy controller at [Route("api/track")]. Two actions: GET api/track/page (proxies paged track metadata) and GET api/track/{trackId} (proxies audio streaming without buffering, forwards offset query param for seek-beyond-buffer). Uses RegisterForDispose for clean connection cleanup.
  • Controllers/CrawlDirectiveController.cs: Second controller; serves GET /robots.txt and GET /sitemap.xml. Reads IWebHostEnvironment.IsProduction() directly (server-side only — no PersistentState bridge). Production robots.txt: Allow: / + Disallow: /FramePlayer + Disallow: /api/ + Sitemap: pointer. Non-production robots.txt: Disallow: /. Production sitemap.xml: walks GET api/release via the "DeepDrft.API" named client, emits six static roots + one <url> per release (loc = SeoOptions.BaseUrl + ReleaseRoutes.DetailHref, lastmod from ReleaseDate); resilient (partial read → well-formed roots-only doc, never 500). Non-production: sitemap returns 404. Routes automatically via MapControllers().
  • Seo/RobotsTxt.cs: Pure builder for the robots.txt body (no HTTP, no DI — composition only).
  • Seo/SitemapXml.cs: Pure builder for the sitemap XML body (no HTTP, no DI — composition only).
  • 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).

What does NOT live here anymore

  • TrackDirectDataService — deleted; no in-process data adapter.
  • DeepDrftContext, TrackRepository, TrackService, Configurations/, Migrations/ — all in DeepDrftData (consumed only by DeepDrftAPI).
  • Any FileDatabase code — that lives in DeepDrftContent.
  • EF Core registration, SQL connection string — DeepDrftPublic has no data layer.

Blazor Web App render modes

Hybrid Blazor with AddInteractiveServerComponents() + AddInteractiveWebAssemblyComponents().

  • Root component is <Routes @rendermode="InteractiveAuto" /> from Components/App.razor.
  • WASM render-mode loads DeepDrftPublic.Client._Imports as an additional assembly.
  • New routable pages go in DeepDrftPublic.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 DeepDrftPublic.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 DeepDrftPublic.Client), which manages DotNetObjectReference lifetimes for progress, end-of-playback, and spectrum callbacks.

HTTP client wiring

Dual-mode wiring in DeepDrftPublic.Client.Startup:

  • Named clients "DeepDrft.API" (SQL metadata) and "DeepDrft.Content" (binary audio).
  • Server-side (SSR prerender): Both clients point directly at DeepDrftAPI via base addresses from appsettings.json (ApiUrls:ContentApi, ApiUrls:SqlApi). These are loaded from environment/api.json (via CredentialTools.ResolvePathOrThrow) in Program.cs.
  • WASM runtime: Both clients point to HostEnvironment.BaseAddress (the Blazor host), so browser requests proxy through TrackProxyController to DeepDrftAPI.
  • Startup.ConfigureApiHttpClient and Startup.ConfigureContentServices are static methods called from both the server Program.cs and the WASM Program.cs. At runtime, WASM overrides the base address to HostEnvironment.BaseAddress.

Server-side Program.cs adds:

  • MudBlazor (AddMudServices)
  • Controllers (AddControllers() and MapControllers())
  • Render-mode components
  • SignalR tuning (if needed)
  • Forwarded headers
  • Calls to Startup.ConfigureApiHttpClient / ConfigureContentServices / ConfigureDomainServices

Request pipeline and middleware

The middleware pipeline in Program.cs is ordered as follows:

  1. UseForwardedHeaders() — reads X-Forwarded-* headers from nginx. HTTPS redirect is conditionally disabled via ForwardedHeaders:DisableHttpsRedirection so the app can sit behind a reverse proxy without forcing HTTPS at the host level.
  2. Exception handler and HTTPS redirect (production only).
  3. UseStatusCodePagesWithReExecute("/404", createScopeForStatusCodePages: true) — must sit before UseAntiforgery() so that re-executed requests (e.g. the /404 route) pass through antiforgery middleware before reaching the endpoint. Placing it after UseAntiforgery() causes an InvalidOperationException on re-execution.
  4. UseAntiforgery() — required by Blazor form handling.
  5. UseStaticFiles() — serves compiled static assets from wwwroot/ (including /js/audio/*.js compiled from TypeScript) with correct MIME types (application/javascript for .js). This must run before MapStaticAssets() to ensure production audio interop loads correctly.
  6. Cache-control middleware (dev only) — disables caching for /_framework and /_content assets.
  7. MapStaticAssets() — endpoint mapper for Blazor framework assets and remaining static content.
  8. Development-only UseStaticFiles() — serves raw TypeScript from /Interop/ for source-map debugging.
  9. MapControllers() and MapRazorComponents() — route controller and component requests.

Controllers

Controllers/ now holds two controllers. Both are thin boundaries — no domain logic, no data layer.

TrackProxyController is the audio/metadata proxy. The WASM client points both named HttpClients ("DeepDrft.API" and "DeepDrft.Content") at the Blazor host's base address, so all browser requests route through this controller to DeepDrftAPI. Server-side SSR calls DeepDrftAPI directly (server-to-server) via the same named clients — no proxy hop on the server side.

The proxy forwards public, unauthenticated routes:

  • GET api/track/page — paged metadata listing
  • GET api/track/{trackId} — WAV audio streaming (handles Range header for seek-beyond-buffer)
  • GET api/track/albums — distinct albums with counts
  • GET api/track/genres — distinct genres with counts
  • GET api/track/random — random track selection
  • GET api/track/meta/by-key/{entryKey} — metadata lookup by vault entry key
  • POST api/event/play — anonymous play-event telemetry (Phase 16; EventProxyController, [IgnoreAntiforgeryToken])
  • POST api/event/share — anonymous share-event telemetry (Phase 16; EventProxyController, [IgnoreAntiforgeryToken])

All actions use HttpCompletionOption.ResponseHeadersRead for streaming efficiency. Audio streaming registers the upstream response with HttpContext.Response.RegisterForDispose() so the stream is properly cleaned up after the response body is sent.

Development commands

# Run the web host (includes WASM from DeepDrftPublic.Client)
dotnet run --project DeepDrftPublic

# Watch during development
dotnet watch run --project DeepDrftPublic

# Build
dotnet build DeepDrftPublic

Configuration

  • appsettings.json: Dev defaults for ApiUrls:* (DeepDrftAPI base addresses), Logging:*, AllowedHosts, ForwardedHeaders. Port binding via Kestrel:Endpoints or ASPNETCORE_URLS.
  • environment/api.json: Secrets file (via CredentialTools.ResolvePathOrThrow) with production DeepDrftAPI URLs (ApiUrls:ContentApi, ApiUrls:SqlApi).
  • 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.

Important patterns

This project is a Blazor host with a proxy layer. The proxy controller is a thin HTTP boundary — no domain logic, no data layer. All domain operations happen in DeepDrftAPI; this controller only forwards public, unauthenticated track routes. When working here, focus on the render surface (components, middleware, config), prerender coordination, and keeping the proxy transparent. New domain logic belongs in DeepDrftData / DeepDrftAPI.