From 068205a84ee0e77a3eb3444071bdc43e94767c1f Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 25 May 2026 12:22:07 -0400 Subject: [PATCH] docs: update CLAUDE.md files for AuthBlocks split, DTO layer boundary, and CLI removal --- CLAUDE.md | 26 +++----- DeepDrftAPI/CLAUDE.md | 107 +++++++++++++++++++++----------- DeepDrftData/CLAUDE.md | 15 +++-- PLAN_authblocks_trackmanager.md | 2 +- 4 files changed, 91 insertions(+), 59 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 655666b..97e09be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,11 +12,10 @@ DeepDrftHome is a **net10.0** solution consisting of ten projects implementing a - **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 `InteractiveServer` render mode. Hosts all CMS Razor components and pages under `Components/Pages/Cms/`, `Components/Pages/Tracks/`, `Components/Layout/CmsLayout.razor`, and `Components/Shared/` (all inlined from the former `DeepDrftCms` RCL). Gated by AuthBlocks login and hierarchical `Admin` role authorization. All track operations (upload, metadata read/write, delete) are HTTP proxies via `ICmsTrackService` / `CmsTrackService` injected directly into Blazor components; no in-process data layer. - **DeepDrftShared.Client**: Razor Class Library. Shared Blazor components consumed by both `DeepDrftPublic` and `DeepDrftManager` for consistency across public and admin surfaces. -- **DeepDrftData**: Class library. EF Core domain logic: `DeepDrftContext`, `TrackConfiguration`, `Migrations`, `TrackRepository`, `TrackService`, `TrackManager`. Consumed by `DeepDrftContent`, `DeepDrftPublic`, and `DeepDrftCli`. -- **DeepDrftContent**: ASP.NET Core host. Dual-database authority (SQL metadata + FileDatabase binary). Seven endpoints: `GET api/track/{id}` unauthenticated streaming; `PUT api/track/{id}` vault write (ApiKey); `POST api/track/upload` upload + SQL persist (ApiKey); `DELETE api/track/{id:long}` SQL delete + vault remove (ApiKey); `GET api/track/page` paged metadata list (ApiKey); `GET api/track/meta/{id:long}` single metadata (ApiKey); `PUT api/track/meta/{id:long}` metadata update (ApiKey). -- **DeepDrftContent.Services**: Class library. The FileDatabase implementation in full (Models, Services, Utils, Abstractions, Constants), `WavOffsetService`, `AudioProcessor`, content-side `TrackService`. Consumed by hosts and tests. +- **DeepDrftData**: Class library. EF Core domain logic: `DeepDrftContext`, `TrackConfiguration`, `Migrations`, `TrackRepository`, `TrackService`, `TrackManager`. Consumed by `DeepDrftAPI`, `DeepDrftPublic`, and 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/upload` upload + SQL persist (ApiKey); `DELETE api/track/{id:long}` SQL delete + vault remove (ApiKey); `GET api/track/page` paged metadata list (ApiKey); `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-side `TrackService`. Consumed by hosts and tests. - **DeepDrftModels**: Shared contracts. `TrackEntity`, `TrackDto`, `PagingParameters`, `PagedResult`. 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`, `ApiResult`, `ApiResultDto`. @@ -56,8 +55,8 @@ The split between host projects (`DeepDrftPublic`, `DeepDrftManager`, `DeepDrftC `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. `DeepDrftContent.Services.UnifiedTrackService.UploadAsync` persists the entity to SQL via `DeepDrftData.TrackManager` and returns the persisted entity with `Id`. +1. `DeepDrftContent.TrackService.AddTrackFromWavAsync` processes WAV, generates entry GUID, stores audio in vault, returns unpersisted `TrackEntity`. +2. `DeepDrftAPI.Services.UnifiedTrackService.UploadAsync` persists the entity to SQL via `DeepDrftData.TrackManager` and returns the persisted entity with `Id`. If step 1 succeeds and step 2 fails, audio is orphaned in the vault (no rollback today). @@ -104,14 +103,8 @@ dotnet test DeepDrftTests/ --filter "ClassName=FileDatabaseTests" # Run main web application dotnet run --project DeepDrftPublic -# 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 +# Run API (dual-database authority and AuthBlocks host) +dotnet run --project DeepDrftAPI ``` ### Entity Framework (SQL Database) @@ -128,9 +121,8 @@ dotnet ef database update --project DeepDrftData --startup-project DeepDrftPubli All projects load secrets via `CredentialTools.ResolvePathOrThrow()` from gitignored `environment/` files: - `DeepDrftPublic/appsettings.json`: Logging and URL config. Secrets loaded from `environment/connections.json` (SQL connection string). -- `DeepDrftManager/appsettings.json`: Logging and URL config. Secrets loaded from `environment/apikey.json` (DeepDrftContent API key), `environment/connections.json` (SQL and Auth connection strings), `environment/authblocks.json` (AuthBlocks settings). -- `DeepDrftContent/appsettings.json`: Logging and hosting config. Secrets loaded from `environment/filedatabase.json` (FileDatabase vault path), `environment/apikey.json` (API key), `environment/connections.json` (SQL connection string). -- `DeepDrftCli/environment/connections.json`: CLI config (`ConnectionString`, `VaultPath`). Loaded via CredentialTools. +- `DeepDrftManager/appsettings.json`: Logging and URL config. Secrets loaded from `environment/api.json` (DeepDrftAPI base URL and API key). +- `DeepDrftAPI/appsettings.json`: Logging and hosting config. Secrets loaded from `environment/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 diff --git a/DeepDrftAPI/CLAUDE.md b/DeepDrftAPI/CLAUDE.md index 04779f0..a546e51 100644 --- a/DeepDrftAPI/CLAUDE.md +++ b/DeepDrftAPI/CLAUDE.md @@ -1,27 +1,28 @@ -# CLAUDE.md - DeepDrftContent +# CLAUDE.md - DeepDrftAPI -Guidance for working in the DeepDrftContent project (the binary content API host). +Guidance for working in the DeepDrftAPI project (the dual-database authority and AuthBlocks API host). See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project. ## One-line purpose -The dual-database authority for tracks: SQL metadata and FileDatabase binary. Seven endpoints expose track CRUD with upload+persist, delete+cleanup, paged listing, and metadata operations. ApiKey middleware, CORS, forwarded headers. **FileDatabase implementation lives in `DeepDrftContent.Services`; SQL services in `DeepDrftData`.** +Dual-database authority for tracks (SQL metadata + FileDatabase binary), and AuthBlocks API host (JWT auth, role/admin seed). Seven track endpoints expose CRUD with upload+persist, delete+cleanup, paged listing, and metadata operations. ApiKey middleware for track endpoints, JWT + AuthBlocks endpoints for auth. CORS, forwarded headers. **FileDatabase implementation lives in `DeepDrftContent`; SQL services in `DeepDrftData`.** ## What lives here now (only) -- `Program.cs`, `Startup.cs`: HTTP host config, DI wiring, middleware setup, port binding. +- `Program.cs`, `Startup.cs`: HTTP host config, DI wiring, middleware setup, port binding. AuthBlocks startup: `AddAuthBlocks`, `UseAuthBlocksStartupAsync`, `MapAuthBlocks`, authentication/authorization middleware. - `Services/UnifiedTrackService.cs`: Host-internal orchestrator. Coordinates vault write + SQL persist for upload (`UploadAsync`), and SQL delete + vault remove for delete (`DeleteAsync`). -- `Controllers/TrackController.cs`: Seven endpoints (see below). -- `Middleware/ApiKeyAuthenticationMiddleware.cs`, `Middleware/ApiKeyAuthorizeAttribute.cs`: ApiKey validation logic. +- `Controllers/TrackController.cs`: Seven track endpoints (see below). +- `Middleware/ApiKeyAuthenticationMiddleware.cs`, `Middleware/ApiKeyAuthorizeAttribute.cs`: ApiKey validation logic (for track endpoints only). - `Models/`: Settings POCOs only (`ApiKeySettings`, `CorsSettings`, `FileDatabaseSettings`). No domain code. - `environment/filedatabase.json`: FileDatabase vault path config (loaded via CredentialTools, not in repo). -- `environment/apikey.json`: API key (loaded via CredentialTools, not in repo, must be created locally or at deployment). -- `environment/connections.json`: SQL connection string (loaded via CredentialTools, not in repo, format: `{ "ConnectionStrings": { "DefaultConnection": "..." } }`). +- `environment/apikey.json`: API key for track endpoints (loaded via CredentialTools, not in repo, must be created locally or at deployment). +- `environment/connections.json`: SQL and Auth connection strings (loaded via CredentialTools, not in repo, format: `{ "ConnectionStrings": { "DefaultConnection": "...", "Auth": "..." } }`). +- `environment/authblocks.json`: AuthBlocks configuration (JWT secret, email sender creds, admin seed creds) (loaded via CredentialTools, not in repo). ## What does NOT live here anymore -- `FileDatabase/`, `Processors/`, media models (`AudioBinary`, `ImageBinary`, etc.), `WavOffsetService` — all in `DeepDrftContent.Services`. +- `FileDatabase/`, `Processors/`, media models (`AudioBinary`, `ImageBinary`, etc.), `WavOffsetService` — all in `DeepDrftContent` (class library). - EF Core context and repository — in `DeepDrftData`. - **Hosts only own HTTP surface and wiring.** New domain code goes in `*.Services` (shared libraries) or host-internal `Services/` folders (e.g., `UnifiedTrackService` here for dual-database orchestration). @@ -50,7 +51,7 @@ Returns the WAV bytes from the `tracks` vault with optional offset support. ### POST api/track/upload ([ApiKeyAuthorize]) -**Authenticated endpoint.** Accepts a raw WAV upload + metadata as `multipart/form-data`, processes the WAV, stores it in the vault, and persists metadata to SQL. Returns the fully persisted `TrackEntity` with `Id` populated. +**Authenticated endpoint.** Accepts a raw WAV upload + metadata as `multipart/form-data`, processes the WAV, stores it in the vault, and persists metadata to SQL. Returns the fully persisted `TrackDto` with `Id` populated. - **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`. - **Form fields**: @@ -64,7 +65,7 @@ Returns the WAV bytes from the `tracks` vault with optional offset support. - The upload stream is copied to a `.wav`-suffixed temp file under `Path.GetTempPath()` (the audio processor requires that extension and reads from disk). The temp file is always deleted in a `finally` block — success or failure. - `[RequestSizeLimit(1 GB)]` + `[RequestFormLimits(MultipartBodyLengthLimit = 1 GB)]` lift the per-request ceiling above the framework default (~28 MB) so production-sized WAVs are accepted. The body is streamed to the temp file, not buffered in memory. - Calls `UnifiedTrackService.UploadAsync`, which orchestrates: `TrackService.AddTrackFromWavAsync` (vault write) → `TrackManager` (SQL persist with `createdByUserId`). -- Returns 200 with the **persisted** `TrackEntity` JSON (Id populated) on success. Returns 400 for missing/invalid form fields. Returns 500 if processing fails. +- Returns 200 with the **persisted** `TrackDto` JSON (Id populated) on success. Returns 400 for missing/invalid form fields. Returns 500 if processing fails. ### DELETE api/track/{id:long} ([ApiKeyAuthorize]) @@ -86,7 +87,7 @@ Returns the WAV bytes from the `tracks` vault with optional offset support. - `sortColumn` (string, optional): sort field. Supported: `"TrackName"`, `"Artist"`, `"Album"`, `"Genre"`, `"ReleaseDate"`. Defaults to `Id`. - `sortDescending` (bool, optional, default false): sort direction. - Calls `ITrackService.GetPaged` (via DI), which is actually `TrackManager` from `DeepDrftData`. -- Returns 200 with `PagedResult` JSON (`Items`, `TotalCount`, `PageNumber`, `PageSize`). Returns 500 on query error. +- Returns 200 with `PagedResult` JSON (`Items`, `TotalCount`, `PageNumber`, `PageSize`). Returns 500 on query error. ### GET api/track/meta/{id:long} ([ApiKeyAuthorize]) @@ -94,8 +95,8 @@ Returns the WAV bytes from the `tracks` vault with optional offset support. - **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`. - **Route parameter `id`** (long): the SQL track ID. -- Calls `ITrackService.GetById`, which returns the track or null. -- Returns 200 with `TrackEntity` JSON on success. Returns 404 if not found. Returns 500 on query error. +- Calls `ITrackService.GetById`, which returns the track as `TrackDto` or null. +- Returns 200 with `TrackDto` JSON on success. Returns 404 if not found. Returns 500 on query error. ### PUT api/track/meta/{id:long} ([ApiKeyAuthorize]) @@ -104,8 +105,8 @@ Returns the WAV bytes from the `tracks` vault with optional offset support. - **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`. - **Route parameter `id`** (long): the SQL track ID. - **Body**: `UpdateTrackMetadataRequest` with fields: `TrackName`, `Artist`, `Album?`, `Genre?`, `ReleaseDate?`. -- Looks up SQL row by ID, updates the provided fields (nulls in the request clear optional fields), and persists via `ITrackService.Update`. -- Returns 200 on success. Returns 404 if track not found. Returns 500 on update error. +- Looks up SQL row by ID (returns `TrackDto`), updates the provided fields (nulls in the request clear optional fields), and persists the DTO via `ITrackService.Update`. +- Returns 200 with the updated `TrackDto` on success. Returns 404 if track not found. Returns 500 on update error. ## ApiKey middleware behaviour @@ -140,16 +141,20 @@ Configured in `Startup.ConfigureDomainServices()`, applied to all endpoints via 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 singletons: `WavOffsetService`, `AudioProcessor`, `TrackService` (the `DeepDrftContent.Data` version for vault operations). +5. Register singletons: `WavOffsetService`, `AudioProcessor`, `TrackService` (the `DeepDrftContent` version for vault operations). -**In `Program.cs`** (SQL + wiring): +**In `Program.cs`** (SQL + AuthBlocks + wiring): -6. Load `environment/connections.json` via `CredentialTools.ResolvePathOrThrow("connections", ...)`. -7. Register `DbContext` (scoped) with connection string from config. -8. Register scoped: `TrackRepository`, `TrackManager`, `ITrackService` (factory resolves to `TrackManager`), `UnifiedTrackService`. -9. Configure forwarded headers (production-only) for reverse proxy support. -10. Load `environment/apikey.json` and register API key middleware. -11. Configure CORS policy (`ContentApiPolicy`): AllowAnyMethod, AllowAnyHeader, AllowCredentials, specific origins from config. +6. Load `environment/connections.json` via `CredentialTools.ResolvePathOrThrow("connections", ...)` — contains both `DefaultConnection` (SQL metadata) and `Auth` (AuthBlocks Identity database). +7. **AuthBlocks startup:** Call `builder.Services.AddAuthBlocks(options => { ... })` with `Auth` connection string and settings from `AuthBlocks:*` config keys. Load `environment/authblocks.json` for JWT secret, email sender creds, and admin seed creds. This registers JWT bearer auth scheme and EF Identity. +8. Register `DbContext` (scoped) with `DefaultConnection` from config. +9. Register scoped: `TrackRepository`, `TrackManager`, `ITrackService` (factory resolves to `TrackManager`), `UnifiedTrackService`. +10. **After `app.Build()`:** Call `await app.Services.UseAuthBlocksStartupAsync()` to apply migrations and seed roles + admin user to the Auth database. +11. Configure forwarded headers (production-only) for reverse proxy support. +12. Load `environment/apikey.json` and register API key middleware. +13. Configure CORS policy (`ContentApiPolicy`): AllowAnyMethod, AllowAnyHeader, AllowCredentials, specific origins from config (includes DeepDrftManager origin for cross-origin auth calls). +14. **Map AuthBlocks endpoints:** Call `app.MapAuthBlocks()` to mount `/api/auth/*` and `/api/users/*` endpoints. Ensure `app.UseAuthentication()` and `app.UseAuthorization()` are in the middleware pipeline (required for JWT bearer auth). +15. Verify ApiKey middleware ordering: it must not interfere with JWT middleware. ApiKey gates only `[ApiKeyAuthorize]`-decorated track endpoints; JWT gates AuthBlocks endpoints. 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. SQL services are scoped (DbContext not thread-safe). @@ -159,9 +164,10 @@ Mapped in `Development` only. Swagger UI at `/swagger` for testing endpoints loc ## Configuration files -- `appsettings.json`: Logging, hosting, and CORS config. **Does not contain secrets.** +- `appsettings.json`: Logging, hosting, CORS, and AuthBlocks config. **Does not contain secrets.** - `Logging`: standard ASP.NET structure. - `CorsSettings.AllowedOrigins`: array of origin URLs allowed to call the API (required; throws on startup if missing). + - `AuthBlocks:Jwt:Issuer`, `AuthBlocks:Jwt:Audience`: JWT validation settings (loaded from `environment/authblocks.json`). - `environment/filedatabase.json` (required, loaded via CredentialTools, not in repo): ```json { @@ -182,7 +188,30 @@ Mapped in `Development` only. Swagger UI at `/swagger` for testing endpoints loc ```json { "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Database=deepdrft;Username=postgres;Password=..." + "DefaultConnection": "Host=localhost;Database=deepdrft;Username=postgres;Password=...", + "Auth": "Host=localhost;Database=deepdrft_auth;Username=postgres;Password=..." + } + } + ``` +- `environment/authblocks.json` (required, loaded via CredentialTools, not in repo): + ```json + { + "AuthBlocks": { + "Jwt": { + "Secret": "your-signing-secret-min-32-chars", + "Issuer": "https://deepdrft.api", + "Audience": "https://deepdrft.com" + }, + "Email": { + "Host": "smtp.provider.com", + "Token": "your-smtp-password" + }, + "Admin": { + "UserName": "admin", + "Email": "admin@deepdrft.com", + "Password": "initial-admin-password" + }, + "SupportEmail": "support@deepdrft.com" } } ``` @@ -190,21 +219,25 @@ Mapped in `Development` only. Swagger UI at `/swagger` for testing endpoints loc ## Development commands ```bash -# Run the content API (default https://localhost:5002) -dotnet run --project DeepDrftContent +# Run the API host (default https://localhost:5002) +dotnet run --project DeepDrftAPI # Watch during development -dotnet watch run --project DeepDrftContent +dotnet watch run --project DeepDrftAPI # Build -dotnet build DeepDrftContent +dotnet build DeepDrftAPI -# Test endpoints (requires API key in environment/apikey.json) -curl -H "ApiKey: your-secret-key" -X PUT https://localhost:5002/api/track/test-id \ +# Test track endpoints (requires API key in environment/apikey.json) +curl -H "ApiKey: your-secret-key" -X GET https://localhost:5002/api/track/page \ + -H "Accept: application/json" + +curl https://localhost:5002/api/track/test-entry-key?offset=0 + +# Test auth endpoints (AuthBlocks API) +curl -X POST https://localhost:5002/api/auth/login \ -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 + -d '{"email":"admin@deepdrft.com","password":"initial-admin-password"}' ``` ## Important patterns @@ -216,6 +249,6 @@ curl https://localhost:5002/api/track/test-id?offset=0 ## The FileDatabase import -See `DeepDrftContent.Services/CLAUDE.md` for the FileDatabase API and semantics. This host only provides the HTTP surface over it. +See `DeepDrftContent/CLAUDE.md` for the FileDatabase API and semantics. This host only provides the HTTP surface over it and the AuthBlocks authority. -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`. +When working with this project, focus on the HTTP surface (controllers, middleware, CORS, forwarded headers, AuthBlocks wiring) and the dual-database orchestration via `UnifiedTrackService`. New domain logic goes in `DeepDrftContent` (FileDatabase) or `DeepDrftData` (SQL). Keep this host focused on HTTP boundaries and wiring. diff --git a/DeepDrftData/CLAUDE.md b/DeepDrftData/CLAUDE.md index 25958a5..22d5b61 100644 --- a/DeepDrftData/CLAUDE.md +++ b/DeepDrftData/CLAUDE.md @@ -45,7 +45,7 @@ DeepDrftData/ ## Service → Repository → DbContext shape -- **Service** (`TrackManager`, implements `ITrackService`): Public contract. Takes `TrackRepository`, catches exceptions at service boundary, returns `ResultContainer`. +- **Service** (`TrackManager`, implements `ITrackService`): Public contract. Takes `TrackRepository`, catches exceptions at service boundary, returns `ResultContainer` with DTO results. - **Repository** (`TrackRepository`): Internal data access. Queries the DbContext. Throws on error (service catches). - **DbContext** (`DeepDrftContext`): EF Core. Directly accessed by repository, never by service (pattern isolation). @@ -53,7 +53,8 @@ Example: ```csharp // TrackManager.GetPaged (public via ITrackService) -public async Task>> GetPaged( +// Returns PagedResult — the repository outputs entities, the service outputs DTOs +public async Task>> GetPaged( int pageNumber = 1, int pageSize = 20, string? sortColumn = null, @@ -70,11 +71,17 @@ public async Task>> GetPaged( IsDescending = sortDescending }; var result = await _repository.GetPagedAsync(parameters, cancellationToken); - return ResultContainer>.CreatePassResult(result); + // Convert to DTO before returning + var dtoResult = new PagedResult( + result.Items.Select(TrackConverter.Convert).ToList(), + result.TotalCount, + result.PageNumber, + result.PageSize); + return ResultContainer>.CreatePassResult(dtoResult); } catch (Exception e) { - return ResultContainer>.CreateFailResult(e.Message); + return ResultContainer>.CreateFailResult(e.Message); } } ``` diff --git a/PLAN_authblocks_trackmanager.md b/PLAN_authblocks_trackmanager.md index 7559854..c84f972 100644 --- a/PLAN_authblocks_trackmanager.md +++ b/PLAN_authblocks_trackmanager.md @@ -5,7 +5,7 @@ Working plan for two connected workstreams. Standalone execution doc; not part o `CONTEXT.md §6` convention, and the `ITrackService` cross-cutting note in `PLAN.md` (line ~192) should be retired since this plan resolves it. -**Status:** awaiting Daniel's approval. Engineering is not dispatched until then. +**Status:** **Completed** — landed on `dev` (2026-05-25). ---