# Business Logic Model — Unit 1: master-backend ## Flow 1 — AddAsync (Add Slave CMS) **Trigger**: `POST /api/v1/CmsInstances` (Owner only) ```mermaid sequenceDiagram box rgba(99,179,237,0.3) API Layer participant Ctrl as CmsInstanceController end box rgba(154,230,180,0.3) Service Layer participant Svc as CmsInstanceService participant Repo as CmsInstanceRepository participant Client as SlaveApiClient end box rgba(246,224,94,0.3) Infrastructure participant DP as IDataProtector participant Ctx as IHttpContextAccessor participant Opts as MasterModuleOptions end box rgba(200,200,200,0.3) Persistence participant DB as MasterDbContext end Ctrl->>Svc: AddAsync(request) Note over Svc: Validate Name, Url, ApiKey not empty Note over Svc: Validate Url starts with http or https Svc->>DP: Protect(request.ApiKey) DP-->>Svc: encryptedApiKey Svc->>Repo: AddAsync(new CmsInstance) Note over Svc,Repo: Status=Available, LastContactedAt=null Svc->>Repo: SaveChangesAsync() Repo->>DB: INSERT CmsInstances Note over Svc: Determine masterUrl Svc->>Ctx: try get base URL from HttpContext alt HttpContext available Ctx-->>Svc: masterUrl from request else HttpContext unavailable Svc->>Opts: read MasterUrl Opts-->>Svc: configured masterUrl end Svc->>DP: Unprotect(encryptedApiKey) DP-->>Svc: plainApiKey Svc->>Client: RegisterMasterAsync(slaveUrl, plainApiKey, masterUrl) alt Registration success Client-->>Svc: true Svc->>Repo: UpdateAsync (LastContactedAt = UtcNow) Svc->>Repo: SaveChangesAsync() Repo->>DB: UPDATE CmsInstances else Registration failed Client-->>Svc: false Note over Svc: LastContactedAt stays null (owner can see) end Svc-->>Ctrl: CmsInstanceDto Ctrl-->>Ctrl: return 201 Created ``` Text alternative: Controller calls service; service validates, encrypts ApiKey, persists entity, determines master URL from HttpContext or config, attempts slave registration, updates LastContactedAt on success; always returns DTO regardless of registration outcome. --- ## Flow 2 — UpdateStatusAsync (Set Slave Status) **Trigger**: `PUT /api/v1/CmsInstances/{id}/status` (Owner only) ```mermaid sequenceDiagram box rgba(99,179,237,0.3) API Layer participant Ctrl as CmsInstanceController end box rgba(154,230,180,0.3) Service Layer participant Svc as CmsInstanceService participant Repo as CmsInstanceRepository participant Client as SlaveApiClient end box rgba(246,224,94,0.3) Infrastructure participant DP as IDataProtector end box rgba(200,200,200,0.3) Persistence participant DB as MasterDbContext end Ctrl->>Svc: UpdateStatusAsync(id, status, disableMessage) Svc->>Repo: GetByIdAsync(id) Repo->>DB: SELECT CmsInstances WHERE Id DB-->>Repo: CmsInstance or null Repo-->>Svc: entity or null alt Entity not found Svc-->>Ctrl: throw NotFoundException end Note over Svc: Validate DisableMessage required if NotAvailable alt Validation fails Svc-->>Ctrl: throw ValidationException end alt newStatus = Inactive Svc->>Repo: UpdateAsync (Status=Inactive, DisableMessage=null) Svc->>Repo: SaveChangesAsync() Repo->>DB: UPDATE CmsInstances Svc->>DP: Unprotect(entity.ApiKey) DP-->>Svc: plainApiKey Svc->>Client: PushStatusAsync(slaveUrl, plainApiKey, isAvailable=true, disableMessage=null) Note over Svc: Releases the master gate — the master no longer manages this slave, so it must not stay stuck on its last pushed status alt Release success Client-->>Svc: true Svc->>Repo: UpdateAsync (LastStatusPushedAt = UtcNow) Svc->>Repo: SaveChangesAsync() Svc-->>Ctrl: UpdateStatusResult(Success=true, SlaveContactSuccess=true) else Release failed Client-->>Svc: false Svc-->>Ctrl: UpdateStatusResult(Success=true, SlaveContactSuccess=false) end else newStatus = Available or NotAvailable Svc->>Repo: UpdateAsync (Status, DisableMessage) Svc->>Repo: SaveChangesAsync() Repo->>DB: UPDATE CmsInstances Svc->>DP: Unprotect(entity.ApiKey) DP-->>Svc: plainApiKey Svc->>Client: PushStatusAsync(slaveUrl, plainApiKey, status, disableMessage) alt Push success Client-->>Svc: true Svc->>Repo: UpdateAsync (LastStatusPushedAt = UtcNow) Svc->>Repo: SaveChangesAsync() Repo->>DB: UPDATE CmsInstances Svc-->>Ctrl: UpdateStatusResult(Success=true, SlaveContactSuccess=true) else Push failed Client-->>Svc: false Svc-->>Ctrl: UpdateStatusResult(Success=true, SlaveContactSuccess=false) end end Ctrl-->>Ctrl: return 200 OK with UpdateStatusResult ``` Text alternative: Controller calls service with id and new status; service loads entity, validates, updates DB, then decrypts ApiKey and pushes status to slave — for `Inactive` this push is always `isAvailable=true, disableMessage=null` (releasing the gate); for `Available`/`NotAvailable` it pushes the new status as-is. Returns SlaveContactSuccess=false if the push fails, but the DB write is always the authority. > **Updated 2026-07-04**: the `Inactive` branch previously did not push anything to the slave at all (see history below) — this left the slave stuck on whatever status it had last received, indefinitely. Fixed by always releasing the gate on deactivation. --- ## Flow 3 — VerifyIntegrityAsync (Background Integrity Check) **Trigger**: `IntegrityCheckBackgroundService` — one tick immediately on host startup, then every `IntegrityCheckIntervalMinutes` **(startup tick added 2026-07-04)** ```mermaid sequenceDiagram box rgba(200,200,200,0.3) Background participant Timer as PeriodicTimer participant BgSvc as IntegrityCheckBackgroundService end box rgba(154,230,180,0.3) Service Layer participant Svc as CmsInstanceService participant Repo as CmsInstanceRepository participant Client as SlaveApiClient end box rgba(246,224,94,0.3) Infrastructure participant DP as IDataProtector participant Opts as MasterModuleOptions end box rgba(200,200,200,0.3) Persistence participant DB as MasterDbContext end Timer->>BgSvc: Tick BgSvc->>Svc: VerifyIntegrityAsync() Svc->>Repo: GetActiveAsync() Repo->>DB: SELECT WHERE Status != Inactive DB-->>Repo: list of CmsInstance Repo-->>Svc: instances loop for each instance Svc->>DP: Unprotect(instance.ApiKey) DP-->>Svc: plainApiKey Svc->>Opts: read MasterUrl Opts-->>Svc: masterUrl Svc->>Client: GetRegisteredMasterUrlAsync(slaveUrl, plainApiKey) alt Slave unreachable Client-->>Svc: throws or returns null Svc->>Repo: UpdateAsync (LastIntegrityCheckFailedAt = UtcNow) Svc->>Repo: SaveChangesAsync() Repo->>DB: UPDATE CmsInstances Note over Svc: Unreachable for URL check — skip the status re-push for this instance this cycle else Slave reachable Client-->>Svc: registeredMasterUrl alt URLs match Svc->>Repo: UpdateAsync (LastContactedAt = UtcNow, LastIntegrityCheckFailedAt = null) Svc->>Repo: SaveChangesAsync() Repo->>DB: UPDATE CmsInstances else URL mismatch Svc->>Client: RegisterMasterAsync(slaveUrl, plainApiKey, masterUrl) alt Re-registration success Client-->>Svc: true Svc->>Repo: UpdateAsync (LastContactedAt = UtcNow, LastIntegrityCheckFailedAt = null) Svc->>Repo: SaveChangesAsync() Repo->>DB: UPDATE CmsInstances else Re-registration failed Client-->>Svc: false Svc->>Repo: UpdateAsync (LastIntegrityCheckFailedAt = UtcNow) Svc->>Repo: SaveChangesAsync() Repo->>DB: UPDATE CmsInstances end end Note over Svc: Status re-push (added 2026-07-04) — runs whenever the slave was reachable, independent of the URL-match outcome Svc->>Client: PushStatusAsync(slaveUrl, plainApiKey, isAvailable=(Status==Available), disableMessage) alt Push success Client-->>Svc: true Svc->>Repo: UpdateAsync (LastStatusPushedAt = UtcNow) Svc->>Repo: SaveChangesAsync() else Push failed Client-->>Svc: false Note over Svc: Logged; no flag change — next cycle (or the immediate startup tick) will retry end end end Svc-->>BgSvc: done ``` Text alternative: Background timer triggers integrity service; for each non-Inactive slave: decrypts key, retrieves registered master URL, clears failure flag on match, re-registers on mismatch, sets LastIntegrityCheckFailedAt when slave is unreachable or re-registration fails. **(Added 2026-07-04)** For every slave that was reachable, the service additionally re-pushes the master's currently persisted `Status`/`DisableMessage` to that slave — this is what lets a slave that reset its in-memory gate (e.g. after a restart) catch up without waiting for the next explicit admin status change. Combined with the new immediate startup tick on `IntegrityCheckBackgroundService`, this reconciliation now also runs right after the master process (re)starts. > **Updated 2026-07-04**: previously this flow only verified/re-registered the master URL and never re-pushed status (see history below) — a restarted slave (whose in-memory master-gate defaults to `Available`) would show the wrong status until the master's next explicit UI-driven change.