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>
9.8 KiB
Components — Master CMS Module
⚠️ Partially superseded, found stale 2026-07-04: the slave-side
IMasterAvailabilityServicedescription below (static_cachedStatus/_lastFetchedAt,CacheMinutes-based staleness,/api/internal/master/*routes) describes an inception-stage design that construction did not build as-is — the actual implementation is push-based (/api/v1/master/*,_masterIsAvailable/_masterDisableMessage), plus a 2026-07-04 addition of a differently-shaped slave-pull (MasterStatusPollingBackgroundService+GET /api/v1/SlaveStatus, time-based fail-open). The Unit 1 (master-backend) component list above the slave-side section is accurate. Seeconstruction/master-backend/functional-design/*.mdandconstruction/slave-availability-extension/functional-design/*.mdfor the as-built design. This gap predates 2026-07-04 and was found (not caused) during today's documentation audit.
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
MasterDbContextmigrations at startup viaUseModule; 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
CmsInstancestable and its migrations; migrations live inSlpModularCms.Modules.Master(NFR-MASTER-06) - Entities owned:
CmsInstance
CmsInstance
- Type: Domain Entity
- Responsibilities: Represents a registered slave CMS instance
- Fields:
Id—Guid, primary keyName—string, friendly display nameUrl—string, base URL of slave CMS APIApiKey—string, secret used by Master to authenticate against slave; never returned in API responses (NFR-MASTER-03)Status—CmsInstanceStatusenum (Available/NotAvailable/Inactive)DisableMessage—string?, required whenStatus = NotAvailableLastContactedAt—DateTimeOffset?LastStatusPushedAt—DateTimeOffset?
CmsInstanceStatus
- Type: Enum
- Values:
Available,NotAvailable,Inactive
ICmsInstanceRepository / CmsInstanceRepository
- Type: Repository (data access only)
- Responsibilities: CRUD operations on
CmsInstanceviaMasterDbContext; no business logic - Lifetime: Scoped
ICmsInstanceService / CmsInstanceService
- Type: Service (orchestration)
- Responsibilities: Business orchestration — calls repository for data access; calls
ISlaveApiClientfor HTTP side-effects (auto-registration, status push, integrity verification); enforces business rules (e.g.,DisableMessagerequired whenNotAvailable) - Lifetime: Scoped
ISlaveApiClient / SlaveApiClient
- Type: Typed HTTP client
- Responsibilities: All Master → Slave HTTP communication (registration, status push, integrity check); adds
X-Master-Api-Keyheader; 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 toICmsInstanceService - 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; injectsIServiceScopeFactoryto resolve scopedICmsInstanceServiceper 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 keyMasterUrl—string, base URL of the Master CMSRegisteredAt—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
MasterRegistrationstable; migrations live inSlpModularCms.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; respectsMasterModuleOptions.CacheMinutes - Cache pattern: Static fields
_cachedStatus(defaultAvailable) +_lastFetchedAt; stale check based onCacheMinutes - Exemption (FR-MASTER-09): No special exemption logic needed — if no
MasterRegistrationrecord 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:- Master gate — calls
IMasterAvailabilityService.GetMasterStatusAsync(); if no master registered → skip to local gate; if master saysNotAvailable→ 503 withDisableMessage; if unreachable → use cached/fallback value (fail-open) - Local gate — existing
IAvailabilityServicecheck, unchanged
- Master gate — calls
- 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
RegisterMasteradded; validatesX-Master-Api-Keyheader against configuredMasterModuleOptions.ApiKey; upsertsMasterRegistrationinAvailabilityDbContext - 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; rendersCmsInstanceList; manages dialog open state for Add and Set Status actions
CmsInstanceList
- Type: React component
- Responsibilities: Renders a table of
CmsInstanceitems; shows Name, URL, Status badge, LastContactedAt, DisableMessage;Inactiverows 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
useAddCmsInstancemutation; closes on success
SetStatusDialog
- Type: React component (modal dialog using shadcn/ui
Dialog) - Responsibilities: Status dropdown (
Available,NotAvailable,Inactive);DisableMessagetext field rendered and required when status isNotAvailable; callsuseUpdateCmsInstanceStatusmutation; 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
useQueryhook - File:
hooks/useCmsInstances.ts - Responsibilities: GET
/api/v1/CmsInstances; returns list ofCmsInstance
useAddCmsInstance
- Type: TanStack Query
useMutationhook - File:
hooks/useAddCmsInstance.ts - Responsibilities: POST
/api/v1/CmsInstances; invalidatesuseCmsInstancesquery on success
useUpdateCmsInstanceStatus
- Type: TanStack Query
useMutationhook - File:
hooks/useUpdateCmsInstanceStatus.ts - Responsibilities: PUT
/api/v1/CmsInstances/{id}/status; invalidatesuseCmsInstancesquery on success
Unit 4 — documentation
No new components. Covers README updates only (FR-MASTER-15). See requirements for scope.