Plans the Gitea deployment feature and refreshes the codebase analysis

Adds the AI-DLC inception record for deploying the CMS as a single .NET
application on hosting where no server configuration is possible.

The reverse-engineering artifacts were regenerated: the previous set
predated the Master module, the Slave host, the solution reorganisation
and single-host serving, all of which matter for deployment. Findings
were verified by running the build, both test suites and the linter
rather than inferred, which surfaced two facts the plan depends on:
the frontend lint gate currently fails (5 errors), and two transitive
packages carry high-severity advisories.

Records 24 functional requirements, 32 traced decisions and a
seven-unit decomposition whose ordering is load-bearing: durability
work must land before the first automated deploy, or the very first
deploy is the one that silently breaks master/slave trust.

Two conflicts found while designing and carried into the units:
- Both modules call AddDataProtection(), which runs after the host and
  would override a persistent key store while still passing any
  registration test.
- The availability gate runs before authentication, so its admin
  bypass cannot read HttpContext.User.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
This commit is contained in:
2026-07-27 23:59:30 +02:00
co-authored by Claude Opus 5
parent 38857038a0
commit 8568ca43c6
25 changed files with 4233 additions and 503 deletions
@@ -0,0 +1,249 @@
# Component Dependencies
Dependency matrix, communication patterns and data flow.
---
## Dependency Diagram
```mermaid
graph TD
subgraph core["SlpModularCms.Core"]
sechdr["C-01 SecurityHeadersMiddleware"]
secopt["C-02 SecurityHeadersOptions"]
csp["C-03 CspPolicyBuilder"]
health["C-04 Health checks"]
dp["C-05 CmsDataProtection"]
appdb["C-06 ApplicationDbContext<br/>plus keys table"]
migrate["C-07 Migration runner"]
logging["C-08 CmsLogging"]
sentry["C-09 CmsSentry"]
end
subgraph apihost["SlpModularCms.Api"]
statics["C-10 Static mounts"]
prog["C-16 Host composition"]
end
subgraph slavehost["SlpModularCms.Api.Slave"]
progslave["C-16 Host composition"]
end
subgraph modules["Modules"]
avail["C-13 AvailabilityMiddleware"]
end
subgraph fe["frontend"]
cfg["C-14 Config"]
feobs["C-15 Sentry plus Umami"]
end
subgraph wf[".gitea/workflows"]
ci["C-12 CI workflow"]
scp["C-11 deploy-scp"]
end
sechdr --> secopt
sechdr --> csp
csp --> secopt
dp --> appdb
migrate --> appdb
sentry --> logging
prog --> sechdr
prog --> health
prog --> dp
prog --> migrate
prog --> logging
prog --> sentry
prog --> statics
progslave --> sechdr
progslave --> health
progslave --> dp
progslave --> migrate
progslave --> logging
progslave --> sentry
avail --> dp
cfg --> feobs
ci --> scp
ci --> fe
ci --> apihost
classDef corelayer fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
classDef host fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef module fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef frontend fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000;
classDef workflow fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000;
class sechdr,secopt,csp,health,dp,appdb,migrate,logging,sentry corelayer;
class statics,prog,progslave host;
class avail module;
class cfg,feobs frontend;
class ci,scp workflow;
```
Text alternative: all new cross-cutting components live in Core and are consumed by both host projects; only the static mounts are exclusive to the Api host; the Availability middleware depends on Core's Data Protection; and the CI workflow orchestrates the frontend, the Api host and the deploy workflow.
---
## Dependency Matrix
| Component | Depends on | Depended on by | Coupling |
|---|---|---|---|
| C-01 `SecurityHeadersMiddleware` | C-02, C-03 | C-16 (both hosts) | Compile |
| C-02 `SecurityHeadersOptions` | Configuration | C-01, C-03 | Configuration binding |
| C-03 `CspPolicyBuilder` | C-02 | C-01 | Compile |
| C-04 Health checks | — | C-16 (both hosts) | Compile |
| C-05 `CmsDataProtection` | C-06 | C-16, and indirectly C-13 | Compile |
| C-06 `ApplicationDbContext` keys table | EF Core, SQL Server | C-05, C-07 | Compile + schema |
| C-07 Migration runner | C-06 | C-16 (both hosts) | Compile |
| C-08 `CmsLogging` | — | C-09, C-16 | Compile |
| C-09 `CmsSentry` | C-08, configuration | C-16 | Compile |
| C-10 Static mounts | Filesystem layout | C-16 (`Api` only) | Runtime (filesystem) |
| C-11 `deploy-scp.yaml` | Host over SSH | C-12 | Workflow call |
| C-12 CI workflow | C-11, both build outputs | — | Workflow |
| C-13 `AvailabilityMiddleware` | C-05 (key ring), validated principal | Both hosts, via module discovery | Compile + runtime |
| C-14 Frontend config | Vite build variables | C-15, `ApiClient` | Build-time |
| C-15 Frontend observability | C-14, Vite build variables | — | Build-time |
| C-16 Host composition | C-01, C-04, C-05, C-07, C-08, C-09, C-10 | — | Compile |
---
## Communication Patterns
### In-process (the majority)
Everything in `Core` is consumed by the hosts through **DI registration and middleware composition**. There are no new service-to-service calls, no new queues and no new network hops inside the application. This is deliberate: the feature adds cross-cutting behaviour, not new interactions.
### Configuration-driven
`C-02` binds the `SecurityHeaders` section; `C-09` reads a Sentry DSN; `C-05` reads Data Protection settings. All follow the existing Options pattern. **Configuration errors must surface at startup, not per request** — an unknown CSP policy name fails the process rather than silently degrading, which given fail-fast startup (Q8 = A) means a misconfigured deployment goes visibly red instead of quietly serving without protection.
### Filesystem-coupled (the fragile one)
`C-10` depends on a directory layout that no code creates:
| Path | Owner | Created by |
|---|---|---|
| `wwwroot/admin/` | This repository | `dotnet publish` (`BuildAndCopyAdminFrontend` target) |
| `wwwroot/web/` | **A separate website workspace** | That workspace's own deployment; persists across releases (ASM-01) |
This is the feature's most fragile coupling, because it is enforced by convention rather than by the type system. Two consequences:
1. `C-10` must **tolerate a missing `wwwroot/web/` at startup** — a fresh deployment has none until a website workspace deploys into it, and the CMS must still start and serve `/admin` and `/api/v1`.
2. The deployment (S-05) must link the persistent `wwwroot/web/` into each new release directory. Getting this wrong destroys the customer's website — the highest-severity risk in the feature (NFR-02).
### Build-time coupling (frontend)
`C-14` and `C-15` read Vite variables baked in at build time, which is precisely why two artifacts are produced (D-15). The backend and frontend are additionally coupled in the *reverse* direction by the publish target, which runs `pnpm install` and `pnpm build` — making Node and pnpm prerequisites of `dotnet publish` and a required step in the CI workflow's toolchain setup.
### Cross-instance (unchanged, but newly protected)
Master↔slave communication is untouched functionally. What changes is its durability: with `C-05` and `C-06`, the encrypted API keys underpinning that trust survive a redeploy. Previously an atomic release switch would have discarded the file-based key ring and broken the protocol silently.
---
## Data Flow
### Request flow with the new components
```mermaid
sequenceDiagram
box rgba(246,224,94,0.4) Client
participant V as Visitor or admin
end
box rgba(144,205,244,0.4) Pipeline
participant E as Exception handler
participant S as Security headers
participant F as Static files
participant A as Availability gate
participant H as Health endpoint
end
V->>E: HTTP request
E->>S: continue
S->>S: resolve CSP policy for path
S->>F: continue with OnStarting callback
alt file exists in web or admin mount
F-->>V: file, headers applied at response start
else no matching file
F->>A: continue
alt path is /health or another bypass prefix
A->>H: continue
H-->>V: 200 Healthy or 503 Unhealthy
else instance disabled
A-->>V: 503 ProblemDetails
else instance available
A-->>V: routed to controllers or SPA fallback
end
end
```
Text alternative: security headers register a response-start callback before static files short-circuit, so both static and dynamic responses carry them; `/health` passes the availability gate via the bypass list, while other paths are blocked with a 503 when the instance is disabled.
### Startup flow
```mermaid
graph TD
start["Process starts"]
log["Configure logging and Sentry"]
disc["Discover modules"]
svc["Register services<br/>including Data Protection"]
build["Build application"]
mig["Migrate ApplicationDbContext"]
modmig["Module contexts migrate<br/>during UseModules"]
serve["Accept traffic;<br/>/health answers"]
dead["Process does not start;<br/>/health silent, UptimeRobot red"]
start --> log
log --> disc
disc --> svc
svc --> build
build --> mig
mig -->|success| modmig
mig -->|failure| dead
modmig --> serve
classDef normal fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef bad fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000;
class start,log,disc,svc,build,modmig,serve normal;
class mig decision;
class dead bad;
```
Text alternative: logging and Sentry are configured first so any later startup failure is captured; Core migrations run before module migrations, and a migration failure stops the process entirely rather than serving a broken application.
### Deployment data flow
```mermaid
graph LR
art["Build artifact"]
backup[("Database backup<br/>production only")]
newrel["New release directory"]
persist[("Persistent wwwroot/web<br/>customer website")]
active["Active release symlink"]
prev["Retained previous release"]
art --> newrel
backup -.->|before any change| newrel
persist -->|linked into| newrel
newrel --> active
active -.->|previous becomes| prev
prev -.->|rollback| active
classDef artifact fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef store fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000;
classDef link fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
class art,newrel artifact;
class backup,persist store;
class active,prev link;
```
Text alternative: the build artifact populates a new release directory into which the persistent customer website is linked; the active pointer then switches atomically, and the previous release is retained so a rollback is a pointer switch rather than a rebuild.
---
## Coupling Concerns
| Concern | Assessment |
|---|---|
| **`Core` is inherited by both hosts** | Intended (Q1 = A, Q2 = A). Every `Core` change reaches `SlpModularCms.Api.Slave`, which has no test project — so it is verified at Build and Test by actually starting it. The Slave is a reference instance, not a throwaway. |
| **Duplicate `AddDataProtection()` in two modules** | **A real conflict**, documented in `services.md` S-01. Module registration runs *after* host registration, so the modules' calls would override the persistent key store and make FR-12 a no-op that looks implemented. Both must be removed. |
| **`AvailabilityMiddleware` runs before `UseAuthentication()`** | Constrains how FR-24 can be implemented — either validate the token in the middleware, or move authentication earlier. Resolved in Functional Design for Unit 2. |
| **Filesystem convention for `wwwroot/web/`** | The weakest link: not enforceable in code, and getting it wrong is destructive. Mitigated by design (persistent path outside the release directory) and by documentation (FR-09), but it stays a convention. |
| **Static files short-circuit the pipeline** | Dictates that security headers precede them. Any future middleware that must see all responses faces the same constraint — worth remembering rather than rediscovering. |
| **Build-time frontend configuration** | Forces two build artifacts. Accepted (D-15), with runtime configuration recorded as a possible later improvement. |