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>
6.9 KiB
Domain Entities — Unit 1: master-backend
Entity Overview
graph TD
MasterDbCtx["MasterDbContext\n(per-module EF Core DbContext)"]
CmsInst["CmsInstance\n(aggregate root)"]
Status["CmsInstanceStatus\n(enum)"]
Opts["MasterModuleOptions\n(config POCO)"]
DP["IDataProtector\n(ApiKey encryption)"]
DTO["CmsInstanceDto\n(API response shape)"]
CreateReq["CreateCmsInstanceRequest\n(API input)"]
UpdateReq["UpdateStatusRequest\n(API input)"]
UpdateRes["UpdateStatusResult\n(API response for status update)"]
MasterDbCtx -->|"owns"| CmsInst
CmsInst -->|"has"| Status
CmsInst -->|"ApiKey encrypted via"| DP
CmsInst -->|"projected to"| DTO
CreateReq -->|"creates"| CmsInst
UpdateReq -->|"mutates status of"| CmsInst
UpdateRes -->|"returned from UpdateStatusAsync"| CmsInst
Opts -->|"IntegrityCheckIntervalMinutes"| MasterDbCtx
Opts -->|"MasterUrl fallback"| MasterDbCtx
classDef entity fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000
classDef infra fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000
classDef dto fill:#FFC107,stroke:#F57F17,stroke-width:1px,color:#000
classDef config fill:#CE93D8,stroke:#6A1B9A,stroke-width:1px,color:#000
class CmsInst entity
class MasterDbCtx,DP infra
class Status,DTO,CreateReq,UpdateReq,UpdateRes dto
class Opts config
Text alternative: MasterDbContext owns CmsInstance; CmsInstance has a Status enum and its ApiKey is encrypted via IDataProtector; request/response shapes map to and from CmsInstance.
CmsInstance
Table: CmsInstances (owned by MasterDbContext, migrations in SlpModularCms.Modules.Master)
| Field | Type | Nullable | Notes |
|---|---|---|---|
Id |
Guid |
No | Primary key |
Name |
string |
No | Friendly display name; required |
Url |
string |
No | Base URL of slave CMS API; must start with http:// or https:// |
ApiKey |
string |
No | Encrypted via ASP.NET Core Data Protection before storage; decrypted before HTTP calls |
Status |
CmsInstanceStatus |
No | Default: Available on creation |
DisableMessage |
string? |
Yes | Required when Status = NotAvailable; null otherwise |
LastContactedAt |
DateTimeOffset? |
Yes | Null = never successfully contacted; set on successful registration or integrity check |
LastStatusPushedAt |
DateTimeOffset? |
Yes | Null = status never successfully pushed; set after successful PushStatusAsync |
LastIntegrityCheckFailedAt |
DateTimeOffset? |
Yes | Null = no pending failure; set when integrity check cannot reach slave or re-registration fails; cleared on next successful contact |
CmsInstanceStatus
public enum CmsInstanceStatus
{
Available = 0,
NotAvailable = 1,
Inactive = 2,
}
| Value | Meaning | Master Contacts Slave? |
|---|---|---|
Available |
Slave is enabled; normal operation | Yes (status push + integrity checks) |
NotAvailable |
Slave is disabled; DisableMessage served to end-users |
Yes (status push + integrity checks) |
Inactive |
Soft-removed; greyed out in UI | On the transition into Inactive: one final push (Available, no message) to release the gate. Afterwards: No further contact — excluded from GetActiveAsync, so no more pushes/integrity checks/re-pushes |
Updated 2026-07-04: previously
Inactivemeant no HTTP contact at all, including on the transition itself — this left slaves stuck on their last pushed status after being detached. See BR-CONTACT-02 inbusiness-rules.md.
MasterModuleOptions
Config section: "MasterModule" in appsettings.json
| Property | Type | Default | Side | Notes |
|---|---|---|---|---|
IntegrityCheckIntervalMinutes |
int |
60 |
Master | Interval for IntegrityCheckBackgroundService |
MasterUrl |
string? |
null |
Master | Fallback public URL of this master CMS; used by background service when HttpContext is unavailable |
CacheMinutes |
int |
60 |
Slave | Slave pull cache interval (used by Unit 2) |
ApiKey |
string? |
null |
Slave | Slave API key for validating incoming master requests (used by Unit 2) |
CmsInstanceDto (API Response)
Rule: ApiKey is never included (NFR-MASTER-03).
| Property | Type | Notes |
|---|---|---|
Id |
Guid |
|
Name |
string |
|
Url |
string |
|
Status |
string |
Serialized as string ("Available" / "NotAvailable" / "Inactive") |
DisableMessage |
string? |
|
LastContactedAt |
DateTimeOffset? |
|
LastStatusPushedAt |
DateTimeOffset? |
|
LastIntegrityCheckFailedAt |
DateTimeOffset? |
Visible in UI so owner knows which slaves have pending check failures |
CreateCmsInstanceRequest (API Input)
| Property | Type | Validation |
|---|---|---|
Name |
string |
Required, non-empty |
Url |
string |
Required; must start with http:// or https:// |
ApiKey |
string |
Required, non-empty |
UpdateStatusRequest (API Input)
| Property | Type | Validation |
|---|---|---|
Status |
CmsInstanceStatus |
Required; must be valid enum value |
DisableMessage |
string? |
Required and non-empty when Status = NotAvailable; ignored otherwise |
UpdateStatusResult (Service Return / API Response)
| Property | Type | Notes |
|---|---|---|
Success |
bool |
Always true when status persisted to DB (DB is the authority) |
SlaveContactSuccess |
bool |
true if the HTTP push to the slave succeeded; false if it failed (slave unreachable). Applies to Inactive transitions too — reflects whether the gate-release push succeeded, not a hardcoded true |
Updated 2026-07-04:
SlaveContactSuccessforInactiveused to always be hardcodedtrue(no push happened, so nothing could fail) — now reflects the real result of the release push.
SlaveStatusPollResponse (API Response) — Added 2026-07-04
Response shape for the new slave-pull endpoint (GET /api/v1/SlaveStatus, SlaveStatusController), used by MasterStatusPollingBackgroundService on the slave side (see slave-availability-extension/functional-design/domain-entities.md). This is the counterpart of the push-based flow above — added to close a gap versus the original inception requirements (FR-MASTER-06/07), which specified a slave-initiated pull in addition to the push that construction actually implemented.
| Property | Type | Notes |
|---|---|---|
IsAvailable |
bool |
true when CmsInstance.Status == Available |
DisableMessage |
string? |
CmsInstance.DisableMessage |
Identified by matching the caller's plain API key (header X-Master-Api-Key) against each active CmsInstance's decrypted key — there is no separate slave-identity field, the shared key doubles as the credential. No [Authorize]/JWT on this endpoint.