feat(hub): v0.69.0 — customer DELETE is the guided full-teardown cascade (R-25b)
POST /configs/{id}/delete now runs hosts -> RESET -> purge behind three
acknowledgements, a typed customer-id, a stale-preview check and the
ONLINE-host refusal (every gate before any write, so a refusal has zero
side effects). The shallow handleConfigDelete is gone.
Two invariants are asserted, not just commented: ruling 3 is preserved by
construction (leg 2 never sees a host row) and retained escrow custody is
purged exactly once, in leg 3 (leg 2 runs with purgeEscrow=false).
handleCustomerReset's committed half was extracted as commitCustomerReset;
the standalone RESET path is byte-identical to v0.68.1 and its suite is
untouched. Five red-proofs run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J55BQE1gE2V4ffud5jweGS
This commit is contained in:
@@ -1,5 +1,74 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.69.0 — customer DELETE becomes the guided full-teardown cascade (R-25b) (2026-07-21)
|
||||
|
||||
Implements the operator ruling of 2026-07-21. The customer page carried two half-truths: **RESET**
|
||||
was the real teardown but refused while any host row existed, and **DELETE** quietly removed only
|
||||
the `customer_configs` row (plus escrow custody) — leaving the Hetzner Storage-Box repo, the PBS
|
||||
namespace + credentials, the tunnel/zone plumbing and the host rows behind. DELETE now does what its
|
||||
name promises.
|
||||
|
||||
### The cascade
|
||||
|
||||
`POST /configs/{id}/delete` (same route, new behaviour) runs three legs in a fixed order:
|
||||
|
||||
1. **hosts** — every host row deleted through host-delete's own rules: an **ONLINE host refuses the
|
||||
whole cascade** (checked for every host up front, so it never half-runs) and escrow is **DEMOTED**
|
||||
to retained custody, never destroyed.
|
||||
2. **reset** — the committed RESET sequence verbatim (Hetzner FIRST → PBS → claim → descriptor → DB
|
||||
purge), reached through the newly extracted `commitCustomerReset`.
|
||||
3. **purge** — `DeleteCustomerConfig`: the customer record **and all escrow ciphertext**.
|
||||
|
||||
Nothing here is newly destructive: the cascade only *sequences* three operations that already
|
||||
existed, each keeping its own safety rules. Two invariants are load-bearing and asserted, not merely
|
||||
commented:
|
||||
|
||||
- **Ruling 3 is preserved BY CONSTRUCTION** — leg 2 can only run after leg 1, so the RESET sequence
|
||||
never sees a host row. The standalone RESET handler's 409 gate is untouched.
|
||||
- **Custody is purged EXACTLY ONCE, in leg 3.** Leg 1 demotes; leg 2 is called with
|
||||
`purgeEscrow=false` so `PurgeCustomerResetDBState` leaves retained blobs alone; leg 3 is the one
|
||||
true purge point (v0.60.1). Both are proven from *inside* leg 2 by a fake that observes store state
|
||||
at the moment the PBS deprovision fires.
|
||||
|
||||
### Gates (all before any write — a refused delete has ZERO side effects)
|
||||
|
||||
Three separate acknowledgements (`ack_hosts`, `ack_reset`, `ack_purge`, each must be exactly `1`),
|
||||
the **typed customer-id**, a **stale-preview** check (the acknowledged host count must still match
|
||||
live — otherwise 409 "re-open the dialog"), and the ONLINE-host refusal. No force flag, no skip flag,
|
||||
no partial-run downgrade anywhere in this path.
|
||||
|
||||
### Resume
|
||||
|
||||
A failed leg retains the journal row (`customer_resets`, per-leg status) and the HTTP error **names
|
||||
the leg**. Re-opening the dialog renders the incomplete journal and offers **Resume**; a re-run is
|
||||
idempotent (leg 1 is a no-op once the hosts are gone). The acknowledgements are **not** cached across
|
||||
attempts — a resume passes every gate again.
|
||||
|
||||
### UI
|
||||
|
||||
Danger zone → **Delete customer…** opens a guided dialog: live inventory panel (hosts by name +
|
||||
status, offsite repository identifier, PBS namespace, custody state), the three consequence
|
||||
checkboxes, the typed customer-id field, one submit. Mid-cascade failures render the journal state.
|
||||
The client-side checks are convenience — every gate is enforced server-side.
|
||||
|
||||
### Refactor (standalone RESET behaviour unchanged)
|
||||
|
||||
`handleCustomerReset`'s committed half became `commitCustomerReset(ctx, cfg, resetID, purgeEscrow)`,
|
||||
returning a `resetLegError` (leg name + status + the exact operator-facing message). The standalone
|
||||
path is byte-identical to v0.68.1: same order, same leg names, same messages, same status codes; its
|
||||
suite is untouched and green. The shallow `handleConfigDelete` is **gone** — do not reintroduce a
|
||||
shallow delete path.
|
||||
|
||||
### Tests
|
||||
|
||||
New `internal/web/customer_delete_test.go`: happy-path leg ORDER (observed from inside leg 2), nine
|
||||
fail-closed gate cases each asserting zero mutations *and* zero external calls *and* no journal row,
|
||||
resume-after-external-failure (custody + customer survive the failure, then converge), resume is
|
||||
still gated, the `purgeEscrow` flag's custody semantics, and a preview test asserting the inventory
|
||||
names real things and leaks no secret. **Five red-proofs run** (ack gate, stale-preview gate,
|
||||
ONLINE-host gate, leg order inverted, `purgeEscrow=true`) — all failed red with the wrong value
|
||||
visible, then restored. Full suite green.
|
||||
|
||||
## v0.68.1 — fix the Configuration page layout broken by the wrapper-sha field (2026-07-21)
|
||||
|
||||
The v0.68.0 wrapper-sha256 row wrapped itself in a `<div>`. The artifacts **`<form>` IS the CSS
|
||||
|
||||
Reference in New Issue
Block a user