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>
This commit is contained in:
2026-07-04 19:53:52 +02:00
co-authored by Claude Sonnet 5
parent 274946dbff
commit 0447993181
81 changed files with 2191 additions and 86 deletions
+80 -4
View File
@@ -182,15 +182,19 @@ De `ModuleOrchestrator` zal de module nu automatisch ontdekken en laden bij het
De `SlpModularCms.Modules.Master` module laat een Owner op één "Master"-CMS de beschikbaarheid van andere ("slave") CMS-instanties centraal beheren. Elke slave die de `SlpModularCms.Modules.Availability`-module draait, respecteert een master-gecontroleerde aan/uit-status naast zijn eigen lokale beschikbaarheidsschakelaar.
### Architectuur
- **Master** (`SlpModularCms.Modules.Master`): eigen `MasterDbContext` met de `CmsInstance`-entiteit (URL, versleutelde API key, status). Bevat `CmsInstanceController` (`/CmsInstances`, Owner-only), `SlaveApiClient` (uitgaande HTTP-calls naar slaves) en `IntegrityCheckBackgroundService` (periodieke reconciliatie, standaard elk uur).
- **Slave-extensie** (`SlpModularCms.Modules.Availability`): eigen `MasterRegistration`-entiteit, `MasterController` (interne endpoints onder `/api/v1/master/*`, buiten de beschikbaarheids-gate om) en een uitgebreide `AvailabilityMiddleware` die zowel de lokale als de master-gate evalueert.
- **Master** (`SlpModularCms.Modules.Master`): eigen `MasterDbContext` met de `CmsInstance`-entiteit (URL, versleutelde API key, status). Bevat `CmsInstanceController` (`/CmsInstances`, Owner-only), `SlaveApiClient` (uitgaande HTTP-calls naar slaves), `SlaveStatusController` (`GET /api/v1/SlaveStatus`, laat een slave zijn eigen status ophalen) en `IntegrityCheckBackgroundService` (periodieke reconciliatie + status-herpush, standaard elk uur).
- **Slave-extensie** (`SlpModularCms.Modules.Availability`): eigen `MasterRegistration`-entiteit (incl. `LastPolledAt`), `MasterController` (interne endpoints onder `/api/v1/master/*`, buiten de beschikbaarheids-gate om), `MasterStatusPollingBackgroundService` (periodiek pullen van de eigen status bij de Master) en een uitgebreide `AvailabilityMiddleware` die zowel de lokale als de master-gate evalueert.
### Registratie- en statusflow
1. Owner voegt op de Master `/cms`-pagina een slave toe met diens URL.
2. De Master genereert een API key, versleutelt deze (Data Protection) en slaat hem op bij de `CmsInstance`.
3. De Master pusht de registratie naar de slave: `POST /api/v1/master/register` met header `X-Master-Api-Key`.
4. Zet de Owner de status van een slave om (Available / NotAvailable / Inactive), dan pusht de Master dit synchroon door naar de slave.
5. **Fail-open**: lukt de push niet, dan wordt de statuswijziging op de Master **niet** teruggedraaid — `IntegrityCheckBackgroundService` haalt de reconciliatie in tijdens de volgende cyclus (`MasterModuleOptions.IntegrityCheckIntervalMinutes`). Een slave die herstart voordat de Master opnieuw pusht, staat standaard weer open (`_masterIsAvailable = true` bij opstarten) — er is bewust geen TTL op de laatst bekende status.
4. Zet de Owner de status van een slave om (Available / NotAvailable / Inactive), dan pusht de Master dit synchroon door naar de slave. Bij **Inactive** stuurt de Master expliciet `Available` (de gate wordt vrijgegeven — de Master beheert de slave niet meer).
**Twee onafhankelijke synchronisatiepaden** (push én pull), zodat lokale manipulatie of een gemiste update op de slave zichzelf herstelt:
- **Push** (Master → Slave, direct): elke statuswijziging via de UI, plus elke `IntegrityCheckBackgroundService`-cyclus (herpusht de laatst opgeslagen status naar elke actieve slave — vangt slaves op die net herstart zijn).
- **Pull** (Slave → Master, periodiek): `MasterStatusPollingBackgroundService` op de slave haalt zelf zijn status op bij `GET /api/v1/SlaveStatus` (`MasterPolling:PollIntervalSeconds`, standaard 30s). Dit is de guard tegen lokale manipulatie van de slave-status en tegen gemiste pushes.
- **Fail-open**: is de Master langer dan `MasterPolling:FailOpenAfterMinutes` (standaard 5 min) onbereikbaar via de pull, dan valt de slave automatisch terug naar `Available` — een dode of onbereikbare Master mag een slave nooit permanent blokkeren.
### Configuratie
Nieuwe sectie `MasterModule` in `appsettings.json` (zie ook `appsettings.Development.json`):
@@ -203,6 +207,78 @@ Nieuwe sectie `MasterModule` in `appsettings.json` (zie ook `appsettings.Develop
```
- `MasterUrl` is de publieke URL van déze master-instantie, gebruikt door `IntegrityCheckBackgroundService` (buiten een HTTP-requestcontext heeft de background service geen `HttpContext` om dit uit af te leiden).
Nieuwe sectie `MasterPolling` (op elke instantie die `Modules.Availability` laadt — dus ook de slave):
```json
"MasterPolling": {
"PollIntervalSeconds": 30,
"FailOpenAfterMinutes": 5,
"HttpTimeoutSeconds": 5
}
```
- Heeft geen effect zolang er geen `MasterRegistration` bestaat (bijv. op de Master zelf, of op een slave die nog niet gekoppeld is).
### Vergrendeld Instellingen-scherm op een master-gecontroleerde slave
Zolang de master-gate een slave op `NotAvailable` heeft gezet (via push of pull), toont de slave's eigen `/settings`-pagina (`SettingsPage.tsx`) dit als een vergrendelde toestand in plaats van een normaal te wijzigen instelling:
- Een banner legt uit dat de Master CMS deze status beheert.
- De modusknoppen, het redenveld en de opslaanknop zijn disabled.
- Een lokale poging om de status alsnog te wijzigen (bijv. via een directe API-call) wordt door de backend geweigerd met `409 Conflict` (`MasterControlledAvailabilityException` in `PersistentAvailabilityService.UpdateStatusAsync`) — de master-gate kan dus niet per ongeluk of expres lokaal worden omzeild.
- `GET /api/v1/Availability/status` geeft dit door via het veld `isMasterControlled`.
## Lokaal Master + Slave Draaien (Dev)
Om de master↔slave-connectie (zie "Master CMS Module" hierboven) lokaal te kunnen testen, kun je twee backend-instanties tegelijk draaien: een volledige "master" (met de `SlpModularCms.Modules.Master`-module) en een "slave"-instantie zonder die module. Dit is puur een lokale ontwikkel-/testopstelling — er is geen nieuwe functionaliteit aan de master/slave-protocol zelf toegevoegd.
### 1. Backends starten
**Master** (bestaande `SlpModularCms.Api`, ongewijzigd):
```powershell
dotnet run --project src/SlpModularCms.Api --launch-profile https
```
Bereikbaar op `https://localhost:7221` (Scalar op `/scalar`).
**Slave** (nieuwe `SlpModularCms.Api.Slave`, zonder de Master-module):
```powershell
dotnet run --project src/SlpModularCms.Api.Slave --launch-profile https
```
Bereikbaar op `https://localhost:7222` (Scalar op `/scalar`). Vereist een eigen `src/SlpModularCms.Api.Slave/appsettings.local.json` — kopieer `appsettings.local.json.example` naar `appsettings.local.json` en vul een **eigen** lokale database in (bijv. `Database=SlpModularCmsSlave`), zodat master- en slave-data gescheiden blijven.
De `Modules.Availability`-migraties worden automatisch toegepast bij het opstarten (zie "Per-module migraties" hierboven). De `SlpModularCms.Core`-migraties (Identity) worden **nooit** automatisch toegepast — dit moet je, net als bij de master, één keer handmatig doen voor de nieuwe slave-database:
```powershell
dotnet ef database update --project src\SlpModularCms.Core --startup-project src\SlpModularCms.Api.Slave --context ApplicationDbContext
```
### 2. Frontend starten
**Tegen de master** (standaard):
```powershell
cd frontend
pnpm dev
```
Draait op `http://localhost:5173`, gebruikt `.env.local` (`VITE_API_BASE_URL=https://localhost:7221`).
**Tegen de slave** (optioneel, alleen nodig als je de slave ook via de admin-UI wilt bekijken):
```powershell
cd frontend
pnpm dev:slave
```
Draait op `http://localhost:5174`, gebruikt `.env.slave.local` (`VITE_API_BASE_URL=https://localhost:7222`) — kopieer eerst `.env.example` naar `.env.slave.local` met die waarde.
**Beide tegelijk** (zoals een Compound-configuratie in Rider):
```powershell
cd frontend
pnpm dev:all
```
Start `pnpm dev` en `pnpm dev:slave` parallel in één terminal, met gekleurde `master`/`slave`-prefixes per regel zodat de output van elkaar te onderscheiden blijft. Stoppen met `Ctrl+C` sluit beide dev-servers af.
### 3. Slave koppelen aan de master
Met beide backends (en de master-frontend) draaiend:
1. Log in op de master-frontend (`http://localhost:5173`) en ga naar de `/cms`-pagina.
2. Gebruik de bestaande **"Add CMS Instance"**-dialoog om de lokale slave toe te voegen met URL `https://localhost:7222`.
3. De master genereert en pusht een API key naar de slave (`POST /api/v1/master/register`); de instantie zou daarna als **verbonden/gezond** moeten worden getoond.
Zie `aidlc-docs/features/local-dev-master-slave-setup/inception/requirements/requirements.md` voor de volledige requirements en rationale achter deze opstelling.
## Productie Setup
### 1. Build & Publish