Files
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

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 Id is a fixed known value (Guid.Parse("00000000-0000-0000-0000-000000000001")). On first POST /api/v1/master/register the 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 MasterStatusPollingBackgroundService actively polls the master (see Rule Set 5 / Flow 5 in the sibling docs) and forces the cache back to Available/null if the master has been unreachable via poll for longer than MasterPolling:FailOpenAfterMinutes (default 5 min, measured from MasterRegistration.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.