# 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()` - **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(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.