Files
slp-modular-cms/aidlc-docs/features/master-cms-module/inception/application-design/services.md
T
SluijsensandClaude Sonnet 5 0447993181 Completes local-dev-master-slave-setup: dual-instance frontend tooling, module-capability gating, and master/slave protocol self-healing fixes
Frontend (Unit 2 completion): dual dev-server tooling (pnpm dev:slave,
pnpm dev:all), per-instance browser tab titles, and a backend
capability check (SystemController + useSystemCapabilities +
ModuleGuard) so a Master-only page is hidden on a slave instance
instead of assuming every backend has every module.

Master/slave protocol fixes surfaced by actually running master and
slave side by side locally:
- Deactivating a CMS instance (Inactive) now releases the slave's
  master gate instead of leaving it stuck on its last pushed status.
- The periodic integrity check now also re-pushes status to every
  reachable slave (previously URL-verification only) and runs once
  immediately on startup.
- Added the originally-specified (but never implemented) slave-pull
  path: a slave now periodically polls its own status from the master
  (GET /api/v1/SlaveStatus) and fails open to Available if the master
  is unreachable for too long, complementing the existing push.
- The slave's own Settings page can no longer "successfully" change
  local availability while the master controls it; it's now locked
  with an explanatory banner and the backend rejects the write with
  409 instead of silently no-op'ing it.
- CMS instance status badges now match the dashboard's color/icon
  styling instead of a plain grey badge.

Also corrected the master-cms-module design docs to match this
as-built behavior, and flagged (without a full rewrite) a larger,
pre-existing divergence between its inception-stage application
design and what construction actually built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 19:53:52 +02:00

236 lines
10 KiB
Markdown

# Services — Master CMS Module
> ⚠️ **Superseded, found stale 2026-07-04**: this document is the *inception-stage* design and describes an earlier pull-based `IMasterAvailabilityService` (static cache + `MasterModuleOptions.CacheMinutes` + `/api/internal/master/*` routes) that was **not what got built**. Construction pivoted to a push-based protocol instead (`/api/v1/master/*` routes, `_masterIsAvailable`/`_masterDisableMessage` fields, no `CacheMinutes`). A slave-pull mechanism was eventually added too, but on 2026-07-04 and with a different shape (`MasterStatusPollingBackgroundService` polling `GET /api/v1/SlaveStatus` on an interval, with a time-based fail-open) than what's described below. **Treat this file as historical intent, not current truth** — the accurate, as-built design lives in `construction/master-backend/functional-design/*.md` and `construction/slave-availability-extension/functional-design/*.md`. This divergence predates 2026-07-04 and was found (not caused) during today's documentation audit; `CacheMinutes`/`ApiKey` dead-config fallout is already tracked as TD-001 in `tech-debt-backlog`.
## 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.