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
|
||||
Reference in New Issue
Block a user