docs(design): two-app split — public site vs. CMS
design/TWO-APP-SPLIT.md proposes splitting the entangled Blazor host into independent public and CMS apps to escape the layout/cascading-auth deadlock. Ten open questions at §10. PLAN.md gains an in-flight pointer.
This commit is contained in:
@@ -6,6 +6,12 @@ Organised by **theme**, not by date. Themes are roughly ordered by current produ
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## In-flight — Two-app architectural split
|
||||||
|
|
||||||
|
The public site and the CMS are being split into two independent Blazor applications. Design doc: `design/TWO-APP-SPLIT.md`. Open questions pending Daniel's answers at `design/TWO-APP-SPLIT.md §10`. Until those resolve, no implementation phases are scheduled here — see the design doc's §8 for the phased rollout plan that will populate this section once unblocked. Supersedes the host-shape pieces of `CMS-PLAN.md §2`; the CMS feature waves in `CMS-PLAN.md` survive unchanged and move to the new host.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 0. Baseline — what just landed
|
## 0. Baseline — what just landed
|
||||||
|
|
||||||
A two-part audit (design + streaming) ran on 2026-05-17 and the fixes for Critical, Major, and Minor findings are now on `dev`. The remainder of this plan assumes that baseline. In summary the audit-pass fixed:
|
A two-part audit (design + streaming) ran on 2026-05-17 and the fixes for Critical, Major, and Minor findings are now on `dev`. The remainder of this plan assumes that baseline. In summary the audit-pass fixed:
|
||||||
|
|||||||
@@ -0,0 +1,514 @@
|
|||||||
|
# TWO-APP-SPLIT.md — Public site / CMS architectural split
|
||||||
|
|
||||||
|
**Status:** Design draft. Daniel has committed to the split; this document is the shape, not the decision. Implementation is for staff-engineer once Daniel signs off on the open questions at §10.
|
||||||
|
|
||||||
|
**Companions:** `CONTEXT.md` (current architecture orientation), `PLAN.md` (forward roadmap), `CMS-PLAN.md` (CMS roadmap — most of its shape survives; the host-of-record changes).
|
||||||
|
|
||||||
|
**Cross-link from `PLAN.md`:** add a top-level entry "**Two-app split**" pointing here once Daniel signs off; the items below supersede pieces of `CMS-PLAN.md §2` and `§3`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Why this design exists
|
||||||
|
|
||||||
|
### 1.1 The proximate problem
|
||||||
|
|
||||||
|
The current `DeepDrftWeb` host is **one** ASP.NET Core Blazor Web App serving both:
|
||||||
|
|
||||||
|
- The **public site** (`Home`, `TracksView`, the audio player stack) — anonymous, must prerender for first-paint quality, lives in the `DeepDrftWeb.Client` WASM assembly.
|
||||||
|
- The **CMS** (`/cms/*`, track admin) — `Admin`-gated via AuthBlocks, runs `InteractiveServer`, lives in the `DeepDrftCms` RCL referenced by `DeepDrftWeb`.
|
||||||
|
|
||||||
|
Both surfaces share:
|
||||||
|
|
||||||
|
- The same root `App.razor` → `Routes.razor` → `MainLayout.razor` chain.
|
||||||
|
- The same global `AddCascadingAuthenticationState` registration (server-side, from `AuthBlocksWeb.Startup.ConfigureAuthServices`).
|
||||||
|
- The same DI scope during prerender.
|
||||||
|
|
||||||
|
Empirically (verified this session): the moment **any** `[Inject]` property is added to `MainLayout.razor` while cascading-auth-state is registered globally, the public Home request hangs forever — `LayoutView` initialisation never completes. Strip every `[Inject]` and the page renders. The reduction in tree state today (`MainLayout.razor` is one line of `<div>@Body</div>` plus a single `IJSRuntime` injection — and the page still hangs with that one inject) is the surviving evidence: the layout has been gutted as a band-aid and the chrome is currently gone from the public site.
|
||||||
|
|
||||||
|
Root cause: the public layout takes a cascading auth state it has no business consuming, and the prerender DI scope for an anonymous, presentation-only page is now coupled to the auth subsystem's scoped services. That coupling is the deadlock.
|
||||||
|
|
||||||
|
### 1.2 Why band-aids stopped working
|
||||||
|
|
||||||
|
Every patch attempted this session has been a rendermode reshuffle around the same coupling:
|
||||||
|
|
||||||
|
- `@rendermode` migrating across `App` → `Routes` → `MainLayout` → individual pages.
|
||||||
|
- `[AllowAnonymous]` sprinkled across public pages to suppress the auth funnel.
|
||||||
|
- The `JwtAuthenticationStateProvider` rewritten to be prerender-safe.
|
||||||
|
- `Routes.razor` moved between the server assembly and the WASM assembly (commit `d31a08b` moved it server-side so the router could discover `AuthBlocksWeb` types; that move is what re-crossed the wires).
|
||||||
|
- Bumping `Cerebellum.AuthBlocks*` from 10.3.31 → 10.3.32 chasing prerender-safety fixes upstream.
|
||||||
|
|
||||||
|
None of these address the architectural shape: **two products, one host, one DI scope, one cascading-auth tree**. The fix is structural.
|
||||||
|
|
||||||
|
### 1.3 The fix Daniel chose
|
||||||
|
|
||||||
|
Two independent ASP.NET Core applications:
|
||||||
|
|
||||||
|
- **Public site** — customer-facing, anonymous, prerender-first, MudBlazor + (optionally) BlazorBlocks. No AuthBlocks. No cascading auth. No `@rendermode` chaos.
|
||||||
|
- **CMS** — staff-facing, fully authenticated via AuthBlocks, built on BlazorBlocks scaffolding. `InteractiveServer` end-to-end. Stealth-routed (`404` to unauthorized callers).
|
||||||
|
|
||||||
|
Each app gets its own host project, its own DI graph, its own deploy unit, its own URL surface. The two share storage and shared models, not Blazor plumbing.
|
||||||
|
|
||||||
|
> "Cramming these two usecases into the same blazor server host is the source of YOUR and MY confusion and all this wasted time." — Daniel, this session.
|
||||||
|
|
||||||
|
The hard constraints flowing into the design:
|
||||||
|
|
||||||
|
1. **Public site MUST prerender.** "Abandoning the pre-rendering benefits for the public portion of the site is exactly the WRONG decision." (Daniel.) First paint speed and SEO/share-card quality are first-class. This rules out a SPA-only or `InteractiveServer`-only public site.
|
||||||
|
2. **CMS gets AuthBlocks + BlazorBlocks.** The substrate is already chosen — design the host around it, not around what would be theoretically prettier.
|
||||||
|
3. **The dual-database storage layer does not change.** `DeepDrftWeb.Services` (EF Core / Postgres), `DeepDrftContent.Services` (FileDatabase), `DeepDrftContent` host, the streaming substrate — all stable, all stay.
|
||||||
|
4. **The single-source-of-truth rule survives.** The CMS list and the public gallery render the same `PagedResult<TrackEntity>`. Two hosts is not permission to fork the view model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Two-app shape
|
||||||
|
|
||||||
|
### 2.1 Recommended project layout
|
||||||
|
|
||||||
|
Working names below. Final naming is at §10 Q1.
|
||||||
|
|
||||||
|
```
|
||||||
|
DeepDrftSite NEW. Public-site host (ASP.NET Core, Blazor Web App).
|
||||||
|
Anonymous. Prerender + InteractiveAuto. MudBlazor.
|
||||||
|
No AuthBlocks. Owns api/track/page (see §5) and the
|
||||||
|
TypeScript audio interop.
|
||||||
|
|
||||||
|
DeepDrftSite.Client NEW. Public-site WASM assembly. Promoted from today's
|
||||||
|
DeepDrftWeb.Client. Contains Home, TracksView, the audio
|
||||||
|
player stack, dark-mode plumbing, HTTP clients.
|
||||||
|
|
||||||
|
DeepDrftCmsHost NEW. CMS host (ASP.NET Core, Blazor Server-only).
|
||||||
|
AuthBlocks + BlazorBlocks. InteractiveServer end-to-end.
|
||||||
|
References the existing DeepDrftCms RCL.
|
||||||
|
|
||||||
|
DeepDrftCms EXISTING (RCL). CMS pages, layouts, components. Becomes
|
||||||
|
consumed by DeepDrftCmsHost only. Stops being referenced
|
||||||
|
from the public host.
|
||||||
|
|
||||||
|
DeepDrftShared.Client NEW (RCL). Shared Razor components and client-side helpers
|
||||||
|
that BOTH apps render: TrackCard, TracksGallery (read-mode),
|
||||||
|
DDIcons, palette tokens, font loading helpers, audio player
|
||||||
|
stack (if the CMS plays previews — see §3.4 open question).
|
||||||
|
See §3 for what does and does not belong here.
|
||||||
|
|
||||||
|
DeepDrftWeb RETIRED. Functionality split across DeepDrftSite and
|
||||||
|
DeepDrftCmsHost. Project file removed from solution.
|
||||||
|
|
||||||
|
DeepDrftWeb.Client RETIRED. Promoted/renamed to DeepDrftSite.Client.
|
||||||
|
|
||||||
|
DeepDrftData EXISTING. EF Core / Postgres / TrackManager / TrackRepository.
|
||||||
|
Referenced by whichever host owns the metadata API (see §5)
|
||||||
|
and by the CMS host for write paths.
|
||||||
|
|
||||||
|
DeepDrftContent EXISTING. Binary content API. Unchanged.
|
||||||
|
DeepDrftContent.Data EXISTING. FileDatabase + audio processing. Unchanged.
|
||||||
|
|
||||||
|
DeepDrftModels EXISTING. Shared contracts. Unchanged.
|
||||||
|
|
||||||
|
DeepDrftCli EXISTING but retired per CMS-PLAN §8 once split lands.
|
||||||
|
Not relevant to the split itself.
|
||||||
|
|
||||||
|
DeepDrftTests EXISTING. May gain a few smoke tests around the new hosts
|
||||||
|
(out of scope here; flag for staff-engineer).
|
||||||
|
```
|
||||||
|
|
||||||
|
**One canonical solution file.** Today the repo carries `DeepDrftHome.sln` plus stray `WebAPI.sln`, `WebUI.sln`, `CLI.sln`. The split is the opportunity to delete the strays; keep `DeepDrftHome.sln` as the single solution.
|
||||||
|
|
||||||
|
### 2.2 Naming — flagged as Daniel's call
|
||||||
|
|
||||||
|
- `DeepDrftSite` vs. `DeepDrftPublic` vs. `DeepDrftWeb` (renaming the existing host instead of retiring it). Recommendation: **rename `DeepDrftWeb` → `DeepDrftSite`** rather than retiring it and standing up a new project. The current `DeepDrftWeb` already has the TypeScript pipeline, `wwwroot`, the `App.razor` host, the `MainLayout` history, and the deploy script entries. Renaming preserves that history; standing up a fresh project loses it. **Counter-argument:** the rename is a heavy git-log churn moment and doesn't compose with the band-aid commits already on `dev` — see §8.
|
||||||
|
- `DeepDrftCmsHost` vs. `DeepDrftCms.Host` vs. just promoting `DeepDrftCms` to be the host directly (changing its Sdk from `Microsoft.NET.Sdk.Razor` to `Microsoft.NET.Sdk.Web`). Recommendation: **new project `DeepDrftCmsHost`**, keep `DeepDrftCms` as an RCL. Reason: an RCL+host pair preserves the option to mount the CMS into a different host later (e.g. embedding CMS into an admin-portal aggregator host); collapsing them forecloses that.
|
||||||
|
- `DeepDrftShared.Client` vs. `DeepDrftUi.Shared` vs. `DeepDrftWeb.Shared`. Recommendation: **`DeepDrftShared.Client`** — clearly an RCL, clearly client-side. Other names invite ambiguity about whether it can hold server-only code.
|
||||||
|
|
||||||
|
All three are Q1 in §10.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Shared layer policy
|
||||||
|
|
||||||
|
The split is only useful if shared concerns stay shared. The risk is duplication or, worse, two diverging copies of the audio player.
|
||||||
|
|
||||||
|
### 3.1 What stays in `DeepDrftModels` (unchanged)
|
||||||
|
|
||||||
|
`TrackEntity`, `TrackDto`, `PagedResult<T>`, `PagingParameters<T>`, `ApiResultDto<T>`. The split does not touch the contracts layer.
|
||||||
|
|
||||||
|
### 3.2 What stays in `DeepDrftData` / `DeepDrftContent.Data` (unchanged)
|
||||||
|
|
||||||
|
The EF Core domain layer and the FileDatabase implementation are class libraries today; they don't care which host references them. The CMS host references both for writes; whichever host owns the metadata API (§5) references `DeepDrftData` for reads.
|
||||||
|
|
||||||
|
### 3.3 What moves to `DeepDrftShared.Client` (new RCL)
|
||||||
|
|
||||||
|
Things both apps render and want to keep visually consistent:
|
||||||
|
|
||||||
|
- **`TrackCard.razor`** — both the public gallery and the CMS list render tracks. Today's CMS list (CMS Wave 1) renders a table; a future CMS view could embed `TrackCard` previews, and the public-side card must not drift visually. Move it here.
|
||||||
|
- **`TracksGallery.razor`** — used by the public gallery today. The CMS does not need the gallery layout, but if any CMS preview surface ever wants it (e.g. "preview as listener sees it"), pre-shared is cheaper than retroactive extraction.
|
||||||
|
- **`DDIcons.cs`** — hand-rolled icons, currently in `DeepDrftWeb.Client/Common/`. Both apps will want the lit/unlit gas-lamp, the brand logo, etc.
|
||||||
|
- **Palette tokens and CSS variables.** Today `deepdrft-styles.css` lives in `DeepDrftWeb/wwwroot/styles/`. Move the **palette layer** (the `:root` and `.deepdrft-theme-dark` custom properties) into a shared CSS file in `DeepDrftShared.Client/wwwroot/` so MudBlazor themes in both apps map to the same tokens. Page-specific CSS (home page, gallery scoped styles) stays in the public app.
|
||||||
|
- **MudBlazor palette objects.** Currently inline in `MainLayout.razor`. Promote to a `DeepDrftPalettes.cs` (static `MudTheme Light` / `MudTheme Dark`) in the shared RCL. The CMS host applies the same palettes so the staff side reads as the same product.
|
||||||
|
- **Font-loading `<link>` helpers.** A `DeepDrftFonts` static or `<DeepDrftFontLinks />` component so both apps emit the same Google Fonts request.
|
||||||
|
|
||||||
|
### 3.4 Audio player stack — open question
|
||||||
|
|
||||||
|
The player stack today lives entirely in `DeepDrftWeb.Client/`:
|
||||||
|
|
||||||
|
- `IPlayerService`, `IStreamingPlayerService`, `AudioPlayerService`, `StreamingAudioPlayerService`, `AudioInteropService`, `StreamingErrorHandler`.
|
||||||
|
- `AudioPlayerProvider.razor`, `AudioPlayerBar.razor`, `PlayerControls.razor`, `TimestampLabel.razor`, `VolumeControls.razor`, `SpectrumVisualizer.razor`.
|
||||||
|
- `TrackMediaClient` (HTTP client for `DeepDrftContent`).
|
||||||
|
|
||||||
|
The public site needs all of it. Whether the **CMS** needs it is the open question (Q4 in §10):
|
||||||
|
|
||||||
|
- **Option A — Public site only.** Audio player stack moves into `DeepDrftSite.Client`. The CMS never previews audio in-browser. CMS users wanting to verify an upload navigate to the public site in another tab. **Simplest.**
|
||||||
|
- **Option B — Shared via `DeepDrftShared.Client`.** The audio player stack moves into the shared RCL and is consumed by both apps. The CMS detail page (`/cms/tracks/{id}`) gains an in-place preview player. **More work; modestly more useful.**
|
||||||
|
|
||||||
|
Recommendation: **Option A in Wave 1, leave Option B as a future move.** The audio player carries non-trivial complexity (streaming, seek, spectrum, dark-mode-aware visualiser, `DotNetObjectReference` lifetimes) and consolidating it into a shared RCL for a CMS use case that isn't on the immediate roadmap is unnecessary work today. If a future "preview in CMS" need arises, the player moves to shared then — its surface is already abstracted behind `IPlayerService`.
|
||||||
|
|
||||||
|
### 3.5 TypeScript interop — stays with the public host
|
||||||
|
|
||||||
|
`DeepDrftWeb/Interop/audio/*.ts` and its `Microsoft.TypeScript.MSBuild` pipeline ship with the audio player. Per §3.4 Option A, they ride with `DeepDrftSite` and do not appear in the CMS host. If §3.4 ever flips to Option B, the TS pipeline needs to move to `DeepDrftShared.Client` (or, simpler, the shared RCL serves the compiled JS as a static web asset and the source pipeline stays with the public host as the canonical compiler).
|
||||||
|
|
||||||
|
### 3.6 Theme prerender — stays with public host
|
||||||
|
|
||||||
|
`DarkModeService` (in `DeepDrftWeb/Services/`) reads the cookie during prerender; `DarkModeSettings` lives in `DeepDrftWeb.Client/Common/`. The dark-mode bridge is only meaningful where prerender happens. Since only the public site prerenders (the CMS is `InteractiveServer` over a logged-in circuit and does not need the cookie-prerender bridge), the dark-mode plumbing stays in the public app.
|
||||||
|
|
||||||
|
The CMS still gets dark mode — it just reads the cookie at circuit init the same way any normal Server component does, no `PersistentComponentState` round-trip needed. Implementation detail for staff-engineer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Render-mode strategy
|
||||||
|
|
||||||
|
This section is the load-bearing one for "why this design avoids the deadlock."
|
||||||
|
|
||||||
|
### 4.1 Public site: prerender-first, `InteractiveAuto` pages
|
||||||
|
|
||||||
|
```
|
||||||
|
App.razor static SSR (host markup)
|
||||||
|
└── Routes.razor static SSR
|
||||||
|
└── MainLayout.razor static SSR ← key change
|
||||||
|
└── Page (Home/Tracks) @rendermode InteractiveAuto
|
||||||
|
```
|
||||||
|
|
||||||
|
- **No `@rendermode` on `App` or `Routes` or `MainLayout`.** They render statically as the page shell.
|
||||||
|
- **`@rendermode InteractiveAuto` on each page** (as already attempted in commit `54865e7`, but that fix was incomplete because the auth coupling survived). Pages opt into interactivity individually.
|
||||||
|
- **MainLayout has no `[Inject]` properties that touch a scoped service participating in cascading auth.** It is presentation chrome only. Anything that needs `IJSRuntime`, `DarkModeSettings`, `IPlayerService` lives **inside** the page (or inside an interactive component the page renders), not in the layout.
|
||||||
|
- The `DarkModeService.CheckDarkMode()` call currently in `App.razor.OnInitialized` survives — `App` is the right place for prerender-time cookie reads; it does not get a `@rendermode`.
|
||||||
|
- `AudioPlayerProvider` (the cascading host for `IPlayerService`) moves from `MainLayout` to a wrapper inside each page that needs it (`TracksView`), or into a single interactive shell component that `MainLayout` renders **inside** its `@Body` slot without static-prerender coupling. Staff-engineer call; both shapes work.
|
||||||
|
|
||||||
|
**Why this avoids the deadlock:** there is no AuthBlocks in this app. `AddCascadingAuthenticationState` is **not registered**. `MainLayout` has no auth state to cascade. The `[Inject]` deadlock symptom is bound to the auth-scoped-service initialisation, not to injection in general — remove the auth context and `[Inject]` becomes safe again.
|
||||||
|
|
||||||
|
If staff-engineer wants the additional belt-and-suspenders: `MainLayout` for the public site ships with **zero `[Inject]` properties**. Cosmetic deviation from typical layout patterns; cheap insurance against any future recurrence.
|
||||||
|
|
||||||
|
### 4.2 CMS: `InteractiveServer` end-to-end
|
||||||
|
|
||||||
|
```
|
||||||
|
App.razor static SSR (host markup, AuthorizeRouteView in Routes)
|
||||||
|
└── Routes.razor static SSR
|
||||||
|
└── CmsLayout.razor @rendermode InteractiveServer
|
||||||
|
└── Page (/cms/*) inherits InteractiveServer from layout
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`AddInteractiveServerComponents()` only.** No WebAssembly render mode in the CMS host. No `InteractiveAuto`, no client assembly.
|
||||||
|
- **No prerender.** CMS pages are gated by `[HierarchicalRoleAuthorize("Admin")]`; prerendering them in an anonymous context is wrong on two counts (data leakage if the page bypasses the gate during prerender; pointless if it does not). The CMS opts out of prerender via `RenderMode(InteractiveServer, prerender: false)` at the layout or page level.
|
||||||
|
- **AuthBlocks fully in scope.** `AddAuthBlocks`, `MapAuthBlocks`, `AddCascadingAuthenticationState`, `AddAuthenticationStateDeserialization` — all live here, none of them leak into the public site.
|
||||||
|
- **`/account/login`, `/account/logout`** (the bundled AuthBlocks UI from `AuthBlocksWeb`) are served by the CMS host. Public site links to them by absolute URL (`https://cms.deepdrft.com/account/login?returnUrl=…` or path on the same domain if §6 picks path-routing).
|
||||||
|
|
||||||
|
**Why this avoids the deadlock:** the CMS deadlock symptom never appeared — the CMS rendered fine. The problem was always the **public** layout being dragged into the auth cascade. Once the public side has no auth at all, the CMS is free to use auth normally.
|
||||||
|
|
||||||
|
### 4.3 Render-mode crosswalk against today's mess
|
||||||
|
|
||||||
|
| Today (`dev` branch) | Two-app design |
|
||||||
|
|---|---|
|
||||||
|
| `MainLayout.razor` gutted to one-liner with `[Inject] IJSRuntime` to avoid deadlock | Public `MainLayout.razor` restored to full chrome (nav, theme switcher, footer). No `[Inject]`s of scoped auth services. |
|
||||||
|
| `Routes.razor` server-side with `AdditionalAssemblies` listing client + CMS + AuthBlocks assemblies | Public `Routes.razor`: only `typeof(DeepDrftSite.Client._Imports).Assembly`. CMS `Routes.razor`: only `typeof(DeepDrftCms._Imports).Assembly` + `typeof(AuthBlocksWeb._Imports).Assembly`. |
|
||||||
|
| `[AllowAnonymous]` decorating `Home.razor` and `TracksView.razor` to escape the global auth funnel | Removed. No global auth in the public host. |
|
||||||
|
| `@rendermode InteractiveAuto` on `MainLayout`, then off, then on individual pages | `@rendermode InteractiveAuto` on individual public pages. Layout stays static SSR. |
|
||||||
|
| `@rendermode InteractiveServer` explicitly declared on AuthBlocks pages | Removed. CMS host's default render mode is `InteractiveServer`; AuthBlocks pages inherit. |
|
||||||
|
| `JwtAuthenticationStateProvider` prerender-safe rewrite needed because public pages prerendered with the provider in scope | The provider is only registered in the CMS host. Public prerender does not see it. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. API ownership — where does `api/track/page` live?
|
||||||
|
|
||||||
|
`DeepDrftWeb` today owns one controller — `TrackController` — exposing `GET api/track/page` against `DeepDrftContext` (Postgres metadata). It also owns three CMS-internal controllers (`CmsUploadController`, `CmsEditController`, `CmsDeleteController`).
|
||||||
|
|
||||||
|
After the split, the CMS controllers follow the CMS — they require `[Authorize(Roles="Admin")]` and live in `DeepDrftCmsHost`. The question is the public read endpoint.
|
||||||
|
|
||||||
|
### 5.1 Option A — Metadata API stays with the public site (recommended)
|
||||||
|
|
||||||
|
`GET api/track/page` lives in `DeepDrftSite` (the renamed/repurposed `DeepDrftWeb`). The public WASM client calls its own host. The CMS host, when it needs to read paged tracks, calls **across to the public site** via HTTP.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Public site is fully self-contained: it hosts its WASM bundle **and** the API that bundle calls. No cross-host dependency for the listener experience.
|
||||||
|
- Anonymous endpoint stays anonymous; no auth middleware to step around.
|
||||||
|
- Same-origin: WASM → `/api/track/page` works with no CORS configuration.
|
||||||
|
- Mirrors today's shape — minimum surgery.
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- CMS reads cross a host boundary (extra hop for `/cms/tracks` list). In practice negligible — paged metadata is small, and the CMS list is a low-traffic surface.
|
||||||
|
- Public site now serves both the WASM bundle and an API. Two responsibilities in one host. Defensible (the API exists to feed the bundle) but not single-purpose.
|
||||||
|
|
||||||
|
### 5.2 Option B — Dedicated metadata-API host (`DeepDrftApi` or `DeepDrftMetadata`)
|
||||||
|
|
||||||
|
A third ASP.NET Core project owns `GET api/track/page`. Both the public site (WASM → cross-host) and the CMS host (server-side → cross-host) call into it.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Each host is single-purpose: public site is presentation, CMS is admin, metadata API is data. Clean separation.
|
||||||
|
- Future-proofs against "what if a mobile app wants metadata too" — the API exists independently of any UI.
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Three hosts instead of two. More deploy units, more nginx routes, more systemd services, more cert wiring.
|
||||||
|
- The public WASM bundle now does **cross-origin** calls to the metadata API. CORS needs configuration. Either that or nginx-rewrite the public site's `/api/track/page` to the metadata host (which trades the CORS problem for a path-routing problem).
|
||||||
|
- Justified only if the API is reused outside the public site. Today it isn't.
|
||||||
|
|
||||||
|
### 5.3 Option C — Metadata API lives in the CMS host
|
||||||
|
|
||||||
|
`GET api/track/page` is hosted on `DeepDrftCmsHost`. Public site calls cross-host.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- The CMS host already references `DeepDrftData` for writes; adding the read endpoint there is zero new wiring.
|
||||||
|
- Public site becomes presentation-only (no `DeepDrftData` reference).
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- The CMS host is auth-gated and `InteractiveServer`. Adding an anonymous controller alongside is fine technically (controllers and Blazor pages share the host but not the auth pipeline) but conceptually muddles the host's role.
|
||||||
|
- The CMS host becomes a critical-path dependency for **anonymous** public traffic. If the CMS host goes down, the public site's track gallery breaks.
|
||||||
|
- Same cross-origin / CORS issue as Option B from the WASM client's perspective.
|
||||||
|
|
||||||
|
### 5.4 Recommendation
|
||||||
|
|
||||||
|
**Option A.** Public site owns the metadata read endpoint. It's the lightest surgery, preserves the same-origin contract, and matches the principle that the host that serves the UI also serves the UI's data API. Revisit only if a non-UI client (mobile, third-party) starts consuming metadata.
|
||||||
|
|
||||||
|
The CMS host's `/cms/tracks` list pages call `GET api/track/page` cross-host (a named `IHttpClientFactory` client pointed at the public site, like the existing `DeepDrft.Content.Cms` client points at `DeepDrftContent`). No auth header needed — the endpoint is anonymous.
|
||||||
|
|
||||||
|
**Implication for the CMS host's `DeepDrftData` reference:** it still needs `DeepDrftData` for the write path (Edit/Delete) but could read paged tracks via HTTP. Two choices:
|
||||||
|
- **Read via HTTP** for consistency with how the CMS reaches `DeepDrftContent`. Cleaner conceptually.
|
||||||
|
- **Read via direct service call** (CMS host has `DeepDrftData` referenced anyway). Faster path, no HTTP roundtrip for the CMS list page.
|
||||||
|
|
||||||
|
The "one source of truth, multiple views" rule (`user_one_source_multiple_views`) is preserved either way — the VM contract is `PagedResult<TrackEntity>` either way; only the transport differs. **Recommend: direct service call for the CMS** since the CMS host already has the dependency, and reserve the HTTP path for the public WASM. Flag as Q5 in §10 if Daniel wants the stricter host-boundary discipline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Auth wiring (CMS only)
|
||||||
|
|
||||||
|
All present in `CMS-PLAN.md` and landed in CMS Wave 1; preserved here for completeness in the new host. **Nothing about the auth model changes in the split.**
|
||||||
|
|
||||||
|
- **AuthBlocks substrate** (`Cerebellum.AuthBlocks*` 10.3.32) — `AddAuthBlocks(...)`, `await app.Services.UseAuthBlocksStartupAsync()`, `app.MapAuthBlocks()` all in `DeepDrftCmsHost/Program.cs`.
|
||||||
|
- **AuthBlocksWeb pages** (`/account/login`, `/account/logout`) — exposed by adding `typeof(AuthBlocksWeb._Imports).Assembly` to the CMS host's `AddAdditionalAssemblies`.
|
||||||
|
- **JWT in localStorage** via `JwtAuthenticationStateProvider`. CMS host calls `AuthBlocksWeb.Startup.ConfigureAuthServices` server-side; no WASM client means no `AddAuthenticationStateDeserialization` bridge needed.
|
||||||
|
- **Auth DB separate from main DB.** `ConnectionStrings:Auth` and `ConnectionStrings:DefaultConnection` are different Postgres databases (or the same instance with different DBs — they share an engine, not a context). The CMS host wires both.
|
||||||
|
- **`Admin` role gate** via `[HierarchicalRoleAuthorize("Admin")]` on every CMS page and `[Authorize(Roles = "Admin")]` on every CMS API endpoint.
|
||||||
|
- **Stealth routing.** Unauthorized hits on `/cms/*` return 404, not 401, not redirect. `CmsStealthRoutingHandler` (today in `DeepDrftWeb/Middleware/`) follows into the CMS host. Path scope is now naturally `/cms/*` because the CMS host serves nothing else routable from outside auth.
|
||||||
|
|
||||||
|
**One thing to verify** (Q6 in §10): the stealth-routing 404 behaviour currently applies only to `/cms/*` because that was the only protected surface in a mixed host. In the dedicated CMS host, more of the surface is protected by default — but `/account/login` is anonymous, the auth API endpoints under `/api/auth/*` are anonymous, and the host's static assets are anonymous. The 404 handler must still target only the `[HierarchicalRoleAuthorize]`-failed cases, not all 401s, or `/account/login` discovery breaks (a legitimate anonymous user with no JWT would get 404 on the login page itself, which is wrong).
|
||||||
|
|
||||||
|
**Admin seeding** (`AdminUserSettings` in `authblocks.json`) carries over unchanged. The seed user is created on first boot of the CMS host.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Deployment topology
|
||||||
|
|
||||||
|
The repo currently deploys two systemd services (`deepdrft-content`, `deepdrft-web`) on each host (dch6 beta, prod.cerebellumsoftworks.com prod) via `dch5-publish-deploy.sh`. The split adds a third unit.
|
||||||
|
|
||||||
|
### 7.1 Recommendation — path-based routing on a single domain
|
||||||
|
|
||||||
|
```
|
||||||
|
nginx (Let's Encrypt cert for deepdrft.com)
|
||||||
|
│
|
||||||
|
┌─────────────────┼─────────────────┬───────────────────┐
|
||||||
|
│ │ │ │
|
||||||
|
location / location /cms location /api/ location /api/
|
||||||
|
track/{id} auth, /api/users,
|
||||||
|
etc. (proxied
|
||||||
|
from CMS host)
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
DeepDrftSite DeepDrftCmsHost DeepDrftContent DeepDrftCmsHost
|
||||||
|
systemd unit systemd unit systemd unit (same as /cms)
|
||||||
|
:5001 :5002 :5003 :5002
|
||||||
|
```
|
||||||
|
|
||||||
|
Three systemd units per host (`deepdrft-site`, `deepdrft-cms`, `deepdrft-content`). nginx terminates TLS once, routes by path:
|
||||||
|
|
||||||
|
- `/` and `/_framework/*` and `/api/track/page` → `DeepDrftSite`.
|
||||||
|
- `/cms/*` and `/account/*` (login/logout) and `/api/auth/*`, `/api/users/*`, `/api/roles/*` → `DeepDrftCmsHost`.
|
||||||
|
- `/api/track/{id}` and `/api/track/upload` → `DeepDrftContent`.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- One domain, one cert. No CORS between public WASM and the metadata API (same origin).
|
||||||
|
- Browser sees one site; the architecture is invisible.
|
||||||
|
- The "CMS is on the same domain at `/cms`" matches the stealth-routing intent (a snooper hitting `https://deepdrft.com/cms/tracks` gets 404, identical to any unmatched path).
|
||||||
|
- Path routing is what nginx is good at; no new tooling.
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Two locations on the CMS host (`/cms/*` and `/account/*`). nginx config has to know about both.
|
||||||
|
- A misconfigured `/account/login` link from the public site (linking to `/account/login` rather than `/cms/account/login`) needs nginx to route `/account/*` to the CMS host explicitly — easy to forget when the CMS host is mostly known by its `/cms` prefix.
|
||||||
|
|
||||||
|
### 7.2 Alternative — subdomain split
|
||||||
|
|
||||||
|
```
|
||||||
|
deepdrft.com → DeepDrftSite (public)
|
||||||
|
cms.deepdrft.com → DeepDrftCmsHost (staff)
|
||||||
|
content.deepdrft.com → DeepDrftContent (binary API)
|
||||||
|
api.deepdrft.com → DeepDrftSite's track/page endpoint (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Each host has one nginx server block. Cleaner config.
|
||||||
|
- CMS can be IP-restricted at the nginx layer (`allow <home-IP>; deny all;` on `cms.deepdrft.com`) without affecting the public site. Layered defence on top of the auth gate.
|
||||||
|
- "cms.deepdrft.com" is unambiguous in browser history and link shares.
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Three certs (or one wildcard, which costs more / requires DNS-challenge ACME).
|
||||||
|
- Public WASM bundle calls **cross-origin** to anything on `content.deepdrft.com` (already cross-origin today, so the CORS config exists). If the metadata API is hosted on `api.deepdrft.com`, that's a new cross-origin path.
|
||||||
|
- Stealth routing changes flavour — the CMS hostname *does* announce its existence by DNS. Mitigated by binding `cms.deepdrft.com` to a non-routable IP behind a VPN, but that's a bigger lift than the current setup.
|
||||||
|
|
||||||
|
### 7.3 Recommendation summary
|
||||||
|
|
||||||
|
**Recommend path-based routing (§7.1).** The current deploy already nginx-proxies; adding a third location is small. Subdomain split is the right call **if** Daniel wants IP-restrictable CMS access as a hard requirement; until then, the stealth-routing 404 is enough.
|
||||||
|
|
||||||
|
Q7 in §10.
|
||||||
|
|
||||||
|
### 7.4 Deploy-script changes
|
||||||
|
|
||||||
|
`dch5-publish-deploy.sh` publishes two projects today. The split makes it three:
|
||||||
|
|
||||||
|
- Publish `DeepDrftSite` (was `DeepDrftWeb`).
|
||||||
|
- Publish `DeepDrftCmsHost` (new).
|
||||||
|
- Publish `DeepDrftContent` (unchanged).
|
||||||
|
- Three `*.tar.gz` artefacts, three `scp`/`tar`/`restart` cycles.
|
||||||
|
- Three EF migration paths to consider — `DeepDrftContext` migrations apply against the metadata DB (driven by whichever host owns the read endpoint and the writes), `AuthDbContext` migrations apply against the auth DB on first run of the CMS host (`UseAuthBlocksStartupAsync` handles this automatically).
|
||||||
|
|
||||||
|
The deploy script edit is a staff-engineer task once §10 Q1 (naming) and Q7 (topology) settle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Migration / rollout plan
|
||||||
|
|
||||||
|
The temptation is to do the split atomically. Don't. The split has too many moving parts (project renames, file moves, deploy-script edits, nginx config) and atomic-everything would mean an unreviewable single change.
|
||||||
|
|
||||||
|
### 8.1 Smallest first slice — "public home page renders correctly"
|
||||||
|
|
||||||
|
**Phase 0: stabilise current `dev`** (small, lands first, can be staff-engineer's first commit).
|
||||||
|
|
||||||
|
Before touching the split, restore the public site to a non-band-aid state on `dev`. The current `MainLayout.razor` (`<div>@Body</div>` one-liner) is unshippable — it has no nav, no theme switcher, no footer. Either:
|
||||||
|
|
||||||
|
- **(a)** Roll forward: a tiny restoration commit that puts the chrome back in `MainLayout` with the *minimum* `[Inject]`s needed (likely none — see §4.1), accepting that it still runs in the entangled host. The home page now renders with chrome but the architectural problem isn't fixed. This gives Daniel a presentable site **while** the split work proceeds in a worktree.
|
||||||
|
- **(b)** Skip Phase 0 and go straight to the split — accept that `dev` is broken-with-chrome-gone until the split lands.
|
||||||
|
|
||||||
|
**Recommend (a).** A demo-grade home page during the split work is valuable; the chrome restoration is a known-shape change.
|
||||||
|
|
||||||
|
### 8.2 Phased split (worktree-friendly)
|
||||||
|
|
||||||
|
**Phase 1: Stand up `DeepDrftCmsHost` as a new project alongside the existing `DeepDrftWeb`.**
|
||||||
|
|
||||||
|
- New `DeepDrftCmsHost` project. `Microsoft.NET.Sdk.Web`. References `DeepDrftCms` (existing RCL), `DeepDrftData`, `DeepDrftModels`, AuthBlocks packages.
|
||||||
|
- Copy `DeepDrftWeb/Program.cs` + `Startup.cs` + `Middleware/CmsStealthRoutingHandler.cs` into `DeepDrftCmsHost/`, strip out everything that isn't CMS (the public-site MudBlazor host, the `DeepDrftWeb.Client` reference, the `TrackController`, the TypeScript pipeline).
|
||||||
|
- Wire `app.MapRazorComponents<App>().AddInteractiveServerRenderMode().AddAdditionalAssemblies(typeof(DeepDrftCms._Imports).Assembly, typeof(AuthBlocksWeb._Imports).Assembly)`. **No** `AddInteractiveWebAssemblyRenderMode`. **No** client assembly.
|
||||||
|
- Spin it up on a third port locally. Verify `/cms/tracks` works against the existing Postgres metadata DB and the existing Auth DB.
|
||||||
|
- `DeepDrftWeb` is unchanged — both hosts can run concurrently against the same DB during this phase. The CMS lives in both hosts temporarily; that's fine, they share the underlying data.
|
||||||
|
|
||||||
|
**Exit criterion:** CMS host stands up, `/account/login` works against the seeded admin, `/cms/tracks` renders. Public site continues to run from `DeepDrftWeb` exactly as today (still entangled, still using band-aid MainLayout — see Phase 0).
|
||||||
|
|
||||||
|
**Phase 2: Strip AuthBlocks out of `DeepDrftWeb`.**
|
||||||
|
|
||||||
|
- Remove the `AddAuthBlocks(...)`, `MapAuthBlocks()`, `UseAuthBlocksStartupAsync()` from `DeepDrftWeb/Program.cs`.
|
||||||
|
- Remove `AddCascadingAuthenticationState` (the implicit registration via `AuthBlocksWeb.Startup.ConfigureAuthServices`).
|
||||||
|
- Remove the `AuthBlocksWeb` assembly from `AddAdditionalAssemblies`.
|
||||||
|
- Remove the `DeepDrftCms` reference from `DeepDrftWeb` — the CMS now lives only in `DeepDrftCmsHost`.
|
||||||
|
- Remove the CMS controllers (`CmsUploadController`, `CmsEditController`, `CmsDeleteController`) from `DeepDrftWeb/Controllers/` — they move to the CMS host.
|
||||||
|
- Remove `[AllowAnonymous]` attributes from `Home.razor` / `TracksView.razor` (no longer needed).
|
||||||
|
- Remove the `JwtAuthenticationStateProvider` registration from `DeepDrftWeb.Client.Startup` (and from `DeepDrftWeb.Client.Program.cs`'s `AuthBlocksWeb.Client.Startup.ConfigureServices(builder.Services)` call) — only the CMS needs it.
|
||||||
|
- Restore `MainLayout.razor` to its full chrome state from before the band-aids (the rendition that existed pre-`d31a08b`-era, adapted to current pages). With AuthBlocks gone, `[Inject]`s in the layout work normally.
|
||||||
|
|
||||||
|
**Exit criterion:** Public site (`DeepDrftWeb` still by its old name) renders home page with chrome, prerenders correctly, no hangs. CMS continues to work from `DeepDrftCmsHost`. The hosts run side-by-side.
|
||||||
|
|
||||||
|
**Phase 3: Extract `DeepDrftShared.Client` (optional, can run in parallel with 2).**
|
||||||
|
|
||||||
|
- New RCL `DeepDrftShared.Client`.
|
||||||
|
- Move `TrackCard.razor`, `TracksGallery.razor`, `DDIcons.cs`, palette objects, font helpers.
|
||||||
|
- Both `DeepDrftWeb.Client` and (eventually) the CMS RCL reference it.
|
||||||
|
|
||||||
|
**Exit criterion:** Both apps render `TrackCard` from the shared RCL. Visual parity confirmed.
|
||||||
|
|
||||||
|
**Phase 4: Rename `DeepDrftWeb` → `DeepDrftSite`, `DeepDrftWeb.Client` → `DeepDrftSite.Client`.**
|
||||||
|
|
||||||
|
- Project file renames, namespace renames, csproj reference updates, solution file edits.
|
||||||
|
- Deploy script edits.
|
||||||
|
- `dch6` directory rename (`/deepdrft/web` → `/deepdrft/site`) + systemd unit rename.
|
||||||
|
|
||||||
|
**Exit criterion:** All references converge on the new names. The rename is a single staff-engineer pass; doing it last means the split has been validated under the old names first.
|
||||||
|
|
||||||
|
**Phase 5: nginx and deploy topology.**
|
||||||
|
|
||||||
|
- Edit `dch5-publish-deploy.sh` to publish three projects.
|
||||||
|
- Add the `DeepDrftCmsHost` systemd unit.
|
||||||
|
- nginx config: add the `/cms/*` and `/account/*` location blocks.
|
||||||
|
- Verify on `dch6` first; promote to prod when stable.
|
||||||
|
|
||||||
|
### 8.3 Can the public site stand up first while CMS stays in the entangled host?
|
||||||
|
|
||||||
|
**Yes — that's exactly Phase 1's shape.** Phase 1 builds the CMS host as a parallel deployment. The public site doesn't move until Phase 2. During Phase 1, the CMS exists in *two* places simultaneously (the old entangled host and the new dedicated host) — that's acceptable because they share storage and the duplication is short-lived.
|
||||||
|
|
||||||
|
The inverse — public site stands up dedicated first, CMS stays in the entangled host — is *not* recommended. The deadlock is in the public path, and ripping out the public bits while leaving the CMS bound to the host that *has* the broken MainLayout creates a worse intermediate state than the current `dev`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. What to throw away
|
||||||
|
|
||||||
|
The band-aid work since the entanglement was discovered exists only to make the entangled host limp along. Once the split lands, these become removable. **None of these are wrong** — they're correct fixes for the wrong-shaped architecture. The work survives in `Cerebellum.AuthBlocks` upstream where relevant; only the DeepDrftHome-side adaptations are scoped here.
|
||||||
|
|
||||||
|
### 9.1 Removable from `DeepDrftWeb` / `DeepDrftSite` once split lands
|
||||||
|
|
||||||
|
- `[AllowAnonymous]` attributes on `Home.razor` and `TracksView.razor`. The public host has no `[Authorize]` policy to escape.
|
||||||
|
- The `AuthBlocksWeb` assembly entry in `AddAdditionalAssemblies`. The public host does not need to know about `/account/login` — it links to it cross-host (or cross-path via nginx).
|
||||||
|
- `AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl)` call.
|
||||||
|
- `AuthBlocksWeb.Client.Startup.ConfigureServices(builder.Services)` call in `DeepDrftWeb.Client.Program.cs`.
|
||||||
|
- The `Cerebellum.AuthBlocks*` package references in `DeepDrftWeb.csproj` and `DeepDrftWeb.Client.csproj`.
|
||||||
|
- The `DeepDrftCms` project reference in `DeepDrftWeb.csproj`.
|
||||||
|
- The CMS-specific configuration in `DeepDrftWeb/Startup.cs` (the `ContentCmsHttpClientName` client setup — moves to the CMS host).
|
||||||
|
- The `environment/authblocks.json` config file in `DeepDrftWeb/environment/` — moves to the CMS host's environment dir.
|
||||||
|
- The `ConnectionStrings:Auth` entry in `DeepDrftWeb/environment/connections.json` — moves.
|
||||||
|
- The `JwtAuthenticationStateProvider` and related auth-prerender-bridge wiring on the client.
|
||||||
|
- The `@rendermode InteractiveServer` declarations on AuthBlocks pages (those pages aren't in this host anymore).
|
||||||
|
- The `<NotAuthorized>` redirect-to-login branch in `Routes.razor` (no auth in this host).
|
||||||
|
|
||||||
|
### 9.2 Removable from the entangled `MainLayout.razor`
|
||||||
|
|
||||||
|
Once restored from the one-liner band-aid:
|
||||||
|
|
||||||
|
- Any defensive try/catch around `[Inject]` properties.
|
||||||
|
- Any `OnAfterRenderAsync`-only guards around what should be server-renderable.
|
||||||
|
- The `JwtAuthenticationStateProvider`-prerender-safety detection logic (the provider isn't in scope here anymore).
|
||||||
|
|
||||||
|
### 9.3 What stays (and why)
|
||||||
|
|
||||||
|
- `DarkModeService` + `DarkModeSettings` + `DarkModeCookieService` + the `PersistentComponentState` bridge. The prerender-cookie pattern is good architecture and unrelated to the auth problem.
|
||||||
|
- `[ApiKeyAuthorize]` on `DeepDrftContent`'s `PUT api/track/{id}` and `POST api/track/upload`. Different auth surface, unaffected.
|
||||||
|
- The full streaming substrate (`StreamingAudioPlayerService`, `WavOffsetService`, the TS interop). Unrelated to render-mode wiring.
|
||||||
|
- The CMS itself, in full. Moves wholesale to the new host with no functional change.
|
||||||
|
|
||||||
|
### 9.4 What stays in AuthBlocks upstream
|
||||||
|
|
||||||
|
The prerender-safety rewrites to `JwtAuthenticationStateProvider` in `Cerebellum.AuthBlocks.Web.Client` (versions 10.3.31 and 10.3.32) are real improvements to the library. They stay. The DeepDrftHome-side concession (consuming the newer versions) is what becomes unnecessary in the public host — the CMS host keeps consuming them and benefits from the fixes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Open questions for Daniel
|
||||||
|
|
||||||
|
These genuinely need decisions before staff-engineer can execute. Numbered for citation.
|
||||||
|
|
||||||
|
1. **Project names.** `DeepDrftSite` vs. `DeepDrftPublic` vs. rename-in-place `DeepDrftWeb`? `DeepDrftCmsHost` vs. `DeepDrftCms.Host` vs. promoting `DeepDrftCms` to a host (collapsing the RCL+host pair)? `DeepDrftShared.Client` for the new shared RCL — acceptable?
|
||||||
|
2. **Shared RCL — yes/no in Wave 1.** Recommendation: yes, with the minimum surface (palette objects, fonts, `DDIcons`, `TrackCard`). But if Daniel prefers to land the split first and extract shared bits as a follow-up, that's a defensible sequencing.
|
||||||
|
3. **Single `App.razor` host page or per-app divergence.** Recommendation: per-app. The public site's `App.razor` keeps the dark-mode preconnect, font links, the `DeepDrftAudio` module import. The CMS's `App.razor` is the AuthBlocks-aware version (cascading auth state, MudBlazor + BlazorBlocks chrome). They diverge.
|
||||||
|
4. **Audio player in CMS — yes/no.** §3.4 Option A (player only in public) vs. Option B (player in shared RCL, CMS gets preview). Recommendation: A for Wave 1.
|
||||||
|
5. **CMS reads — direct service call vs. HTTP across to public site.** Recommendation: direct service call (CMS host has `DeepDrftData` referenced for writes; reusing the dependency for reads is free). If Daniel wants the stricter host-boundary discipline (no DB access from anything but the host that owns the API), flip to HTTP. The VM contract is the same either way.
|
||||||
|
6. **Stealth routing scope.** Confirm: only `[HierarchicalRoleAuthorize]`-failed cases return 404. `/account/login` (anonymous-allowed) still serves 200 to unauthenticated users. The middleware predicate must distinguish.
|
||||||
|
7. **Deployment topology.** Path-based on `deepdrft.com` (recommended) vs. subdomain split (`cms.deepdrft.com`). Subdomain split is required if Daniel wants IP-restricted CMS access; otherwise path-based is lighter.
|
||||||
|
8. **CMS first-paint experience.** Today the CMS is `InteractiveServer` and that's fine for a logged-in workflow. Confirm prerender is off for `/cms/*` (recommended) — keeps the auth gate honest at the page level rather than relying on the stealth handler to clean up after a prerendered anonymous response.
|
||||||
|
9. **BlazorBlocks adoption depth.** Daniel said "CMS should be based on BlazorBlocks and AuthBlocks." `Cerebellum.BlazorBlocks.Web` provides "entity management views, modals, and form scaffolding" — does the CMS Wave 1 surface (track list/edit/delete) get rebuilt on BlazorBlocks scaffolding, or does the existing `DeepDrftCms` RCL keep its bespoke pages and BlazorBlocks comes in for Wave 2 (user admin, audit log, etc.)? The split is independent of this, but staff-engineer needs to know whether to refactor or preserve in Phase 1.
|
||||||
|
10. **Phase 0 chrome restoration on `dev`.** Yes (restore MainLayout to presentable state on `dev` now, before split work begins) or no (live with the one-liner until the split lands)? Recommendation: yes. The home page should be presentable while the structural work happens in a worktree.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Working with this document
|
||||||
|
|
||||||
|
- This doc captures the **shape**, not the implementation. Staff-engineer takes the answers to §10, follows §8's phased plan, executes.
|
||||||
|
- When phases land, archive their entries here to `COMPLETED.md` with the original "What / Why / Shape" body preserved (per `CONTEXT.md §6`).
|
||||||
|
- The supersession against `CMS-PLAN.md §2` (which placed the CMS inside `DeepDrftWeb`) takes effect once Daniel signs off — at that point doc-keeper updates `CMS-PLAN.md` to point here for the host shape, and the CMS-PLAN's auth/wave content remains authoritative for the CMS feature roadmap.
|
||||||
|
- Cross-reference rather than duplicate. If `PLAN.md` adds an item that touches the split (e.g. metadata-API extension), the new item points here for the host context.
|
||||||
Reference in New Issue
Block a user