hub v0.67.0 — auto-minted self-bind link, post-RESET staleness, unprovisioned-offsite warning
Four small items, each a case where the hub already knew something and said
nothing. Green: build, vet, tests all pass.
(a) Self-bind link is minted automatically at customer creation AND at RESET
completion (R-36 sub-item). The console banner tells the customer to open
"az e-mailben kapott link"; until now that email existed only once the
operator remembered the button, so the banner could point at something that
did not exist — during the 2026-07-18 rehearsal the box waited ~11.7 min on
exactly that. handleSelfBindLinkSend's body was extracted into a shared
mintAndSendSelfBindLink core so the button and the auto-mint callers cannot
drift apart on the honesty rules: F1 (no address -> mint nothing) and F2
(send failed -> delete the token, never leave it live). The wrapper NEVER
fails the operation it rides on — a create that provisioned Cloudflare,
offsite and PBS must not 500 over a courtesy email.
Gap found and closed while wiring it: PurgeCustomerResetDBState does NOT
clear selfbind_tokens, so a link minted BEFORE a reset would have stayed
live across it. A successful mint already replaces it (delete-then-insert,
single-active); the skip paths would not have, so they now clear stale
tokens too. Invariant: after auto-mint runs the only live link is one it
just issued, or none.
(b) Post-RESET staleness banner (R-37). When a RESET COMPLETED after the newest
report, every health figure on the page describes a lifecycle that no longer
exists, and the page kept showing pre-RESET warnings as current. Narrow on
purpose: an in-flight reset does not trigger it, and it clears itself when a
report arrives. Ties resolve to STALE — SQLite timestamps are second-
resolution and a same-second report almost certainly predates the reset;
erring the other way would hide the banner exactly when it matters.
(c) Unprovisioned-offsite warning (R-36 interim). enabled==true with type=="" is
a real, stable, silent state: provisioning is Save-triggered and the
re-enroll auto-re-issue deliberately skips an unprovisioned target, so
nothing self-heals it. Reuses the exact predicate the offsite re-issue
handler already refuses on.
(d) pbsdr_reissued rendered an EMPTY flash box — the key had no template branch,
so re-issuing PBS credentials showed a success box with no words (observed
live 2026-07-18). Now describes what was staged plus the R-39 caveat:
confirm `pvesm status` shows the entry active, because a converged agent can
report `applied` while the storage still 401s.
New .flash-warn (amber, --warn tokens) for the deviation tier between success
and error — exception-color principle: only on deviation, never on a healthy
page.
Tests assert each banner is ABSENT in the nominal cases as well as present in
the deviating one — a banner that always renders is worse than none. Both
red-proofed: deleting the pbsdr_reissued branch reproduces the original empty
box; neutering the staleness predicate fails the banner assertion. New
read-only store accessor CountSelfBindTokens makes the single-active invariant
assertable.
NOT in this train: the R-39 hub-side generation-bump fix the pre-travel task
made conditional. Its condition was REFUTED (SetHostDesired bumps
unconditionally; applyPBSDR is idempotent as documented) — the real mechanism is
the agent's descriptor-hash convergence and needs its own spec.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
This commit is contained in:
@@ -341,6 +341,18 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
// Claim (v0.50.0, customer-claim arc): the dashboard claim state for the Setup-tab card —
|
||||
// nil when no code has been issued yet (pre-arc / never-pulled customer).
|
||||
Claim *store.ClaimState
|
||||
|
||||
// StaleSinceReset (v0.67.0, R-37): a RESET completed AFTER the newest report, so every
|
||||
// health number on this page describes a lifecycle that no longer exists. Without this the
|
||||
// page keeps showing pre-RESET warnings as if they were current.
|
||||
StaleSinceReset bool
|
||||
ResetAt string
|
||||
|
||||
// OffsiteUnprovisioned (v0.67.0, R-36 interim): the customer's config says offsite is
|
||||
// ENABLED, but no descriptor was ever provisioned (type == ""). Provisioning is
|
||||
// Save-triggered (applyOffsite), and the re-enroll auto-re-issue deliberately skips an
|
||||
// unprovisioned target — so this state is stable and silent until someone presses Save.
|
||||
OffsiteUnprovisioned bool
|
||||
}
|
||||
|
||||
pendingSet := make(map[string]bool, len(pendingTails))
|
||||
@@ -369,6 +381,35 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
drUpdated = rec.UpdatedAt
|
||||
}
|
||||
|
||||
// R-37 (v0.67.0): is every health figure on this page pre-RESET? True only when a reset actually
|
||||
// COMPLETED and no report has arrived since. A reset still running, or one older than the newest
|
||||
// report, leaves the page exactly as it was.
|
||||
var staleSinceReset bool
|
||||
var resetAt string
|
||||
if cr, err := s.store.LatestCustomerReset(customerID); err != nil {
|
||||
s.logger.Printf("[WARN] LatestCustomerReset %s: %v", customerID, err)
|
||||
} else if cr != nil && cr.CompletedAt != nil {
|
||||
resetAt = cr.CompletedAt.Format("2006-01-02 15:04 MST")
|
||||
// Stale unless a report is STRICTLY newer than the reset. SQLite timestamps are
|
||||
// second-resolution, so a report and a reset can tie; a tie resolves to STALE because a
|
||||
// same-second report almost certainly arrived just before the reset destroyed the state it
|
||||
// describes. Erring the other way would hide the banner exactly when it matters most.
|
||||
staleSinceReset = customer == nil || !customer.ReceivedAt.After(*cr.CompletedAt)
|
||||
}
|
||||
|
||||
// R-36 interim (v0.67.0): enabled-but-unprovisioned is a real, stable state — the same predicate
|
||||
// the offsite re-issue handler already uses to refuse ("No provisioned offsite tier").
|
||||
var offsiteView struct {
|
||||
Offsite struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Type string `json:"type"`
|
||||
} `json:"offsite"`
|
||||
}
|
||||
if cfg != nil {
|
||||
_ = json.Unmarshal([]byte(cfg.ConfigJSON), &offsiteView)
|
||||
}
|
||||
offsiteUnprovisioned := offsiteView.Offsite.Enabled && offsiteView.Offsite.Type == ""
|
||||
|
||||
data := pageData{
|
||||
CustomerID: customerID,
|
||||
CustomerName: name,
|
||||
@@ -386,6 +427,10 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
OverallStatus: overallStatus,
|
||||
HostCause: hostCause,
|
||||
|
||||
StaleSinceReset: staleSinceReset,
|
||||
ResetAt: resetAt,
|
||||
OffsiteUnprovisioned: offsiteUnprovisioned,
|
||||
|
||||
LatestVersion: latestVersion,
|
||||
UpdateAvailable: updateAvailable,
|
||||
ControllerURL: controllerURL,
|
||||
@@ -561,6 +606,10 @@ func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] Customer config created: %s", customerID)
|
||||
// v0.67.0 (R-36 sub-item): mint the self-bind link NOW, not when the operator remembers. The
|
||||
// box's console banner tells the customer to open „az e-mailben kapott link" — that email should
|
||||
// already exist by the time anyone reads the banner. Never fails the create (see autoMint…).
|
||||
s.autoMintSelfBindLink(customerID, cfg.Email, "customer creation")
|
||||
s.bumpIntent(customerID) // Direction-2: wake a long-polling box in seconds
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=created", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user