feat(hub): v0.70.0 — a deleted customer actually disappears (residue leg + ghost cleanup)
Found validating v0.69.0 against the live hub. demo-vm-felhom was deleted on 07-18 and was still on the Customers list AND still raising offsite_stale (10 events, latest 07-21 17:34, operator email at 19:34) — because GetCustomers() is report-derived and no lifecycle tier ever deleted a report. New leg 3 (residue), before the record purge: reports, app_telemetry, app_log_tails, log_tail_requests, customer_notifications, plus the credential-bearing appliance_registrations and selfbind_tokens. Audit (events, notification_log) and F-14 provenance still survive. Ghost customers are now deletable: 404 means "nothing here", not "no config row". With no config row the offsite descriptor is unknowable, so the Hetzner and descriptor legs record skipped_no_config rather than a bare "skipped". Two more red-proofs. 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,55 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.70.0 — a deleted customer actually disappears (the ghost + its alerts) (2026-07-21)
|
||||
|
||||
Found while validating v0.69.0 against the live hub, on the operator's report that `demo-vm-felhom`
|
||||
"was deleted but is still here". The delete HAD worked — config row gone, both hosts deleted, escrow
|
||||
tables empty, the RESET journal complete. The customer was still on the list because **`GetCustomers()`
|
||||
builds the Customers list purely from the REPORT stream** (`SELECT ... FROM reports GROUP BY
|
||||
customer_id`), and no lifecycle tier — host delete, RESET or DELETE — has ever deleted a report.
|
||||
|
||||
Not cosmetic: the **staleness and offsite checkers iterate that same report-derived list**, so the
|
||||
hub kept raising `offsite_stale` and kept **emailing the operator about a customer that no longer
|
||||
exists** — 10 events for `demo-vm-felhom`, the last one 3 days after its deletion.
|
||||
|
||||
### Leg 3: residue (new)
|
||||
|
||||
The cascade is now `hosts → RESET → residue → purge`. The residue leg deletes, in one transaction:
|
||||
`reports`, `app_telemetry`, `app_log_tails`, `log_tail_requests`, `customer_notifications` — plus two
|
||||
rows that are not telemetry at all but **credential-bearing**, and outliving their customer is a
|
||||
security defect rather than noise:
|
||||
|
||||
- `appliance_registrations` — a `token_hash` + `status='delivered'` row binding a box to the customer
|
||||
id. Deleting it returns a still-living box to the unclaimed pool on its next registration, which is
|
||||
the correct state for a decommissioned appliance.
|
||||
- `selfbind_tokens` — an unconsumed 7-day bind token is a working path to bind a box to a customer
|
||||
that does not exist.
|
||||
|
||||
It runs BEFORE the record purge on purpose: `customer_configs` is the identifying descriptor and goes
|
||||
last. `events`, `notification_log`, `host_deletions` and `customer_resets` still SURVIVE — the audit
|
||||
trail outlives every lifecycle tier, and that rule is not relaxed here. The counter and the purge walk
|
||||
**one shared table list**, so a table can never be counted-but-not-purged.
|
||||
|
||||
### Ghost customers are deletable
|
||||
|
||||
`handleCustomerDelete` / `handleCustomerDeletePreview` used to 404 whenever the config row was
|
||||
missing — so a customer deleted by any pre-v0.70.0 path could not be cleaned up by ANY operator
|
||||
surface. **404 now means "there is nothing here"** (no config, no host, no residue), not "there is no
|
||||
config row". With no config row the offsite descriptor is unknowable, so `commitCustomerReset` skips
|
||||
the Hetzner and descriptor legs and records **`skipped_no_config`** in the journal — never a bare
|
||||
`skipped`, which would read as "there was nothing to do". PBS is customer-id-keyed and idempotent, so
|
||||
it still runs. The dialog labels the case explicitly as a ghost and names the row count.
|
||||
|
||||
### Tests
|
||||
|
||||
`TestDeleteCascade_PurgesResidueAndUnlistsCustomer` (residue zeroed, customer gone from
|
||||
`GetCustomers()`, appliance + self-bind rows gone BY NAME, audit/provenance intact, journal legs
|
||||
`residue=ok customer_delete=ok`), `TestDeleteCascade_GhostCustomerIsDeletable` (the exact
|
||||
`demo-vm-felhom` shape: preview 200 with `has_config:false`, cascade completes, journal records
|
||||
`skipped_no_config`), `TestDeleteCascade_404WhenNothingRemains`. **Two more red-proofs**: dropping the
|
||||
residue leg leaves 5 residue rows and the customer still listed; restoring the `cfg == nil` 404 makes
|
||||
the ghost preview 404 again. Full suite green.
|
||||
|
||||
## 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**
|
||||
|
||||
Reference in New Issue
Block a user