diff --git a/DeepDrftHome.sln b/DeepDrftHome.sln
index b214d54..74b85ff 100644
--- a/DeepDrftHome.sln
+++ b/DeepDrftHome.sln
@@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Data", "Dee
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftCms", "DeepDrftCms\DeepDrftCms.csproj", "{81F1D47F-F892-45FB-9E35-D7775805FFD3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftManager", "DeepDrftManager\DeepDrftManager.csproj", "{E50071B2-A59F-4FB7-A435-5D966C538DDD}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -150,6 +152,18 @@ Global
{81F1D47F-F892-45FB-9E35-D7775805FFD3}.Release|x64.Build.0 = Release|Any CPU
{81F1D47F-F892-45FB-9E35-D7775805FFD3}.Release|x86.ActiveCfg = Release|Any CPU
{81F1D47F-F892-45FB-9E35-D7775805FFD3}.Release|x86.Build.0 = Release|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|x64.Build.0 = Debug|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|x86.Build.0 = Debug|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|x64.ActiveCfg = Release|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|x64.Build.0 = Release|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|x86.ActiveCfg = Release|Any CPU
+ {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/DeepDrftManager/Components/App.razor b/DeepDrftManager/Components/App.razor
new file mode 100644
index 0000000..3f117cd
--- /dev/null
+++ b/DeepDrftManager/Components/App.razor
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DeepDrftManager/Components/Routes.razor b/DeepDrftManager/Components/Routes.razor
new file mode 100644
index 0000000..0102731
--- /dev/null
+++ b/DeepDrftManager/Components/Routes.razor
@@ -0,0 +1,19 @@
+
+
+
+
+ @{
+ NavigationManager.NavigateTo($"account/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
+ }
+
+
+
+
+
+ Not found
+ Sorry, there's nothing at this address.
+
+
+
+@inject NavigationManager NavigationManager
diff --git a/DeepDrftManager/Components/_Imports.razor b/DeepDrftManager/Components/_Imports.razor
new file mode 100644
index 0000000..d2fb54e
--- /dev/null
+++ b/DeepDrftManager/Components/_Imports.razor
@@ -0,0 +1,8 @@
+@using Microsoft.AspNetCore.Authorization
+@using Microsoft.AspNetCore.Components.Authorization
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using MudBlazor
+@using DeepDrftManager
+@using DeepDrftManager.Components
diff --git a/DeepDrftManager/DeepDrftManager.csproj b/DeepDrftManager/DeepDrftManager.csproj
new file mode 100644
index 0000000..78c0b25
--- /dev/null
+++ b/DeepDrftManager/DeepDrftManager.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DeepDrftManager/Program.cs b/DeepDrftManager/Program.cs
new file mode 100644
index 0000000..e72ab3f
--- /dev/null
+++ b/DeepDrftManager/Program.cs
@@ -0,0 +1,189 @@
+using AuthBlocksLib;
+using AuthBlocksLib.Options;
+using DeepDrftCms;
+using DeepDrftData;
+using DeepDrftData.Data;
+using DeepDrftData.Repositories;
+using DeepDrftManager.Components;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.EntityFrameworkCore;
+using MudBlazor.Services;
+using NetBlocks.Utilities.Environment;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Required credential files — must exist before the app will start.
+// Production secrets stay gitignored; the *.example.json templates at the project root show the shape.
+// - environment/apikey.json: { "DeepDrftContent": { "ApiKey": "..." } }
+// - environment/connections.json: { "ConnectionStrings": { "DefaultConnection": "...", "Auth": "..." } }
+// - environment/authblocks.json: { "AuthBlocks": { "Jwt": {...}, "Email": {...}, "Admin": {...} } }
+// Content API key — not consumed by this host in Phase 1. Required by the CredentialTools
+// pattern (the file must exist); will be used by CmsUploadController when it migrates here.
+var apiKeyPath = CredentialTools.ResolvePathOrThrow("apikey", "environment/apikey.json");
+builder.Configuration.AddJsonFile(apiKeyPath, optional: false, reloadOnChange: false);
+
+var connectionsPath = CredentialTools.ResolvePathOrThrow("connections", "environment/connections.json");
+builder.Configuration.AddJsonFile(connectionsPath, optional: false, reloadOnChange: false);
+
+var authBlocksPath = CredentialTools.ResolvePathOrThrow("authblocks", "environment/authblocks.json");
+builder.Configuration.AddJsonFile(authBlocksPath, optional: false, reloadOnChange: false);
+
+// MudBlazor.
+builder.Services.AddMudServices();
+
+// CMS-specific services (currently a no-op placeholder; reserved for future RCL additions).
+builder.Services.AddCmsServices();
+
+// SQL metadata domain — DbContext + repository + manager. The CMS pages inject ITrackService
+// and resolve the same scoped TrackManager instance, so the DTO and entity surfaces share state.
+builder.Services.AddDbContext(options =>
+ options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
+
+builder.Services
+ .AddScoped()
+ .AddScoped()
+ .AddScoped(sp => sp.GetRequiredService());
+
+// AuthBlocks: JWT Bearer auth, Identity, EF schema, admin seeding.
+// Auth schema runs in its own database (separate from DefaultConnection by design).
+builder.Services.AddAuthBlocks(options =>
+{
+ options.ConnectionString = builder.Configuration.GetConnectionString("Auth")
+ ?? throw new InvalidOperationException("ConnectionStrings:Auth is required");
+ options.ApplicationName = "DeepDrft";
+ options.SupportEmail = builder.Configuration["AuthBlocks:SupportEmail"] ?? "admin@deepdrft.com";
+
+ options.JwtSettings.Secret = builder.Configuration["AuthBlocks:Jwt:Secret"]
+ ?? throw new InvalidOperationException("AuthBlocks:Jwt:Secret is required");
+ options.JwtSettings.Issuer = builder.Configuration["AuthBlocks:Jwt:Issuer"]
+ ?? throw new InvalidOperationException("AuthBlocks:Jwt:Issuer is required");
+ options.JwtSettings.Audience = builder.Configuration["AuthBlocks:Jwt:Audience"]
+ ?? throw new InvalidOperationException("AuthBlocks:Jwt:Audience is required");
+
+ options.EmailConnection.Host = builder.Configuration["AuthBlocks:Email:Host"]
+ ?? throw new InvalidOperationException("AuthBlocks:Email:Host is required");
+ options.EmailConnection.Token = builder.Configuration["AuthBlocks:Email:Token"]
+ ?? throw new InvalidOperationException("AuthBlocks:Email:Token is required");
+
+ options.AdminUserSettings = new AdminUserSettings
+ {
+ UserName = builder.Configuration["AuthBlocks:Admin:UserName"]
+ ?? throw new InvalidOperationException("AuthBlocks:Admin:UserName is required"),
+ Email = builder.Configuration["AuthBlocks:Admin:Email"]
+ ?? throw new InvalidOperationException("AuthBlocks:Admin:Email is required"),
+ Password = builder.Configuration["AuthBlocks:Admin:Password"]
+ ?? throw new InvalidOperationException("AuthBlocks:Admin:Password is required")
+ };
+});
+
+// AuthBlocksWeb: server-side cascading auth state plus the JWT client services used by the
+// /account/login + /account/logout Razor pages that ship in the AuthBlocksWeb RCL.
+var baseUrl = GetKestrelUrl(builder);
+AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl);
+
+// Named HttpClient used by CMS pages for delete/upload calls.
+// Phase 1: points at DeepDrftWeb (https://localhost:5001) where the CMS mutation controllers
+// (CmsUploadController, CmsEditController, CmsDeleteController) currently live.
+// When those controllers migrate to DeepDrftManager, update ApiUrls:ApiHost to this host's URL.
+var apiHostUrl = builder.Configuration["ApiUrls:ApiHost"]
+ ?? throw new InvalidOperationException("ApiUrls:ApiHost is required");
+builder.Services.AddHttpClient("DeepDrft.API", client =>
+{
+ client.BaseAddress = new Uri(apiHostUrl);
+});
+
+// Reverse-proxy support (nginx in production).
+builder.Services.Configure(options =>
+{
+ options.ForwardedHeaders =
+ ForwardedHeaders.XForwardedFor |
+ ForwardedHeaders.XForwardedProto |
+ ForwardedHeaders.XForwardedHost;
+ options.KnownNetworks.Clear();
+ options.KnownProxies.Clear();
+});
+
+// Controllers: no-op until CMS mutation controllers migrate from DeepDrftWeb, but registered
+// now so they are discovered automatically when they arrive. Matches DeepDrftWeb precedent.
+builder.Services.AddControllers();
+
+// InteractiveServer only — no WASM render mode on the CMS host.
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents();
+
+// SignalR tuning for InteractiveServer circuits. Long-running CMS operations (upload progress,
+// large table loads) benefit from tighter keepalive and detailed errors in dev.
+builder.Services.AddSignalR(options =>
+{
+ if (builder.Environment.IsDevelopment())
+ {
+ options.EnableDetailedErrors = true;
+ options.KeepAliveInterval = TimeSpan.FromSeconds(10);
+ options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
+ }
+});
+
+var app = builder.Build();
+
+// Apply AuthBlocks EF migrations, seed system roles, seed admin user on first boot.
+await app.Services.UseAuthBlocksStartupAsync();
+
+app.UseForwardedHeaders();
+
+if (!app.Environment.IsDevelopment())
+{
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
+ app.UseHsts();
+
+ var disableHttpsRedirection = app.Configuration.GetValue("ForwardedHeaders:DisableHttpsRedirection");
+ if (!disableHttpsRedirection)
+ {
+ app.UseHttpsRedirection();
+ }
+}
+
+app.UseAuthentication();
+app.UseAuthorization();
+app.UseAntiforgery();
+
+app.MapStaticAssets();
+
+// Mount AuthBlocks API surface (/api/auth/*, /api/users/*, etc.) and the AuthBlocksWeb
+// Razor pages (/account/login, /account/logout).
+app.MapAuthBlocks();
+
+// No-op today; picks up CMS mutation controllers when they migrate from DeepDrftWeb.
+app.MapControllers();
+
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode()
+ .AddAdditionalAssemblies(
+ typeof(DeepDrftCms._Imports).Assembly,
+ typeof(AuthBlocksWeb._Imports).Assembly);
+
+app.Run();
+
+// Local helper — mirrors DeepDrftWeb.Startup.GetKestrelUrl. Kept inline because this host's
+// only consumer is right here; promoting to a shared library would be premature.
+static string GetKestrelUrl(WebApplicationBuilder builder)
+{
+ var urls = builder.Configuration["ASPNETCORE_URLS"]
+ ?? builder.Configuration["urls"];
+
+ if (!string.IsNullOrEmpty(urls))
+ {
+ return urls.Split(';')[0].Trim();
+ }
+
+ var firstEndpoint = builder.Configuration.GetSection("Kestrel:Endpoints").GetChildren().FirstOrDefault();
+ var endpointUrl = firstEndpoint?["Url"];
+
+ if (!string.IsNullOrEmpty(endpointUrl))
+ {
+ return endpointUrl;
+ }
+
+ return builder.Environment.IsDevelopment()
+ ? "https://localhost:5004"
+ : "http://localhost:5000";
+}
diff --git a/DeepDrftManager/apikey.example.json b/DeepDrftManager/apikey.example.json
new file mode 100644
index 0000000..9300155
--- /dev/null
+++ b/DeepDrftManager/apikey.example.json
@@ -0,0 +1,5 @@
+{
+ "DeepDrftContent": {
+ "ApiKey": "your-secret-api-key-here"
+ }
+}
diff --git a/DeepDrftManager/appsettings.json b/DeepDrftManager/appsettings.json
new file mode 100644
index 0000000..3353dcd
--- /dev/null
+++ b/DeepDrftManager/appsettings.json
@@ -0,0 +1,15 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ApiUrls": {
+ "ApiHost": "https://localhost:5001"
+ },
+ "ForwardedHeaders": {
+ "DisableHttpsRedirection": false
+ }
+}
diff --git a/DeepDrftManager/authblocks.example.json b/DeepDrftManager/authblocks.example.json
new file mode 100644
index 0000000..1cf53de
--- /dev/null
+++ b/DeepDrftManager/authblocks.example.json
@@ -0,0 +1,19 @@
+{
+ "AuthBlocks": {
+ "SupportEmail": "admin@deepdrft.com",
+ "Jwt": {
+ "Secret": "your-jwt-secret-here",
+ "Issuer": "https://deepdrft.com",
+ "Audience": "deepdrft-users"
+ },
+ "Email": {
+ "Host": "smtp.your-provider.com",
+ "Token": "your-email-token-here"
+ },
+ "Admin": {
+ "UserName": "admin",
+ "Email": "admin@deepdrft.com",
+ "Password": "your-admin-password-here"
+ }
+ }
+}
diff --git a/DeepDrftManager/connections.example.json b/DeepDrftManager/connections.example.json
new file mode 100644
index 0000000..d411981
--- /dev/null
+++ b/DeepDrftManager/connections.example.json
@@ -0,0 +1,6 @@
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=localhost;Port=5433;Database=deepdrft;Username=postgres;Password=your-password-here",
+ "Auth": "Host=localhost;Port=5433;Database=deepdrft_auth;Username=postgres;Password=your-password-here"
+ }
+}