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
@@ -0,0 +1,41 @@
# Build and Test Summary — Local Dev Master/Slave Setup
## Build Status
- **Build Tool**: .NET 10 SDK (`dotnet build`)
- **Build Status**: ✅ Success
- **Build Artifacts**: `src/SlpModularCms.Api.Slave/bin/` (new), plus unchanged outputs for all existing projects
- **Build Time**: ~5 seconds (incremental)
## Test Execution Summary
### Unit Tests
- **Total Tests**: 193 (54 + 60 + 37 + 42 across `Core.Tests`, `Modules.Availability.Tests`, `Modules.Identity.Tests`, `Modules.Master.Tests`)
- **Passed**: 193
- **Failed**: 0
- **Coverage**: Unchanged from before this feature (no new business logic; relocated code retains its existing tests)
- **Status**: ✅ Pass
### Integration Tests
- **Test Scenarios**: 2 (module isolation; end-to-end master/slave connection via existing Add CMS Instance flow)
- **Passed**: 1 (module isolation — verified via manual `dotnet run` of both hosts, confirmed via log output: master loads 3 modules, slave loads exactly 2, Master excluded)
- **Failed**: 0
- **Not Run**: 1 (end-to-end connection scenario — requires a local SQL Server instance; no container runtime available in this sandboxed session. Documented as a manual step for the developer in `integration-test-instructions.md` and the root `README.md` runbook.)
- **Status**: ⚠️ Partial — the scenario this feature actually changes (module isolation) is fully verified; the scenario exercising the pre-existing, unmodified master/slave protocol requires manual follow-up.
### Performance Tests
- **Status**: N/A — this feature is local developer tooling with no performance requirements (NFR-3, local-only scope).
### Additional Tests
- **Contract Tests**: N/A — no API contracts changed
- **Security Tests**: N/A — no new attack surface; `appsettings.local.json` secrets-hygiene pattern (NFR-4) followed and verified via `git check-ignore`
- **E2E Tests**: See Integration Tests Scenario 2 above
## Overall Status
- **Build**: ✅ Success
- **All Automated Tests**: ✅ Pass (193/193)
- **Manual Follow-up Required**: Yes — developer should run the end-to-end connection test (root `README.md`, "Lokaal Master + Slave Draaien (Dev)") at least once with a real local SQL Server to confirm the full workflow before relying on it.
- **Ready for Operations**: Yes (Operations is a placeholder for this project; no deployment/monitoring work applicable to local dev tooling)
## Next Steps
- Developer runs the manual end-to-end verification (Scenario 2) locally to close out the one remaining unverified item.
- Feature is otherwise complete: `SlpModularCms.Api.Slave` exists and correctly excludes the Master module, `SlpModularCms.Core.Hosting` avoids code duplication between the two hosts, frontend tooling and documentation are in place.
@@ -0,0 +1,32 @@
# Build Instructions — Local Dev Master/Slave Setup
## Prerequisites
- **Build Tool**: .NET 10 SDK
- **Dependencies**: NuGet packages restore automatically on build (no new external dependencies introduced by this feature)
- **Environment Variables**: None required to build (runtime config is via `appsettings.local.json`, see below)
- **System Requirements**: Same as the rest of the repository — no new system requirements
## Build Steps
### 1. Restore & Build
```powershell
dotnet build SlpModularCms.sln
```
### 2. Verify Build Success
- **Expected Output**: `Build succeeded.` with 0 errors (NuGet advisory warnings for `Microsoft.OpenApi` and a few `NU1510`/prune warnings are pre-existing and unrelated to this feature).
- **Build Artifacts**: `src/SlpModularCms.Api/bin/`, `src/SlpModularCms.Api.Slave/bin/` (new), plus all existing project outputs.
- **Common Warnings**: NU1903 (Microsoft.OpenApi advisory) and NU1510 (package pruning) appear across multiple projects — pre-existing, not introduced by this feature.
### Actual Result (this session)
Ran `dotnet build SlpModularCms.sln`**Build succeeded**, 0 errors.
## Troubleshooting
### Build Fails with `CS0246` in `SlpModularCms.Core/Hosting/*.cs`
- **Cause**: `SlpModularCms.Core` is a plain `Microsoft.NET.Sdk` project (not `Sdk.Web`), so ASP.NET Core implicit usings (`Microsoft.Extensions.DependencyInjection`, `Microsoft.Extensions.Configuration`, `Microsoft.AspNetCore.Builder`, `Microsoft.AspNetCore.Http`) aren't automatically available like they are in `SlpModularCms.Api`.
- **Solution**: Already fixed during Code Generation — explicit `using` statements were added to `ServiceCollectionExtensions.cs`. If this recurs after further edits, add the missing explicit `using`.
### `SlpModularCms.Api.Slave` fails to start with a SQL connection error
- **Cause**: Missing `src/SlpModularCms.Api.Slave/appsettings.local.json` (gitignored, must be created locally per developer).
- **Solution**: Copy `appsettings.local.json.example` to `appsettings.local.json` and fill in your local SQL Server credentials, using a **different** `Database=` name than the master instance (e.g. `SlpModularCmsSlave`).
@@ -0,0 +1,52 @@
# Integration Test Instructions — Local Dev Master/Slave Setup
## Purpose
Verify that the two backend instances (Unit 1) and frontend tooling (Unit 2) work together correctly: the slave instance genuinely excludes the Master module, and the existing master↔slave connection mechanism (unchanged by this feature) can be exercised end-to-end using the two local instances.
## Test Scenarios
### Scenario 1: Module isolation — Slave excludes Master, Master keeps all modules
**Description**: Confirm `SlpModularCms.Api.Slave`'s `ModuleOrchestrator` never discovers `Modules.Master`, while `SlpModularCms.Api` is unaffected.
**Setup**: None beyond a successful build (no database required — module discovery happens before any DB access).
**Test Steps** (already executed in this session):
```powershell
dotnet run --project src/SlpModularCms.Api --launch-profile https --no-build
dotnet run --project src/SlpModularCms.Api.Slave --launch-profile https --no-build
```
**Expected Results**:
- Master log output: `Module ontdekt: Availability`, `Module ontdekt: Identity`, `Module ontdekt: Master`, `3 modules succesvol geladen.`
- Slave log output: `Module ontdekt: Availability`, `Module ontdekt: Identity`, `2 modules succesvol geladen.`**no** `Master` line.
**Actual Result**: ✅ **Passed** — confirmed exactly as expected in this session's console output (see Unit 1 code generation summary).
**Cleanup**: Stop both processes (Ctrl+C / process termination).
### Scenario 2: End-to-end master/slave connection via the existing "Add CMS Instance" flow
**Description**: With both instances running against separate local databases, use the master frontend's existing "Add CMS Instance" dialog to register the local slave and confirm the connection is established (per FR-4 and the runbook in root `README.md`).
**Setup**:
1. A local SQL Server instance reachable from both backends (e.g. via the `podman run ... mcr.microsoft.com/mssql/server` command in root `README.md`).
2. `src/SlpModularCms.Api/appsettings.local.json` (master) and `src/SlpModularCms.Api.Slave/appsettings.local.json` (slave, from `appsettings.local.json.example`) pointing at **different** database names on that SQL Server.
3. Master and slave backends running (`dotnet run --project src/SlpModularCms.Api --launch-profile https` and `dotnet run --project src/SlpModularCms.Api.Slave --launch-profile https`).
4. Master frontend running (`pnpm dev` in `frontend/`), logged in as an Owner.
**Test Steps**:
1. Navigate to the `/cms` page on the master frontend.
2. Use "Add CMS Instance" with URL `https://localhost:7222` (the local slave).
3. Observe the instance's status in the UI.
**Expected Results**: The instance appears in the list and its status reflects a successful connection (per the existing, unchanged `CmsInstanceService`/`SlaveApiClient``MasterController` protocol documented in root `README.md`'s "Master CMS Module" section).
**Actual Result**: ⚠️ **Not run in this session** — this sandboxed environment has no accessible container runtime (`docker`/`podman` both unavailable), so no local SQL Server instance could be provisioned to run either backend past module discovery. **This step requires manual execution by the developer** following the runbook in root `README.md` ("Lokaal Master + Slave Draaien (Dev)"). Scenario 1 (module isolation, which does not require a database) was fully verified automatically and is the aspect this feature actually changes — Scenario 2 exercises the pre-existing, unmodified master/slave protocol and primarily validates that the new run configuration (ports, separate databases, CORS) doesn't get in the way of it.
**Cleanup**: Remove the CMS instance registration if desired; stop both backends and frontends; stop the SQL Server container if it was started solely for this test.
## Notes
- No automated integration test suite was added for Scenario 2 — it's an inherently manual, cross-process, cross-database verification of local developer tooling, not a candidate for CI automation (per NFR-3, this feature is explicitly local-only in scope).
@@ -0,0 +1,29 @@
# Unit Test Execution — Local Dev Master/Slave Setup
## Run Unit Tests
### 1. Execute All Unit Tests
```powershell
dotnet test SlpModularCms.sln
```
### 2. Review Test Results
- **Expected**: All existing test suites pass unchanged — this feature adds no new business logic, so no new unit tests were written (per Unit of Work Q3 = A).
- **Test Coverage**: Unchanged from before this feature (the relocated `ModuleOrchestrator`/`ApiPrefixConvention` classes retain their existing test coverage, now in `SlpModularCms.Core.Tests/Hosting/` instead of `SlpModularCms.Modules.Identity.Tests/Infrastructure/`).
- **Test Report Location**: Console output from `dotnet test`; no separate report file generated by default.
### Actual Result (this session)
Ran `dotnet test SlpModularCms.sln`:
| Test Project | Passed | Failed | Skipped |
|---|---|---|---|
| `SlpModularCms.Core.Tests` (incl. relocated `Hosting` tests) | 54 | 0 | 0 |
| `SlpModularCms.Modules.Availability.Tests` | 60 | 0 | 0 |
| `SlpModularCms.Modules.Identity.Tests` | 37 | 0 | 0 |
| `SlpModularCms.Modules.Master.Tests` | 42 | 0 | 0 |
| **Total** | **193** | **0** | **0** |
No regressions from relocating `ModuleOrchestrator`, `ServiceCollectionExtensions`, and `ApiPrefixConvention` into `SlpModularCms.Core`, or from moving their tests into `Core.Tests`.
### 3. Fix Failing Tests
Not applicable this run — all tests passed on first execution after Code Generation.
@@ -10,14 +10,14 @@
## Steps
- [ ] **Step 1 — Frontend env files for slave mode**
- [x] **Step 1 — Frontend env files for slave mode**
- Create `frontend/.env.slave.local` (gitignored via existing `frontend/.gitignore` `*.local` pattern — verified via `git check-ignore`): `VITE_API_BASE_URL=https://localhost:7222`
- Modify `frontend/.env.example`: add a second documented block showing the slave-mode value, alongside the existing master-mode `VITE_API_BASE_URL` example
- [ ] **Step 2 — `dev:slave` npm script**
- [x] **Step 2 — `dev:slave` npm script**
- Modify `frontend/package.json`: add `"dev:slave": "vite --mode slave --port 5174"` to the `scripts` section. Vite's mode-based env loading will load `.env.slave.local` when run with `--mode slave` (Vite loads `.env.[mode].local` in addition to `.env.local`; since both files would apply, and `.env.local` takes precedence per Vite's env-file priority for the same key when both exist for a mode, name the slave file `.env.slave.local` specifically — this file only loads when `--mode slave` is passed, so there is no conflict with the default `.env.local` used by `pnpm dev`)
- [ ] **Step 3 — Runbook documentation**
- [x] **Step 3 — Runbook documentation**
- Modify root `README.md`: add new section **"Lokaal Master + Slave Draaien (Dev)"** immediately after the existing "Master CMS Module" section, covering:
1. Starting the master backend (`dotnet run --project src/SlpModularCms.Api --launch-profile https`)
2. Starting the slave backend (`dotnet run --project src/SlpModularCms.Api.Slave --launch-profile https`), noting it needs its own `appsettings.local.json` (from `appsettings.local.json.example`) with a separate local database
@@ -25,7 +25,7 @@
4. Using the existing "Add CMS Instance" dialog on the master frontend to register the slave (URL `https://localhost:7222`) and confirm it shows as connected/healthy
5. Cross-reference to this feature's requirements doc for anyone wanting the full rationale
- [ ] **Step 4 — Documentation summary**
- [x] **Step 4 — Documentation summary**
- Create `aidlc-docs/features/local-dev-master-slave-setup/construction/unit-2-frontend-dual-instance-tooling/code/summary.md` documenting what was created/modified
## Notes
@@ -43,3 +43,27 @@
- **Master** (`SlpModularCms.Api`): discovers and loads all 3 modules — Availability, Identity, Master.
- **Slave** (`SlpModularCms.Api.Slave`): discovers and loads exactly 2 modules — Availability, Identity. **Master is correctly excluded.**
- Full end-to-end run (requiring a real local SQL Server/localdb instance and manual "Add CMS Instance" registration) is deferred to Build and Test / Unit 2, per the plan.
## Follow-up: Real `appsettings.local.json` for the Slave (user request)
The user reported the slave's connection string looked wrong and asked for a real `src/SlpModularCms.Api.Slave/appsettings.local.json` (gitignored, mirroring `src/SlpModularCms.Api/appsettings.local.json`'s credentials but with its own database). Created with `Database=SlpModularCmsSlave` (vs. master's `SlpModularCms`), same `Server=127.0.0.1,1433;User ID=sa;Password=...` credentials and same `JwtSettings.Secret`. Verified via `git check-ignore` that it's not tracked.
**Verified working**: running `dotnet run --launch-profile https --no-build` in `SlpModularCms.Api.Slave` with this file present successfully connected to the local SQL Server, created the `SlpModularCmsSlave` database, and applied EF Core migrations (`CREATE DATABASE`, `__EFMigrationsHistory` setup, migration application) — confirming the connection string is correct. This environment does have a reachable SQL Server at `127.0.0.1:1433`, unlike assumed earlier in Build and Test.
**Known issue hit during this verification, not related to the fix**: a subsequent attempt to run both master and slave simultaneously hit `Failed to bind to address ... address already in use` on both `:7221` and `:7222` — leftover `dotnet run` child processes from earlier manual verification steps in this session likely survived their parent `timeout` calls and are still holding those ports. This is a session/environment artifact, not a defect in the generated code. Resolved: user closed their own processes and confirmed via `Get-CimInstance`/`Get-NetTCPConnection` that no `SlpModularCms` processes or listeners remained on ports 7221/7222/5284/5285.
## Follow-up: Slave Database Migration Gap (discovered while checking migration status)
User asked whether the necessary migrations exist and both databases are up to date. Checked via `dotnet ef migrations list` for every `DbContext`/startup-project combination:
| Database | Context | Status before fix |
|---|---|---|
| `SlpModularCms` (master) | `ApplicationDbContext` (Core/Identity) | ✅ Applied (2/2) |
| `SlpModularCms` (master) | `AvailabilityDbContext` | ✅ Applied (1/1) |
| `SlpModularCms` (master) | `MasterDbContext` | ✅ Applied (1/1) |
| `SlpModularCmsSlave` (slave) | `AvailabilityDbContext` | ✅ Applied (1/1) — auto-migrated via `Database.Migrate()` in `AvailabilityModule.UseModule` |
| `SlpModularCmsSlave` (slave) | `ApplicationDbContext` (Core/Identity) | ❌ **2 migrations pending**`SlpModularCms.Core`'s `ApplicationDbContext` is never auto-migrated (only `Modules.Availability` and `Modules.Master` call `Database.Migrate()` in their `UseModule`); it always requires the manual `dotnet ef database update` step documented in root `README.md`, and nobody had run it yet for the new slave database.
**Fixed**: ran `dotnet ef database update --project src/SlpModularCms.Core --startup-project src/SlpModularCms.Api.Slave --context ApplicationDbContext`. Re-checked with `migrations list` — both `20260612191736_InitialCreate` and `20260619130625_AddsDisplayName` now show as applied (no `(Pending)` marker). Slave database is now fully up to date.
**Documentation fix**: added a note + the exact command to the "Lokaal Master + Slave Draaien (Dev)" runbook in root `README.md`, right after the slave-startup instructions, so this doesn't get missed again by whoever (re)creates the slave database.
@@ -0,0 +1,32 @@
# Fix — CMS Page Was Visible on Slave (No Backend Capability Check)
## Problem
The `/cms` page (managing registered CMS instances — an Owner-only feature backed by `SlpModularCms.Modules.Master`) was gated purely by role (`Owner`), with no awareness of whether the *connected backend* actually has the Master module loaded. Before this feature, this was never an issue — every deployed instance always had `Modules.Master` loaded. Now that a Master-less slave instance exists, an Owner using the frontend against the slave could still see the nav link and open `/cms`, where its data calls (`GET /api/v1/CmsInstances`) would 404 against a backend that has no such controller.
## Fix
### Backend
- `SlpModularCms.Core.Hosting.ModuleOrchestrator` — added `ModuleNames` (public `IReadOnlyList<string>`), listing the names of modules actually discovered on this instance.
- New `SlpModularCms.Core.Hosting.SystemController``GET /api/v1/System/capabilities` returns `{ "modules": [...] }` for whichever instance is asked.
- `SlpModularCms.Api/Program.cs` and `SlpModularCms.Api.Slave/Program.cs` — registered the `ModuleOrchestrator` instance itself as a DI singleton (`builder.Services.AddSingleton(orchestrator)`) so the new controller can inject it.
### Frontend
- `src/api/types.ts` — added `SystemCapabilities { modules: string[] }`.
- `src/api/useSystemCapabilities.ts` — new React Query hook (`staleTime: Infinity` — a backend's module set never changes mid-session), calling `/api/v1/System/capabilities`.
- `src/components/auth/ModuleGuard.tsx` — new guard component (mirrors `RoleGuard`), hides its children and shows a "not available on this instance" message when the required module isn't in the backend's capability list.
- `src/router.tsx``/cms` route now wraps its page in `<ModuleGuard requiredModule="Master">` (inside the existing `<RoleGuard allowedRoles={['Owner']}>`).
- `src/components/layout/Sidebar.tsx` — the CMS nav item now also requires `capabilities.modules.includes('Master')` before rendering.
- `src/i18n/locales/{nl,en}/translation.json` — added `errors.featureUnavailableTitle` / `errors.featureUnavailable`.
- `src/mocks/system/handlers.ts` — new MSW handler for `*/System/capabilities`, defaulting to `['Availability', 'Identity', 'Master']` so existing CMS-related tests keep passing unchanged; registered in `src/mocks/index.ts`.
- `src/components/layout/Sidebar.test.tsx` — updated the "Owner sees ... CMS" assertion to `findByTestId` (now async, since nav-cms visibility depends on the capabilities fetch), and added a new regression test: "Owner does not see CMS when the backend has no Master module (slave instance)".
## Verification Performed
- `dotnet build` / `dotnet test` — succeed; all 193 backend tests still pass (module relocation from the earlier fix is untouched; this only adds new code).
- `pnpm build` — succeeds, no type errors.
- `pnpm test` — 210/210 pass in isolation (209/210 in the full parallel run; the 1 failure is the same pre-existing flaky `AddCmsInstanceDialog.test.tsx` timeout seen earlier in this feature's Build and Test, unrelated to this change — confirmed passing 5/5 when re-run alone).
- **Live verification against real running instances**: started both `SlpModularCms.Api` (master) and `SlpModularCms.Api.Slave` against the local SQL Server and curled the new endpoint directly:
- Master: `curl https://localhost:7221/api/v1/System/capabilities``{"modules":["Availability","Identity","Master"]}`
- Slave: `curl https://localhost:7222/api/v1/System/capabilities``{"modules":["Availability","Identity"]}`
- Confirms the capability check reflects each instance's actual loaded modules, not just a hardcoded assumption.
@@ -0,0 +1,28 @@
# Code Generation Summary — Unit 2: Frontend Dual-Instance Tooling & Runbook
## Created
- `frontend/.env.slave.local``VITE_API_BASE_URL=https://localhost:7222`, `VITE_APP_TITLE=SlpModularCms (Slave)` (gitignored via existing `frontend/.gitignore` `*.local` pattern, verified via `git check-ignore`)
- `aidlc-docs/features/local-dev-master-slave-setup/construction/unit-2-frontend-dual-instance-tooling/code/summary.md` (this file)
## Modified
- `frontend/.env.example` — added a documented block explaining how to create `.env.slave.local` and use `pnpm dev:slave`, and how `VITE_APP_TITLE` distinguishes tabs
- `frontend/.env.local` (developer's existing local file, gitignored) — added `VITE_APP_TITLE=SlpModularCms (Master)` for symmetry with the slave
- `frontend/package.json` — added `"dev:slave": "vite --mode slave --port 5174"` script
- `frontend/src/vite-env.d.ts` — added optional `VITE_APP_TITLE` to the typed env interface
- `frontend/src/lib/config.ts` — added `appTitle` to `AppConfig`, sourced from `VITE_APP_TITLE` with a `'SlpModularCms'` fallback when unset
- `frontend/src/main.tsx` — sets `document.title` from `getAppConfig().appTitle` at startup, so each instance's browser tab is recognizable
- `README.md` (root) — added new section **"Lokaal Master + Slave Draaien (Dev)"** after "Master CMS Module", covering: starting both backends, starting the frontend against either instance, and using the existing "Add CMS Instance" dialog to connect them
## Notes
- No new tests — this unit is env/config/documentation only, no testable logic.
- `pnpm dev:slave` uses Vite's `--mode slave` flag, which loads `.env.slave.local` in addition to the default `.env`/`.env.local` files; `--port 5174` overrides `vite.config.ts`'s default port (5173) for this invocation only.
- **Added after initial Build and Test review** (user request): distinguishable browser tab titles per instance via `VITE_APP_TITLE`, defaulting to `"SlpModularCms"` so existing setups without the var keep today's title unchanged.
- Full end-to-end verification (starting both backends with real local databases, running both frontends, and confirming the "Add CMS Instance" flow actually connects them) is performed in Build and Test, since it requires a running local SQL Server instance that isn't available in this automated environment.
## Verification Performed (Tab Title Change)
- `pnpm build` (tsc -b + vite build) — succeeds, no type errors.
- `pnpm test` — 208/209 pass; the 1 failure (`AddCmsInstanceDialog.test.tsx`, a findByTestId timeout) reproduced as flaky under this session's load and passed 5/5 when re-run in isolation — unrelated to the title change (no title-related assertions, and `main.tsx` is not exercised by component tests).