Closes round 2 of the deployment feature

Records the stage as complete and logs the two defects that local testing
turned up after U3 and U4 were generated: ciphertext predating the key-ring
move, and an integrity check that could not tell an unreachable slave from
one that does not recognise the master.

Both were fixed in this branch rather than filed. The second is master/slave
domain behaviour rather than deployment work, so the state file says so
plainly — it sits here by decision, not because it belongs to the feature.

Verified at close: build 0 errors, 372 backend tests, 237 frontend tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
This commit is contained in:
2026-07-28 12:35:44 +02:00
co-authored by Claude Opus 5
parent 6957ec7c60
commit 247d8be241
2 changed files with 86 additions and 2 deletions
@@ -947,3 +947,83 @@ Also noted: `MigrationFailure` uses synchronous `SentrySdk.Flush`, because `Migr
**No blocking security findings. No new deviation.**
---
## 2026-07-28 — CONSTRUCTION: Round 2 closed, plus two fixes found in local testing
Round 2 (U3 + U4) is complete. Running both hosts afterwards surfaced two defects that were not
part of the unit scope. Both were small and contained, so both were fixed in this branch rather
than filed — per the user's standing rule that tech debt is reserved for large or high-impact
changes and code should be left better than it was found.
### Fix 1 — the key ring move leaves older ciphertext unreadable
Covered in full in the previous entry and in `u2-data-durability/code/generation-summary.md`.
Recorded as **ASM-08**; no migration ships because the user confirmed nothing is deployed yet.
Local state was repaired by clearing the stale registration rows on both sides.
### Fix 2 — the integrity check could not distinguish "down" from "does not know us"
**Symptom.** A slave registered with the frontend URL (`http://localhost:5174/`, the vite dev
server) instead of the API URL. The only log line was `Status push failed for slave <url>`, and the
instance could not recover.
**Cause.** `SlaveApiClient.GetRegisteredMasterUrlAsync` mapped four distinct outcomes onto a single
`null` — no HTTP response, a 404, a rejected key, and a genuine answer. `VerifyIntegrityAsync`
treated any `null` as "unreachable" and re-registered only when a *non-null* URL differed. A slave
with no registration returns exactly `null`, so the self-healing path could never repair the one
state that was actually repairable. Recovery required editing the database by hand.
**Fix.** The call now returns `RegisteredMasterUrlResult` carrying a `SlaveContactOutcome`:
| Outcome | Integrity check behaviour |
|---|---|
| `Unreachable` | Mark failed, retry next tick — unchanged |
| `NotAProtocolEndpoint` (404) | Mark failed, and log that the host does not serve the protocol |
| `Unauthorized` (401) | **Register** — repairs a slave that has no registration |
| `Ok` + differing master URL | Re-register — unchanged |
| `Ok` + matching | Healthy |
The 404 branch is the one that would have saved the diagnosis time: it names the actual mistake —
a URL pointing at something other than an instance's API — instead of hiding it behind a generic
contact failure.
**Why automatic registration on a rejected key is safe.** The slave accepts a registration only when
it has none, and refuses any key that does not match an existing one
(`MasterAvailabilityService.RegisterAsync`). Registration therefore succeeds exactly for a slave
that was never registered and fails harmlessly for one that belongs to another master — no takeover
is possible. **The guarantee lives on the slave, not the master**, which makes it easy to weaken by
accident, so `RegisterAsync_ReturnsFalse_WhenKeyMismatch` now states in its documentation that the
master's automatic registration depends on it.
**Scope note.** This is master/slave domain behaviour, not deployment work — the same separation the
user drew when correcting the earlier framing of availability as a health check. It sits in this
branch by the user's explicit decision (no separate branch), and should be read as an in-passing
fix rather than part of the feature.
### Verification at stage close
- `dotnet build SlpModularCms.sln -c Release` — 0 errors
- Backend tests — **372 passed, 0 failed** (Core 196, Availability 82, Master 57, Identity 37)
- Frontend tests — **237 passed, 0 failed**
- `npx tsc -b` clean; eslint clean on all changed frontend files; full `pnpm run lint` unchanged at
the pre-existing 5 errors / 1 warning (FR-21, U5)
### Commits on `feature/gitea-deployment-workflow`
| Commit | Contents |
|---|---|
| `8568ca4` | Inception: reverse engineering, requirements, design, unit decomposition |
| `29a93ef` | U1 — website/admin split, `/health`, availability gate hardening |
| `5f3eda2` | U2 — key ring in the database, automatic migration, host wiring |
| `589146a` | README: sample password removed (pre-existing change, kept separate) |
| `357d395` | Functional design U3 + U4 |
| `5102f86` | NFR design U3 + U4; OPEN-01 closed, REF-U3-01 raised |
| `a122548` | U3 — security headers and CSP |
| `8e79a72` | U4 — logging, Sentry, tunnel, Umami, six security events |
| `980dc80` | ASM-08 — key ring migration gap recorded |
| `6957ec7` | Integrity check: distinguishes unreachable from unregistered |
Nothing pushed. Next: Round 3 — U5 CI Workflow & Gates and U6 Deploy Workflow, the latter preceded
by Infrastructure Design. U5 carries REF-U3-01's Umami origin gate plus FR-21 and FR-22.
---