# REPORT — hub v0.70.1: the ghost customer's Delete button must exist **Date:** 2026-07-22 · **Repo:** `felhom.eu` (hub only) · **XS render-gate fix, DEPLOYED** · **STOP honoured: the ghost delete itself is NOT run — awaiting the operator's click.** ## The defect (fourth inert seam — a template gate this time) v0.70.0's ghost-delete was fully implemented server-side (`handleCustomerDelete`/`Preview` accept `cfg == nil`) and fully unreachable: the Danger-zone card — and the `customerDeleteOpen/Submit` script — sat inside the `{{if .HasConfig}}` block that also wraps the RESET card (`customer_unified.html` old ~L736–L945). A ghost rendered no Danger zone at all. Handler tests POST directly and proved nothing about reachability. Confirmed live on `demo-vm-felhom` pre-fix. ## The change - **`configs.go` `handleCustomerUnified`**: new `Deletable` flag = the **exact negation** of the preview's 404 predicate (`cfg == nil && no hosts && residue empty`). Hosts were already fetched for the Host tab, so only the residue count is a new read — and it runs solely on the ghost shape. Lookup error → log + `Deletable=false` (fail toward hiding a destructive control). - **Template**: gate split — RESET card + its JS stay `{{if .HasConfig}}`; Danger zone + delete dialog + delete JS gate on `{{if .Deletable}}`; Block/Unblock forms get an inner `{{if .HasConfig}}`; ghost shape gets a one-sentence intro prefix (the dialog explains the rest). ## Tests (`customer_ghost_delete_render_test.go`) + red-proofs Render tests against the real template set, real store on `t.TempDir()`, asserting the delete form's `action` attribute and the `customerDeleteOpen(` call site (not free-text): - **A — ghost with residue** (reports only, no config, 0 hosts): delete form + opener + script present; RESET form ABSENT; Block/Unblock ABSENT. **PASS** - **B — configured customer**: every affordance of today present (RESET, Block, delete machinery); plus the blocked→Unblock branch (fixture note: `SaveCustomerConfig` never writes status — blocking goes through `SetCustomerConfigStatus`, same as the /block handler). **PASS** - **C — nothing left**: page-level **404**, mirroring the preview's semantics. There is **no renderable `Deletable=false` state** with real store reads — `customer != nil` requires a report row (`GetCustomer` selects from `reports`), and a report row makes `residue.Total() ≥ 1`. The only false-branch shape short of that is a residue-lookup error (fail-safe, compile-trivial). **Red-proof 1 (RUN):** Danger-zone gate reverted to `{{if .HasConfig}}` → test A **FAILED** on all three delete-affordance assertions ("ghost page must render the delete form … dead UI"). Restored → green. **Red-proof 2 (RUN):** RESET card gated on `Deletable` (the lookalike) → the RESET-absent assertion **FAILED** ("RESET card must NOT render for a ghost (found action=/configs/ghost/reset)"). Restored → green. `git diff` clean of scaffolding (0 REDPROOF markers). Green gate: `go build ./... && go vet ./... && go test ./...` — **green** (web suite 12.0 s). ## Ship (GitOps) | Step | Evidence | |---|---| | Feature commit | `f4c2c41` (code + template + tests + CHANGELOG + ROADMAP + CONTEXT + CLAUDE.md) | | Image | `felhom-hub:0.70.1` built + pushed, ID `f1741b46bfbb` | | Manifest bump | **separate chore commit** `068bce6` (operator preference honoured) | | Sync | ArgoCD hard-refresh + patch-sync → **Synced/Healthy**, rollout complete | | Live verify | pod image `felhom-hub:0.70.1`, imageID digest `sha256:f1741b46bfbb…` (= the built image), clean startup log, `/login` 200. Footer is on password-gated pages — the visual check lands on the operator's next login (endpoint-level method stated per convention) | ## AWAITING OPERATOR — the live leg is yours Open `demo-vm-felhom` → Edit tab (Danger zone now renders with the ghost prefix) → **Delete customer…** → three acks + typed id → confirm. Expected: customer gone from the Customers list, journal legs `residue=ok customer_delete=ok` with `skipped_no_config` on the Hetzner/descriptor legs, staleness emails stop. That click doubles as the v0.70.0 + v0.70.1 live validation; the ROADMAP R-25b row flips its "PENDING" note once done. ## Docs also in this train - `hub/CHANGELOG.md` v0.70.1 (inert-seam framing, template/handler lines cited). - `CLAUDE.md`: **the seam-wiring rule now covers template gates** — any conditional UI affordance ships with a render test per branch. - ROADMAP: R-25b annotated (v0.70.1 render fix, operator live leg PENDING); **R-17 → CLOSED** (operator console action: `u629193-sub1` deleted, no sub-accounts remain on PBS-storage-1, 2026-07-22). ## Observations (recorded, not acted on) - Pre-existing page-reachability quirk: a customer with host rows but neither config nor reports 404s on the unified page (configs.go `cfg == nil && customer == nil`) while the delete preview would 200. No such state exists in practice (hosts imply enrollment implies reports); noted only. - The Create Config button for ghosts is untouched (re-adopting a ghost stays a legitimate path).