# Business Logic Model — Unit 2: slave-availability-extension ## Flow 1: Register Master (POST /api/v1/master/register) ```mermaid sequenceDiagram box rgba(33,150,243,0.15) Master CMS participant MC as MasterCms end box rgba(76,175,80,0.15) Slave CMS participant Ctrl as MasterController participant Svc as MasterAvailabilityService participant DB as AvailabilityDbContext end MC->>Ctrl: POST register with X-Master-Api-Key header and MasterUrl body Ctrl->>Svc: RegisterAsync(masterUrl, apiKey) Svc->>DB: GetRegistrationAsync() alt No registration exists DB-->>Svc: null Svc->>DB: Insert MasterRegistration with masterUrl apiKey RegisteredAt LastContactedAt=now DB-->>Svc: saved Svc-->>Ctrl: success Ctrl-->>MC: 200 OK else Registration exists and ApiKey matches DB-->>Svc: existing record Svc->>DB: Update MasterUrl and LastContactedAt=now DB-->>Svc: saved Svc-->>Ctrl: success Ctrl-->>MC: 200 OK else Registration exists and ApiKey does not match DB-->>Svc: existing record Svc-->>Ctrl: unauthorized Ctrl-->>MC: 401 Unauthorized end ``` Text alternative: Master posts to slave register endpoint. Service checks DB for existing registration. If none: insert new row. If exists and key matches: update MasterUrl. If exists but key mismatch: return 401. --- ## Flow 2: Status Push (POST /api/v1/master/status) ```mermaid sequenceDiagram box rgba(33,150,243,0.15) Master CMS participant MC as MasterCms end box rgba(76,175,80,0.15) Slave CMS participant Ctrl as MasterController participant Svc as MasterAvailabilityService participant DB as AvailabilityDbContext participant Cache as StaticCache end MC->>Ctrl: POST status with X-Master-Api-Key header isAvailable and disableMessage Ctrl->>Svc: PushStatusAsync(apiKey, isAvailable, disableMessage) Svc->>DB: GetRegistrationAsync() alt No registration DB-->>Svc: null Svc-->>Ctrl: unauthorized Ctrl-->>MC: 401 Unauthorized else ApiKey mismatch DB-->>Svc: record with different key Svc-->>Ctrl: unauthorized Ctrl-->>MC: 401 Unauthorized else ApiKey valid DB-->>Svc: valid registration Svc->>Cache: set _masterIsAvailable and _masterDisableMessage Svc->>DB: Update LastContactedAt=now DB-->>Svc: saved Svc-->>Ctrl: success Ctrl-->>MC: 200 OK end ``` Text alternative: Master posts status. Service validates API key. If valid: update static cache and LastContactedAt. On any validation failure: 401. --- ## Flow 3: Get Registered URL (GET /api/v1/master/registered-url) ```mermaid sequenceDiagram box rgba(33,150,243,0.15) Master CMS participant MC as MasterCms end box rgba(76,175,80,0.15) Slave CMS participant Ctrl as MasterController participant Svc as MasterAvailabilityService participant DB as AvailabilityDbContext end MC->>Ctrl: GET registered-url with X-Master-Api-Key header Ctrl->>Svc: GetRegisteredUrlAsync(apiKey) Svc->>DB: GetRegistrationAsync() alt No registration DB-->>Svc: null Svc-->>Ctrl: unauthorized Ctrl-->>MC: 401 Unauthorized else ApiKey mismatch DB-->>Svc: record Svc-->>Ctrl: unauthorized Ctrl-->>MC: 401 Unauthorized else ApiKey valid DB-->>Svc: valid registration Svc->>DB: Update LastContactedAt=now DB-->>Svc: saved Svc-->>Ctrl: return MasterUrl Ctrl-->>MC: 200 OK with MasterUrl payload end ``` Text alternative: Master requests the URL it registered on this slave. Service validates key, updates LastContactedAt, returns stored MasterUrl. 401 on any validation failure. --- ## Flow 4: Middleware Gate Evaluation (every request) ```mermaid sequenceDiagram box rgba(33,150,243,0.15) Incoming Request participant Req as HttpRequest end box rgba(76,175,80,0.15) Slave CMS Pipeline participant MW as AvailabilityMiddleware participant Cache as StaticCache participant Local as IAvailabilityService participant Next as NextMiddleware end Req->>MW: any request alt Path in bypass list MW-->>Next: pass through unconditionally else Admin JWT bearer token present MW-->>Next: pass through unconditionally else Check master gate MW->>Cache: read _masterIsAvailable alt Master is available or no status received yet MW->>Local: IsAvailableAsync() alt Locally available MW-->>Next: pass through else Locally unavailable MW-->>Req: 503 with local disable message end else Master says unavailable MW-->>Req: 503 with master disable message end end ``` Text alternative: Middleware first checks bypass paths, then admin JWT. If neither applies: checks static master cache; if master unavailable return 503. If master available: checks local availability service; if locally unavailable return 503. Otherwise pass through. --- ## Flow 5: Slave Poll (Slave → Master) — Added 2026-07-04 **Trigger**: `MasterStatusPollingBackgroundService` — one tick immediately on slave startup, then every `MasterPolling:PollIntervalSeconds` (default 30s). ```mermaid sequenceDiagram box rgba(76,175,80,0.15) Slave CMS participant Timer as PeriodicTimer participant BgSvc as MasterStatusPollingBackgroundService participant Svc as MasterAvailabilityService participant Client as MasterStatusPollClient participant DB as AvailabilityDbContext participant Cache as StaticCache end box rgba(33,150,243,0.15) Master CMS participant MC as SlaveStatusController end Timer->>BgSvc: Tick BgSvc->>Svc: GetPollTargetAsync() Svc->>DB: GetRegistrationAsync() alt No registration DB-->>Svc: null Svc-->>BgSvc: null BgSvc-->>Timer: no-op, wait for next tick else Registration exists DB-->>Svc: MasterUrl, encrypted ApiKey Svc-->>BgSvc: MasterUrl, plain ApiKey BgSvc->>Client: GetStatusAsync(masterUrl, plainApiKey) Client->>MC: GET /api/v1/SlaveStatus with X-Master-Api-Key header alt Poll succeeds MC-->>Client: 200 OK { IsAvailable, DisableMessage } Client-->>BgSvc: PolledMasterStatus BgSvc->>Svc: ApplyPolledStatusAsync(isAvailable, disableMessage) Svc->>Cache: set _masterIsAvailable / _masterDisableMessage Svc->>DB: Update LastPolledAt=now, LastContactedAt=now else Poll fails (network error, timeout, 401, etc.) Client-->>BgSvc: null BgSvc->>Svc: RecordPollFailureAsync(failOpenAfter) Svc->>DB: read LastPolledAt (or RegisteredAt if never polled) alt Unreachable longer than failOpenAfter Svc->>Cache: force _masterIsAvailable=true, _masterDisableMessage=null Note over Svc: Fail-open — a dead/unreachable master must never permanently block this slave else Still within grace period Note over Svc: No change — leave the existing cached gate as-is end end end ``` Text alternative: Background timer triggers the poller; if no master is registered, it's a no-op. Otherwise it calls `GET /api/v1/SlaveStatus` on the registered master. On success, the response overwrites the in-memory gate and updates `LastPolledAt`/`LastContactedAt`. On failure, the gate is left alone unless the master has been unreachable (via poll) for longer than `MasterPolling:FailOpenAfterMinutes`, in which case the gate is forced open (`Available`, no message). --- ## Flow 6: Local Availability Status Read/Write with Master-Gate Override — Added 2026-07-04 Applies to the slave's own `/settings` admin UI/API — `GET /api/v1/Availability/status` and `POST /api/v1/Availability/admin/status` — layered on top of `PersistentAvailabilityService`, which previously only ever reflected the locally-persisted `GlobalAvailabilityState` regardless of the master gate. ```mermaid sequenceDiagram box rgba(33,150,243,0.15) Frontend (SettingsPage) participant FE as Owner Browser end box rgba(76,175,80,0.15) Slave CMS participant Ctrl as AvailabilityController participant Svc as PersistentAvailabilityService participant MasterSvc as MasterAvailabilityService participant DB as ApplicationDbContext end FE->>Ctrl: GET /api/v1/Availability/status Ctrl->>Svc: GetStatusDetailsAsync() Svc->>MasterSvc: GetMasterStatus() alt Master gate closed (IsAvailable = false) MasterSvc-->>Svc: NotAvailable, masterMessage Svc-->>Ctrl: Status=NotAvailable, Message=masterMessage, IsMasterControlled=true else Master gate open MasterSvc-->>Svc: Available Svc->>DB: read GlobalAvailabilityState DB-->>Svc: local Status/Message Svc-->>Ctrl: Status, Message, IsMasterControlled=false end Ctrl-->>FE: 200 OK FE->>Ctrl: POST /api/v1/Availability/admin/status (attempted local change) Ctrl->>Svc: UpdateStatusAsync(newStatus, reason, updatedBy) Svc->>MasterSvc: GetMasterStatus() alt Master gate closed MasterSvc-->>Svc: NotAvailable Svc-->>Ctrl: throw MasterControlledAvailabilityException Ctrl-->>FE: 409 Conflict (ProblemDetails) else Master gate open MasterSvc-->>Svc: Available Svc->>DB: persist newStatus/reason Svc-->>Ctrl: success Ctrl-->>FE: 200 OK end ``` Text alternative: Reading status now checks the master gate first — if closed, the response reflects the master's forced status and message, with `IsMasterControlled=true`, regardless of what's persisted locally. Writing a status change now performs the same master-gate check first: if closed, the write is rejected outright with `409 Conflict` instead of silently succeeding with no visible effect, since the master gate would have overridden it on the very next read anyway. > Previously (before 2026-07-04): `GetStatusDetailsAsync` ignored the master gate entirely and always returned the locally-persisted status; `UpdateStatusAsync` always wrote the requested change regardless of the master gate, giving a misleading "success" for a change that was immediately invisible.