hub v0.61.0 + felhom-tenantsync v1.1.0: Customer RESET (middle lifecycle tier)

One operator action returns a customer to pre-first-install: all operational
state dies (offsite repo, PBS namespace+backups, DR recipe, one-time secret,
claim state, retained escrow custody); identity + basic config + provenance +
events survive. Sits between host delete and customer Delete.

- store/customer_reset.go: customer_resets journal, live inventory, ack-gated
  purge (never touches identity/provenance/events), DeleteClaim.
- claim.ResetToUnclaimed: delete claim row -> fresh code next onboarding.
- offsite.Deprovision (idempotent) + OffsiteIdentifier + ClearProvisionedDescriptor.
- tenantsync.Deprovision + felhom-tenantsync.sh deprovision op (destroys ns +
  backup groups + token; shared user untouched; idempotent).
- web/customer_reset.go: GET reset -> inventory JSON; POST -> orchestration
  (external teardown FIRST, DB purge LAST; refuse-while-hosts; typed-id +
  separate escrow ack). Amber RESET card distinct from red Danger-zone Delete.
- Red-proofs: ack-gate + partial-failure resumability (both proven red);
  store ack-gating + journal round-trip; offsite idempotency + descriptor clear;
  RESET-card render. Green: build + vet + test.
This commit is contained in:
2026-07-17 13:09:04 +02:00
parent 6b1fbca51d
commit 4009401f46
17 changed files with 1193 additions and 13 deletions
@@ -55,6 +55,7 @@
{{else if eq .Flash "log_tail_requested"}}Log tail requested — the controller delivers it on its next report cycle (a few minutes). A customer-visible event line was recorded.
{{else if eq .Flash "claim-resent"}}Code re-sent to the registered address. A kód a doboz következő jelentésekor (~15 percen belül) aktiválódik.
{{else if eq .Flash "claim-resend-failed"}}Claim code resend FAILED — check the hub log (email delivery / send error).
{{else if eq .Flash "reset_done"}}Customer RESET complete — every operational trace was destroyed (offsite repo, PBS namespace, DR recipe, claim state, retained escrow custody). Identity and basic config survive; the audit event stream records it.
{{end}}
</div>
{{end}}
@@ -702,6 +703,84 @@
{{end}}
{{if .HasConfig}}
<!-- Reset customer (v0.61.0): the MIDDLE lifecycle tier — host delete < RESET < customer Delete.
One action returns the customer to pre-first-install: every OPERATIONAL trace dies (offsite
repo, PBS namespace, DR recipe, one-time secret, claim state, retained escrow custody), while
IDENTITY and the basic config SURVIVE. Amber (--warn), deliberately distinct from the red
Danger-zone Delete below it. Refuses while any host row exists (delete hosts first). -->
<section class="card" style="border-color: var(--warn);">
<h2>Ügyfél-visszaállítás <span class="text-muted" style="font-size: 0.8em; font-weight: normal;">(RESET — pre-első-telepítés)</span></h2>
<p class="text-muted">Egyetlen művelettel visszaállítja az ügyfelet az első telepítés előtti állapotba: <strong>minden működési állapot törlődik</strong> (offsite tároló, PBS névtér, DR-recept, egyszeri jelszó, azonosítási állapot). Az <strong>azonosság és az alapkonfiguráció megmarad</strong> (ügyfélrekord, előzmények, események). Ez NEM törli a hostokat — ha még van host, előbb azt kell törölni. Kevesebb, mint a Danger zone Delete: az ügyfél megmarad, csak a működési nyomok tűnnek el.</p>
<button type="button" class="btn btn-sm" style="border-color: var(--warn); color: var(--warn);" onclick="customerResetConfirm('{{.CustomerID}}')">Ügyfél visszaállítása&hellip;</button>
<div id="cust-reset-confirm-{{.CustomerID}}" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid var(--warn); background: var(--warn-dim); border-radius: var(--radius); max-width: 46em;">
<p id="cust-reset-inv-{{.CustomerID}}" style="margin: 0 0 0.5rem; font-size: 0.9em;">&hellip;</p>
<label id="cust-reset-escrow-row-{{.CustomerID}}" style="display: none; margin: 0 0 0.6rem; font-size: 0.85em; color: var(--crit);">
<input type="checkbox" id="cust-reset-escrow-{{.CustomerID}}">
<strong>Megőrzött helyreállítási-kulcs letét megsemmisítése</strong> — külön megerősítés (ez visszafordíthatatlanul törli a megőrzött escrow blobokat).
</label>
<p style="margin: 0 0 0.4rem; font-size: 0.85em; color: var(--text-2);">Írd be az ügyfél azonosítóját a megerősítéshez:</p>
<form method="POST" action="/configs/{{.CustomerID}}/reset" id="cust-reset-form-{{.CustomerID}}" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
{{.CSRFField}}
<input type="hidden" name="confirm_id" id="cust-reset-confirm-hidden-{{.CustomerID}}" value="">
<input type="hidden" name="escrow_ack" id="cust-reset-escrow-hidden-{{.CustomerID}}" value="">
<input type="text" id="cust-reset-input-{{.CustomerID}}" placeholder="ügyfél-azonosító&hellip;" style="padding: 0.3em 0.5em; width: 16em;">
<button type="button" class="btn btn-sm" id="cust-reset-go-{{.CustomerID}}" style="border-color: var(--warn); color: var(--warn);" onclick="customerResetSubmit('{{.CustomerID}}')">Megerősítés &amp; visszaállítás</button>
<button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('cust-reset-confirm-{{.CustomerID}}').style.display='none';">Mégse</button>
</form>
<p id="cust-reset-err-{{.CustomerID}}" style="margin: 0.4em 0 0; font-size: 0.8em; color: var(--crit);"></p>
</div>
</section>
<script>
function customerResetConfirm(cid) {
var box = document.getElementById('cust-reset-confirm-' + cid);
var inv = document.getElementById('cust-reset-inv-' + cid);
var go = document.getElementById('cust-reset-go-' + cid);
document.getElementById('cust-reset-input-' + cid).value = '';
document.getElementById('cust-reset-err-' + cid).textContent = '';
box.style.display = 'block';
inv.textContent = 'Leltár lekérése…';
go.disabled = false;
fetch('/configs/' + encodeURIComponent(cid) + '/reset')
.then(function(r){ return r.json(); })
.then(function(d){
if (d.refused) {
inv.innerHTML = '<strong style="color: var(--crit)">Elutasítva:</strong> ehhez az ügyfélhez még ' + d.host_count +
' host tartozik. A RESET soha nem töröl hostot — előbb töröld a host(oka)t.';
go.disabled = true;
document.getElementById('cust-reset-escrow-row-' + cid).style.display = 'none';
return;
}
var dies = [];
if (d.offsite_enabled) dies.push('offsite tároló' + (d.offsite_identifier ? ' (' + d.offsite_identifier + ')' : ''));
if (d.pbs_tenancy_configured) dies.push('PBS névtér + mentések');
if (d.dr_recipe_present) dies.push('DR-recept');
if (d.one_time_secret) dies.push('egyszeri jelszó');
if (d.claim_present) dies.push('azonosítási állapot (friss kód a következő onboardingnál)');
if (d.superseded_blobs > 0) dies.push(d.superseded_blobs + ' megőrzött escrow blob');
inv.innerHTML = '<strong>Törlődik:</strong> ' + (dies.length ? dies.join(', ') : 'nincs működési állapot') +
'. <strong>Megmarad:</strong> ügyfélrekord, alapkonfiguráció, előzmények, események.';
var escrowRow = document.getElementById('cust-reset-escrow-row-' + cid);
escrowRow.style.display = d.escrow_ack_required ? 'block' : 'none';
document.getElementById('cust-reset-escrow-' + cid).checked = false;
})
.catch(function(){ inv.textContent = 'A leltár nem kérhető le — a szerver minden feltételt így is kikényszerít.'; });
}
function customerResetSubmit(cid) {
var typed = document.getElementById('cust-reset-input-' + cid).value.trim();
var err = document.getElementById('cust-reset-err-' + cid);
if (typed !== cid) { err.textContent = 'A beírt azonosító nem egyezik.'; return; }
var escrowRow = document.getElementById('cust-reset-escrow-row-' + cid);
var escrowCb = document.getElementById('cust-reset-escrow-' + cid);
if (escrowRow.style.display !== 'none' && !escrowCb.checked) {
err.textContent = 'A megőrzött kulcs-letét megsemmisítéséhez pipáld be a külön megerősítést.';
return;
}
document.getElementById('cust-reset-confirm-hidden-' + cid).value = typed;
document.getElementById('cust-reset-escrow-hidden-' + cid).value = escrowCb.checked ? '1' : '';
document.getElementById('cust-reset-form-' + cid).submit();
}
</script>
<!-- Danger zone (v0.48.0 edit-a): the Block/Delete forms relocated verbatim from the
Customer Info header — endpoints and confirm() handlers unchanged. -->
<section class="card">