Adds 2 units and docs for unit 3. nfr-requirements plan
This commit is contained in:
+164
@@ -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` |
|
||||
+187
@@ -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` |
|
||||
+142
@@ -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.
|
||||
+103
@@ -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 |
|
||||
+62
@@ -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 1–3 (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
|
||||
@@ -0,0 +1,154 @@
|
||||
# Application Design Plan — Master CMS Module
|
||||
|
||||
## Overview
|
||||
|
||||
This plan covers the high-level component identification and service layer design for the Master CMS Module.
|
||||
The feature spans four units: `master-backend`, `slave-availability-extension`, `frontend-cms-page`, and `documentation`.
|
||||
|
||||
Before generating design artifacts, a set of design questions must be answered below.
|
||||
|
||||
---
|
||||
|
||||
## Design Questions
|
||||
|
||||
Answer each question by filling in your choice after the `[Answer]:` tag.
|
||||
|
||||
---
|
||||
|
||||
### Q1 — HTTP Client for Master → Slave Communication
|
||||
|
||||
The Master CMS needs to call slave CMS REST endpoints for:
|
||||
- Auto-registration (FR-MASTER-03)
|
||||
- Status push (FR-MASTER-05)
|
||||
- Integrity verification (FR-MASTER-04)
|
||||
|
||||
How should the HTTP client be organized in `SlpModularCms.Modules.Master`?
|
||||
|
||||
A) **Typed client** — define `ISlaveApiClient` interface + `SlaveApiClient` implementation; registered via `services.AddHttpClient<ISlaveApiClient, SlaveApiClient>()`. Clean, testable, injectable.
|
||||
|
||||
B) **Named client** — register a named `HttpClient` ("slave") via `IHttpClientFactory` and inject `IHttpClientFactory` into the service that makes calls. Less abstraction, but familiar .NET pattern.
|
||||
|
||||
C) **Direct `HttpClient` injection** — inject `IHttpClientFactory` directly in `CmsInstanceService` and create a client per call. Simplest approach; no separate client abstraction.
|
||||
|
||||
D) Other
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
### Q2 — Two-Phase Availability Gate: Middleware Strategy
|
||||
|
||||
The slave must implement a two-phase gate: Master gate (outer) → Local gate (inner) (FR-MASTER-08).
|
||||
The existing `AvailabilityMiddleware` implements the local gate.
|
||||
|
||||
Which approach should be used to add the Master gate on the slave?
|
||||
|
||||
A) **New separate `MasterGateMiddleware`** — registered before the existing `AvailabilityMiddleware` in the pipeline. Clean separation; existing middleware is untouched; Master gate is skipped at registration if no Master URL is stored.
|
||||
|
||||
B) **Extend `AvailabilityMiddleware`** — add the Master gate logic at the top of the existing middleware class. Single file; simpler pipeline registration; slightly more coupling between Master and Availability module.
|
||||
|
||||
C) Other
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
### Q3 — Slave-Side Master Status Caching
|
||||
|
||||
The slave must cache the master-pulled availability status (FR-MASTER-06, FR-MASTER-07).
|
||||
The existing codebase uses a simple `static` field + timestamp in `PersistentAvailabilityService` for circuit breaker caching.
|
||||
|
||||
Which caching mechanism should be used for the master status cache on the slave?
|
||||
|
||||
A) **Static field with timestamp** (same pattern as existing circuit breaker) — a `static` field in `MasterAvailabilityService` holding the last known status and last-fetched timestamp. Zero dependencies; consistent with existing code style.
|
||||
|
||||
B) **`IMemoryCache`** — inject `IMemoryCache` and use a keyed cache entry with a sliding/absolute expiry. Standard .NET caching abstraction; easier to test via mock; slightly more infrastructure.
|
||||
|
||||
C) Other
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
### Q4 — `CmsInstanceService` Responsibilities
|
||||
|
||||
The master-side service needs to handle: CRUD on `CmsInstance`, status push to slave (HTTP), and auto-registration (HTTP). How should these responsibilities be organized?
|
||||
|
||||
A) **Single unified `CmsInstanceService`** — one service handles CRUD (EF Core), HTTP status push, and auto-registration. Simple; consistent with the existing single-service pattern (e.g. `PersistentAvailabilityService`).
|
||||
|
||||
B) **Split: `CmsInstanceRepository` + `CmsInstanceService`** — repository handles EF Core data access; service handles business orchestration (status push, registration). Cleaner separation; slightly more files.
|
||||
|
||||
C) **Split: `CmsInstanceService` (CRUD) + `SlaveStatusService` (HTTP calls)** — data + business logic in one service; all HTTP slave interactions in a dedicated service. Best for unit testing HTTP logic separately.
|
||||
|
||||
D) Other
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
### Q5 — Master-Side Controller Granularity
|
||||
|
||||
The Master module needs REST endpoints for: listing slaves, adding a slave, and setting slave status.
|
||||
|
||||
Which controller structure is preferred?
|
||||
|
||||
A) **Single `CmsInstanceController`** — all actions in one controller: `GET /api/cms-instances`, `POST /api/cms-instances`, `PUT /api/cms-instances/{id}/status`. Consistent with how `AvailabilityController` works.
|
||||
|
||||
B) **Two controllers** — `CmsInstanceController` for CRUD (`GET`, `POST`) and `CmsInstanceStatusController` for the `PUT /status` action. Clearer separation of read vs. write-with-side-effect.
|
||||
|
||||
C) Other
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
### Q6 — Slave-Side Internal Endpoint Placement
|
||||
|
||||
The slave needs an internal registration endpoint (`POST /api/internal/master/register`) (FR-MASTER-03).
|
||||
Where should this endpoint be defined?
|
||||
|
||||
A) **New `MasterRegistrationController`** in `SlpModularCms.Modules.Availability` — a dedicated controller for internal master endpoints. Clean; extensible if more internal endpoints are needed.
|
||||
|
||||
B) **Added to the existing `AvailabilityController`** — keeps all availability-related endpoints in one file. Simpler; no extra controller class.
|
||||
|
||||
C) Other
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
### Q7 — Frontend: CMS Page API Hooks Organization
|
||||
|
||||
The frontend `/cms` page needs TanStack Query hooks for: listing CMS instances, adding an instance, and updating status.
|
||||
|
||||
How should the API hooks be organized?
|
||||
|
||||
A) **Single `useCmsInstances` hook file** — one file exports all hooks: `useCmsInstances()`, `useAddCmsInstance()`, `useUpdateCmsInstanceStatus()`. Consistent and simple.
|
||||
|
||||
B) **Separate hook files per concern** — `useCmsInstances.ts`, `useAddCmsInstance.ts`, `useUpdateCmsInstanceStatus.ts`. More files, but each file is focused.
|
||||
|
||||
C) **Follow existing pattern** — check how existing hooks (e.g. availability hooks) are organized and mirror that pattern.
|
||||
|
||||
D) Other
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
## Execution Steps
|
||||
|
||||
After all questions above are answered, the following artifacts will be generated:
|
||||
|
||||
- [x] **Step 1** — Analyze all answers; flag any ambiguities for follow-up
|
||||
- [x] **Step 2** — Generate `components.md` with component definitions and responsibilities
|
||||
- [x] **Step 3** — Generate `component-methods.md` with method signatures and purpose
|
||||
- [x] **Step 4** — Generate `services.md` with service definitions and orchestration patterns
|
||||
- [x] **Step 5** — Generate `component-dependency.md` with dependency matrix and data flow diagrams
|
||||
- [x] **Step 6** — Generate `application-design.md` consolidating all design artifacts
|
||||
- [x] **Step 7** — Validate all content (Mermaid diagrams, no ASCII trees, color styles present)
|
||||
- [x] **Step 8** — Update `aidlc-state.md` to mark Application Design as In Progress → Complete
|
||||
- [x] **Step 9** — Present completion message for user approval
|
||||
|
||||
---
|
||||
|
||||
*Artifact path*: `aidlc-docs/features/master-cms-module/inception/plans/application-design-plan.md`
|
||||
@@ -0,0 +1,160 @@
|
||||
# Execution Plan — Master CMS Module
|
||||
|
||||
## Detailed Analysis Summary
|
||||
|
||||
### Transformation Scope
|
||||
- **Transformation Type**: Multi-component addition — new module + slave-side middleware extension + frontend page + documentation
|
||||
- **Primary Changes**: New `SlpModularCms.Modules.Master` project; extended `SlpModularCms.Modules.Availability`; updated `/cms` frontend page
|
||||
- **Related Components**: Core (new entity), Api shell (module registration), Availability module (middleware extension), Frontend (CMS page)
|
||||
|
||||
### Change Impact Assessment
|
||||
- **User-facing changes**: Yes — `/cms` page gets a full slave management UI; slave CMS users see a disable message on 503
|
||||
- **Structural changes**: Yes — new module project, per-module DbContext pattern introduced
|
||||
- **Data model changes**: Yes — new `CmsInstance` entity (master), new `MasterRegistration` entity (slave)
|
||||
- **API changes**: Yes — new Master module endpoints; new internal slave registration endpoint; extended 503 response body
|
||||
- **NFR impact**: Yes — API key security, availability caching strategy, background service, fail-open design
|
||||
|
||||
### Component Relationships
|
||||
|
||||
**Primary new component**: `SlpModularCms.Modules.Master`
|
||||
- Depends on: `SlpModularCms.Core` (shared DbContext base, IModule), `SlpModularCms.Api` (module registration)
|
||||
|
||||
**Modified component**: `SlpModularCms.Modules.Availability`
|
||||
- Extended with: master registration endpoint, two-phase availability check, `MasterAvailabilityService`, slave-side `MasterDbContext`
|
||||
- Depends on: `SlpModularCms.Core`
|
||||
|
||||
**Modified component**: `SlpModularCms.Frontend`
|
||||
- Extended with: CMS page slave management UI, new TanStack Query hooks, new API types
|
||||
|
||||
### Risk Assessment
|
||||
- **Risk Level**: Medium-High
|
||||
- **Rollback Complexity**: Moderate — new module can be unregistered from Api; slave-side changes are additive; frontend changes are isolated to one route
|
||||
- **Testing Complexity**: Complex — involves network calls between Master and Slave, background service timing, cache behavior, fallback logic
|
||||
|
||||
---
|
||||
|
||||
## Workflow Visualization
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["Master CMS Module Request"])
|
||||
|
||||
subgraph INCEPTION["🔵 INCEPTION PHASE"]
|
||||
WD["Workspace Detection\nCOMPLETED"]
|
||||
RE["Reverse Engineering\nSKIPPED (artifacts exist)"]
|
||||
RA["Requirements Analysis\nCOMPLETED"]
|
||||
US["User Stories\nSKIPPED"]
|
||||
WP["Workflow Planning\nIN PROGRESS"]
|
||||
AD["Application Design\nEXECUTE"]
|
||||
UG["Units Generation\nEXECUTE"]
|
||||
end
|
||||
|
||||
subgraph CONSTRUCTION["🟢 CONSTRUCTION PHASE — Per Unit"]
|
||||
FD["Functional Design\nEXECUTE"]
|
||||
NFRA["NFR Requirements\nEXECUTE"]
|
||||
NFRD["NFR Design\nEXECUTE"]
|
||||
ID["Infrastructure Design\nSKIPPED"]
|
||||
CG["Code Generation\nEXECUTE"]
|
||||
BT["Build and Test\nEXECUTE"]
|
||||
end
|
||||
|
||||
subgraph OPERATIONS["🟡 OPERATIONS PHASE"]
|
||||
OPS["Operations\nPLACEHOLDER"]
|
||||
end
|
||||
|
||||
Start --> WD --> RA --> WP --> AD --> UG
|
||||
UG --> FD --> NFRA --> NFRD --> CG
|
||||
ID -.->|skipped| CG
|
||||
CG -->|repeat per unit| FD
|
||||
CG --> BT --> OPS --> End(["Complete"])
|
||||
|
||||
style WD fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style RA fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style WP fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style CG fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style BT fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
|
||||
style RE fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray:5 5,color:#000
|
||||
style US fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray:5 5,color:#000
|
||||
style ID fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray:5 5,color:#000
|
||||
style AD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||
style UG fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||
style FD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||
style NFRA fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||
style NFRD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray:5 5,color:#000
|
||||
style OPS fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray:5 5,color:#000
|
||||
style Start fill:#CE93D8,stroke:#6A1B9A,stroke-width:3px,color:#000
|
||||
style End fill:#CE93D8,stroke:#6A1B9A,stroke-width:3px,color:#000
|
||||
style INCEPTION fill:#BBDEFB,stroke:#1565C0,stroke-width:3px,color:#000
|
||||
style CONSTRUCTION fill:#C8E6C9,stroke:#2E7D32,stroke-width:3px,color:#000
|
||||
style OPERATIONS fill:#FFF59D,stroke:#F57F17,stroke-width:3px,color:#000
|
||||
linkStyle default stroke:#333,stroke-width:2px
|
||||
```
|
||||
|
||||
Text alternative: Inception (WD→RA→WP→AD→UG completed/executing), Construction per-unit loop (FD→NFR Req→NFR Design→CodeGen, Infrastructure skipped), Build & Test, Operations placeholder.
|
||||
|
||||
---
|
||||
|
||||
## Phases to Execute
|
||||
|
||||
### 🔵 INCEPTION PHASE
|
||||
- [x] Workspace Detection — COMPLETED
|
||||
- [x] Reverse Engineering — SKIPPED (shared artifacts already exist in `aidlc-docs/_shared/`)
|
||||
- [x] Requirements Analysis — COMPLETED
|
||||
- [ ] User Stories — **SKIP**
|
||||
- *Rationale*: Feature is owner-operated and technical in nature. Requirements are clear and detailed. No multiple personas or acceptance criteria gaps.
|
||||
- [x] Workflow Planning — IN PROGRESS
|
||||
- [ ] Application Design — **EXECUTE**
|
||||
- *Rationale*: New module project, new services, new controller, background service, new frontend components — all need component definition and dependency mapping before code generation.
|
||||
- [ ] Units Generation — **EXECUTE**
|
||||
- *Rationale*: 4 distinct units spanning backend (master), backend (slave extension), frontend, and documentation. Sequencing and dependencies must be planned.
|
||||
|
||||
### 🟢 CONSTRUCTION PHASE (per unit)
|
||||
- [ ] Functional Design — **EXECUTE**
|
||||
- *Rationale*: Complex business logic per unit (registration handshake, two-phase middleware, background integrity check, cache + fallback)
|
||||
- [ ] NFR Requirements — **EXECUTE**
|
||||
- *Rationale*: New security concerns (API key handling), caching strategy, fail-open requirements, test coverage targets
|
||||
- [ ] NFR Design — **EXECUTE**
|
||||
- *Rationale*: Design patterns for background service, per-module DbContext, middleware extension, client-side caching
|
||||
- [ ] Infrastructure Design — **SKIP**
|
||||
- *Rationale*: No new cloud/infrastructure resources. Same deployment model (single .NET process + React SPA). Module registration is code-level, not infrastructure-level.
|
||||
- [ ] Code Generation — **EXECUTE** (always)
|
||||
- [ ] Build and Test — **EXECUTE** (always)
|
||||
|
||||
### 🟡 OPERATIONS PHASE
|
||||
- [ ] Operations — PLACEHOLDER
|
||||
|
||||
---
|
||||
|
||||
## Unit Decomposition (Proposed)
|
||||
|
||||
| # | Unit Name | Scope | Depends On |
|
||||
|---|-----------|-------|------------|
|
||||
| 1 | master-backend | New `SlpModularCms.Modules.Master` project: `CmsInstance` entity, `MasterDbContext`, migrations, `CmsInstanceService`, `MasterController`, `IntegrityCheckBackgroundService`, `MasterModule : IModule`, test project | Core |
|
||||
| 2 | slave-availability-extension | Extended `SlpModularCms.Modules.Availability`: `MasterRegistration` entity, slave `MasterDbContext`, migrations, `MasterAvailabilityService` (pull/cache/fallback), registration endpoint, two-phase `AvailabilityMiddleware` | Unit 1 (API contract) |
|
||||
| 3 | frontend-cms-page | `/cms` page: `CmsInstanceList`, `AddCmsInstanceDialog`, `SetStatusDialog`, new TanStack Query hooks, API types | Unit 1 (REST API) |
|
||||
| 4 | documentation | Update `README.md` (migrations section, module guide, prod env vars), replace `frontend/README.md` | Units 1–3 (documents final patterns) |
|
||||
|
||||
---
|
||||
|
||||
## Package Change Sequence
|
||||
|
||||
```
|
||||
SlpModularCms.Core ← no changes (CmsInstance owned by Modules.Master)
|
||||
↓
|
||||
SlpModularCms.Modules.Master [Unit 1] ← new project
|
||||
↓
|
||||
SlpModularCms.Modules.Availability [Unit 2] ← extended
|
||||
↓
|
||||
SlpModularCms.Api ← registers new Master module
|
||||
↓
|
||||
frontend/ [Unit 3] ← CMS page updated
|
||||
↓
|
||||
README.md / frontend/README.md [Unit 4] ← documentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
- **Primary Goal**: Owner on the Master CMS can register slave CMSes and toggle their availability; slaves enforce the master-controlled status with a two-phase check
|
||||
- **Key Deliverables**: `SlpModularCms.Modules.Master` project, extended Availability module, updated `/cms` frontend page, updated documentation
|
||||
- **Quality Gates**: ≥80% test coverage on new backend code; fail-open behavior verified; API key not exposed in list responses; two-phase middleware verified for all status combinations
|
||||
@@ -0,0 +1,12 @@
|
||||
# Language Preference
|
||||
|
||||
All documentation artifacts (requirements, designs, plans, code comments, etc.) will be written in **English** by default. Questions, prompts, and AI responses will be in your language.
|
||||
|
||||
Would you like to change this?
|
||||
|
||||
A) English for documentation, your language for conversation (default)
|
||||
B) English for everything (documentation and conversation)
|
||||
C) My language for everything (documentation and conversation)
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: A
|
||||
@@ -0,0 +1,80 @@
|
||||
# Unit of Work Plan — Master CMS Module
|
||||
|
||||
## Overview
|
||||
|
||||
The four units are pre-established from the execution plan and confirmed by the Application Design stage. This plan validates the decomposition and generates the formal unit artifacts.
|
||||
|
||||
**Pre-established units:**
|
||||
|
||||
| # | Unit | Scope | Depends On |
|
||||
|---|------|-------|------------|
|
||||
| 1 | `master-backend` | New `SlpModularCms.Modules.Master` project | Core |
|
||||
| 2 | `slave-availability-extension` | Extended `SlpModularCms.Modules.Availability` | Unit 1 (API contract + ISlaveApiClient) |
|
||||
| 3 | `frontend-cms-page` | `/cms` page in `frontend/` | Unit 1 (REST API endpoints) |
|
||||
| 4 | `documentation` | README updates | Units 1–3 (documents final patterns) |
|
||||
|
||||
---
|
||||
|
||||
## Decomposition Questions
|
||||
|
||||
Answer each question by filling in your choice after the `[Answer]:` tag.
|
||||
|
||||
---
|
||||
|
||||
### Q1 — Construction Cycle for Unit 4 (Documentation)
|
||||
|
||||
Unit 4 covers README.md updates — no new code, entities, or services. How should it be handled in the Construction phase?
|
||||
|
||||
A) **Full cycle** — run Functional Design, NFR Requirements, NFR Design, and Code Generation for Unit 4 as for the other units. Consistent process; documentation gets explicit design attention.
|
||||
|
||||
B) **Code Generation only** — skip Functional Design, NFR Requirements, and NFR Design for Unit 4; go straight to Code Generation (which in this case means drafting the README content). More efficient for a documentation-only unit.
|
||||
|
||||
C) Other
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
### Q2 — Test Project for Unit 1
|
||||
|
||||
Unit 1 adds `SlpModularCms.Modules.Master` — a new project. How should tests be organized?
|
||||
|
||||
A) **New `SlpModularCms.Modules.Master.Tests` project** — separate test project for the new module, parallel to the existing `SlpModularCms.Modules.Availability.Tests`. Clean isolation; follows existing pattern.
|
||||
|
||||
B) **Single shared test project** — add master module tests to an existing test project to avoid creating a new project.
|
||||
|
||||
C) Other
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
### Q3 — Test Project for Unit 2
|
||||
|
||||
Unit 2 extends `SlpModularCms.Modules.Availability`. How should new slave-side tests be organized?
|
||||
|
||||
A) **Extend existing `SlpModularCms.Modules.Availability.Tests`** — add new test files for `MasterAvailabilityService`, extended `AvailabilityMiddleware`, and `AvailabilityController` registration endpoint. Minimal new project overhead.
|
||||
|
||||
B) **New `SlpModularCms.Modules.Availability.Master.Tests`** — separate test project for the master-related slave-side extensions. Cleaner isolation for cross-unit changes.
|
||||
|
||||
C) Other
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
## Execution Steps
|
||||
|
||||
After all questions above are answered, the following artifacts will be generated:
|
||||
|
||||
- [x] **Step 1** — Analyze all answers; flag any ambiguities
|
||||
- [x] **Step 2** — Generate `unit-of-work.md` with unit definitions, responsibilities, and construction cycle per unit
|
||||
- [x] **Step 3** — Generate `unit-of-work-dependency.md` with dependency matrix and sequencing
|
||||
- [x] **Step 4** — Generate `unit-of-work-story-map.md` (requirement-to-unit mapping; no user stories in this feature)
|
||||
- [x] **Step 5** — Validate unit boundaries and completeness
|
||||
- [x] **Step 6** — Update `aidlc-state.md` to mark Units Generation as complete
|
||||
- [x] **Step 7** — Present completion message for user approval
|
||||
|
||||
---
|
||||
|
||||
*Artifact path*: `aidlc-docs/features/master-cms-module/inception/plans/unit-of-work-plan.md`
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
# Requirements Clarification Questions — Master CMS Module (Round 2)
|
||||
|
||||
Your answers were very clear on the overall model. A few follow-up questions to resolve remaining ambiguities before generating the requirements document.
|
||||
|
||||
---
|
||||
|
||||
## Clarification 1 — How does the slave CMS know the Master's URL?
|
||||
|
||||
Your Q4 answer says the client cannot configure this. But the slave still needs to know where to pull availability status from.
|
||||
|
||||
How is the Master URL configured on the slave CMS?
|
||||
|
||||
A) Developer sets it in the slave's `appsettings.json` at deployment time (e.g., `MasterModule:MasterUrl`) — clients see the config file but cannot change it via the UI
|
||||
B) Environment variable only — completely invisible to the client in normal deployments
|
||||
C) The Master registers itself with the slave at first connection — no manual config needed on slave side
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: C, but I want something to prevent the client from changing the url and this the slave being unable to contact the master. So the master should be able to check whether everything is still stored correctly. We might need an extra service that runs as a cronjob or something similar.
|
||||
|
||||
---
|
||||
|
||||
## Clarification 2 — Authentication between Master and Slave
|
||||
|
||||
When the Master calls the slave's `PUT /api/availability/status` to set its availability, how does it authenticate?
|
||||
|
||||
A) The API key stored in the Master's `CmsInstance` record is sent as a header — the slave validates it as a special "master key"
|
||||
B) The Master uses a JWT token from the slave (Owner account credentials stored in the Master's DB)
|
||||
C) The slave exposes a separate unauthenticated (but secret-URL-protected) internal endpoint for this
|
||||
D) A shared API key configured in both Master and slave `appsettings.json`
|
||||
E) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## Clarification 3 — Slave Availability Cache Duration
|
||||
|
||||
Your Q3 answer mentions the slave caches the Master's availability status to reduce traffic. How long should this cache be valid?
|
||||
|
||||
A) Configurable — set in the slave's `appsettings.json` (e.g., `MasterModule:CacheMinutes: 60`)
|
||||
B) Fixed at a reasonable default (e.g., 60 minutes) — no configuration needed
|
||||
C) Session-based — re-check when the application restarts or a specific event occurs
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: A, but B as a fallback.
|
||||
|
||||
---
|
||||
|
||||
## Clarification 4 — Slave's Fallback Behavior When Master is Unreachable
|
||||
|
||||
Your Q3 answer says: if the Master is unavailable, the slave falls back to its own DB value. What should the default value in the slave DB be before the Master has ever contacted the slave?
|
||||
|
||||
A) `Available` — default to open; the Master will disable it if needed
|
||||
B) `NotAvailable` — default to closed; the Master must explicitly enable it after registration
|
||||
C) Configurable per slave registration in the Master's `CmsInstance` record
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: A, this whole feature is a safety measure, but it should not block the client from doing anything when something doesn't work correctly.
|
||||
|
||||
---
|
||||
|
||||
## Clarification 5 — Master UI: Adding / Removing Slave CMSes
|
||||
|
||||
On the `/cms` page the Owner manages slave CMS registrations. What actions should be available?
|
||||
|
||||
A) Add (name + URL + API key), view list with current status, toggle Available/NotAvailable, remove
|
||||
B) Add (name + URL + API key), view list with status — no remove (registrations are permanent)
|
||||
C) Full CRUD: add, edit (name/URL/key), view list with status, toggle, remove
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: B, but also option to set status Available/NotAvailable/Inactive where Inactive makes it greyed out meaning the CMS is no longer used. Also the option to set a message when the master disables a slave. That should be mandatory.
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
# Requirements Clarification Questions — Master CMS Module
|
||||
|
||||
Please answer each question by filling in the letter choice after the `[Answer]:` tag.
|
||||
If none of the options match your needs, choose the last option (Other) and describe your preference.
|
||||
|
||||
---
|
||||
|
||||
## Question 1
|
||||
The CMS page (`/cms`) currently exists in the frontend but shows no content. What should the Master Module display on this page?
|
||||
|
||||
A) A list of registered slave CMS instances with their current availability status and controls to enable/disable them
|
||||
B) A dashboard combining both slave CMS management and module configuration (e.g., toggle Master Module on/off)
|
||||
C) Only module configuration — the slave CMS list is managed elsewhere (e.g., a separate admin API)
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## Question 2
|
||||
What are "other CMSes" in this context? How should slave CMS instances be registered with the Master?
|
||||
|
||||
A) Other deployed instances of the same SlpModularCms application — registered via URL + API key in the Master's database
|
||||
B) Abstract "tenants" or "sites" stored in the Master's database — not necessarily running SlpModularCms
|
||||
C) The slave CMSes share the same database as the Master — just different data rows (multi-tenant, single DB)
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## Question 3
|
||||
When the Master Module sets a slave CMS as "unavailable", how does the slave CMS enforce this?
|
||||
|
||||
A) The slave CMS calls the Master's API on every request to check if it is still available (pull model)
|
||||
B) The Master pushes availability status to the slave CMS (webhook / push model)
|
||||
C) The slave CMS and Master share a database — the slave reads the Master-controlled status directly from a shared table
|
||||
D) The Master sets availability via the slave's own availability API endpoint (the existing `PUT /api/availability/status`)
|
||||
E) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: E, probably a combination of A and D. For context: As a developer I have clients using the slave CMSes. In the event the client doesn't pay or violate another agreement I want to have the possibility to disable the slave CMS. To prevent the client from circumventing this by editing a value in their own database I want it to pull it from the master. But in the event the master is unavailable it is also stored in their own DB. The slave CMS can also just check their own value to prevent excessive traffic to the master and just get the status only a few times or once per session within a few hours for example.
|
||||
|
||||
---
|
||||
|
||||
## Question 4
|
||||
The existing availability middleware currently checks a local `IAvailabilityService`. How should the middleware on a slave CMS know to consult the Master instead of its local service?
|
||||
|
||||
A) A new configuration flag in `appsettings.json` (e.g., `MasterModule:MasterUrl`) — if set, the slave uses the Master; if not set, local check
|
||||
B) The Master Module is installed on the slave CMS too but in "slave mode" — it overrides the local availability service
|
||||
C) A separate middleware or service replaces the existing one when a Master URL is configured
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: D, The availability check should be hardcoded to check the master if the master module is enabled, but it should also keep sits current functionality. The master module should be enabled by the owner of the master CMS and not by the client. The client should not be able to configure this.
|
||||
|
||||
---
|
||||
|
||||
## Question 5
|
||||
The CMS that has the Master Module enabled should be exempt from the external availability check. How should this exemption be implemented?
|
||||
|
||||
A) Configuration-based: a flag in `appsettings.json` (e.g., `MasterModule:IsMaster: true`) bypasses the external check entirely
|
||||
B) Auto-detected: if the Master Module is registered and active, skip the external check automatically
|
||||
C) The Master CMS still has a local availability check (its own `IAvailabilityService`) but ignores external Master checks
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: B + C
|
||||
|
||||
---
|
||||
|
||||
## Question 6
|
||||
Which roles can use the Master Module features (viewing/managing slave CMSes)?
|
||||
|
||||
A) Owner only
|
||||
B) Owner and Administrator
|
||||
C) All authenticated users can view; only Owner can modify
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## Question 7
|
||||
Does the Master Module require changes to the existing availability middleware (`AvailabilityMiddleware`) in the `Availability` module, or should it be implemented as a new separate module?
|
||||
|
||||
A) Extend the existing Availability module — add Master Module logic there
|
||||
B) Create a new separate module `SlpModularCms.Modules.Master` that works alongside the Availability module
|
||||
C) Create a new module that REPLACES the Availability module on Master CMS instances
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: B, it should be a separate module, because only the master CMS should install the module. The slaves should not have the module installed and should still be able to use the current functionality.
|
||||
|
||||
---
|
||||
|
||||
## Question 8
|
||||
Should the Master Module include a new database entity to store slave CMS registrations (name, URL, current availability status), or reuse an existing entity?
|
||||
|
||||
A) Yes — new `CmsInstance` entity in the database (name, URL, status, last updated)
|
||||
B) New entity but only in-memory / configuration — no database persistence for slave CMSes
|
||||
C) Reuse the existing `GlobalAvailabilityState` concept, extended with a multi-tenant key
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## Question 9
|
||||
For the frontend: the `/cms` route is currently restricted to the `Owner` role. Should this change?
|
||||
|
||||
A) No change — keep `/cms` Owner-only
|
||||
B) Allow `Administrator` role as well (read-only or full access)
|
||||
C) Allow any authenticated user to view, but restrict modifications to Owner
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## Question 10
|
||||
Should the Master Module be part of this same codebase (monorepo), or is it a separate deployment concern?
|
||||
|
||||
A) Same codebase — a new project `SlpModularCms.Modules.Master` within the existing solution
|
||||
B) Same codebase AND the frontend changes to show the Master UI are included in this feature
|
||||
C) Backend only — frontend changes are a separate follow-up feature
|
||||
D) Other (please describe after [Answer]: tag below)
|
||||
|
||||
[Answer]: B
|
||||
@@ -0,0 +1,169 @@
|
||||
# Requirements — Master CMS Module
|
||||
|
||||
## Intent Analysis
|
||||
|
||||
- **User Request**: Add a new "Master" module that fills the `/cms` page, allows the owner to manage the availability of registered slave CMS instances, and integrates with the existing availability check mechanism — while exempting the Master CMS itself from external checks.
|
||||
- **Request Type**: New Feature
|
||||
- **Scope Estimate**: Multiple Components — new backend module, new frontend UI, modifications to the existing availability flow on slave CMS instances
|
||||
- **Complexity Estimate**: Complex — involves a Master/Slave network topology, bi-directional registration, background integrity checks, caching, fallback logic, and API key authentication
|
||||
|
||||
---
|
||||
|
||||
## System Context
|
||||
|
||||
The existing system (`SlpModularCms`) is a modular ASP.NET Core CMS. The Availability module manages system status via `IAvailabilityService` and `AvailabilityMiddleware`. Slave CMSes are other deployed instances of the same SlpModularCms application. The Master Module adds a control plane layer on top of the existing availability system.
|
||||
|
||||
---
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### FR-MASTER-01 — New Module: `SlpModularCms.Modules.Master`
|
||||
A new separate module `SlpModularCms.Modules.Master` is created within the existing solution. This module is installed **only on the Master CMS**. Slave CMS instances do **not** install this module. The module registers its own services and HTTP endpoints following the existing `IModule` pattern.
|
||||
|
||||
### FR-MASTER-02 — `CmsInstance` Entity
|
||||
A new `CmsInstance` database entity is added to `SlpModularCms.Core` with the following fields:
|
||||
- `Id` (Guid, PK)
|
||||
- `Name` (string, required) — friendly name for the slave CMS
|
||||
- `Url` (string, required) — base URL of the slave CMS API
|
||||
- `ApiKey` (string, required) — secret key used by the Master to authenticate against the slave
|
||||
- `Status` (enum: `Available` / `NotAvailable` / `Inactive`) — the master-controlled availability state
|
||||
- `DisableMessage` (string, nullable) — required when `Status = NotAvailable`; shown to end-users of the slave
|
||||
- `LastContactedAt` (DateTimeOffset, nullable) — timestamp of the last successful master → slave contact
|
||||
- `LastStatusPushedAt` (DateTimeOffset, nullable) — timestamp of the last status push to the slave
|
||||
|
||||
### FR-MASTER-03 — Auto-Registration: Master Registers Itself with Slave
|
||||
When a slave is added to the Master's `CmsInstance` registry, the Master automatically contacts the slave and registers itself (pushes its own URL). The slave stores the master's URL in its local database. No manual configuration is required on the slave side to know the Master URL.
|
||||
|
||||
The slave exposes a dedicated internal registration endpoint (`POST /api/internal/master/register`) that accepts the Master's URL and API key. After successful registration, the slave uses the stored Master URL for all future availability pulls.
|
||||
|
||||
### FR-MASTER-04 — Integrity Check Service (Background Service)
|
||||
A background service (periodic, configurable interval) runs on the Master CMS and periodically re-verifies that each registered slave still has the correct Master URL stored. If a slave's registered master does not match, the Master re-pushes its registration to that slave. This prevents clients from circumventing the master by removing or altering the stored master URL.
|
||||
|
||||
### FR-MASTER-05 — Status Push: Master Sets Slave Availability
|
||||
When the Master owner changes a slave's status (to `Available` or `NotAvailable`), the Master immediately calls the slave's existing `PUT /api/availability/status` endpoint (or a dedicated internal endpoint) to push the new status. Authentication uses the `ApiKey` from the `CmsInstance` record, sent as an `X-Master-Api-Key` header.
|
||||
|
||||
### FR-MASTER-06 — Slave Pull Model: Periodic Master Check
|
||||
The slave CMS periodically pulls its availability status from the Master. The pull interval is configurable in the slave's `appsettings.json` (`MasterModule:CacheMinutes`). If this value is not configured, a default of **60 minutes** is used. The pulled status is cached locally in memory.
|
||||
|
||||
### FR-MASTER-07 — Slave Fallback Behavior
|
||||
If the Master CMS is unreachable when the slave attempts a pull:
|
||||
- The slave falls back to the value currently stored in its **local database** (`GlobalAvailabilityState`).
|
||||
- The local database value defaults to `Available` before any Master contact has occurred.
|
||||
- This ensures the feature is a **fail-open** safety measure — clients are not blocked if the Master is down.
|
||||
|
||||
### FR-MASTER-08 — Slave Availability Middleware Integration
|
||||
On a slave CMS, the availability check becomes a **two-phase gate**. The existing `IAvailabilityService` behavior is preserved — the Master adds an additional outer gate, not a replacement.
|
||||
|
||||
**Check order:**
|
||||
1. **Master gate** (outer): If a Master URL is registered in the slave's DB, check the Master-sourced cached status (refreshed per FR-MASTER-06).
|
||||
- If Master status = `NotAvailable` → block all requests. Only the frontend dashboard route is accessible so that users can see the availability widget with the `DisableMessage`.
|
||||
- If Master is unreachable → fall back to locally stored Master status (last known value; default `Available`).
|
||||
- If no Master registered → skip Master gate entirely.
|
||||
2. **Local gate** (inner): If the Master gate passes (status = `Available` or no Master registered), apply the existing `IAvailabilityService` check as it works today (local `Available` / `Maintenance` / `NotAvailable` logic, Owner/Admin bypass, etc.).
|
||||
|
||||
This means: when the Master sets a slave to `Available`, all existing local availability behavior continues unchanged. When the Master sets `NotAvailable`, the local gate is never reached.
|
||||
|
||||
The `DisableMessage` from the Master is included in the `503 Service Unavailable` response body and delivered to the slave's frontend availability widget.
|
||||
|
||||
### FR-MASTER-09 — Master CMS Exemption
|
||||
When the Master Module is installed and active on a CMS instance, that CMS is **automatically exempt** from the external Master availability check. It does not pull status from any Master. The Master CMS retains and uses its own local `IAvailabilityService` check (existing behavior unchanged).
|
||||
|
||||
### FR-MASTER-10 — Role Access Control
|
||||
All Master Module management features (viewing and modifying slave CMS registrations) are restricted to the **Owner** role only. This applies to both the backend API endpoints and the frontend `/cms` page.
|
||||
|
||||
### FR-MASTER-11 — `/cms` Page: Slave CMS List
|
||||
The frontend `/cms` page displays a list of all registered slave CMSes. For each slave, the following is shown:
|
||||
- Name
|
||||
- URL
|
||||
- Current status badge (`Available` / `NotAvailable` / `Inactive`)
|
||||
- Last contacted timestamp
|
||||
- Disable message (if status is `NotAvailable`)
|
||||
|
||||
`Inactive` entries are visually greyed out to indicate they are no longer in active use.
|
||||
|
||||
### FR-MASTER-12 — `/cms` Page: Add Slave CMS
|
||||
The Owner can add a new slave CMS registration by providing:
|
||||
- Name (required)
|
||||
- URL (required)
|
||||
- API key (required)
|
||||
|
||||
On save, the Master immediately attempts to register itself with the slave (FR-MASTER-03) and stores the result.
|
||||
|
||||
### FR-MASTER-13 — `/cms` Page: Set Slave Status
|
||||
The Owner can change the status of a registered slave CMS to:
|
||||
- `Available` — slave is enabled (normal operation)
|
||||
- `NotAvailable` — slave is disabled; a **mandatory disable message** must be provided
|
||||
- `Inactive` — slave is greyed out in the UI; no availability enforcement is applied (the Master does not contact the slave)
|
||||
|
||||
Slave registrations cannot be deleted; setting to `Inactive` is the "soft removal" mechanism.
|
||||
|
||||
### FR-MASTER-14 — Mandatory Disable Message
|
||||
When the Owner sets a slave's status to `NotAvailable`, a non-empty `DisableMessage` is required. This message is pushed to the slave together with the status change and included in the slave's 503 error response to end-users.
|
||||
|
||||
### FR-MASTER-15 — Project Documentation
|
||||
|
||||
The root `README.md` already exists and covers project structure, dev setup, authentication, frontend development, database migrations, adding a module, and production setup. The following sections need to be updated or added to reflect this feature.
|
||||
|
||||
**Updated: `README.md` — "Database Migraties" section**
|
||||
The current section only covers migrations in `SlpModularCms.Core`. It must be updated to document the per-module migration pattern (NFR-MASTER-06):
|
||||
- How to add a migration for a specific module (using `--project src\SlpModularCms.Modules.<Name>`)
|
||||
- How to apply module-specific migrations
|
||||
- Note that each module owns its own tables and migrations
|
||||
|
||||
**Updated: `README.md` — "Nieuwe Module Toevoegen" section**
|
||||
Step 3 (`IModule implementeren`) must be extended to document the optional per-module `DbContext` pattern introduced by the Master Module:
|
||||
- How to add a module-specific `DbContext`
|
||||
- How to register it at startup via `RegisterServices`
|
||||
- How to apply its migrations in `UseModule`
|
||||
|
||||
**Updated: `README.md` — "Productie Setup" section**
|
||||
The environment variables list must be extended with the new `MasterModule:` keys:
|
||||
- `MasterModule__CacheMinutes` (slave instances only)
|
||||
- `MasterModule__IntegrityCheckIntervalMinutes` (master instance only)
|
||||
|
||||
**Updated: `frontend/README.md`**
|
||||
Currently contains Vite template boilerplate. Replace with project-specific frontend developer documentation (prerequisites, scripts, environment variables). The root README already covers most of this — the frontend README can be a brief pointer to the root README plus frontend-specific notes.
|
||||
|
||||
---
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
### NFR-MASTER-01 — Fail-Open Safety
|
||||
The entire Master Module is designed as a safety measure, not a blocker. If any part of the Master → Slave communication fails (network error, timeout, misconfiguration), the slave must continue serving requests using its local fallback. End-users must never be blocked solely because the Master is unreachable.
|
||||
|
||||
### NFR-MASTER-02 — Configurable Cache Interval
|
||||
The slave pull interval is configurable via `appsettings.json` (`MasterModule:CacheMinutes`). Default: 60 minutes. This follows the existing `dotnet-appsettings` pattern used in this project.
|
||||
|
||||
### NFR-MASTER-03 — API Key Security
|
||||
The `ApiKey` stored in `CmsInstance` is a secret token used for Master → Slave authentication. It must not be exposed in API list responses. The slave validates the `X-Master-Api-Key` header on all Master-initiated requests.
|
||||
|
||||
### NFR-MASTER-04 — Background Service Interval
|
||||
The Master's integrity check background service interval is configurable in Master CMS `appsettings.json` (`MasterModule:IntegrityCheckIntervalMinutes`). Default: 60 minutes.
|
||||
|
||||
### NFR-MASTER-05 — Test Coverage
|
||||
New backend code must follow the existing test coverage standard (≥ 80%). New services and controllers in `SlpModularCms.Modules.Master` and any slave-side extensions require unit tests.
|
||||
|
||||
### NFR-MASTER-06 — Per-Module Database Migrations
|
||||
Each module manages its own database schema through a module-specific EF Core `DbContext` and a dedicated migrations assembly within that module's project. This ensures that tables belonging to a module are only created on CMS instances where that module is installed:
|
||||
|
||||
- `SlpModularCms.Modules.Master` owns the `CmsInstances` table → migrations live in `Modules.Master`
|
||||
- Slave-side tables (e.g., stored Master registration URL) belong to the module or service that introduces them → migrations live in the corresponding project
|
||||
- `SlpModularCms.Core` retains only the shared/core entities (users, roles, tokens, `GlobalAvailabilityState`)
|
||||
- Each module's `Configure(IApplicationBuilder)` method applies its own pending EF Core migrations at startup
|
||||
- A migration is created for every discrete schema change (one migration per logical change, not batched)
|
||||
|
||||
---
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
| In Scope | Out of Scope |
|
||||
|----------|-------------|
|
||||
| New `SlpModularCms.Modules.Master` project | Slave module as a separate installable package |
|
||||
| Frontend `/cms` page with slave management UI | Multi-level master hierarchy (master of masters) |
|
||||
| Slave-side auto-registration endpoint | Real-time push notifications to slave (WebSockets/SignalR) |
|
||||
| Slave-side availability pull + cache + fallback | Authentication delegation (SSO between master and slave) |
|
||||
| Master integrity check background service | Slave removal / permanent delete |
|
||||
| `CmsInstance` entity in `SlpModularCms.Modules.Master` (own DbContext + migrations) | |
|
||||
| Frontend changes included in this feature | |
|
||||
| Updated `README.md` (migrations + module guide + prod env vars) | Full README rewrite |
|
||||
| Updated `frontend/README.md` (project-specific content) | |
|
||||
Reference in New Issue
Block a user