Adds 2 units and docs for unit 3. nfr-requirements plan

This commit is contained in:
2026-06-29 22:18:37 +02:00
parent 0e01ca1e1c
commit c156107cb1
126 changed files with 15204 additions and 80199 deletions
@@ -0,0 +1,164 @@
# Application Design — Master CMS Module
## Design Decisions Summary
| Question | Decision |
|----------|----------|
| Q1 — HTTP client (Master → Slave) | **A) Typed client**`ISlaveApiClient` / `SlaveApiClient` via `AddHttpClient<>` |
| Q2 — Two-phase gate middleware | **B) Extend `AvailabilityMiddleware`** — Master gate added at top of `InvokeAsync` |
| Q3 — Slave-side status caching | **A) Static field + timestamp** — consistent with existing circuit breaker pattern |
| Q4 — Service responsibility split | **B) `CmsInstanceRepository` + `CmsInstanceService`** — data and orchestration separated |
| Q5 — Master controller granularity | **A) Single `CmsInstanceController`** — all actions in one controller |
| Q6 — Slave internal endpoint placement | **B) Extended `AvailabilityController`** — registration endpoint added to existing controller |
| Q7 — Frontend hooks organization | **B) Separate hook files** — one file per hook: `useCmsInstances.ts`, `useAddCmsInstance.ts`, `useUpdateCmsInstanceStatus.ts` |
---
## Architecture Overview
```mermaid
graph TD
subgraph MasterCms["Master CMS Instance"]
MasterModule["MasterModule\n(IModule)"]
CmsCtrl["CmsInstanceController\n/api/v1/CmsInstances"]
CmsService["CmsInstanceService\n(orchestration)"]
CmsRepo["CmsInstanceRepository\n(data access)"]
MasterDb["MasterDbContext\nCmsInstances table"]
SlaveClient["SlaveApiClient\n(typed HTTP client)"]
BgService["IntegrityCheckBackgroundService\n(PeriodicTimer)"]
MasterOpts["MasterModuleOptions"]
end
subgraph SlaveCms["Slave CMS Instance"]
ExtAvailMw["AvailabilityMiddleware\n(EXTENDED — two-phase gate)"]
MasterAvailSvc["MasterAvailabilityService\n(pull + cache + fallback)"]
LocalAvailSvc["IAvailabilityService\n(existing local gate)"]
AvailDb["AvailabilityDbContext\nMasterRegistrations table"]
ExtAvailCtrl["AvailabilityController\n(EXTENDED + RegisterMaster)"]
SlaveOpts["MasterModuleOptions\n(ApiKey, CacheMinutes)"]
end
subgraph FrontendApp["Frontend (Master UI)"]
CmsPage["CmsPage\n(/cms route)"]
Hooks["TanStack Query Hooks\n(useCmsInstances, useAddCmsInstance,\nuseUpdateCmsInstanceStatus)"]
Components["Components\n(CmsInstanceList,\nAddCmsInstanceDialog,\nSetStatusDialog)"]
end
FrontendApp -->|"REST /api/v1/CmsInstances"| MasterCms
MasterCms -->|"HTTP slave API"| SlaveCms
SlaveCms -->|"HTTP pull status"| MasterCms
CmsPage --> Components
Components --> Hooks
Hooks -->|"GET/POST/PUT"| CmsCtrl
CmsCtrl --> CmsService
CmsService --> CmsRepo
CmsService --> SlaveClient
CmsRepo --> MasterDb
BgService --> CmsService
CmsService --> MasterOpts
BgService --> MasterOpts
ExtAvailMw --> MasterAvailSvc
ExtAvailMw --> LocalAvailSvc
MasterAvailSvc --> AvailDb
MasterAvailSvc --> SlaveOpts
ExtAvailCtrl --> AvailDb
ExtAvailCtrl --> SlaveOpts
classDef module fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000
classDef controller fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000
classDef service fill:#FFC107,stroke:#F57F17,stroke-width:1px,color:#000
classDef data fill:#CE93D8,stroke:#6A1B9A,stroke-width:1px,color:#000
classDef frontend fill:#FC8181,stroke:#C53030,stroke-width:1px,color:#000
classDef config fill:#B0BEC5,stroke:#546E7A,stroke-width:1px,color:#000
class MasterModule module
class CmsCtrl,ExtAvailCtrl controller
class CmsService,MasterAvailSvc,SlaveClient,LocalAvailSvc,BgService service
class CmsRepo,MasterDb,AvailDb data
class CmsPage,Hooks,Components frontend
class MasterOpts,SlaveOpts config
```
Text alternative: Master CMS has new module with controller, service, repository, typed HTTP client, and background service; Slave CMS has extended middleware with two-phase gate, new MasterAvailabilityService, new AvailabilityDbContext, and extended controller; Frontend has CmsPage with hooks and components calling Master REST API.
---
## Component Inventory
### Unit 1 — master-backend (`SlpModularCms.Modules.Master`)
| Component | Type | New/Modified |
|-----------|------|--------------|
| `MasterModule` | `IModule` | New |
| `MasterDbContext` | EF Core `DbContext` | New |
| `CmsInstance` | Entity | New |
| `CmsInstanceStatus` | Enum | New |
| `ICmsInstanceRepository` / `CmsInstanceRepository` | Repository | New |
| `ICmsInstanceService` / `CmsInstanceService` | Service | New |
| `ISlaveApiClient` / `SlaveApiClient` | Typed HTTP client | New |
| `CmsInstanceController` | Controller | New |
| `IntegrityCheckBackgroundService` | `BackgroundService` | New |
| `MasterModuleOptions` | Config POCO | New |
| `CmsInstanceDto` | DTO | New |
| `CreateCmsInstanceRequest` | Request model | New |
| `UpdateStatusRequest` | Request model | New |
### Unit 2 — slave-availability-extension (`SlpModularCms.Modules.Availability`)
| Component | Type | New/Modified |
|-----------|------|--------------|
| `MasterRegistration` | Entity | New |
| `AvailabilityDbContext` | EF Core `DbContext` | New |
| `IMasterAvailabilityService` / `MasterAvailabilityService` | Service | New |
| `AvailabilityMiddleware` | Middleware | Modified |
| `AvailabilityController` | Controller | Modified |
| `MasterGateResult` | Result record | New |
| `RegisterMasterRequest` | Request model | New |
### Unit 3 — frontend-cms-page (`frontend/`)
| Component | Type | New/Modified |
|-----------|------|--------------|
| `CmsPage` | React page | New |
| `CmsInstanceList` | React component | New |
| `AddCmsInstanceDialog` | React component | New |
| `SetStatusDialog` | React component | New |
| `useCmsInstances` | TanStack Query hook | New |
| `useAddCmsInstance` | TanStack Query hook | New |
| `useUpdateCmsInstanceStatus` | TanStack Query hook | New |
| `CmsInstance` | TypeScript type | New |
| `CmsInstanceStatus` | TypeScript enum | New |
### Unit 4 — documentation
| Artifact | Type | New/Modified |
|----------|------|--------------|
| `README.md` — Migrations section | Documentation | Modified |
| `README.md` — Module guide section | Documentation | Modified |
| `README.md` — Production env vars | Documentation | Modified |
| `frontend/README.md` | Documentation | Modified |
---
## Key Design Constraints
| Constraint | Source | Impact |
|-----------|--------|--------|
| `ApiKey` never returned in API responses | NFR-MASTER-03 | `CmsInstanceDto` excludes `ApiKey`; only accepted in `CreateCmsInstanceRequest` |
| Fail-open on Master unreachable | NFR-MASTER-01 | `MasterAvailabilityService` returns last cached status (default Available) on HTTP failure |
| Per-module DbContext + migrations | NFR-MASTER-06 | New `MasterDbContext` in `Modules.Master`; new `AvailabilityDbContext` in `Modules.Availability` |
| Master exemption from own gate | FR-MASTER-09 | Handled naturally: no `MasterRegistration` record exists on Master instance → gate skipped |
| Disable message required for NotAvailable | FR-MASTER-14 | Validated in `CmsInstanceService.UpdateStatusAsync` before persistence |
| Inactive slaves: no HTTP contact | FR-MASTER-13 | `GetActiveAsync()` filters out Inactive before integrity checks and status pushes |
| Owner role only | FR-MASTER-10 | `[Authorize(Policy = "OwnerOnly")]` on all `CmsInstanceController` actions |
---
## Artifact References
| Artifact | Path |
|----------|------|
| Component definitions | `aidlc-docs/features/master-cms-module/inception/application-design/components.md` |
| Method signatures | `aidlc-docs/features/master-cms-module/inception/application-design/component-methods.md` |
| Service orchestration + flows | `aidlc-docs/features/master-cms-module/inception/application-design/services.md` |
| Dependency diagrams + matrix | `aidlc-docs/features/master-cms-module/inception/application-design/component-dependency.md` |
@@ -0,0 +1,187 @@
# Component Dependencies — Master CMS Module
## Package-Level Dependency Diagram
```mermaid
graph TD
Api["SlpModularCms.Api\n(shell)"]
Master["SlpModularCms.Modules.Master\n(NEW)"]
Availability["SlpModularCms.Modules.Availability\n(EXTENDED)"]
Core["SlpModularCms.Core\n(unchanged)"]
Frontend["Frontend\n(EXTENDED)"]
SlaveCms["Slave CMS\n(another SlpModularCms instance)"]
Api --> Master
Api --> Availability
Api --> Core
Master --> Core
Availability --> Core
Frontend -->|"REST /api/v1/*"| Api
Master -->|"HTTP slave API calls"| SlaveCms
SlaveCms -->|"HTTP pull master status"| Master
classDef new fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000
classDef extended fill:#FFC107,stroke:#F57F17,stroke-width:2px,color:#000
classDef unchanged fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000
classDef external fill:#FC8181,stroke:#C53030,stroke-width:2px,color:#000
class Master new
class Availability,Frontend extended
class Api,Core unchanged
class SlaveCms external
```
Text alternative: Master and Availability modules both depend on Core; Api depends on all three; Frontend calls Api REST; Master calls Slave CMS via HTTP and Slave pulls back.
---
## Master-Backend Component Dependencies
```mermaid
graph TD
Ctrl["CmsInstanceController"]
Svc["CmsInstanceService"]
Repo["CmsInstanceRepository"]
DbCtx["MasterDbContext"]
SlaveClient["SlaveApiClient"]
BgSvc["IntegrityCheckBackgroundService"]
Opts["MasterModuleOptions\n(IOptions)"]
CmsEntity["CmsInstance\n(entity)"]
Ctrl --> Svc
Svc --> Repo
Svc --> SlaveClient
Svc --> Opts
Repo --> DbCtx
Repo --> CmsEntity
DbCtx --> CmsEntity
BgSvc --> Svc
BgSvc --> Opts
classDef controller fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000
classDef service fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000
classDef data fill:#FFC107,stroke:#F57F17,stroke-width:2px,color:#000
classDef infra fill:#CE93D8,stroke:#6A1B9A,stroke-width:1px,color:#000
class Ctrl controller
class Svc,SlaveClient service
class Repo,DbCtx,CmsEntity data
class BgSvc,Opts infra
```
Text alternative: Controller depends on Service; Service depends on Repository, SlaveApiClient, and Options; Repository owns DbContext and entity; BackgroundService depends on Service and Options.
---
## Slave-Availability-Extension Component Dependencies
```mermaid
graph TD
Middleware["AvailabilityMiddleware\n(EXTENDED)"]
LocalSvc["IAvailabilityService\n(existing)"]
MasterSvc["MasterAvailabilityService\n(NEW)"]
AvailDb["AvailabilityDbContext\n(NEW)"]
MasterRegEntity["MasterRegistration\n(entity)"]
HttpFactory["IHttpClientFactory"]
Opts["MasterModuleOptions\n(IOptions)"]
AvailCtrl["AvailabilityController\n(EXTENDED)"]
MasterCms["Master CMS\n(HTTP endpoint)"]
Middleware --> LocalSvc
Middleware --> MasterSvc
MasterSvc --> AvailDb
MasterSvc --> HttpFactory
MasterSvc --> Opts
AvailDb --> MasterRegEntity
AvailCtrl --> AvailDb
AvailCtrl --> Opts
HttpFactory -->|"HTTP GET status"| MasterCms
classDef middleware fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000
classDef service fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000
classDef data fill:#FFC107,stroke:#F57F17,stroke-width:2px,color:#000
classDef infra fill:#CE93D8,stroke:#6A1B9A,stroke-width:1px,color:#000
classDef external fill:#FC8181,stroke:#C53030,stroke-width:2px,color:#000
class Middleware middleware
class MasterSvc,LocalSvc service
class AvailDb,MasterRegEntity,AvailCtrl data
class HttpFactory,Opts infra
class MasterCms external
```
Text alternative: Extended middleware calls both MasterAvailabilityService and existing IAvailabilityService; MasterAvailabilityService reads DB for registration and pulls from master via HTTP; AvailabilityController handles registration writes.
---
## Frontend Component Dependencies
```mermaid
graph TD
CmsPage["CmsPage\n(/cms route)"]
List["CmsInstanceList"]
AddDialog["AddCmsInstanceDialog"]
StatusDialog["SetStatusDialog"]
HookList["useCmsInstances"]
HookAdd["useAddCmsInstance"]
HookStatus["useUpdateCmsInstanceStatus"]
ApiTypes["CmsInstance types\nCmsInstanceStatus enum"]
MasterApi["Master CMS REST API\n/api/v1/CmsInstances"]
CmsPage --> List
CmsPage --> AddDialog
CmsPage --> StatusDialog
List --> HookList
AddDialog --> HookAdd
StatusDialog --> HookStatus
HookList --> ApiTypes
HookAdd --> ApiTypes
HookStatus --> ApiTypes
HookList -->|"GET"| MasterApi
HookAdd -->|"POST"| MasterApi
HookStatus -->|"PUT"| MasterApi
classDef page fill:#63b3ed,stroke:#2b6cb0,stroke-width:2px,color:#000
classDef component fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000
classDef hook fill:#FFC107,stroke:#F57F17,stroke-width:1px,color:#000
classDef types fill:#CE93D8,stroke:#6A1B9A,stroke-width:1px,color:#000
classDef api fill:#FC8181,stroke:#C53030,stroke-width:2px,color:#000
class CmsPage page
class List,AddDialog,StatusDialog component
class HookList,HookAdd,HookStatus hook
class ApiTypes types
class MasterApi api
```
Text alternative: CmsPage renders List + dialogs; List uses useCmsInstances; dialogs use mutation hooks; all hooks use shared API types and call Master REST API.
---
## Dependency Matrix
| Component | Depends On | Used By |
|-----------|-----------|---------|
| `CmsInstanceController` | `ICmsInstanceService` | `SlpModularCms.Api` (module registration) |
| `CmsInstanceService` | `ICmsInstanceRepository`, `ISlaveApiClient`, `IOptions<MasterModuleOptions>` | `CmsInstanceController`, `IntegrityCheckBackgroundService` |
| `CmsInstanceRepository` | `MasterDbContext` | `CmsInstanceService` |
| `MasterDbContext` | EF Core, `CmsInstance` entity | `CmsInstanceRepository` |
| `SlaveApiClient` | `HttpClient` (via `IHttpClientFactory`) | `CmsInstanceService` |
| `IntegrityCheckBackgroundService` | `IServiceScopeFactory`, `ICmsInstanceService`, `IOptions<MasterModuleOptions>` | `IHostedService` (framework) |
| `AvailabilityMiddleware` (ext.) | `IAvailabilityService`, `IMasterAvailabilityService` | ASP.NET Core pipeline |
| `MasterAvailabilityService` | `AvailabilityDbContext`, `IHttpClientFactory`, `IOptions<MasterModuleOptions>` | `AvailabilityMiddleware` |
| `AvailabilityDbContext` | EF Core, `MasterRegistration` entity | `MasterAvailabilityService`, `AvailabilityController` |
| `AvailabilityController` (ext.) | `IAvailabilityService`, `AvailabilityDbContext`, `IOptions<MasterModuleOptions>` | `SlpModularCms.Api` |
| `CmsPage` | `CmsInstanceList`, `AddCmsInstanceDialog`, `SetStatusDialog` | React Router |
| `useCmsInstances` | TanStack Query, API types | `CmsInstanceList` |
| `useAddCmsInstance` | TanStack Query, API types | `AddCmsInstanceDialog` |
| `useUpdateCmsInstanceStatus` | TanStack Query, API types | `SetStatusDialog` |
---
## Cross-Cutting Concerns
| Concern | Mechanism |
|---------|-----------|
| Authentication (API) | JWT Bearer on all master endpoints; `[Authorize(Policy = "OwnerOnly")]` on `CmsInstanceController` |
| Authentication (Slave internal) | `X-Master-Api-Key` header validated against `MasterModuleOptions.ApiKey` |
| API key secrecy | `ApiKey` never included in `CmsInstanceDto`; only in `CreateCmsInstanceRequest` input |
| Fail-open | `MasterAvailabilityService` returns cached status (default `Available`) on HTTP failure |
| Per-module migrations | `MasterDbContext` in `Modules.Master`; `AvailabilityDbContext` in `Modules.Availability`; applied at startup in `UseModule` |
| Configuration | `MasterModuleOptions` via `IOptions<MasterModuleOptions>`; section `"MasterModule"` in `appsettings.json` |
@@ -0,0 +1,142 @@
# Component Methods — Master CMS Module
> Method signatures at the interface level. Detailed business rules and implementation logic are deferred to Functional Design (CONSTRUCTION phase).
---
## Unit 1 — master-backend
### ICmsInstanceRepository
| Method | Signature | Purpose |
|--------|-----------|---------|
| `GetAllAsync` | `Task<IReadOnlyList<CmsInstance>> GetAllAsync()` | Returns all registered slave CMS instances |
| `GetActiveAsync` | `Task<IReadOnlyList<CmsInstance>> GetActiveAsync()` | Returns all non-Inactive instances (used by integrity check) |
| `GetByIdAsync` | `Task<CmsInstance?> GetByIdAsync(Guid id)` | Returns instance by primary key; null if not found |
| `AddAsync` | `Task AddAsync(CmsInstance instance)` | Adds new entity to the change tracker |
| `UpdateAsync` | `Task UpdateAsync(CmsInstance instance)` | Marks entity as modified in the change tracker |
| `SaveChangesAsync` | `Task<int> SaveChangesAsync()` | Persists pending changes via `MasterDbContext` |
### ICmsInstanceService
| Method | Signature | Purpose |
|--------|-----------|---------|
| `GetAllAsync` | `Task<IReadOnlyList<CmsInstanceDto>> GetAllAsync()` | Returns all instances as DTOs; `ApiKey` excluded (NFR-MASTER-03) |
| `AddAsync` | `Task<CmsInstanceDto> AddAsync(CreateCmsInstanceRequest request)` | Creates entity, persists, triggers auto-registration with slave (FR-MASTER-03); returns DTO |
| `UpdateStatusAsync` | `Task UpdateStatusAsync(Guid id, CmsInstanceStatus status, string? disableMessage)` | Updates entity status, persists, pushes status to slave via HTTP (FR-MASTER-05); `disableMessage` required when `status = NotAvailable` (FR-MASTER-14) |
| `VerifyIntegrityAsync` | `Task VerifyIntegrityAsync()` | Called by `IntegrityCheckBackgroundService`; checks all active slaves have correct master URL; re-registers if mismatch (FR-MASTER-04) |
### ISlaveApiClient
| Method | Signature | Purpose |
|--------|-----------|---------|
| `RegisterMasterAsync` | `Task<bool> RegisterMasterAsync(string slaveUrl, string apiKey, string masterUrl)` | POST `/api/internal/master/register` on slave; returns `true` on success (FR-MASTER-03) |
| `PushStatusAsync` | `Task<bool> PushStatusAsync(string slaveUrl, string apiKey, CmsInstanceStatus status, string? disableMessage)` | Pushes new status to slave's availability endpoint; returns `true` on success (FR-MASTER-05) |
| `GetRegisteredMasterUrlAsync` | `Task<string?> GetRegisteredMasterUrlAsync(string slaveUrl, string apiKey)` | GET slave's currently registered master URL; used for integrity check (FR-MASTER-04); null if no master registered |
### IntegrityCheckBackgroundService
| Method | Signature | Purpose |
|--------|-----------|---------|
| `ExecuteAsync` | `override Task ExecuteAsync(CancellationToken stoppingToken)` | Main background loop; uses `PeriodicTimer` with interval from `MasterModuleOptions.IntegrityCheckIntervalMinutes`; creates `IServiceScope` per tick to resolve scoped services |
### CmsInstanceController
| Method | HTTP | Route | Purpose |
|--------|------|-------|---------|
| `GetAll` | GET | `/api/v1/CmsInstances` | Returns `IReadOnlyList<CmsInstanceDto>` |
| `Add` | POST | `/api/v1/CmsInstances` | Body: `CreateCmsInstanceRequest`; returns created `CmsInstanceDto` (201) |
| `UpdateStatus` | PUT | `/api/v1/CmsInstances/{id}/status` | Body: `UpdateStatusRequest`; returns 200 OK or 404 if not found |
---
## Unit 2 — slave-availability-extension
### IMasterAvailabilityService
| Method | Signature | Purpose |
|--------|-----------|---------|
| `GetMasterStatusAsync` | `Task<MasterGateResult> GetMasterStatusAsync()` | Checks DB for `MasterRegistration`; if no registration → returns `HasMaster = false`; if registration exists → returns cached or freshly-pulled status with fail-open fallback |
### MasterGateResult
| Property | Type | Purpose |
|----------|------|---------|
| `HasMaster` | `bool` | Whether a master URL is registered on this slave |
| `Status` | `CmsInstanceStatus?` | Master-controlled status (null when `HasMaster = false`) |
| `DisableMessage` | `string?` | Message to include in 503 when `Status = NotAvailable` |
### AvailabilityController (new method)
| Method | HTTP | Route | Purpose |
|--------|------|-------|---------|
| `RegisterMaster` | POST | `/api/internal/master/register` | Header: `X-Master-Api-Key`; Body: `RegisterMasterRequest`; validates key, upserts `MasterRegistration`; returns 200 OK or 401 Unauthorized |
### RegisterMasterRequest
| Property | Type | Notes |
|----------|------|-------|
| `MasterUrl` | `string` | Base URL of the Master CMS |
### AvailabilityMiddleware.InvokeAsync (extended signature)
```csharp
public async Task InvokeAsync(
HttpContext context,
IAvailabilityService availabilityService,
IMasterAvailabilityService masterAvailabilityService)
```
---
## Unit 3 — frontend-cms-page
### useCmsInstances
```typescript
function useCmsInstances(): UseQueryResult<CmsInstance[], Error>
```
### useAddCmsInstance
```typescript
interface CreateCmsInstancePayload {
name: string;
url: string;
apiKey: string;
}
function useAddCmsInstance(): UseMutationResult<CmsInstance, Error, CreateCmsInstancePayload>
```
### useUpdateCmsInstanceStatus
```typescript
interface UpdateStatusPayload {
id: string;
status: CmsInstanceStatus;
disableMessage?: string;
}
function useUpdateCmsInstanceStatus(): UseMutationResult<void, Error, UpdateStatusPayload>
```
### CmsInstance (TypeScript)
```typescript
interface CmsInstance {
id: string;
name: string;
url: string;
status: CmsInstanceStatus;
disableMessage?: string;
lastContactedAt?: string; // ISO 8601
lastStatusPushedAt?: string; // ISO 8601
}
enum CmsInstanceStatus {
Available = 'Available',
NotAvailable = 'NotAvailable',
Inactive = 'Inactive',
}
```
@@ -0,0 +1,167 @@
# Components — Master CMS Module
## Unit 1 — master-backend (`SlpModularCms.Modules.Master`)
### MasterModule
- **Type**: Module registration (`IModule`)
- **Responsibilities**: Registers all master-side services (repository, service, typed HTTP client, background service, options); applies `MasterDbContext` migrations at startup via `UseModule`; does NOT register middleware (master instance has no availability gate)
- **Interface**: `IModule` (`RegisterServices`, `UseModule`)
### MasterDbContext
- **Type**: EF Core `DbContext`
- **Responsibilities**: Per-module DbContext; owns the `CmsInstances` table and its migrations; migrations live in `SlpModularCms.Modules.Master` (NFR-MASTER-06)
- **Entities owned**: `CmsInstance`
### CmsInstance
- **Type**: Domain Entity
- **Responsibilities**: Represents a registered slave CMS instance
- **Fields**:
- `Id``Guid`, primary key
- `Name``string`, friendly display name
- `Url``string`, base URL of slave CMS API
- `ApiKey``string`, secret used by Master to authenticate against slave; never returned in API responses (NFR-MASTER-03)
- `Status``CmsInstanceStatus` enum (`Available` / `NotAvailable` / `Inactive`)
- `DisableMessage``string?`, required when `Status = NotAvailable`
- `LastContactedAt``DateTimeOffset?`
- `LastStatusPushedAt``DateTimeOffset?`
### CmsInstanceStatus
- **Type**: Enum
- **Values**: `Available`, `NotAvailable`, `Inactive`
### ICmsInstanceRepository / CmsInstanceRepository
- **Type**: Repository (data access only)
- **Responsibilities**: CRUD operations on `CmsInstance` via `MasterDbContext`; no business logic
- **Lifetime**: Scoped
### ICmsInstanceService / CmsInstanceService
- **Type**: Service (orchestration)
- **Responsibilities**: Business orchestration — calls repository for data access; calls `ISlaveApiClient` for HTTP side-effects (auto-registration, status push, integrity verification); enforces business rules (e.g., `DisableMessage` required when `NotAvailable`)
- **Lifetime**: Scoped
### ISlaveApiClient / SlaveApiClient
- **Type**: Typed HTTP client
- **Responsibilities**: All Master → Slave HTTP communication (registration, status push, integrity check); adds `X-Master-Api-Key` header; handles HTTP errors and returns success flags
- **Registration**: `services.AddHttpClient<ISlaveApiClient, SlaveApiClient>()`
- **Lifetime**: Transient (managed by `IHttpClientFactory`)
### CmsInstanceController
- **Type**: ASP.NET Core `ControllerBase`
- **Responsibilities**: REST API for slave CMS management; `[Authorize(Policy = "OwnerOnly")]`; delegates to `ICmsInstanceService`
- **Route**: `/api/v1/CmsInstances`
- **Actions**: GET list, POST add, PUT update status
### IntegrityCheckBackgroundService
- **Type**: `BackgroundService`
- **Responsibilities**: Periodic background loop; verifies each non-Inactive slave still has the correct master URL registered; re-registers if mismatch found; interval configurable via `MasterModuleOptions.IntegrityCheckIntervalMinutes` (default 60)
- **Pattern**: Uses `PeriodicTimer`; injects `IServiceScopeFactory` to resolve scoped `ICmsInstanceService` per tick
- **Lifetime**: Singleton (as required by `BackgroundService`)
### MasterModuleOptions
- **Type**: Configuration POCO
- **Fields**:
- `IntegrityCheckIntervalMinutes``int`, default 60 (master-side)
- `CacheMinutes``int`, default 60 (slave-side)
- `ApiKey``string` (slave-side; key the slave uses to validate incoming master requests)
- **Registration**: `services.Configure<MasterModuleOptions>(configuration.GetSection("MasterModule"))`
### DTOs and Request Models
| Type | Fields | Notes |
|------|--------|-------|
| `CmsInstanceDto` | `Id`, `Name`, `Url`, `Status`, `DisableMessage`, `LastContactedAt`, `LastStatusPushedAt` | No `ApiKey` (NFR-MASTER-03) |
| `CreateCmsInstanceRequest` | `Name`, `Url`, `ApiKey` | API key stored securely, never returned |
| `UpdateStatusRequest` | `Status`, `DisableMessage?` | `DisableMessage` required when `Status = NotAvailable` |
---
## Unit 2 — slave-availability-extension (`SlpModularCms.Modules.Availability`)
### MasterRegistration
- **Type**: Domain Entity
- **Responsibilities**: Stores the registered Master CMS URL on the slave side; zero or one records per slave (the slave knows at most one master)
- **Fields**:
- `Id``Guid`, primary key
- `MasterUrl``string`, base URL of the Master CMS
- `RegisteredAt``DateTimeOffset`
### AvailabilityDbContext
- **Type**: EF Core `DbContext` (new, per-module)
- **Responsibilities**: Per-module DbContext introduced in the Availability module for the slave-side entity; owns the `MasterRegistrations` table; migrations live in `SlpModularCms.Modules.Availability`
- **Entities owned**: `MasterRegistration`
### IMasterAvailabilityService / MasterAvailabilityService
- **Type**: Service
- **Responsibilities**: Checks whether a master URL is registered (DB lookup); pulls master-controlled availability status via HTTP GET; caches last known status using static fields + timestamp (same pattern as `PersistentAvailabilityService`); implements fail-open fallback when master is unreachable; respects `MasterModuleOptions.CacheMinutes`
- **Cache pattern**: Static fields `_cachedStatus` (default `Available`) + `_lastFetchedAt`; stale check based on `CacheMinutes`
- **Exemption (FR-MASTER-09)**: No special exemption logic needed — if no `MasterRegistration` record exists in DB (which is the case on a Master instance that never registered itself), the gate is skipped automatically
- **Lifetime**: Scoped (static fields provide cross-request caching)
### AvailabilityMiddleware (extended)
- **Type**: ASP.NET Core Middleware
- **Responsibilities**: Extended with Master gate logic at the **top** of `InvokeAsync`; two-phase check:
1. **Master gate** — calls `IMasterAvailabilityService.GetMasterStatusAsync()`; if no master registered → skip to local gate; if master says `NotAvailable` → 503 with `DisableMessage`; if unreachable → use cached/fallback value (fail-open)
2. **Local gate** — existing `IAvailabilityService` check, unchanged
- **Bypass prefixes**: Extended to also bypass internal master endpoints (`/api/internal/master/`) so registration calls are never blocked
### AvailabilityController (extended)
- **Type**: ASP.NET Core `ControllerBase` (existing class extended)
- **Responsibilities**: New action `RegisterMaster` added; validates `X-Master-Api-Key` header against configured `MasterModuleOptions.ApiKey`; upserts `MasterRegistration` in `AvailabilityDbContext`
- **New route**: `POST /api/internal/master/register`
- **Authentication**: API key validation (no JWT; the registration endpoint is called machine-to-machine)
### RegisterMasterRequest
- **Type**: Request model
- **Fields**: `MasterUrl``string`
### MasterGateResult
- **Type**: Result record
- **Fields**: `HasMaster` (`bool`), `Status` (`CmsInstanceStatus?`), `DisableMessage` (`string?`)
---
## Unit 3 — frontend-cms-page (`frontend/`)
### CmsPage
- **Type**: React page component
- **Route**: `/cms`
- **Responsibilities**: Owner-only route guard; fetches slave list via `useCmsInstances`; renders `CmsInstanceList`; manages dialog open state for Add and Set Status actions
### CmsInstanceList
- **Type**: React component
- **Responsibilities**: Renders a table of `CmsInstance` items; shows Name, URL, Status badge, LastContactedAt, DisableMessage; `Inactive` rows are visually greyed out; provides action triggers (Add button, Set Status button per row)
### AddCmsInstanceDialog
- **Type**: React component (modal dialog using shadcn/ui `Dialog`)
- **Responsibilities**: Form with fields Name, URL, ApiKey (all required); validates before submission; calls `useAddCmsInstance` mutation; closes on success
### SetStatusDialog
- **Type**: React component (modal dialog using shadcn/ui `Dialog`)
- **Responsibilities**: Status dropdown (`Available`, `NotAvailable`, `Inactive`); `DisableMessage` text field rendered and required when status is `NotAvailable`; calls `useUpdateCmsInstanceStatus` mutation; closes on success
### CmsInstanceStatus (TypeScript enum)
- **Values**: `Available`, `NotAvailable`, `Inactive`
### CmsInstance (TypeScript type)
- **Fields**: `id`, `name`, `url`, `status`, `disableMessage`, `lastContactedAt`, `lastStatusPushedAt`
### useCmsInstances
- **Type**: TanStack Query `useQuery` hook
- **File**: `hooks/useCmsInstances.ts`
- **Responsibilities**: GET `/api/v1/CmsInstances`; returns list of `CmsInstance`
### useAddCmsInstance
- **Type**: TanStack Query `useMutation` hook
- **File**: `hooks/useAddCmsInstance.ts`
- **Responsibilities**: POST `/api/v1/CmsInstances`; invalidates `useCmsInstances` query on success
### useUpdateCmsInstanceStatus
- **Type**: TanStack Query `useMutation` hook
- **File**: `hooks/useUpdateCmsInstanceStatus.ts`
- **Responsibilities**: PUT `/api/v1/CmsInstances/{id}/status`; invalidates `useCmsInstances` query on success
---
## Unit 4 — documentation
No new components. Covers README updates only (FR-MASTER-15). See requirements for scope.
@@ -0,0 +1,233 @@
# Services — Master CMS Module
## Service Definitions
### Master-Side Services
#### CmsInstanceService
- **Interface**: `ICmsInstanceService`
- **Lifetime**: Scoped
- **Injected Dependencies**: `ICmsInstanceRepository`, `ISlaveApiClient`, `IOptions<MasterModuleOptions>`, `ILogger<CmsInstanceService>`
- **Responsibilities**: Business orchestration — coordinates repository (data) and SlaveApiClient (HTTP side-effects); enforces business rules (mandatory DisableMessage, Inactive cannot be pushed)
- **Key orchestration**: On `AddAsync` → persist entity then call `RegisterMasterAsync`; on `UpdateStatusAsync` → validate, persist, then call `PushStatusAsync`; on `VerifyIntegrityAsync` → query all active instances, call `GetRegisteredMasterUrlAsync` per instance, re-register on mismatch
#### SlaveApiClient
- **Interface**: `ISlaveApiClient`
- **Lifetime**: Transient (managed by `IHttpClientFactory`)
- **Registration**: `services.AddHttpClient<ISlaveApiClient, SlaveApiClient>()`
- **Injected Dependencies**: `HttpClient` (injected by framework)
- **Responsibilities**: Typed HTTP client for all Master → Slave API calls; sets `X-Master-Api-Key` header per call; deserializes responses; returns success flags rather than throwing (callers decide error handling)
- **Endpoints called**:
- `POST {slaveUrl}/api/internal/master/register`
- `PUT {slaveUrl}/api/v1/Availability/admin/status` (reuses existing slave endpoint)
- `GET {slaveUrl}/api/internal/master/registration` (integrity check)
#### IntegrityCheckBackgroundService
- **Lifetime**: Singleton (registered via `services.AddHostedService<IntegrityCheckBackgroundService>()`)
- **Injected Dependencies**: `IServiceScopeFactory`, `IOptions<MasterModuleOptions>`, `ILogger<IntegrityCheckBackgroundService>`
- **Responsibilities**: Periodic background loop; resolves `ICmsInstanceService` via `IServiceScopeFactory` per tick (required because `ICmsInstanceService` is Scoped); runs `VerifyIntegrityAsync()`; interval from `MasterModuleOptions.IntegrityCheckIntervalMinutes`
---
### Slave-Side Services
#### MasterAvailabilityService
- **Interface**: `IMasterAvailabilityService`
- **Lifetime**: Scoped
- **Injected Dependencies**: `AvailabilityDbContext`, `IHttpClientFactory`, `IOptions<MasterModuleOptions>`, `ILogger<MasterAvailabilityService>`
- **Responsibilities**: Checks DB for `MasterRegistration`; if none exists returns `HasMaster = false`; if exists checks cache freshness against `MasterModuleOptions.CacheMinutes`; pulls from Master via HTTP on cache miss; returns cached value on HTTP failure (fail-open, NFR-MASTER-01)
- **Cache fields** (static): `_cachedStatus` (default `Available`), `_cachedDisableMessage`, `_lastFetchedAt`, `_cachedMasterUrl`
- **Exemption**: No special logic needed — a Master CMS instance never calls `RegisterMaster` on itself, so `MasterRegistrations` table is empty → `HasMaster = false` always on a Master instance (FR-MASTER-09)
---
## Orchestration Flows
### Flow 1 — Add Slave CMS
```mermaid
sequenceDiagram
box rgba(99,179,237,0.3) Frontend
participant UI as Browser
end
box rgba(154,230,180,0.3) Master CMS
participant Ctrl as CmsInstanceController
participant Svc as CmsInstanceService
participant Repo as CmsInstanceRepository
participant Client as SlaveApiClient
end
box rgba(246,224,94,0.3) Slave CMS
participant SlaveEndpoint as AvailabilityController
end
UI->>Ctrl: POST /api/v1/CmsInstances
Ctrl->>Svc: AddAsync(request)
Svc->>Repo: AddAsync(entity)
Repo-->>Svc: entity tracked
Svc->>Repo: SaveChangesAsync()
Svc->>Client: RegisterMasterAsync(slaveUrl, apiKey, masterUrl)
Client->>SlaveEndpoint: POST /api/internal/master/register
SlaveEndpoint-->>Client: 200 OK
Client-->>Svc: true
Svc->>Repo: UpdateAsync (LastContactedAt)
Repo-->>Svc: updated
Svc->>Repo: SaveChangesAsync()
Svc-->>Ctrl: CmsInstanceDto
Ctrl-->>UI: 201 Created
```
Text alternative: Browser posts new slave to master controller → service persists → calls slave registration endpoint → updates LastContactedAt → returns DTO.
---
### Flow 2 — Set Slave Status
```mermaid
sequenceDiagram
box rgba(99,179,237,0.3) Frontend
participant UI as Browser
end
box rgba(154,230,180,0.3) Master CMS
participant Ctrl as CmsInstanceController
participant Svc as CmsInstanceService
participant Repo as CmsInstanceRepository
participant Client as SlaveApiClient
end
box rgba(246,224,94,0.3) Slave CMS
participant SlaveCtrl as AvailabilityController
end
UI->>Ctrl: PUT /api/v1/CmsInstances/{id}/status
Ctrl->>Svc: UpdateStatusAsync(id, status, disableMessage)
Svc->>Repo: GetByIdAsync(id)
Repo-->>Svc: CmsInstance
Note over Svc: Validate DisableMessage required if NotAvailable
Svc->>Repo: UpdateAsync (Status, DisableMessage)
Svc->>Repo: SaveChangesAsync()
Svc->>Client: PushStatusAsync(url, apiKey, status, disableMessage)
Client->>SlaveCtrl: PUT /api/v1/Availability/admin/status
SlaveCtrl-->>Client: 200 OK
Client-->>Svc: true
Svc->>Repo: UpdateAsync (LastStatusPushedAt)
Svc->>Repo: SaveChangesAsync()
Svc-->>Ctrl: void
Ctrl-->>UI: 200 OK
```
Text alternative: Browser sends status update → master validates, persists, pushes to slave endpoint → updates LastStatusPushedAt → returns 200.
---
### Flow 3 — Integrity Check (Background)
```mermaid
sequenceDiagram
box rgba(200,200,200,0.3) Background
participant Timer as PeriodicTimer
participant BgSvc as IntegrityCheckBackgroundService
end
box rgba(154,230,180,0.3) Master CMS
participant Svc as CmsInstanceService
participant Repo as CmsInstanceRepository
participant Client as SlaveApiClient
end
box rgba(246,224,94,0.3) Slave CMS
participant SlaveEndpoint as Slave API
end
Timer->>BgSvc: Tick (every IntegrityCheckIntervalMinutes)
BgSvc->>Svc: VerifyIntegrityAsync()
Svc->>Repo: GetActiveAsync()
Repo-->>Svc: list of active CmsInstances
loop for each active instance
Svc->>Client: GetRegisteredMasterUrlAsync(slaveUrl, apiKey)
Client->>SlaveEndpoint: GET /api/internal/master/registration
SlaveEndpoint-->>Client: registeredMasterUrl
Client-->>Svc: registeredMasterUrl
alt URL mismatch
Svc->>Client: RegisterMasterAsync(slaveUrl, apiKey, masterUrl)
Client->>SlaveEndpoint: POST /api/internal/master/register
SlaveEndpoint-->>Client: 200 OK
Client-->>Svc: true
end
end
Svc-->>BgSvc: done
```
Text alternative: Background timer triggers integrity service → per active slave checks registered URL → re-registers if mismatch.
---
### Flow 4 — Two-Phase Availability Gate (Slave Middleware)
```mermaid
flowchart TD
Start(["Incoming HTTP Request"])
Bypass{"Bypass prefix?"}
HasMaster{"MasterRegistration\nexists in DB?"}
CacheFresh{"Cache fresh?"}
PullMaster["Pull status from Master\n(HTTP GET)"]
PullOk{"Pull successful?"}
MasterStatus{"Master status?"}
LocalCheck["Existing local gate\nIAvailabilityService.IsAvailableAsync()"]
AdminBypass{"Admin bypass\n(Owner/Admin JWT)?"}
Block503Master["503 Service Unavailable\n+ DisableMessage"]
Block503Local["503 / Maintenance"]
Pass(["Pass request to next middleware"])
Start --> Bypass
Bypass -->|yes| Pass
Bypass -->|no| HasMaster
HasMaster -->|no| LocalCheck
HasMaster -->|yes| CacheFresh
CacheFresh -->|yes| MasterStatus
CacheFresh -->|no| PullMaster
PullMaster --> PullOk
PullOk -->|yes| MasterStatus
PullOk -->|no - use cached| MasterStatus
MasterStatus -->|NotAvailable| Block503Master
MasterStatus -->|Available| LocalCheck
LocalCheck -->|Available| Pass
LocalCheck -->|NotAvailable or Maintenance| AdminBypass
AdminBypass -->|yes| Pass
AdminBypass -->|no| Block503Local
classDef decision fill:#FFC107,stroke:#F57F17,stroke-width:2px,color:#000
classDef action fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000
classDef terminal fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000
classDef block fill:#FC8181,stroke:#C53030,stroke-width:2px,color:#000
class Bypass,HasMaster,CacheFresh,PullOk,MasterStatus,AdminBypass decision
class PullMaster,LocalCheck action
class Start terminal
class Pass terminal
class Block503Master,Block503Local block
```
Text alternative: Request enters middleware → check bypass → if master registered check cached/pulled status → if NotAvailable return 503 with DisableMessage → else proceed to existing local availability gate.
---
### Flow 5 — Slave Registration (Master Registers Itself)
```mermaid
sequenceDiagram
box rgba(154,230,180,0.3) Master CMS
participant Client as SlaveApiClient
end
box rgba(246,224,94,0.3) Slave CMS
participant Ctrl as AvailabilityController
participant DB as AvailabilityDbContext
end
Client->>Ctrl: POST /api/internal/master/register\nHeader: X-Master-Api-Key\nBody: {masterUrl}
Ctrl->>Ctrl: Validate API key vs MasterModuleOptions.ApiKey
alt Invalid key
Ctrl-->>Client: 401 Unauthorized
else Valid key
Ctrl->>DB: Upsert MasterRegistration (masterUrl)
DB-->>Ctrl: saved
Ctrl-->>Client: 200 OK
end
```
Text alternative: Master posts registration with API key header → slave validates key → upserts MasterRegistration record → returns 200.
@@ -0,0 +1,103 @@
# Unit of Work Dependencies — Master CMS Module
## Dependency Matrix
| Unit | Depends On | Reason |
|------|-----------|--------|
| Unit 1 — master-backend | `SlpModularCms.Core` only | No unit dependencies; builds the API contract that others consume |
| Unit 2 — slave-availability-extension | Unit 1 (API contract) | Slave endpoints must match what `ISlaveApiClient` calls; registration endpoint schema defined in Unit 1 |
| Unit 3 — frontend-cms-page | Unit 1 (REST API) | Frontend hooks call `/api/v1/CmsInstances`; endpoint shapes must be finalized |
| Unit 4 — documentation | Units 1, 2, 3 | Documents final patterns after all code is complete |
## Implementation Sequence
```mermaid
graph LR
U1["Unit 1\nmaster-backend"]
U2["Unit 2\nslave-availability-extension"]
U3["Unit 3\nfrontend-cms-page"]
U4["Unit 4\ndocumentation"]
U1 --> U2
U1 --> U3
U2 --> U4
U3 --> U4
classDef unit1 fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000
classDef unit2 fill:#63b3ed,stroke:#2b6cb0,stroke-width:2px,color:#000
classDef unit3 fill:#FFC107,stroke:#F57F17,stroke-width:2px,color:#000
classDef unit4 fill:#CE93D8,stroke:#6A1B9A,stroke-width:2px,color:#000
class U1 unit1
class U2 unit2
class U3 unit3
class U4 unit4
```
Text alternative: Unit 1 (master-backend) must complete first; Unit 2 and Unit 3 both depend on Unit 1 and can be worked on in parallel after Unit 1 is done; Unit 4 (documentation) depends on all three.
## Package Change Sequence
```mermaid
graph TD
Core["SlpModularCms.Core\n(no changes)"]
MasterMod["SlpModularCms.Modules.Master\nUnit 1 — new project"]
AvailMod["SlpModularCms.Modules.Availability\nUnit 2 — extended"]
Api["SlpModularCms.Api\nregisters Master module"]
Frontend["frontend/\nUnit 3 — /cms page"]
Docs["README.md\nfrontend/README.md\nUnit 4"]
MasterTests["SlpModularCms.Modules.Master.Tests\nUnit 1 — new test project"]
AvailMasterTests["SlpModularCms.Modules.Availability.Master.Tests\nUnit 2 — new test project"]
Core --> MasterMod
Core --> AvailMod
MasterMod --> AvailMod
MasterMod --> Api
AvailMod --> Api
Api --> Frontend
Frontend --> Docs
MasterMod --> MasterTests
AvailMod --> AvailMasterTests
classDef unchanged fill:#B0BEC5,stroke:#546E7A,stroke-width:1px,color:#000
classDef unit1 fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000
classDef unit2 fill:#63b3ed,stroke:#2b6cb0,stroke-width:2px,color:#000
classDef unit3 fill:#FFC107,stroke:#F57F17,stroke-width:2px,color:#000
classDef unit4 fill:#CE93D8,stroke:#6A1B9A,stroke-width:2px,color:#000
classDef tests fill:#FC8181,stroke:#C53030,stroke-width:1px,color:#000
class Core,Api unchanged
class MasterMod unit1
class AvailMod unit2
class Frontend unit3
class Docs unit4
class MasterTests,AvailMasterTests tests
```
Text alternative: Core unchanged; Unit 1 (Modules.Master) created first; Unit 2 (Availability extended) and Unit 3 (frontend) build on top; Api shell registers new module; Unit 4 docs come last; two new test projects created.
## Inter-Unit API Contracts
The following interfaces form the boundary between units. These must be finalized during Unit 1 before Unit 2 and Unit 3 can proceed.
| Contract | Defined In | Consumed By |
|----------|-----------|-------------|
| `POST /api/internal/master/register` | Unit 2 (slave exposes it) | Unit 1 (`SlaveApiClient` calls it) |
| `GET /api/internal/master/registration` | Unit 2 (slave exposes it) | Unit 1 (`SlaveApiClient` calls it for integrity check) |
| `PUT /api/v1/Availability/admin/status` | Unit 2 (existing, unchanged) | Unit 1 (`SlaveApiClient` reuses it for status push) |
| `GET /api/v1/CmsInstances` | Unit 1 (master exposes it) | Unit 3 (`useCmsInstances` hook) |
| `POST /api/v1/CmsInstances` | Unit 1 (master exposes it) | Unit 3 (`useAddCmsInstance` hook) |
| `PUT /api/v1/CmsInstances/{id}/status` | Unit 1 (master exposes it) | Unit 3 (`useUpdateCmsInstanceStatus` hook) |
## Parallel Development Opportunities
After Unit 1 is complete and its API contracts are finalized:
- **Unit 2 and Unit 3 can be developed in parallel** — they share no direct dependency on each other; both only depend on Unit 1's REST API contract
- **Unit 4** must wait for all three units to be complete
## Risk Notes
| Risk | Unit | Mitigation |
|------|------|-----------|
| Slave endpoint schema changes after Unit 2 starts | 2 | Finalize `ISlaveApiClient` method signatures in Unit 1 before Unit 2 construction begins |
| Frontend type drift from actual API response shape | 3 | Generate TypeScript types from Unit 1 `CmsInstanceDto` definition; keep in sync during code generation |
| Per-module migration pattern new to codebase | 1, 2 | Apply pattern in Unit 1 first; Unit 2 follows the same pattern |
@@ -0,0 +1,62 @@
# Unit of Work — Requirement Map — Master CMS Module
> No user stories were generated for this feature (skipped — owner-operated, technical feature with clear requirements). This document maps functional requirements and NFRs to units instead.
---
## Functional Requirements → Unit Mapping
| Requirement | Description | Unit |
|-------------|-------------|------|
| FR-MASTER-01 | New `SlpModularCms.Modules.Master` module | **Unit 1** |
| FR-MASTER-02 | `CmsInstance` entity | **Unit 1** |
| FR-MASTER-03 | Auto-registration: Master registers itself with slave | **Unit 1** (client call) + **Unit 2** (slave endpoint) |
| FR-MASTER-04 | Integrity Check background service | **Unit 1** |
| FR-MASTER-05 | Status push: Master sets slave availability | **Unit 1** (client call) + **Unit 2** (slave endpoint reused) |
| FR-MASTER-06 | Slave pull model: periodic master check + cache | **Unit 2** |
| FR-MASTER-07 | Slave fallback behavior (fail-open) | **Unit 2** |
| FR-MASTER-08 | Two-phase availability gate on slave | **Unit 2** |
| FR-MASTER-09 | Master CMS exemption from own gate | **Unit 2** (natural: no registration record on master) |
| FR-MASTER-10 | Owner role access control | **Unit 1** (controller auth) + **Unit 2** (API key auth) |
| FR-MASTER-11 | `/cms` page: slave list | **Unit 3** |
| FR-MASTER-12 | `/cms` page: add slave CMS | **Unit 1** (API) + **Unit 3** (UI) |
| FR-MASTER-13 | `/cms` page: set slave status | **Unit 1** (API) + **Unit 3** (UI) |
| FR-MASTER-14 | Mandatory disable message for NotAvailable | **Unit 1** (service validation) + **Unit 3** (UI validation) |
| FR-MASTER-15 | Project documentation updates | **Unit 4** |
---
## Non-Functional Requirements → Unit Mapping
| Requirement | Description | Unit |
|-------------|-------------|------|
| NFR-MASTER-01 | Fail-open safety | **Unit 2** (`MasterAvailabilityService` fallback) |
| NFR-MASTER-02 | Configurable cache interval (`MasterModule:CacheMinutes`) | **Unit 2** + **Unit 4** (documented in README) |
| NFR-MASTER-03 | API key security (never exposed in responses) | **Unit 1** (`CmsInstanceDto` excludes `ApiKey`) |
| NFR-MASTER-04 | Configurable integrity check interval | **Unit 1** (`IntegrityCheckBackgroundService`) + **Unit 4** (documented) |
| NFR-MASTER-05 | ≥ 80% test coverage on new backend code | **Unit 1** + **Unit 2** (test projects) |
| NFR-MASTER-06 | Per-module database migrations | **Unit 1** (`MasterDbContext`) + **Unit 2** (`AvailabilityDbContext`) |
---
## Unit Coverage Summary
| Unit | FR Coverage | NFR Coverage |
|------|-------------|--------------|
| Unit 1 — master-backend | FR-01, FR-02, FR-03 (client), FR-04, FR-05 (client), FR-10 (controller), FR-12 (API), FR-13 (API), FR-14 (validation) | NFR-03, NFR-04, NFR-05 (partial), NFR-06 (partial) |
| Unit 2 — slave-availability-extension | FR-03 (endpoint), FR-05 (endpoint), FR-06, FR-07, FR-08, FR-09, FR-10 (API key) | NFR-01, NFR-02 (partial), NFR-05 (partial), NFR-06 (partial) |
| Unit 3 — frontend-cms-page | FR-11, FR-12 (UI), FR-13 (UI), FR-14 (UI validation) | — |
| Unit 4 — documentation | FR-15 | NFR-02 (partial), NFR-04 (partial) |
---
## Cross-Unit Requirements
Requirements that span multiple units and require coordination:
| Requirement | Units Involved | Coordination Point |
|-------------|---------------|-------------------|
| FR-MASTER-03 (auto-registration) | 1 + 2 | `ISlaveApiClient.RegisterMasterAsync` contract must match `POST /api/internal/master/register` schema |
| FR-MASTER-05 (status push) | 1 + 2 | `ISlaveApiClient.PushStatusAsync` reuses existing `PUT /api/v1/Availability/admin/status`; no schema change needed |
| FR-MASTER-12/13/14 | 1 + 3 | Frontend DTOs must match `CmsInstanceDto` and `UpdateStatusRequest` from Unit 1 |
| NFR-MASTER-06 (per-module migrations) | 1 + 2 | Both units introduce a new `DbContext`; same pattern applied consistently |
@@ -0,0 +1,124 @@
# Unit of Work — Master CMS Module
## Unit Decomposition Overview
| # | Unit Slug | Project(s) | Test Project | Construction Cycle |
|---|-----------|-----------|--------------|-------------------|
| 1 | `master-backend` | `SlpModularCms.Modules.Master` (NEW) | `SlpModularCms.Modules.Master.Tests` (NEW) | FD → NFR Req → NFR Design → Code Gen |
| 2 | `slave-availability-extension` | `SlpModularCms.Modules.Availability` (EXTENDED) | `SlpModularCms.Modules.Availability.Master.Tests` (NEW) | FD → NFR Req → NFR Design → Code Gen |
| 3 | `frontend-cms-page` | `frontend/` (EXTENDED) | Existing frontend test setup | FD → NFR Req → NFR Design → Code Gen |
| 4 | `documentation` | `README.md`, `frontend/README.md` | N/A | Code Gen only |
---
## Unit 1 — master-backend
**Project**: `src/SlpModularCms.Modules.Master/` (new project added to solution)
**Test Project**: `src/SlpModularCms.Modules.Master.Tests/` (new; parallel to existing `Availability.Tests`)
**Construction Cycle**: Functional Design → NFR Requirements → NFR Design → Code Generation
**Scope**:
- New `SlpModularCms.Modules.Master` class library project
- `MasterModule : IModule` — module registration, service wiring, migration application
- `MasterDbContext` — per-module EF Core DbContext; owns `CmsInstances` table
- `CmsInstance` entity + `CmsInstanceStatus` enum
- `ICmsInstanceRepository` / `CmsInstanceRepository` — data access
- `ICmsInstanceService` / `CmsInstanceService` — business orchestration
- `ISlaveApiClient` / `SlaveApiClient` — typed HTTP client for master → slave calls
- `CmsInstanceController` — REST endpoints (`GET`, `POST`, `PUT /status`) with `[Authorize(Policy = "OwnerOnly")]`
- `IntegrityCheckBackgroundService` — periodic master URL integrity verification
- `MasterModuleOptions` — configuration POCO
- DTOs: `CmsInstanceDto`, `CreateCmsInstanceRequest`, `UpdateStatusRequest`
- EF Core migrations for `CmsInstances` table
**Dependencies**: `SlpModularCms.Core` (for `IModule`, shared types)
**Deliverables**:
- Functional, tested module registered in `SlpModularCms.Api`
- REST API endpoints accessible to Owner role
- Background service running on master CMS startup
- EF Core migration applied at startup
---
## Unit 2 — slave-availability-extension
**Project**: `src/SlpModularCms.Modules.Availability/` (existing project, extended)
**Test Project**: `src/SlpModularCms.Modules.Availability.Master.Tests/` (new; separate from existing `Availability.Tests` to isolate master-related slave changes)
**Construction Cycle**: Functional Design → NFR Requirements → NFR Design → Code Generation
**Scope**:
- `MasterRegistration` entity — stores master URL on slave side
- `AvailabilityDbContext` — new per-module EF Core DbContext in Availability module; owns `MasterRegistrations` table
- `IMasterAvailabilityService` / `MasterAvailabilityService` — pull/cache/fallback service; static field caching
- `AvailabilityMiddleware` (extended) — two-phase gate: Master gate (outer) + existing local gate (inner)
- `AvailabilityController` (extended) — `POST /api/internal/master/register` endpoint added
- `RegisterMasterRequest` request model
- `MasterGateResult` result record
- EF Core migrations for `MasterRegistrations` table
- `GET /api/internal/master/registration` endpoint (read registered master URL, used by integrity check)
**Dependencies**: Unit 1 API contract (endpoint schemas that slave exposes must match what `ISlaveApiClient` calls)
**Deliverables**:
- Two-phase availability gate active on slave CMS instances
- Slave accepts master registration calls with API key validation
- Slave pulls and caches master status with fail-open fallback
- EF Core migration applied at startup
---
## Unit 3 — frontend-cms-page
**Project**: `frontend/` (existing React SPA, extended)
**Test Project**: Existing frontend test setup (no separate test project added)
**Construction Cycle**: Functional Design → NFR Requirements → NFR Design → Code Generation
**Scope**:
- `CmsPage` — page component at route `/cms`; Owner-only guard
- `CmsInstanceList` — table component with status badges; Inactive rows greyed out
- `AddCmsInstanceDialog` — modal form (Name, URL, ApiKey)
- `SetStatusDialog` — modal with status dropdown and conditional DisableMessage field
- `useCmsInstances.ts` — TanStack Query hook: GET `/api/v1/CmsInstances`
- `useAddCmsInstance.ts` — TanStack Query mutation: POST `/api/v1/CmsInstances`
- `useUpdateCmsInstanceStatus.ts` — TanStack Query mutation: PUT `/api/v1/CmsInstances/{id}/status`
- TypeScript types: `CmsInstance`, `CmsInstanceStatus` enum
- Route registration in existing router
**Dependencies**: Unit 1 REST API (endpoint definitions must be finalized before frontend hooks)
**Deliverables**:
- `/cms` page renders list of slave CMSes
- Owner can add a slave and set its status
- Status badge display for all three states; Inactive greyed out
- Mandatory DisableMessage enforced in UI when NotAvailable selected
---
## Unit 4 — documentation
**Files**: `README.md` (root), `frontend/README.md`
**Test Project**: N/A
**Construction Cycle**: Code Generation only (Functional Design, NFR Requirements, NFR Design skipped — documentation-only unit)
**Scope** (per FR-MASTER-15):
- `README.md` — "Database Migraties" section: document per-module migration pattern
- `README.md` — "Nieuwe Module Toevoegen" section: document optional per-module DbContext pattern
- `README.md` — "Productie Setup" section: add `MasterModule__CacheMinutes` and `MasterModule__IntegrityCheckIntervalMinutes` env vars
- `frontend/README.md` — replace boilerplate with project-specific content
**Dependencies**: Units 13 (must be complete so final patterns are known before documenting)
**Deliverables**:
- README accurately reflects per-module migration workflow
- Module guide covers optional DbContext pattern
- Production env vars list is complete
- Frontend README is project-specific and useful