* fix(hetzner-purge): close volumes/primary_ips/floating_ips gap — wipe was leaving Crossplane orphans
Founder caught the gap on omantel.biz post-decommission: Hetzner
console showed 0 servers/LBs/IPs but 1 Volume + 2 Networks + 1
Firewall lingering. Networks/Firewall were the existing async-detach
window (handled by name-prefix fallback in the next provision); the
**Volume** was a hard miss — Purge() never called /v1/volumes.
Root cause: post-handover, the Hetzner Cloud Volume CSI driver
allocates Hetzner Volumes for every CNPG/Harbor/Loki/Mimir
StatefulSet PVC. tofu state never tracks them. When the operator
decommissions, `tofu destroy` is a no-op for the Volume and the
existing label-sweep didn't list /v1/volumes either. Result: orphan
volumes accrue cloud cost across re-provision cycles.
Same architectural gap for primary_ips (CCM-allocated for LoadBalancer
services since Hetzner's 2023 IP-decoupling) and floating_ips
(rare in Catalyst stack but listed for completeness).
Fix: extend Purge() + purgeByNamePrefix() to walk three additional
endpoints in dependency order:
servers → load_balancers → firewalls → networks → ssh_keys
→ volumes (after servers detach)
→ primary_ips (after LBs free their IPs)
→ floating_ips
Both label-pass AND name-prefix-pass cover all 8 kinds. PurgeReport
extended with Volumes/PrimaryIPs/FloatingIPs slices; Total() updated.
CSI-named volumes (`pvc-<uid>` form) won't match either pass — those
need the canonical `catalyst.openova.io/sovereign=<fqdn>` label which
the Crossplane composition for VolumeClaim must apply. That's a
separate composition-layer fix tracked separately; this PR closes
the wipe gap for everything labelled OR name-prefixed.
Bump chart 1.4.80 → 1.4.81.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(wizard): KServe was wrongly under Always Included on every Sovereign
Founder caught on console.openova.io/sovereign/wizard step 4: KServe
appeared in the "Always Included" section as if every Sovereign had
to install it. False positive — KServe is conditionally mandatory
ONLY when the operator opts into the CORTEX (AI/ML) product family.
Two coupled bugs:
(1) Data model: kserve was tagged tier:'mandatory' inside the CORTEX
product family, but tier:'mandatory' is consumed everywhere in
the wizard as "always-on regardless of family selection":
- componentGroups.ts:543 — seedIds.add(c.id) → auto-selected at
wizard init for every Sovereign
- applicationCatalog.ts:97 — seeded into the apps grid
- store.ts:642 — special-cased as undeselectable
- StepComponents.tsx — surfaced under "Always Included" tab
Demote to tier:'recommended'. CORTEX has
cascadeOnMemberSelection:true so picking any CORTEX member (vLLM,
Specter, BGE, Milvus, …) still auto-pulls KServe via the cascade
— that's the right semantics. KServe stays visible under CORTEX
in Tab 1 ("Choose Your Stack") and locks-in once CORTEX is
selected.
(2) UI filter: AlwaysIncludedTab was iterating every PRODUCTS entry
regardless of product.tier and listing every member with
component.tier === 'mandatory'. That mixes the platform-mandatory
layer (PILOT/SPINE/SURGE/SILO/GUARDIAN tier:'mandatory' families)
with conditional-mandatory members of opt-in families
(CORTEX/RELAY tier:'optional', INSIGHTS/FABRIC tier:'recommended').
Filter by product.tier === 'mandatory' so only the always-on
families' mandatory members appear. Defence-in-depth — even if a
new opt-in family ships with internal-mandatory members, they
won't leak into "Always Included".
Audit confirmed kserve was the only offender across all 9 product
families today. PILOT/SPINE/SURGE/SILO/GUARDIAN remain unchanged
(their members rightfully tier:'mandatory'); CORTEX kserve fixed;
others have no internal mandatories.
Bump chart 1.4.81 → 1.4.82.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>