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>
4.2 KiB
Domain Entities — Unit 2: slave-availability-extension
Entity Relationship Diagram
graph TD
subgraph AvailabilityModule["Availability Module (slave side)"]
DbCtx["AvailabilityDbContext"]
MR["MasterRegistration\n(singleton row)"]
Cache["MasterStatusCache\n(static fields)"]
MAS["IMasterAvailabilityService\n/MasterAvailabilityService"]
end
DbCtx -->|owns| MR
MAS -->|reads/writes| DbCtx
MAS -->|updates| Cache
MW["AvailabilityMiddleware\n(extended)"] -->|reads synchronously| Cache
MC["MasterController\n(new)"] -->|delegates to| MAS
classDef entity fill:#FFC107,stroke:#F57F17,color:#000
classDef service fill:#9ae6b4,stroke:#2f855a,color:#000
classDef infra fill:#63b3ed,stroke:#2b6cb0,color:#000
classDef cache fill:#CE93D8,stroke:#6A1B9A,color:#000
class MR entity
class MAS service
class DbCtx,MC infra
class Cache,MW cache
Text alternative: AvailabilityDbContext owns the singleton MasterRegistration row. MasterAvailabilityService reads/writes the DbContext and updates the in-process MasterStatusCache (static fields). AvailabilityMiddleware reads the cache synchronously. MasterController delegates all logic to MasterAvailabilityService.
Entity: MasterRegistration
Singleton row — at most one record exists per slave instance. Upserted on each successful registration.
| Field | Type | Constraints | Notes |
|---|---|---|---|
Id |
Guid |
PK | Fixed value (e.g. Guid.Empty) enforces singleton |
MasterUrl |
string |
Required, max 500 | URL of the master CMS that registered this slave |
ApiKey |
string |
Required, max 1000 | API key sent in first registration, encrypted via IMasterApiKeyProtector (ASP.NET Core Data Protection) before storage, decrypted for each subsequent validation — corrected 2026-07-04, this was previously (incorrectly) documented as stored plain-text |
RegisteredAt |
DateTimeOffset |
Required | Timestamp of first registration |
LastContactedAt |
DateTimeOffset? |
Optional | Updated on every successful master-initiated call (register, status push, get-url) |
LastPolledAt |
DateTimeOffset? |
Optional | Added 2026-07-04. Updated on every successful slave-initiated poll (MasterStatusPollingBackgroundService) — the counterpart to LastContactedAt, tracking the opposite direction of contact. Also the basis for the fail-open timeout (see below). |
Singleton enforcement: The
Idis a fixed known value (Guid.Parse("00000000-0000-0000-0000-000000000001")). On firstPOST /api/v1/master/registerthe row is created; on re-registration the same row is updated in-place. This avoids a composite unique constraint and makes EF upsert trivial.
In-Process Cache: MasterStatusCache (static fields)
Not a DB entity — lives in memory on the slave process.
| Field | Type | Default | Notes |
|---|---|---|---|
_masterIsAvailable |
bool |
true |
Set by status push or successful poll; forced back to true on prolonged poll failure (fail-open) |
_masterDisableMessage |
string? |
null |
Message forwarded from master to 503 response; cleared to null on fail-open |
Updated 2026-07-04: originally documented as having "no expiry" (Q4=A), valid indefinitely until the next push. This is no longer accurate now that
MasterStatusPollingBackgroundServiceactively polls the master (see Rule Set 5 / Flow 5 in the sibling docs) and forces the cache back toAvailable/nullif the master has been unreachable via poll for longer thanMasterPolling:FailOpenAfterMinutes(default 5 min, measured fromMasterRegistration.LastPolledAt). Push-driven updates (this section's original description) are unchanged and still apply; the poll is an additional, independent path that can also write to this same cache.
DbContext: AvailabilityDbContext
New per-module DbContext in SlpModularCms.Modules.Availability. Separate from ApplicationDbContext (Core).
| DbSet | Entity | Table Name |
|---|---|---|
MasterRegistrations |
MasterRegistration |
AvailabilityMasterRegistrations |
Migration assembly: SlpModularCms.Modules.Availability. Applied at startup in AvailabilityModule.UseModule.