hub: customer-claim password arc parts 1+2 — code engine, emails, ACK, configgen bake, UI (v0.50.0)

Closes DRILL-day0-vm F-4 hub-side: per-customer claim state (customer_claims,
bcrypt-only custody), the claim engine (issue at real config retrieve = Day-0
bake; first-report issue for live boxes; resend rotates generation; reset
rate-limited 3/day), three Hungarian emails via the dispatcher, report-ACK
claim object {code_hash, generation, issued_at} + set-only claimed ingest,
web.claim_code_* baked into generated controller.yaml, Setup-tab status chip
+ resend button, POST /api/v1/claim/reset-request (self-scoped), claim_lockout
event allowlisted. 13 new tests; full repo green.

Claude-Session: https://claude.ai/code/session_01NptTCFtu7dz2Ru89qHRagN
This commit is contained in:
2026-07-12 18:12:48 +02:00
parent b904477ed9
commit 6b40eb8619
14 changed files with 1103 additions and 7 deletions
@@ -421,6 +421,38 @@
</div>
</section>
<section class="card">
<h2>Dashboard access — customer claim</h2>
<div class="credential-row">
<div>
<span class="label">Claim status</span>
<div style="margin: 0.3rem 0;">
{{if .Claim}}
{{if .Claim.ClaimedAt}}
<span class="badge badge-ok">Claimed {{timeAgoPtr .Claim.ClaimedAt}}</span>
{{else if .Claim.EmailedAt}}
<span class="badge badge-warn">Nyitott — kód kiküldve {{timeAgoPtr .Claim.EmailedAt}}</span>
{{else}}
<span class="badge badge-error">Nyitott — a kód e-mail NEM ment ki (resend!)</span>
{{end}}
<span class="form-hint" style="margin-left: 0.5rem;">generation {{.Claim.Generation}} · issued {{timeAgo .Claim.IssuedAt}}</span>
{{else}}
<span class="badge badge-neutral">no code issued yet</span>
<span class="form-hint" style="margin-left: 0.5rem;">issued automatically at the first config pull or report</span>
{{end}}
</div>
<span class="form-hint">The customer sets + owns the dashboard password (claim code → own password). The code goes ONLY to the registered address ({{.Email}}); the hub stores a hash — no plaintext code exists to display.</span>
</div>
{{if .Claim}}
<form method="POST" action="/configs/{{.CustomerID}}/claim-resend" style="margin-top: 0.5rem;"
onsubmit="return confirm('Send a fresh code to the registered address? The previous code stops working immediately.')">
{{.CSRFField}}
<button type="submit" class="btn btn-outline btn-sm">{{if .Claim.ClaimedAt}}Visszaállító kód küldése{{else}}Kód újraküldése{{end}}</button>
</form>
{{end}}
</div>
</section>
<section class="card">
<h2>Setup Command</h2>
<p class="text-muted" style="margin-bottom: 1rem; font-size: 0.85rem;">