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:
+185
@@ -0,0 +1,185 @@
|
||||
# Application Design — Gitea Deployment Workflow
|
||||
|
||||
**Feature**: `gitea-deployment-workflow`
|
||||
**Date**: 2026-07-27
|
||||
**Consolidates**: `components.md`, `component-methods.md`, `services.md`, `component-dependency.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Design Summary
|
||||
|
||||
This feature adds **cross-cutting infrastructure** to an existing modular monolith, plus the CI/CD pipeline that deploys it. It introduces no new business capability and no new domain entity beyond a Data Protection keys table.
|
||||
|
||||
The design is shaped by four decisions taken during this stage:
|
||||
|
||||
| Decision | Choice | Consequence |
|
||||
|---|---|---|
|
||||
| **Q1 = A** — placement | All new cross-cutting concerns live in `SlpModularCms.Core` as extension methods | Both hosts get identical behaviour; nothing is implemented twice |
|
||||
| **Q2 = A** — Slave scope | The Slave host receives everything except static mounts | The Slave is a **reference instance** showing what a customer-facing API looks like, not a stripped-down dev tool. Its behaviour must match production |
|
||||
| **Q9 = A** — composition | Each host keeps its own `Program.cs` | What remains duplicated is a readable list of calls, not logic |
|
||||
| **Q8 = A** — failure mode | Fail fast on migration failure | Makes the liveness-only health check meaningful: a process that cannot migrate never starts, so `/health` goes silent and monitoring goes red |
|
||||
|
||||
**14 code components** (9 new, 5 modified) and **2 workflow components**, across `Core`, `Api`, `Modules.Availability`, `frontend` and `.gitea/workflows/`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Components
|
||||
|
||||
See `components.md` for full definitions. Summary:
|
||||
|
||||
### New in `SlpModularCms.Core`
|
||||
- **C-01 `SecurityHeadersMiddleware`** — applies headers that would normally come from nginx or IIS, which NFR-01 forbids relying on
|
||||
- **C-02 `SecurityHeadersOptions`** — binds the `SecurityHeaders` section
|
||||
- **C-03 `CspPolicyBuilder`** — composes CSP strings from code-defined policies plus configured origins
|
||||
- **C-04 Health-check registration** — `GET /health`, liveness only
|
||||
- **C-05 `CmsDataProtection`** — database-backed key ring
|
||||
- **C-07 Startup migration runner** — Core migrations, fail fast
|
||||
- **C-08 `CmsLogging`** — structured logging, independent of Sentry
|
||||
- **C-09 `CmsSentry`** — optional; absent DSN is a supported state
|
||||
|
||||
### Modified
|
||||
- **C-06 `ApplicationDbContext`** — implements `IDataProtectionKeyContext`; one new Core migration
|
||||
- **C-13 `AvailabilityMiddleware`** — `/health` bypass, plus the FR-24 token-validation fix
|
||||
- **C-14/C-15 frontend** — same-origin config, Sentry, Umami
|
||||
- **C-16 host composition** — both `Program.cs` files
|
||||
|
||||
### Host-specific and workflow
|
||||
- **C-10 Static mounts** — `Api` only: `wwwroot/web/` at `/`, `wwwroot/admin/` at `/admin`
|
||||
- **C-11 `deploy-scp.yaml`**, **C-12 `continuous_integration.yaml`**
|
||||
|
||||
---
|
||||
|
||||
## 3. Key Interfaces
|
||||
|
||||
See `component-methods.md` for full signatures. The contracts that carry design weight:
|
||||
|
||||
**`AddCmsHealthChecks()` takes no options.** Deliberate — "just add a database check" then becomes a visible code change rather than configuration drift, keeping D-21 (liveness only) enforced by shape rather than by discipline.
|
||||
|
||||
**`MigrateCoreDatabase()` has no try/catch.** Exceptions propagate by design (Q8 = A).
|
||||
|
||||
**`AddCmsSentry()` treats an absent DSN as normal.** Local development and any Sentry-less deployment run unchanged.
|
||||
|
||||
**`SecurityHeadersOptions` configures assignment, not definition.** Policy *content* is in code; path *assignment* and environment-specific *origins* are configuration (FU2 = A). A misconfiguration can misroute a path but cannot invent a broken policy.
|
||||
|
||||
**Both deploy workflows share one input interface** (Q11 = B), so switching transport changes only a `uses:` line.
|
||||
|
||||
---
|
||||
|
||||
## 4. Orchestration
|
||||
|
||||
See `services.md` for the full pipeline. The two orderings that matter most:
|
||||
|
||||
**Security headers precede static files.** Static files short-circuit the pipeline; anything registered after them never reaches the public website — the very surface the CSP is for. Headers are applied at response start via `OnStarting`, because the content type is unknown earlier and per-header scoping (FU1 = A) depends on it.
|
||||
|
||||
**`/health` is an endpoint, so it runs after the availability gate.** That is exactly why `/health` must be on the bypass list (D-22) — otherwise a deliberately disabled instance would report itself as unhealthy, re-creating the conflation this feature exists to avoid.
|
||||
|
||||
**Startup order**: logging and Sentry first (so later failures are captured) → module discovery → services including Data Protection → build → Core migration (fail fast) → module migrations → serve.
|
||||
|
||||
---
|
||||
|
||||
## 5. Two Conflicts Found During Design
|
||||
|
||||
Both were discovered by tracing the composition order, and both would have produced code that looks correct while doing nothing useful.
|
||||
|
||||
### 5.1 Duplicate `AddDataProtection()` would silently defeat FR-12
|
||||
|
||||
`AvailabilityModule.RegisterServices` and `MasterModule.RegisterServices` each call `services.AddDataProtection()` today. Module registration runs **after** the host's registration, so the modules' bare calls would override the persistent key store configured by `AddCmsDataProtection()`.
|
||||
|
||||
The result: FR-12 appears implemented, tests that check registration pass, and the key ring is still ephemeral — so the first atomic release switch silently breaks master↔slave trust in a way that presents as a network fault. Exactly the failure FR-12 exists to prevent.
|
||||
|
||||
**Resolution**: remove `AddDataProtection()` from both modules; the host configures Data Protection once. Assigned to Unit 2, with a test asserting the persistent store survives module registration.
|
||||
|
||||
**Second-order requirement**: the Data Protection **application discriminator must be set explicitly**. By default it derives from the content root path, which changes on every atomic release-directory switch (FR-06) — which would defeat FR-12 by a different route.
|
||||
|
||||
### 5.2 `AvailabilityMiddleware` runs before authentication
|
||||
|
||||
FR-24 requires the admin bypass to stop trusting an unvalidated token. But `AvailabilityMiddleware` is installed by `orchestrator.UseModules(app)` at step 8, while `UseAuthentication()` runs at step 9 — so `HttpContext.User` is not yet populated when the bypass is evaluated.
|
||||
|
||||
Two options, decided in Functional Design for Unit 2:
|
||||
|
||||
| Option | Trade-off |
|
||||
|---|---|
|
||||
| **(a)** Validate the token inside the middleware with the same `TokenValidationParameters` as the bearer scheme | Contained, but duplicates validation parameters — which must then be shared from one source rather than copied |
|
||||
| **(b)** Move `UseAuthentication()` before the module middleware | Smaller code change, but alters the pipeline for every module including future ones — a wider blast radius than this feature warrants |
|
||||
|
||||
Neither is obviously correct, which is why it is recorded rather than decided here.
|
||||
|
||||
---
|
||||
|
||||
## 6. Dependencies
|
||||
|
||||
See `component-dependency.md` for the matrix and data flows. The coupling that deserves attention:
|
||||
|
||||
**The `wwwroot/web/` filesystem convention is the feature's weakest link.** It is enforced by convention, not by the type system, and getting it wrong destroys a customer's website (NFR-02). Two design obligations follow:
|
||||
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. Deployment must link the **persistent** `wwwroot/web/` into each new release directory (ASM-01)
|
||||
|
||||
**`Core` reaches the Slave.** Every change lands in a host with no test project, verified at Build and Test by starting it.
|
||||
|
||||
**Build-time frontend configuration** forces two artifacts (D-15), and the publish target makes Node and pnpm prerequisites of `dotnet publish`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Requirements Coverage
|
||||
|
||||
| Requirement | Covered by |
|
||||
|---|---|
|
||||
| FR-01, FR-03, FR-04, FR-05 | C-12 |
|
||||
| FR-02 | C-11, S-06 |
|
||||
| FR-06, FR-20 | S-05 |
|
||||
| FR-07, FR-08 | C-10, S-01 |
|
||||
| FR-09 | Unit 7 documentation (Infrastructure Design input) |
|
||||
| FR-10 | C-04, C-13, S-01 |
|
||||
| FR-11 | C-07, S-03 |
|
||||
| FR-12 | C-05, C-06 — **and § 5.1** |
|
||||
| FR-13 | C-14 |
|
||||
| FR-14 | C-09 |
|
||||
| FR-15, FR-16 | C-15 |
|
||||
| FR-17 | Operations phase |
|
||||
| FR-18 | C-01, C-02, C-03, S-02 |
|
||||
| FR-19 | C-09 + NFR Design, Unit 4 |
|
||||
| FR-21, FR-22 | Unit 1 — no design needed |
|
||||
| FR-23 | Operations phase |
|
||||
| FR-24 | C-13 — **and § 5.2** |
|
||||
|
||||
All 24 functional requirements are accounted for: 18 by a designed component, 3 by the Operations phase, 2 needing no design, and 1 (FR-09) depending on Infrastructure Design output.
|
||||
|
||||
---
|
||||
|
||||
## 8. Security Compliance (Security Baseline extension — enabled, blocking)
|
||||
|
||||
Assessed against the design.
|
||||
|
||||
| Rule | Status | Notes |
|
||||
|---|---|---|
|
||||
| SECURITY-01 | Addressed | TLS enforced in connection strings; HSTS on all responses (FU1 = A) |
|
||||
| SECURITY-02 | N/A | No load balancer, API gateway or CDN in this architecture |
|
||||
| SECURITY-03 | Addressed | C-08 structured logging with correlation ID; no secrets or PII. Mechanism decided in NFR Design (OPEN-01) |
|
||||
| SECURITY-04 | Addressed | C-01/C-02/C-03, with per-header scoping so `nosniff` covers assets — the specific gap caught in follow-up round 2 |
|
||||
| SECURITY-05 | Unchanged | `/health` is the only new endpoint and accepts no input |
|
||||
| SECURITY-06 | Addressed | Deploy credentials scoped to the target; Gitea secrets repository-scoped |
|
||||
| SECURITY-07 | Partially N/A | No cloud networking; applicable parts are documented host setup |
|
||||
| SECURITY-08 | **Improved** | C-13 closes the forged-token bypass (FR-24) — a pre-existing finding this feature now fixes rather than inherits |
|
||||
| SECURITY-09 | Addressed | Directory browsing stays disabled; Scalar remains Development-only; production errors stay generic; no default credentials |
|
||||
| SECURITY-10 | Addressed | Blocking vulnerability gate; pinned tool versions; `--frozen-lockfile`. DEV-02 records the missing `packages.lock.json` and SBOM |
|
||||
| SECURITY-11 | Addressed | Rate limiting pre-exists; security logic stays in `Core/Identity`; the misuse cases explicitly designed against are website destruction (NFR-02) and silent key-ring loss (§ 5.1) |
|
||||
| SECURITY-12 | Unchanged | Pre-existing; DEV-03 records absent MFA and breached-password checking |
|
||||
| SECURITY-13 | Addressed | SRI for external scripts where supported; CSP constrains them; pipeline definitions are version-controlled and reviewable |
|
||||
| SECURITY-14 | Addressed with DEV-01 | Alerting via C-09; retention deviation accepted |
|
||||
| SECURITY-15 | Unchanged | Global exception handler pre-exists. **New fail-closed decision**: an unknown CSP policy name fails at startup rather than degrading silently. The master gate's deliberate fail-open remains an intentional, business-driven exception |
|
||||
|
||||
**No blocking security findings.** SECURITY-08 improves from "pre-existing, unchanged" to "improved" because Q12 = A folded the fix into this feature.
|
||||
|
||||
---
|
||||
|
||||
## 9. Carried Forward to Construction
|
||||
|
||||
| Item | Resolved at |
|
||||
|---|---|
|
||||
| **§ 5.1** — remove duplicate `AddDataProtection()`; set an explicit application discriminator | Functional Design + Code Generation, Unit 2 |
|
||||
| **§ 5.2** — FR-24 implementation approach (validate in middleware versus move authentication) | Functional Design, Unit 2 |
|
||||
| OPEN-01 — correlation-ID mechanism | NFR Design, Unit 4 |
|
||||
| OPEN-03 — patched versions for the two vulnerable packages | Code Generation, Unit 1 |
|
||||
| ASM-01 — `wwwroot/web/` outside the swapped release directory | Infrastructure Design, Unit 6 |
|
||||
| Definition of an alertable security event | NFR Design, Unit 4 |
|
||||
| `C-10` tolerating a missing `wwwroot/web/` at startup | Functional Design, Unit 2 |
|
||||
+249
@@ -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. |
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
# Component Methods
|
||||
|
||||
Method signatures, purpose and input/output types. **Detailed business rules are defined per unit in Functional Design (CONSTRUCTION phase)** — this document establishes the interface contracts only.
|
||||
|
||||
Signatures are indicative C# and may be refined during Code Generation, but the shape of each contract is a design decision recorded here.
|
||||
|
||||
---
|
||||
|
||||
## C-01 `SecurityHeadersMiddleware`
|
||||
|
||||
```csharp
|
||||
public sealed class SecurityHeadersMiddleware
|
||||
{
|
||||
public SecurityHeadersMiddleware(RequestDelegate next, IOptions<SecurityHeadersOptions> options, CspPolicyBuilder policyBuilder);
|
||||
public Task InvokeAsync(HttpContext context);
|
||||
}
|
||||
```
|
||||
|
||||
| Member | Purpose | Input | Output |
|
||||
|---|---|---|---|
|
||||
| `InvokeAsync` | Register a response-start callback that applies the appropriate headers, then continue the pipeline | `HttpContext` | `Task` |
|
||||
|
||||
**Interface notes**:
|
||||
- Headers are applied through `HttpResponse.OnStarting`, **not** before calling `next`. The response content type is unknown until the response begins, and HTML-only headers (FU1 = A) cannot be decided without it.
|
||||
- The CSP policy for the request path is resolved once per request, before the callback, so path matching does not run at response-start time.
|
||||
- Existing headers are never overwritten — a downstream component that deliberately set one wins.
|
||||
|
||||
---
|
||||
|
||||
## C-02 `SecurityHeadersOptions`
|
||||
|
||||
```csharp
|
||||
public sealed class SecurityHeadersOptions
|
||||
{
|
||||
public List<PathPolicyRule> PathPolicies { get; set; } = new();
|
||||
public string DefaultPolicy { get; set; } = "Relaxed";
|
||||
public List<string> AllowedScriptOrigins { get; set; } = new();
|
||||
public List<string> AllowedConnectOrigins { get; set; } = new();
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class PathPolicyRule
|
||||
{
|
||||
public string PathPrefix { get; set; } = string.Empty;
|
||||
public string Policy { get; set; } = string.Empty;
|
||||
}
|
||||
```
|
||||
|
||||
| Member | Purpose |
|
||||
|---|---|
|
||||
| `PathPolicies` | Ordered path-prefix to policy-name assignment. Configuration, so paths can be added without code changes (Q6 = B) |
|
||||
| `DefaultPolicy` | Policy applied when no prefix matches — `Relaxed`, covering the public website |
|
||||
| `AllowedScriptOrigins` | Origins added to the CSP `script-src` directive — the Umami script host |
|
||||
| `AllowedConnectOrigins` | Origins added to `connect-src` — the Sentry ingest host |
|
||||
| `Enabled` | Escape hatch for local development or diagnosis |
|
||||
|
||||
**Design rule (FU2 = A)**: policy *definitions* are in code; only *assignment* and environment-specific *origins* are configuration. A misconfiguration can therefore misroute a path but cannot invent a broken policy.
|
||||
|
||||
---
|
||||
|
||||
## C-03 `CspPolicyBuilder`
|
||||
|
||||
```csharp
|
||||
public sealed class CspPolicyBuilder
|
||||
{
|
||||
public CspPolicyBuilder(IOptions<SecurityHeadersOptions> options);
|
||||
public string Build(string policyName);
|
||||
public string ResolvePolicyName(PathString path);
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Purpose | Input | Output |
|
||||
|---|---|---|---|
|
||||
| `Build` | Compose the full CSP header value for a named policy, injecting configured origins | `string policyName` | `string` — the header value |
|
||||
| `ResolvePolicyName` | Determine which policy applies to a request path by prefix match, falling back to `DefaultPolicy` | `PathString` | `string` — policy name |
|
||||
|
||||
**Interface notes**:
|
||||
- Policy strings are built **once at startup** and cached by name; `Build` returns the cached value. Composing a CSP per request would be wasteful on a static-file-heavy workload.
|
||||
- Two policies are defined in code: `Strict` (baseline `default-src 'self'`) and `Relaxed` (permissive enough that a website author who never saw this repository is not broken by it — D-31).
|
||||
- An unknown policy name is a configuration error and must fail at startup, not silently fall back.
|
||||
|
||||
---
|
||||
|
||||
## C-04 Health-check registration
|
||||
|
||||
```csharp
|
||||
public static IServiceCollection AddCmsHealthChecks(this IServiceCollection services);
|
||||
public static IEndpointRouteBuilder MapCmsHealthChecks(this IEndpointRouteBuilder endpoints);
|
||||
```
|
||||
|
||||
| Method | Purpose | Input | Output |
|
||||
|---|---|---|---|
|
||||
| `AddCmsHealthChecks` | Register framework health-check services | `IServiceCollection` | same, for chaining |
|
||||
| `MapCmsHealthChecks` | Map `GET /health` | `IEndpointRouteBuilder` | same, for chaining |
|
||||
|
||||
**Interface notes**:
|
||||
- **No database check and no dependency probes** (D-21). The registration takes no options precisely so that "just add one more check" is a visible code change rather than a configuration drift.
|
||||
- Response is the framework default: `200` with `Healthy`, or `503` with `Unhealthy`.
|
||||
- The endpoint is anonymous and exposes no information beyond liveness.
|
||||
|
||||
---
|
||||
|
||||
## C-05 `CmsDataProtection` registration
|
||||
|
||||
```csharp
|
||||
public static IServiceCollection AddCmsDataProtection(this IServiceCollection services, IConfiguration configuration);
|
||||
```
|
||||
|
||||
| Method | Purpose | Input | Output |
|
||||
|---|---|---|---|
|
||||
| `AddCmsDataProtection` | Configure Data Protection to persist keys in `ApplicationDbContext` with a stable application discriminator | `IServiceCollection`, `IConfiguration` | same, for chaining |
|
||||
|
||||
**Interface notes**:
|
||||
- Replaces the bare `services.AddDataProtection()` calls currently made independently by `AvailabilityModule` and `MasterModule`. Those must be removed, or a later registration could silently override the persistent store.
|
||||
- The application discriminator must be **stable and explicit**. By default it derives from the content root path, which changes with every atomic release-directory switch (FR-06) — which would defeat the entire purpose of FR-12.
|
||||
|
||||
---
|
||||
|
||||
## C-06 `ApplicationDbContext` extension
|
||||
|
||||
```csharp
|
||||
public class ApplicationDbContext : IdentityDbContext<...>, IDataProtectionKeyContext
|
||||
{
|
||||
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
| Member | Purpose |
|
||||
|---|---|
|
||||
| `DataProtectionKeys` | Backing store for the Data Protection key ring, required by `IDataProtectionKeyContext` |
|
||||
|
||||
Requires one new Core migration, applied automatically by C-07.
|
||||
|
||||
---
|
||||
|
||||
## C-07 Startup migration runner
|
||||
|
||||
```csharp
|
||||
public static WebApplication MigrateCoreDatabase(this WebApplication app);
|
||||
```
|
||||
|
||||
| Method | Purpose | Input | Output |
|
||||
|---|---|---|---|
|
||||
| `MigrateCoreDatabase` | Apply pending `ApplicationDbContext` migrations before the app serves traffic | `WebApplication` | same, for chaining |
|
||||
|
||||
**Interface notes**:
|
||||
- **Exceptions propagate (Q8 = A).** No try/catch, no logged-and-continue. A host that cannot reach or migrate its database must not start.
|
||||
- Called before `app.Run()` and before any request is accepted, so no request ever sees a partially migrated schema.
|
||||
- Deliberately covers only `ApplicationDbContext`; the two module contexts already migrate themselves in their `UseModule` implementations, and moving that would change existing behaviour outside this feature's scope.
|
||||
|
||||
---
|
||||
|
||||
## C-08 `CmsLogging` registration
|
||||
|
||||
```csharp
|
||||
public static IHostApplicationBuilder AddCmsLogging(this IHostApplicationBuilder builder);
|
||||
```
|
||||
|
||||
| Method | Purpose | Input | Output |
|
||||
|---|---|---|---|
|
||||
| `AddCmsLogging` | Configure structured console logging with a correlation identifier on every entry | `IHostApplicationBuilder` | same, for chaining |
|
||||
|
||||
**Interface notes**:
|
||||
- Independent of Sentry (Q10 = B) — structured logging must work with no DSN configured.
|
||||
- The correlation-ID mechanism is **not fixed here**; OPEN-01 is decided in NFR Design for Unit 4.
|
||||
- Must not log secrets, tokens or PII (SECURITY-03).
|
||||
|
||||
---
|
||||
|
||||
## C-09 `CmsSentry` registration
|
||||
|
||||
```csharp
|
||||
public static IHostApplicationBuilder AddCmsSentry(this IHostApplicationBuilder builder);
|
||||
```
|
||||
|
||||
| Method | Purpose | Input | Output |
|
||||
|---|---|---|---|
|
||||
| `AddCmsSentry` | Initialise Sentry when a DSN is configured; do nothing when it is not | `IHostApplicationBuilder` | same, for chaining |
|
||||
|
||||
**Interface notes**:
|
||||
- Absent DSN is a **normal, supported state**, not an error — local development and any deployment without Sentry must run unchanged with console logging only (FR-14).
|
||||
- Tags events with environment and release.
|
||||
- Security-relevant events for alerting (FR-19) are emitted by application code; what qualifies as alertable is defined in NFR Design for Unit 4.
|
||||
|
||||
---
|
||||
|
||||
## C-10 Static-file mount composition (`SlpModularCms.Api` only)
|
||||
|
||||
Composed inline in `Program.cs` rather than behind an abstraction, since it is host-specific and there is exactly one host that needs it.
|
||||
|
||||
| Registration | Purpose |
|
||||
|---|---|
|
||||
| `UseDefaultFiles` + `UseStaticFiles` with `PhysicalFileProvider(wwwroot/web)` at `/` | Serve the customer's public website |
|
||||
| `UseDefaultFiles` + `UseStaticFiles` with `PhysicalFileProvider(wwwroot/admin)`, `RequestPath = "/admin"` | Serve the admin SPA |
|
||||
| `MapFallbackToFile("/admin/{*path:nonfile}", …)` | Admin SPA client-side routes |
|
||||
| `MapFallbackToFile("{*path:nonfile}", …)` | Public website client-side routes |
|
||||
|
||||
**Interface notes**:
|
||||
- Order matters: the `/admin` mount must be registered before the root mount, so `/admin/...` is not captured by the root provider.
|
||||
- The `nonfile` constraint is retained on both fallbacks — a missing asset must still `404` rather than receive HTML (existing behaviour worth preserving deliberately).
|
||||
- Directory browsing stays disabled (SECURITY-09).
|
||||
- Both providers must tolerate a **missing directory at startup**: a fresh deployment has no `wwwroot/web/` until a website workspace deploys into it, and the CMS must still start.
|
||||
|
||||
---
|
||||
|
||||
## C-13 `AvailabilityMiddleware` (modified)
|
||||
|
||||
```csharp
|
||||
private static readonly string[] _bypassPrefixes = [ /* existing */, "/health" ];
|
||||
private bool IsAdminBypass(HttpContext context);
|
||||
```
|
||||
|
||||
| Member | Change | Purpose |
|
||||
|---|---|---|
|
||||
| `_bypassPrefixes` | Add `/health` | The availability gate must never mask infrastructure liveness (FR-10, D-22) |
|
||||
| `IsAdminBypass` | Stop using `ReadJwtToken`; rely on a validated principal | Close the forged-token bypass (FR-24, SECURITY-08) |
|
||||
|
||||
**Interface notes**:
|
||||
- Signature is unchanged; only the implementation and the constant change.
|
||||
- **Preserved behaviour**: an Owner or Administrator with a valid token still bypasses the gate, so administrators can always reach a disabled instance.
|
||||
- If the implementation moves to reading `HttpContext.User`, note that `AvailabilityMiddleware` currently runs **before** `UseAuthentication()`. Either authentication must run earlier, or the middleware must validate the token itself with the same parameters as the bearer scheme. **This ordering constraint is the substance of the fix and is resolved in Functional Design for Unit 2.**
|
||||
|
||||
---
|
||||
|
||||
## C-14 Frontend configuration (modified)
|
||||
|
||||
```typescript
|
||||
export function getAppConfig(): AppConfig;
|
||||
|
||||
export interface AppConfig {
|
||||
apiBaseUrl: string; // '' means same-origin
|
||||
appTitle: string;
|
||||
}
|
||||
```
|
||||
|
||||
| Change | Purpose |
|
||||
|---|---|
|
||||
| `apiBaseUrl` accepts empty string | Same-origin default when `VITE_API_BASE_URL` is unset (FR-13) |
|
||||
| Zod schema relaxed | Accept either an empty string or a valid absolute URL — **not** any string, so a malformed value is still caught |
|
||||
|
||||
**Interface notes**:
|
||||
- `ApiClient` composes request URLs as `${baseUrl}${path}`, so an empty base yields a root-relative URL — same-origin without further change.
|
||||
- Local development against `https://localhost:7221` (master) or `:7222` (slave) must keep working exactly as today.
|
||||
|
||||
---
|
||||
|
||||
## C-15 Frontend observability (new)
|
||||
|
||||
| Element | Purpose |
|
||||
|---|---|
|
||||
| Sentry initialisation in `main.tsx` | Error and performance reporting; skipped when `VITE_SENTRY_DSN` is absent |
|
||||
| Umami script component | Analytics; renders nothing when the website ID is absent or in local development |
|
||||
|
||||
**Interface notes**: both read build-time Vite variables, which is why two separate builds are produced (D-15).
|
||||
|
||||
---
|
||||
|
||||
## C-11 / C-12 Workflow interfaces
|
||||
|
||||
### `deploy-scp.yaml` (and later `deploy-ftps.yaml`) — `workflow_call` inputs
|
||||
|
||||
| Input | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `artifact_name` | string | Build artifact to download |
|
||||
| `environment` | string | `test` or `production` — used for naming and tagging |
|
||||
| `deploy_path` | string | Target base path on the host |
|
||||
| `release_retention` | number | How many previous releases to retain for fast rollback (FR-06, D-26) |
|
||||
|
||||
Secrets are inherited. **The input interface is identical across transports (Q11 = B)**, so a caller can switch workflow file without changing arguments.
|
||||
|
||||
### `continuous_integration.yaml` — `workflow_dispatch` inputs
|
||||
|
||||
| Input | Type | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `deploy_production` | boolean | `false` | The only route to production (FR-04) |
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
# Components
|
||||
|
||||
Component definitions and high-level responsibilities. Detailed business logic is designed per unit in Functional Design.
|
||||
|
||||
**Placement rule (Q1 = A)**: every new cross-cutting concern lives in `SlpModularCms.Core` and is exposed as an extension method. Both hosts therefore get identical behaviour with no duplicated implementation. **Q2 = A**: `SlpModularCms.Api.Slave` receives everything except the static-file mounts, because it serves as a reference for what a customer-facing API instance looks like — not merely as a local dev tool.
|
||||
|
||||
---
|
||||
|
||||
## New Components
|
||||
|
||||
### C-01 `SecurityHeadersMiddleware`
|
||||
- **Project**: `SlpModularCms.Core` (`Hosting/Security/`)
|
||||
- **Purpose**: Apply HTTP security headers that would normally come from nginx or IIS configuration, which NFR-01 forbids relying on.
|
||||
- **Responsibilities**:
|
||||
- Attach headers at response start, so the response content type is known before deciding what applies
|
||||
- Apply per-header scoping (FU1 = A): `X-Content-Type-Options` and `Strict-Transport-Security` on **all** responses; `Content-Security-Policy`, `X-Frame-Options` and `Referrer-Policy` on **HTML** responses only
|
||||
- Select the CSP policy for the request path
|
||||
- Never overwrite a header another component has already set
|
||||
- **Interfaces**: standard middleware — `InvokeAsync(HttpContext)`. Registered via `UseCmsSecurityHeaders()`.
|
||||
- **Requirements**: FR-18, SECURITY-04
|
||||
|
||||
### C-02 `SecurityHeadersOptions`
|
||||
- **Project**: `SlpModularCms.Core` (`Hosting/Security/`)
|
||||
- **Purpose**: Bind the `SecurityHeaders` configuration section (Options pattern, consistent with `JwtSettings`, `MasterModule`, `MasterPolling`).
|
||||
- **Responsibilities**: Carry the path-to-policy assignment, the default policy, and the environment-specific allowed origins.
|
||||
- **Design rule (Q5 = B + Q6 = B, confirmed FU2 = A)**: **policy definitions live in code; path assignment and origins live in configuration.** Adding a path later needs no code change; inventing a new policy does.
|
||||
- **Requirements**: FR-18
|
||||
|
||||
### C-03 `CspPolicyBuilder`
|
||||
- **Project**: `SlpModularCms.Core` (`Hosting/Security/`)
|
||||
- **Purpose**: Compose a Content-Security-Policy string from a named policy plus the configured origins.
|
||||
- **Responsibilities**:
|
||||
- Define the two named policies in code: `Strict` (for `/admin` and `/api/v1`) and `Relaxed` (for the public website)
|
||||
- Inject the configured Umami script origin and Sentry ingest origin into the relevant directives
|
||||
- Build each policy once at startup rather than per request
|
||||
- **Rationale for existing separately from C-01**: keeps policy composition unit-testable without a request pipeline, and keeps the middleware free of string building.
|
||||
- **Requirements**: FR-18, D-31
|
||||
|
||||
### C-04 Health-check registration
|
||||
- **Project**: `SlpModularCms.Core` (`Hosting/Health/`)
|
||||
- **Purpose**: Expose infrastructure liveness, strictly separate from CMS domain state.
|
||||
- **Responsibilities**:
|
||||
- Register the framework health-check services (no package required)
|
||||
- Map `GET /health` returning `200`/`Healthy` or `503`/`Unhealthy`
|
||||
- **Liveness only** — no database call, no dependency probing (D-21)
|
||||
- **Explicit non-responsibility**: this component says nothing about availability or capabilities. Those are CMS domain functionality and are never to be used for monitoring.
|
||||
- **Requirements**: FR-10
|
||||
|
||||
### C-05 `CmsDataProtection` registration
|
||||
- **Project**: `SlpModularCms.Core` (`Hosting/`)
|
||||
- **Purpose**: Persist the Data Protection key ring in the database so redeploys and atomic release switches cannot render stored slave API keys unreadable.
|
||||
- **Responsibilities**: Configure `PersistKeysToDbContext<ApplicationDbContext>` and set a stable application discriminator so both hosts and all replicas derive the same keys.
|
||||
- **Requirements**: FR-12, D-17
|
||||
|
||||
### C-06 `ApplicationDbContext` extension — `IDataProtectionKeyContext`
|
||||
- **Project**: `SlpModularCms.Core` (`Data/`) — **modification of an existing component**
|
||||
- **Purpose**: Host the Data Protection keys table (Q7 = A).
|
||||
- **Responsibilities**: Add `DbSet<DataProtectionKey> DataProtectionKeys` and implement `IDataProtectionKeyContext`. Requires one new Core migration.
|
||||
- **Why here rather than a fourth context**: keys are application-wide infrastructure, not module-owned, and `ApplicationDbContext` now migrates automatically (FR-11) so the table is created without manual steps.
|
||||
- **Requirements**: FR-12
|
||||
|
||||
### C-07 Startup migration runner
|
||||
- **Project**: `SlpModularCms.Core` (`Hosting/`)
|
||||
- **Purpose**: Apply `ApplicationDbContext` migrations at startup, removing the need for CLI access on the host.
|
||||
- **Responsibilities**:
|
||||
- Run `Database.Migrate()` for `ApplicationDbContext` during startup, before the request pipeline accepts traffic
|
||||
- **Fail fast (Q8 = A)**: on failure, let the exception propagate so the process does not start
|
||||
- **Design interaction worth stating**: fail-fast is what makes the liveness-only health check meaningful. A process that cannot migrate never starts, `/health` stops answering, and UptimeRobot goes red. Had this logged-and-continued, the app would look healthy while being unusable.
|
||||
- **Requirements**: FR-11, D-13
|
||||
|
||||
### C-08 `CmsLogging` registration
|
||||
- **Project**: `SlpModularCms.Core` (`Hosting/Observability/`)
|
||||
- **Purpose**: Configure structured logging, independent of whether Sentry is enabled (Q10 = B).
|
||||
- **Responsibilities**: Console logging with structured output and a correlation/request identifier on every entry; exclude secrets and PII.
|
||||
- **Open item**: OPEN-01 — the correlation-ID mechanism (ASP.NET Core `TraceIdentifier` versus W3C `traceparent`) is decided in NFR Design for Unit 4.
|
||||
- **Requirements**: D-20, SECURITY-03
|
||||
|
||||
### C-09 `CmsSentry` registration
|
||||
- **Project**: `SlpModularCms.Core` (`Hosting/Observability/`)
|
||||
- **Purpose**: Report errors and structured logs to Sentry, tagged by environment.
|
||||
- **Responsibilities**:
|
||||
- Initialise `Sentry.AspNetCore` when a DSN is configured, and **skip silently when it is not**, leaving console logging active
|
||||
- Tag events with environment and release
|
||||
- Emit security-relevant events for alerting (FR-19)
|
||||
- **Separate from C-08 (Q10 = B)**: structured logging must work without Sentry.
|
||||
- **Requirements**: FR-14, FR-19, D-19
|
||||
|
||||
### C-10 Static-file mount composition
|
||||
- **Project**: `SlpModularCms.Api` **only** — host-specific, not in `Core` (Q2 = A: the Slave has no static content)
|
||||
- **Purpose**: Serve two independent front-ends from one process.
|
||||
- **Responsibilities** (Q3 = A):
|
||||
- Mount `wwwroot/web/` at `/` with its own `PhysicalFileProvider`
|
||||
- Mount `wwwroot/admin/` at `/admin` with its own `PhysicalFileProvider`
|
||||
- Provide default-file handling per mount
|
||||
- Map two SPA fallbacks, preserving the `nonfile` route constraint so missing assets still return `404`
|
||||
- **Design consequence**: two explicit registrations rather than one, so each mount can later carry its own headers or caching without disturbing the other.
|
||||
- **Requirements**: FR-07, FR-08, D-06
|
||||
|
||||
### C-11 Deploy transport workflows
|
||||
- **Project**: `.gitea/workflows/` — not C#
|
||||
- **Purpose**: Transfer a published release to a target host.
|
||||
- **Responsibilities** (Q11 = B): one reusable workflow per transport, sharing an identical input interface. `deploy-scp.yaml` is implemented now; `deploy-ftps.yaml` can be added later without changing callers.
|
||||
- **Requirements**: FR-02, D-02, NFR-09
|
||||
|
||||
### C-12 CI workflow
|
||||
- **Project**: `.gitea/workflows/continuous_integration.yaml`
|
||||
- **Purpose**: Validate every change and drive deployment.
|
||||
- **Responsibilities**: Six blocking gates, two environment-specific builds, artifact publication, and the calls into C-11 for test and production.
|
||||
- **Requirements**: FR-01, FR-03, FR-04, FR-05
|
||||
|
||||
---
|
||||
|
||||
## Modified Existing Components
|
||||
|
||||
### C-13 `AvailabilityMiddleware`
|
||||
- **Project**: `SlpModularCms.Modules.Availability` (`Middleware/`)
|
||||
- **Changes**:
|
||||
1. Add `/health` to `_bypassPrefixes` so the availability gate cannot mask infrastructure liveness (FR-10, D-22)
|
||||
2. **Fix `IsAdminBypass` to stop trusting an unvalidated token** (FR-24, Q12 = A) — currently `ReadJwtToken` parses without signature verification, so an unauthenticated caller can forge an `Owner` claim and bypass the gate
|
||||
- **Behaviour that must be preserved**: an Owner or Administrator with a *valid* token still passes, so administrators can always reach a disabled instance to switch it back on.
|
||||
- **Requirements**: FR-10, FR-24
|
||||
|
||||
### C-14 Frontend application configuration (`frontend/src/lib/config.ts`)
|
||||
- **Changes**: treat an absent or empty `VITE_API_BASE_URL` as same-origin while still accepting an explicit absolute URL for local development against `https://localhost:7221` / `:7222`. Zod validation relaxed accordingly, without silently accepting malformed values.
|
||||
- **Requirements**: FR-13, D-14
|
||||
|
||||
### C-15 Frontend observability
|
||||
- **Project**: `frontend/src/`
|
||||
- **Changes**: initialise `@sentry/react` with environment and release tags, skipping gracefully without a DSN; add the Umami tracking script with a per-environment website ID, absent during local development.
|
||||
- **Requirements**: FR-15, FR-16
|
||||
|
||||
### C-16 Host composition (`Program.cs`, both hosts)
|
||||
- **Changes**: call the new `Core` extension methods in the correct order. **Q9 = A**: the two files stay separate — with the implementation in `Core`, what remains duplicated is an explicit list of calls, which is intentional readability rather than accidental duplication.
|
||||
- **Requirements**: FR-07, FR-10, FR-11, FR-12, FR-14, FR-18
|
||||
|
||||
---
|
||||
|
||||
## Component Summary
|
||||
|
||||
| ID | Component | Project | Type | Slave gets it? |
|
||||
|---|---|---|---|---|
|
||||
| C-01 | `SecurityHeadersMiddleware` | Core | New | Yes |
|
||||
| C-02 | `SecurityHeadersOptions` | Core | New | Yes |
|
||||
| C-03 | `CspPolicyBuilder` | Core | New | Yes |
|
||||
| C-04 | Health-check registration | Core | New | Yes |
|
||||
| C-05 | `CmsDataProtection` registration | Core | New | Yes |
|
||||
| C-06 | `ApplicationDbContext` keys table | Core | Modified | Yes |
|
||||
| C-07 | Startup migration runner | Core | New | Yes |
|
||||
| C-08 | `CmsLogging` registration | Core | New | Yes |
|
||||
| C-09 | `CmsSentry` registration | Core | New | Yes |
|
||||
| C-10 | Static-file mount composition | Api | New | **No** |
|
||||
| C-11 | Deploy transport workflows | `.gitea/` | New | n/a |
|
||||
| C-12 | CI workflow | `.gitea/` | New | n/a |
|
||||
| C-13 | `AvailabilityMiddleware` | Modules.Availability | Modified | Yes |
|
||||
| C-14 | Frontend config | frontend | Modified | n/a |
|
||||
| C-15 | Frontend observability | frontend | New | n/a |
|
||||
| C-16 | Host composition | Both hosts | Modified | Yes |
|
||||
|
||||
**14 code components** (9 new, 5 modified) plus **2 workflow components**.
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
# Services and Orchestration
|
||||
|
||||
Service definitions, responsibilities and orchestration patterns.
|
||||
|
||||
This feature introduces few *stateful* services. Its service layer is mostly **composition orchestration**: the order in which registrations and middleware are applied, which for a single process serving three surfaces is where the real design lives. A correct set of components in the wrong order produces security headers that never reach the public website, or a health endpoint that a disabled instance hides.
|
||||
|
||||
---
|
||||
|
||||
## S-01 Host composition service (`Program.cs`, both hosts)
|
||||
|
||||
**Responsibility**: compose configuration, services and the request pipeline in an order that satisfies all 24 functional requirements simultaneously.
|
||||
|
||||
**Orchestration pattern**: explicit sequential composition. Per Q9 = A the two hosts keep their own `Program.cs`; with implementations in `Core` (Q1 = A), what is duplicated is a readable list of calls, not logic.
|
||||
|
||||
### Service registration order
|
||||
|
||||
| # | Registration | Notes |
|
||||
|---|---|---|
|
||||
| 1 | `AddJsonFile("appsettings.local.json", optional: true)` | Existing |
|
||||
| 2 | `AddCmsLogging()` | **Early** — so subsequent startup work is already logged structurally |
|
||||
| 3 | `AddCmsSentry()` | Separate from logging (Q10 = B); no-op without a DSN |
|
||||
| 4 | `ModuleOrchestrator.DiscoverModules()` | Existing |
|
||||
| 5 | `AddCoreInfrastructure(configuration)` | Existing — DbContext, Identity, JWT, policies, exception handling |
|
||||
| 6 | `AddCmsDataProtection(configuration)` | **Before module registration** — see the conflict note below |
|
||||
| 7 | `AddCmsCors` / `AddCmsRateLimiting` | Existing |
|
||||
| 8 | `AddCmsHealthChecks()` | New |
|
||||
| 9 | `AddCmsSecurityHeaders(configuration)` | New — binds options, registers `CspPolicyBuilder` |
|
||||
| 10 | `orchestrator.RegisterModuleServices(services)` | Existing |
|
||||
| 11 | `AddControllers(...)` with `ApiPrefixConvention` | Existing |
|
||||
|
||||
> **Registration conflict that must be resolved (step 6 versus step 10)**
|
||||
> `AvailabilityModule.RegisterServices` and `MasterModule.RegisterServices` each call `services.AddDataProtection()` today. Whichever runs last wins the configuration. If those calls remain, module registration at step 10 would silently discard the persistent key store configured at step 6 — and FR-12 would appear implemented while doing nothing.
|
||||
> **Resolution**: remove `AddDataProtection()` from both modules; the host configures Data Protection once. Assigned to Unit 2 and verified in that unit's tests.
|
||||
|
||||
### Middleware pipeline order
|
||||
|
||||
| # | Middleware | Why here |
|
||||
|---|---|---|
|
||||
| 1 | `UseExceptionHandler()` | Existing — must be outermost to catch everything |
|
||||
| 2 | `UseCmsSecurityHeaders()` | **New.** Before static files, because static files short-circuit the pipeline — anything registered after them never reaches the public website. Placed after the exception handler so error responses also carry headers. Applies per-header scoping at response start (FU1 = A) |
|
||||
| 3 | `UseRateLimiter()` | Existing |
|
||||
| 4 | Dev-only: `MapOpenApi()`, `MapScalarApiReference()` | Existing — Development only (SECURITY-09) |
|
||||
| 5 | `UseHttpsRedirection()` | Existing |
|
||||
| 6 | Static files — `/admin` mount, then `/` mount | New arrangement (C-10). `Api` host only. `/admin` first so it is not captured by the root provider |
|
||||
| 7 | `UseCors()` | Existing |
|
||||
| 8 | `orchestrator.UseModules(app)` → installs `AvailabilityMiddleware` | Existing position. Consequence, deliberately unchanged: the public website is served *before* the availability gate, so a disabled instance still serves the website while blocking `/api/v1` and `/admin` |
|
||||
| 9 | `UseAuthentication()` / `UseAuthorization()` | Existing — but see the FR-24 ordering constraint below |
|
||||
| 10 | `MapControllers()` | Existing |
|
||||
| 11 | `MapCmsHealthChecks()` | New. An endpoint, therefore after the gate — which is exactly why `/health` must be on the bypass list (D-22) |
|
||||
| 12 | Two `MapFallbackToFile` registrations | Existing pattern, retargeted to the two mounts |
|
||||
|
||||
> **Ordering constraint for FR-24**
|
||||
> `AvailabilityMiddleware` (step 8) runs **before** `UseAuthentication()` (step 9), so `HttpContext.User` is not yet populated when the admin bypass is evaluated. Two options, decided in Functional Design for Unit 2:
|
||||
> **(a)** validate the token inside the middleware using the same `TokenValidationParameters` as the bearer scheme, or
|
||||
> **(b)** move `UseAuthentication()` before the module middleware.
|
||||
> Option (b) is a smaller change but alters the pipeline for every module, including any future one — a wider blast radius than this feature should take on. Option (a) is contained but duplicates validation parameters, which must then be shared rather than copied.
|
||||
|
||||
---
|
||||
|
||||
## S-02 Security-header application service
|
||||
|
||||
**Responsibility**: decide and apply the correct headers for each response.
|
||||
|
||||
**Orchestration**:
|
||||
1. On request: resolve the policy name for the path via `CspPolicyBuilder.ResolvePolicyName` (prefix match, `DefaultPolicy` fallback)
|
||||
2. Register an `OnStarting` callback carrying that policy name
|
||||
3. At response start, inspect `Content-Type` and apply:
|
||||
- **Always**: `X-Content-Type-Options`, `Strict-Transport-Security`
|
||||
- **HTML responses only**: `Content-Security-Policy`, `X-Frame-Options`, `Referrer-Policy`
|
||||
4. Skip any header already present
|
||||
|
||||
**Why response-start rather than pre-`next`**: the content type is unknown until the response begins, and per-header scoping (FU1 = A) depends on it. Setting headers before calling `next` would force an all-or-nothing choice.
|
||||
|
||||
**Failure behaviour**: header application never throws into the response path. A misconfigured policy is a **startup** failure (unknown policy name), not a per-request one.
|
||||
|
||||
---
|
||||
|
||||
## S-03 Startup migration orchestration
|
||||
|
||||
**Responsibility**: bring the database schema to the required version before serving traffic.
|
||||
|
||||
**Orchestration**:
|
||||
1. After `builder.Build()`, before `app.Run()`
|
||||
2. `MigrateCoreDatabase()` applies `ApplicationDbContext` migrations — **fail fast** (Q8 = A)
|
||||
3. `orchestrator.UseModules(app)` triggers the two module contexts' existing `Database.Migrate()` calls
|
||||
|
||||
**Sequencing note**: Core migrates before the modules. All three contexts share one connection string and one database, and the Data Protection keys table lives in `ApplicationDbContext` (Q7 = A) — so the keys table must exist before any module resolves an `IDataProtector`.
|
||||
|
||||
**Failure behaviour**: propagate. The process does not start, `/health` does not answer, UptimeRobot goes red. This is the intended chain and the reason a liveness-only check is sufficient.
|
||||
|
||||
---
|
||||
|
||||
## S-04 Observability orchestration
|
||||
|
||||
**Responsibility**: make errors and usage visible without host access.
|
||||
|
||||
**Orchestration**:
|
||||
- `AddCmsLogging()` first, so Sentry initialisation problems are themselves logged
|
||||
- `AddCmsSentry()` second, reading the DSN from configuration; **absent DSN is a supported state**, not an error
|
||||
- Both registered before any other service, so startup failures — including a fail-fast migration — are captured
|
||||
|
||||
**Degradation model**: three levels, each fully functional.
|
||||
|
||||
| Configuration | Behaviour |
|
||||
|---|---|
|
||||
| No DSN | Structured console logging only |
|
||||
| DSN present | Console plus Sentry, environment-tagged |
|
||||
| DSN present, Sentry unreachable | Sentry's own buffering and drop behaviour; the application is never blocked |
|
||||
|
||||
---
|
||||
|
||||
## S-05 Deployment orchestration (`.gitea/workflows/`)
|
||||
|
||||
**Responsibility**: turn a commit into a running release without endangering data the deployment does not own.
|
||||
|
||||
**Orchestration**:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
trigger["Trigger:<br/>PR, push to master,<br/>or workflow_dispatch"]
|
||||
gates["Quality gates<br/>build, test, vulnerability scan,<br/>frontend build, test, lint"]
|
||||
buildtest["Build artifact: test<br/>env-specific Vite vars"]
|
||||
buildprod["Build artifact: production<br/>env-specific Vite vars"]
|
||||
deploytest["deploy-scp: test<br/>auto on master"]
|
||||
deployprod["deploy-scp: production<br/>only with deploy_production"]
|
||||
done(["Running release"])
|
||||
|
||||
trigger --> gates
|
||||
gates --> buildtest
|
||||
gates --> buildprod
|
||||
buildtest --> deploytest
|
||||
buildprod --> deployprod
|
||||
deploytest --> done
|
||||
deployprod --> done
|
||||
|
||||
classDef trig fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef gate fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef build fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef deploy fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000;
|
||||
classDef fin fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000;
|
||||
class trigger trig;
|
||||
class gates gate;
|
||||
class buildtest,buildprod build;
|
||||
class deploytest,deployprod deploy;
|
||||
class done fin;
|
||||
```
|
||||
|
||||
Text alternative: every trigger runs the quality gates; a test build always follows and deploys automatically on master, while a production build and deploy run only when the `deploy_production` input is set.
|
||||
|
||||
**Per-deployment sequence** (inside `deploy-scp.yaml`):
|
||||
1. Download the artifact
|
||||
2. **Production only**: take a database backup (FR-20) — before anything is changed
|
||||
3. Upload into a **new** release directory
|
||||
4. Link the persistent `wwwroot/web/` into the new release (ASM-01) — the step that keeps the customer's website alive across the switch
|
||||
5. Switch the active release atomically
|
||||
6. Restart the process
|
||||
7. Verify `/health` responds
|
||||
8. Prune old releases beyond the retention count, keeping at least the previous one (D-26)
|
||||
|
||||
**Rollback**: switch back to the retained previous release directory and restart — no rebuild needed. Forward-compatible, non-destructive migrations are what make this safe (FR-11, D-26).
|
||||
|
||||
---
|
||||
|
||||
## S-06 Transport selection
|
||||
|
||||
**Responsibility**: move files to a host over whatever protocol that host offers.
|
||||
|
||||
**Orchestration (Q11 = B)**: one reusable workflow per transport, all sharing an identical `workflow_call` input interface. `deploy-scp.yaml` exists now; `deploy-ftps.yaml` is added when production moves to shared hosting (D-02, OPEN-04). The caller changes only the `uses:` line — satisfying NFR-09 without building an abstraction for a transport that does not yet exist.
|
||||
|
||||
**Why not one workflow with a `transport` input**: the two transports differ in more than a command — atomic directory switching and process restart are natural over SSH but not available over FTPS, where `app_offline.htm` becomes the mechanism instead. Separate files keep each honest about what it can actually guarantee, rather than hiding a materially different deployment model behind a shared conditional.
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# Unit of Work Dependencies
|
||||
|
||||
---
|
||||
|
||||
## Dependency Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
u1["U1 Hosting and Serving"]
|
||||
u2["U2 Data Durability"]
|
||||
u3["U3 Security Headers and CSP"]
|
||||
u4["U4 Observability"]
|
||||
u5["U5 CI Workflow and Gates"]
|
||||
u6["U6 Deploy Workflow"]
|
||||
u7["U7 Documentation"]
|
||||
ops["Operations Phase"]
|
||||
|
||||
u1 -->|"path layout for CSP scoping"| u3
|
||||
u4 -->|"Umami and Sentry origins"| u3
|
||||
u4 -->|"env-specific Vite variables"| u5
|
||||
u1 --> u6
|
||||
u2 -->|"durability must precede first deploy"| u6
|
||||
u3 --> u5
|
||||
u5 -->|"invokes"| u6
|
||||
u6 -->|"settled host layout"| u7
|
||||
u6 --> ops
|
||||
u7 --> ops
|
||||
|
||||
classDef r1 fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef r2 fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef r3 fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef r4 fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000;
|
||||
classDef opsphase fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
class u1,u2 r1;
|
||||
class u3,u4 r2;
|
||||
class u5,u6 r3;
|
||||
class u7 r4;
|
||||
class ops opsphase;
|
||||
```
|
||||
|
||||
Text alternative: U1 and U2 are independent and run first; U3 needs U1's path layout and U4's origins; U5 and U6 need the application work complete; U7 needs U6's settled host layout; the Operations phase follows.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Matrix
|
||||
|
||||
| Unit | Depends on | Depended on by | Nature of dependency |
|
||||
|---|---|---|---|
|
||||
| **U1** Hosting & Serving | — | U3, U6 | Establishes the path layout that U3's CSP scopes against and U6 deploys into |
|
||||
| **U2** Data Durability | — | U6 | Must land before any automated deploy, or the first atomic switch destroys the key ring |
|
||||
| **U3** Security Headers | U1, U4 | U5 | Needs U1's final paths and U4's external origins |
|
||||
| **U4** Observability | — | U3, U5 | Introduces the origins U3 must permit and the build variables U5 must supply |
|
||||
| **U5** CI Workflow | U3, U4 | U6 | Gates must pass against the finished application; the two builds need U4's variables |
|
||||
| **U6** Deploy Workflow | U1, U2, U5 | U7, Operations | Deploys what the application units produced, invoked by U5 |
|
||||
| **U7** Documentation | U6 | Operations | Documents the layout U6 settles |
|
||||
|
||||
---
|
||||
|
||||
## Ordering Constraints That Are Load-Bearing
|
||||
|
||||
These are not preferences. Reordering any of them produces a broken or dangerous result.
|
||||
|
||||
### 1. U2 before U6 — otherwise the first deploy is the dangerous one
|
||||
The atomic release switch (U6) changes the content root path on every deploy. Until U2 configures a database-backed key ring **with an explicit application discriminator**, that switch discards the Data Protection keys and makes every stored slave API key undecryptable. The symptom presents as a network fault between master and slave, so it would be misdiagnosed.
|
||||
|
||||
Deploying first and hardening afterwards means the very first production deploy carries the failure.
|
||||
|
||||
### 2. U4 before or with U3 — otherwise the CSP is written blind
|
||||
U3's `Strict` policy must permit the Umami script origin and the Sentry ingest origin. Those origins are introduced by U4. Writing U3 first means either guessing them or shipping a CSP that blocks the observability U4 then adds — a failure that appears only in a real browser.
|
||||
|
||||
This is why R2 groups them rather than running them in sequence.
|
||||
|
||||
### 3. U1 before U3 — otherwise path scoping is provisional
|
||||
U3 assigns policies by path prefix. Until U1 settles which paths exist and where they are served from, that assignment is written against a layout still in flux.
|
||||
|
||||
### 4. U3 and U4 before U5 — otherwise the gates fail on incomplete work
|
||||
U5's six gates run against the whole application. Switching them on before the application units are complete produces failures that reflect unfinished work rather than defects.
|
||||
|
||||
### 5. U5 with U6 — one interface, two files
|
||||
U6 is a reusable workflow invoked by U5 with a fixed input set. Designing them apart risks an interface mismatch that only surfaces on the first real run.
|
||||
|
||||
### 6. U6 before U7 — documentation cannot precede the layout it documents
|
||||
U7's website contract states target paths, reserved paths and the persistent-directory arrangement. U6 settles those in its Infrastructure Design.
|
||||
|
||||
---
|
||||
|
||||
## What Is *Not* Dependent
|
||||
|
||||
Worth stating explicitly, because it justifies the grouping:
|
||||
|
||||
- **U1 and U2 do not touch each other.** U1 changes serving and middleware; U2 changes persistence and startup. They share `Program.cs` as a file, but not as logic — U1 adds pipeline and endpoint registrations, U2 adds service registration and a startup call. A merge conflict is possible; a behavioural conflict is not.
|
||||
- **U4 does not depend on U1, U2 or U3.** Observability can be added to the application as it stands today.
|
||||
- **U7 does not depend on U3, U4 or U5** beyond describing their results.
|
||||
|
||||
---
|
||||
|
||||
## Shared Resources and Coordination Points
|
||||
|
||||
| Resource | Touched by | Coordination needed |
|
||||
|---|---|---|
|
||||
| `SlpModularCms.Api/Program.cs` | U1, U2, U3, U4 | Four units modify the same file in different places. Registration and pipeline order is specified in `services.md` § S-01, so each unit inserts at a defined position rather than appending |
|
||||
| `SlpModularCms.Api.Slave/Program.cs` | U1 (health only), U2, U3, U4 | Same, minus the static mounts. The Slave is a reference instance (Q2 of Application Design = A) and must keep working; it has no test project, so it is verified by starting it |
|
||||
| `SlpModularCms.Core` | U1, U2, U3, U4 | Each unit adds its own subfolder under `Hosting/` — `Health/`, `Security/`, `Observability/` — so files do not collide |
|
||||
| `AvailabilityModule.cs` / `MasterModule.cs` | U2 | Removing `AddDataProtection()` from both. No other unit touches them |
|
||||
| `appsettings.json` | U2, U3, U4 | Three new sections. Additive, no overlap |
|
||||
| `frontend/` | U4 (features), U5 (lint fixes) | U5's lint fixes touch `AddCmsInstanceDialog.tsx`, `InviteUserDialog.tsx`, `SettingsPage.tsx` and `SetStatusDialog.tsx`; U4 touches `main.tsx`, `config.ts` and adds a Umami component. **No overlapping files** |
|
||||
| `.gitea/workflows/` | U5, U6 | Separate files sharing one input contract |
|
||||
|
||||
---
|
||||
|
||||
## Consequence of Merging the Quality Gates into U5
|
||||
|
||||
Q2 = B moved the lint fixes and package pins from a standalone first unit into U5. This is coherent — gates and their prerequisites land in one commit, so the pipeline is never red on arrival — but it has one side effect worth managing:
|
||||
|
||||
**`pnpm run lint` stays failing through U3 and U4**, and U4 changes frontend files. New violations introduced during U4 would therefore hide among the five pre-existing ones.
|
||||
|
||||
**Mitigation**: run lint on the **changed files** during U4 rather than the whole tree. The blocking gate still arrives with U5, but nothing new accumulates in the meantime.
|
||||
|
||||
The overlap is limited: U5's fixes and U4's changes touch disjoint files, so there is no merge risk — only a detection gap.
|
||||
|
||||
---
|
||||
|
||||
## Rollback Between Units
|
||||
|
||||
Every unit is a self-contained commit on `feature/gitea-deployment-workflow`, with a single pull request at the end (Q6 = A).
|
||||
|
||||
| Unit | Revertible independently? | Notes |
|
||||
|---|---|---|
|
||||
| U1 | Yes | Pipeline and endpoint registrations |
|
||||
| U2 | Yes, with care | The Core migration adds a table; reverting the code leaves the table in place, which is harmless |
|
||||
| U3 | Yes | Additive middleware plus one config section |
|
||||
| U4 | Yes | Additive |
|
||||
| U5 | Yes | New file plus lint and package changes |
|
||||
| U6 | Yes | New file only |
|
||||
| U7 | Yes | Documentation only |
|
||||
|
||||
**Nothing is deployed to any environment until U6 is complete and explicitly triggered**, so a mid-sequence failure cannot affect a running environment.
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
# Unit of Work — Requirement Map
|
||||
|
||||
**Note on this artifact**: the User Stories stage was skipped for this feature (infrastructure and operations work with no new end-user functionality or persona). Per Q7 = A, this map assigns the **24 functional requirements** to units instead of stories. They serve the same purpose here — they are the units of value being delivered, and mapping them gives complete coverage verification.
|
||||
|
||||
---
|
||||
|
||||
## Requirement-to-Unit Map
|
||||
|
||||
| Requirement | Summary | Unit |
|
||||
|---|---|---|
|
||||
| FR-01 | Continuous integration workflow with six blocking gates | **U5** |
|
||||
| FR-02 | Reusable deploy workflow with transport abstraction | **U6** |
|
||||
| FR-03 | Automatic test deployment on `master` | **U6** |
|
||||
| FR-04 | Production deployment only via explicit `workflow_dispatch` | **U5** |
|
||||
| FR-05 | Separate test and production builds | **U5** |
|
||||
| FR-06 | Atomic release switch with retained previous release | **U6** |
|
||||
| FR-07 | `wwwroot` restructuring — serve `/` from `wwwroot/web/` | **U1** |
|
||||
| FR-08 | The public website must survive every CMS deploy | **U6** |
|
||||
| FR-09 | Website workspace contract | **U7** |
|
||||
| FR-10 | Health-check endpoint, liveness only, on the bypass list | **U1** |
|
||||
| FR-11 | Automatic `ApplicationDbContext` migration at startup | **U2** |
|
||||
| FR-12 | Persistent Data Protection key ring | **U2** |
|
||||
| FR-13 | Same-origin API base URL for the admin SPA | **U4** |
|
||||
| FR-14 | Sentry on the backend | **U4** |
|
||||
| FR-15 | Sentry in the admin SPA | **U4** |
|
||||
| FR-16 | Umami analytics | **U4** |
|
||||
| FR-17 | UptimeRobot monitors | *Operations — Monitoring Setup* |
|
||||
| FR-18 | HTTP security headers with path-scoped CSP | **U3** |
|
||||
| FR-19 | Security alerting | **U4** (event emission) + *Operations* (alert rules) |
|
||||
| FR-20 | Database backup before production deploy | **U6** |
|
||||
| FR-21 | Fix the 5 existing lint errors | **U5** |
|
||||
| FR-22 | Pin the 2 vulnerable packages | **U5** |
|
||||
| FR-23 | Deployment and rollback documentation | *Operations — Deployment Setup* |
|
||||
| FR-24 | Validate the token in the availability gate's admin bypass | **U1** |
|
||||
|
||||
### Split requirements
|
||||
|
||||
Two requirements are deliberately delivered across a boundary rather than assigned wholly to one place:
|
||||
|
||||
- **FR-08** (public website survives) — the *serving* half is U1's `wwwroot/web/` mount, but the requirement is really about deployment behaviour, so it is assigned to **U6** where the persistent-directory linking happens. U1 contributes the precondition.
|
||||
- **FR-19** (security alerting) — the application must *emit* alertable events (U4), and the alert *rules* are configured in Sentry during Operations. Neither half is useful alone.
|
||||
|
||||
---
|
||||
|
||||
## Coverage Verification
|
||||
|
||||
### By unit
|
||||
|
||||
| Unit | Requirements | Count |
|
||||
|---|---|---|
|
||||
| U1 Hosting & Serving | FR-07, FR-10, FR-24 | 3 |
|
||||
| U2 Data Durability | FR-11, FR-12 | 2 |
|
||||
| U3 Security Headers & CSP | FR-18 | 1 |
|
||||
| U4 Observability | FR-13, FR-14, FR-15, FR-16, FR-19 | 5 |
|
||||
| U5 CI Workflow & Gates | FR-01, FR-04, FR-05, FR-21, FR-22 | 5 |
|
||||
| U6 Deploy Workflow | FR-02, FR-03, FR-06, FR-08, FR-20 | 5 |
|
||||
| U7 Documentation | FR-09 | 1 |
|
||||
| Operations phase | FR-17, FR-23, and the rules half of FR-19 | 2½ |
|
||||
|
||||
**All 24 requirements assigned. None orphaned, none duplicated.**
|
||||
|
||||
U3 carries a single requirement but is not undersized — FR-18 specifies five headers, two policies, path scoping and a configuration surface. Requirement count is not a proxy for effort.
|
||||
|
||||
---
|
||||
|
||||
## Component-to-Unit Map
|
||||
|
||||
Included as a second coverage check against the 16 Application Design components.
|
||||
|
||||
| Component | Unit |
|
||||
|---|---|
|
||||
| C-01 `SecurityHeadersMiddleware` | U3 |
|
||||
| C-02 `SecurityHeadersOptions` | U3 |
|
||||
| C-03 `CspPolicyBuilder` | U3 |
|
||||
| C-04 Health-check registration | U1 |
|
||||
| C-05 `CmsDataProtection` registration | U2 |
|
||||
| C-06 `ApplicationDbContext` keys table | U2 |
|
||||
| C-07 Startup migration runner | U2 |
|
||||
| C-08 `CmsLogging` registration | U4 |
|
||||
| C-09 `CmsSentry` registration | U4 |
|
||||
| C-10 Static-file mount composition | U1 |
|
||||
| C-11 `deploy-scp.yaml` | U6 |
|
||||
| C-12 `continuous_integration.yaml` | U5 |
|
||||
| C-13 `AvailabilityMiddleware` | U1 |
|
||||
| C-14 Frontend configuration | U4 |
|
||||
| C-15 Frontend observability | U4 |
|
||||
| C-16 Host composition | U1, U2, U3, U4 (each inserts at its defined position) |
|
||||
|
||||
**All 16 components assigned.** C-16 is intentionally shared: four units modify both `Program.cs` files at distinct, specified positions in the registration and pipeline order defined in `services.md` § S-01.
|
||||
|
||||
---
|
||||
|
||||
## Design Items and Open Items Assigned
|
||||
|
||||
| Item | Unit | Resolved at |
|
||||
|---|---|---|
|
||||
| § 5.1 — duplicate `AddDataProtection()` overriding the persistent key store | U2 | Functional Design + Code Generation |
|
||||
| § 5.1 second-order — explicit application discriminator | U2 | Code Generation |
|
||||
| § 5.2 — FR-24 approach: validate in middleware versus move authentication earlier | U1 | Functional Design |
|
||||
| `C-10` must start without `wwwroot/web/` present | U1 | Functional Design |
|
||||
| Unknown CSP policy name must fail at startup | U3 | Functional Design |
|
||||
| OPEN-01 — correlation-ID mechanism | U4 | NFR Design |
|
||||
| Definition of an alertable security event | U4 | NFR Design |
|
||||
| OPEN-03 — patched versions for the two vulnerable packages | U5 | Code Generation |
|
||||
| ASM-01 — `wwwroot/web/` outside the swapped release directory | U6 | Infrastructure Design |
|
||||
| OPEN-04 — when FTPS is actually built | — | Deferred by design (D-02) |
|
||||
|
||||
Every carried-forward item has an owning unit and a resolving stage. Nothing is left to be remembered.
|
||||
|
||||
---
|
||||
|
||||
## Per-Unit Construction Stages
|
||||
|
||||
From the execution plan, adjusted for the new unit boundaries. The original plan's per-unit stage assignments were written against the pre-split numbering; this table is authoritative.
|
||||
|
||||
| Unit | Functional Design | NFR Design | Infrastructure Design | Code Generation |
|
||||
|---|---|---|---|---|
|
||||
| U1 Hosting & Serving | **Yes** — fallback precedence, missing-directory behaviour, FR-24 approach | No | No | Yes |
|
||||
| U2 Data Durability | **Yes** — migration failure behaviour, registration-order conflict, discriminator | No | No | Yes |
|
||||
| U3 Security Headers & CSP | **Yes** — policy composition, per-header applicability | **Yes** — SECURITY-04 patterns | No | Yes |
|
||||
| U4 Observability | **Yes** — Sentry-absent behaviour, same-origin resolution | **Yes** — SECURITY-03/14 patterns, correlation ID | No | Yes |
|
||||
| U5 CI Workflow & Gates | No — declarative YAML and mechanical fixes | No | No | Yes |
|
||||
| U6 Deploy Workflow | No — declarative | No | **Yes** — host layout, release directories, ASM-01 | Yes |
|
||||
| U7 Documentation | No | No | **Yes** — depends on U6's layout | Yes |
|
||||
|
||||
**Change from the execution plan**: the plan assigned Functional Design to "units 2, 3, 4" under the old numbering, where old-unit-2 bundled hosting and durability. After the split (Q3 = B), both halves need it — U1 for the FR-24 pipeline-ordering decision and the missing-directory behaviour, U2 for the registration-order conflict and migration failure semantics. Neither is mechanical enough to skip.
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
# Units of Work — Gitea Deployment Workflow
|
||||
|
||||
**Date**: 2026-07-27
|
||||
**Decomposition basis**: Q1 = C (split the oversized unit), Q2 = B (merge the quality-gate fixes into the CI unit), Q3 = B (split into Hosting & Serving plus Data Durability)
|
||||
|
||||
---
|
||||
|
||||
## Decomposition Outcome
|
||||
|
||||
The proposed 7-unit split changed in two ways that cancel out numerically:
|
||||
|
||||
- **Unit 2 was split in two** (Q3 = B) — it had bundled three different kinds of work whose only commonality was the deadline "before the first deploy"
|
||||
- **The quality-gate prerequisites were merged into the CI unit** (Q2 = B) — the lint fixes and package pins exist *because* the gates are being switched on, so they land in the same commit as the gates
|
||||
|
||||
Net result: **still 7 units**, but with boundaries drawn along the work rather than along the deadline.
|
||||
|
||||
| # | Unit | Type |
|
||||
|---|---|---|
|
||||
| U1 | Hosting & Serving | Application |
|
||||
| U2 | Data Durability | Application |
|
||||
| U3 | HTTP Security Headers & CSP | Application |
|
||||
| U4 | Observability Integration | Application + Frontend |
|
||||
| U5 | CI Workflow & Quality Gates | Pipeline |
|
||||
| U6 | Deploy Workflow | Pipeline |
|
||||
| U7 | Repository Documentation | Documentation |
|
||||
|
||||
---
|
||||
|
||||
## Execution Rounds (Q4 = B)
|
||||
|
||||
Serial where dependent, grouped where independent. **One commit per unit** regardless of grouping (Q6 = A); a round is an approval boundary, not a commit boundary.
|
||||
|
||||
| Round | Units | Why grouped |
|
||||
|---|---|---|
|
||||
| **R1** | U1 + U2 | Mutually independent — one changes serving and middleware, the other changes persistence and startup. Neither reads the other's output |
|
||||
| **R2** | U3 + U4 | Tightly coupled — U3's CSP configuration is populated with the Umami and Sentry origins that U4 introduces. Splitting them means writing a CSP against origins that do not exist yet |
|
||||
| **R3** | U5 + U6 | U6 is invoked by U5; the two workflow files are designed against one shared input interface |
|
||||
| **R4** | U7 | Depends on U6's settled host layout |
|
||||
|
||||
Everything in R1 and R2 must land before R3's deploy workflow can safely run — the reason the durability work is not left until later.
|
||||
|
||||
---
|
||||
|
||||
## U1 — Hosting & Serving
|
||||
|
||||
**Purpose**: make one process serve two independent front-ends correctly, and expose infrastructure liveness that the CMS's own on/off state cannot mask.
|
||||
|
||||
**Scope**:
|
||||
- Remount static files: `wwwroot/web/` at `/`, `wwwroot/admin/` at `/admin`, each with its own `PhysicalFileProvider` (Q3 of Application Design = A)
|
||||
- Retarget both SPA fallbacks, preserving the `nonfile` constraint so missing assets still `404`
|
||||
- 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`
|
||||
- Add `AddCmsHealthChecks()` / `MapCmsHealthChecks()` exposing `GET /health`, liveness only, no database call
|
||||
- Add `/health` to `AvailabilityMiddleware._bypassPrefixes`
|
||||
- **Fix `IsAdminBypass`** to stop trusting an unvalidated token (FR-24)
|
||||
|
||||
**Components**: C-04, C-10, C-13, and the U1 portion of C-16
|
||||
**Requirements**: FR-07, FR-10, FR-24
|
||||
**Projects touched**: `SlpModularCms.Core`, `SlpModularCms.Api`, `SlpModularCms.Api.Slave`, `SlpModularCms.Modules.Availability`
|
||||
|
||||
**Carried-in design item**: § 5.2 of `application-design.md` — `AvailabilityMiddleware` runs *before* `UseAuthentication()`, so `HttpContext.User` is unpopulated when the admin bypass is evaluated. Functional Design for this unit decides between validating the token in the middleware (contained, duplicates validation parameters) and moving authentication earlier (smaller change, wider blast radius).
|
||||
|
||||
**Definition of done** (Q5 = B): builds; all existing tests pass; new tests for `/health` reachability including while the instance is disabled, for the two static mounts' fallback precedence, for graceful startup without `wwwroot/web/`, and for the admin bypass rejecting a forged token while still accepting a valid one.
|
||||
|
||||
---
|
||||
|
||||
## U2 — Data Durability
|
||||
|
||||
**Purpose**: make a redeploy safe. Nothing this unit delivers is visible in normal operation; its entire value is that the atomic release switch in U6 does not silently destroy trust or schema state.
|
||||
|
||||
**Scope**:
|
||||
- `ApplicationDbContext` implements `IDataProtectionKeyContext` with a `DataProtectionKeys` set; one new Core migration
|
||||
- `AddCmsDataProtection()` configuring `PersistKeysToDbContext<ApplicationDbContext>`
|
||||
- **Set an explicit application discriminator** — the default derives from the content root path, which changes on every atomic release switch, defeating the purpose by a different route
|
||||
- **Remove `services.AddDataProtection()` from `AvailabilityModule` and `MasterModule`** — module registration runs after the host's, so those calls would override the persistent key store
|
||||
- `MigrateCoreDatabase()` applying `ApplicationDbContext` migrations at startup, **fail fast** on failure (Q8 of Application Design = A)
|
||||
|
||||
**Components**: C-05, C-06, C-07, and the U2 portion of C-16
|
||||
**Requirements**: FR-11, FR-12
|
||||
**Projects touched**: `SlpModularCms.Core`, `SlpModularCms.Modules.Availability`, `SlpModularCms.Modules.Master`, both hosts
|
||||
|
||||
**Carried-in design item**: § 5.1 of `application-design.md` — the duplicate `AddDataProtection()` conflict. This is the unit's highest-value test: without it, FR-12 passes registration tests while remaining ephemeral, and the failure only surfaces later as an apparent network fault between master and slave.
|
||||
|
||||
**Definition of done** (Q5 = B): builds; all existing tests pass; new tests asserting that the persistent key store **survives module registration**, that the application discriminator is explicit and stable, and that a protected value round-trips across a simulated content-root change. Both hosts start successfully.
|
||||
|
||||
---
|
||||
|
||||
## U3 — HTTP Security Headers & CSP
|
||||
|
||||
**Purpose**: supply, from inside the application, the headers that would normally come from nginx or IIS configuration — which NFR-01 forbids relying on.
|
||||
|
||||
**Scope**:
|
||||
- `SecurityHeadersMiddleware` applying headers at response start via `OnStarting`
|
||||
- Per-header scoping (FU1 = A): `X-Content-Type-Options` and `Strict-Transport-Security` on **all** responses; `Content-Security-Policy`, `X-Frame-Options` and `Referrer-Policy` on HTML responses only
|
||||
- `SecurityHeadersOptions` binding a new `SecurityHeaders` section
|
||||
- `CspPolicyBuilder` with two code-defined policies — `Strict` and `Relaxed` — composed once at startup
|
||||
- Path-to-policy assignment and allowed origins in configuration; policy definitions in code (FU2 = A)
|
||||
- Registration **before static files**, since static files short-circuit the pipeline
|
||||
- An unknown policy name fails at **startup**, not per request
|
||||
|
||||
**Components**: C-01, C-02, C-03, and the U3 portion of C-16
|
||||
**Requirements**: FR-18
|
||||
**Projects touched**: `SlpModularCms.Core`, both hosts
|
||||
|
||||
**Definition of done** (Q5 = B): builds; all existing tests pass; new tests for policy composition per name, path-to-policy resolution including the default fallback, per-header applicability across HTML and non-HTML responses, headers reaching **static-file responses**, not overwriting pre-set headers, and startup failure on an unknown policy name.
|
||||
|
||||
---
|
||||
|
||||
## U4 — Observability Integration
|
||||
|
||||
**Purpose**: make it possible to tell, without host access, whether the application is erroring and whether it is being used.
|
||||
|
||||
**Scope**:
|
||||
- `AddCmsLogging()` — structured logging with a correlation identifier, independent of Sentry (Q10 of Application Design = B)
|
||||
- `AddCmsSentry()` — initialises only when a DSN is configured; absent DSN is a supported state, not an error
|
||||
- Environment and release tagging
|
||||
- Emit security-relevant events for alerting (FR-19)
|
||||
- Frontend: `@sentry/react` initialisation, Umami tracking script with a per-environment website ID, absent in local development
|
||||
- Frontend: `config.ts` treats an absent or empty `VITE_API_BASE_URL` as same-origin while still accepting an explicit absolute URL for local development
|
||||
|
||||
**Components**: C-08, C-09, C-14, C-15, and the U4 portion of C-16
|
||||
**Requirements**: FR-13, FR-14, FR-15, FR-16, FR-19
|
||||
**Projects touched**: `SlpModularCms.Core`, both hosts, `frontend/`
|
||||
|
||||
**Carried-in open item**: OPEN-01 — the correlation-ID mechanism (`TraceIdentifier` versus W3C `traceparent`) is decided in this unit's NFR Design, along with the definition of an alertable security event.
|
||||
|
||||
**Note on lint**: because the quality-gate fixes moved to U5 (Q2 = B), `pnpm run lint` is still failing for pre-existing reasons while this unit changes frontend files. Lint should be run on the **changed files** during this unit so no new violations accumulate, even though the blocking gate is not switched on until U5.
|
||||
|
||||
**Definition of done** (Q5 = B): builds; all existing tests pass; new tests for logging configuration with correlation ID present, Sentry registration being a no-op without a DSN, same-origin resolution when `VITE_API_BASE_URL` is empty, explicit-URL behaviour preserved, and the Umami component rendering nothing without a website ID.
|
||||
|
||||
---
|
||||
|
||||
## U5 — CI Workflow & Quality Gates
|
||||
|
||||
**Purpose**: validate every change, and be the only route to production.
|
||||
|
||||
**Scope**:
|
||||
- Fix the 5 frontend lint errors and 1 warning (FR-21) — merged here per Q2 = B, so the gates and the fixes land together and the pipeline is never red on arrival
|
||||
- Pin `Microsoft.OpenApi` and `System.Security.Cryptography.Xml` to patched versions (FR-22, OPEN-03)
|
||||
- `.gitea/workflows/continuous_integration.yaml` with triggers on `pull_request`, `push` to `master`, and `workflow_dispatch` with a `deploy_production` boolean defaulting to `false`
|
||||
- Six blocking gates: backend build, backend tests, vulnerability scan, frontend build, frontend tests, frontend lint and format-check
|
||||
- Toolchain installed explicitly and pinned (`actions/setup-dotnet`, `pnpm/action-setup`); no `latest` tags
|
||||
- Two environment-specific builds with their own Vite variables (FR-05)
|
||||
- Production reachable **only** via `workflow_dispatch` with the flag set (FR-04)
|
||||
|
||||
**Components**: C-12
|
||||
**Requirements**: FR-01, FR-04, FR-05, FR-21, FR-22
|
||||
**Projects touched**: `.gitea/workflows/`, `frontend/src/` (lint fixes), `*.csproj` (package pins)
|
||||
|
||||
**Definition of done** (Q5 = B): all six gates pass locally against the current tree — `pnpm run lint` clean, `dotnet list package --vulnerable` clean, all tests green. Workflow YAML is syntactically valid. Production cannot be triggered by a push.
|
||||
|
||||
---
|
||||
|
||||
## U6 — Deploy Workflow
|
||||
|
||||
**Purpose**: turn a validated build into a running release without endangering the customer's website, the database, or master↔slave trust.
|
||||
|
||||
**Scope**:
|
||||
- `.gitea/workflows/deploy-scp.yaml` as a reusable `workflow_call` workflow (Q11 of Application Design = B — one workflow per transport, identical input interface)
|
||||
- Plain shell steps, no container actions (they fail on the Podman-backed runner)
|
||||
- Deployment sequence: download artifact → **production only**: database backup before any change → upload to a new release directory → link the persistent `wwwroot/web/` into it → switch the active release atomically → restart the process → verify `/health` → prune old releases keeping at least the previous one
|
||||
- Automatic test deployment on `master`; production deployment only when invoked with the flag
|
||||
- Environment-specific paths from Gitea variables, credentials from secrets
|
||||
|
||||
**Components**: C-11
|
||||
**Requirements**: FR-02, FR-03, FR-06, FR-08, FR-20
|
||||
**Projects touched**: `.gitea/workflows/`
|
||||
|
||||
**Carried-in assumption**: ASM-01 — `wwwroot/web/` must live **outside** the swapped release directory and be linked into each new release. Confirmed in this unit's Infrastructure Design. Getting this wrong destroys the customer's website, the highest-severity risk in the feature.
|
||||
|
||||
**Definition of done** (Q5 = B): workflow YAML valid; the deployment sequence documented step by step including the rollback path; the `wwwroot/web/` linking step explicit and justified. Note that end-to-end verification requires the actual Pi, SSH credentials and a database, so it cannot be fully proven in CI — real-run verification belongs to the Operations phase.
|
||||
|
||||
---
|
||||
|
||||
## U7 — Repository Documentation
|
||||
|
||||
**Purpose**: let a website workspace deliver a site that works, without its author needing to read this repository's code.
|
||||
|
||||
**Scope** (Q8 = A — repository documentation here; operational documents in the Operations phase):
|
||||
- Website workspace contract (FR-09): target path `wwwroot/web/`, required structure, forbidden paths (`admin/`, the application root), reserved paths (`/admin`, `/api/v1`, `/health`), SPA-fallback behaviour, how to call `/api/v1` same-origin without CORS, which CSP applies, and how to include the Umami script
|
||||
- README updates: the new `wwwroot` layout, the health endpoint and what it does *not* mean, the changed production setup section
|
||||
- `frontend/.env.example` updates for the same-origin default and the new observability variables
|
||||
|
||||
**Components**: none — documentation only
|
||||
**Requirements**: FR-09
|
||||
**Projects touched**: repository root, `frontend/`
|
||||
|
||||
**Definition of done**: documentation is accurate against the code as built in U1–U6, and the website contract is complete enough to follow without reading source.
|
||||
|
||||
---
|
||||
|
||||
## Out of Unit Scope — Delivered by the Operations Phase
|
||||
|
||||
| Requirement | Delivered at |
|
||||
|---|---|
|
||||
| FR-17 — UptimeRobot monitors for `/health`, `/` and `/admin` | Monitoring Setup |
|
||||
| FR-19 — Sentry alert rules (the application-side event emission is in U4) | Monitoring Setup |
|
||||
| FR-23 — deployment instructions, host setup, rollback plan, FTPS switch path | Deployment Setup |
|
||||
| DEV-01…04 re-confirmation, appsettings compliance gate | Production Readiness Validation |
|
||||
|
||||
---
|
||||
|
||||
## Code Organization
|
||||
|
||||
Brownfield — the existing structure is retained. New code follows the solution layout mandated by `CLAUDE.md` / `AGENTS.md`:
|
||||
|
||||
- Cross-cutting concerns go in `src/SlpModularCms.Core/Hosting/`, in new subfolders `Security/`, `Health/` and `Observability/`
|
||||
- No new project is added to the solution
|
||||
- Workflow files go in `.gitea/workflows/` at the repository root
|
||||
- Tests mirror their production project, per the existing Tests solution folder convention
|
||||
Reference in New Issue
Block a user