hub v0.114.0: self-bind auto-send while a customer waits for a box (R-509); node_* bypass the quiet hour (ruling 2026-09-15); PBS re-issue adopts an endpoint token (R-511); controller supervisor events (R-523); event registers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -3,6 +3,7 @@ package web
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -118,6 +119,7 @@ func (s *Server) autoMintSelfBindLink(customerID, email, occasion string) {
|
||||
switch outcome {
|
||||
case selfBindSent:
|
||||
s.logger.Printf("[INFO] self-bind link auto-minted for %s on %s (the console banner's promised email now exists)", customerID, occasion)
|
||||
s.recordSelfBindSent(customerID, occasion)
|
||||
case selfBindSkippedNoMailer:
|
||||
s.logger.Printf("[INFO] self-bind link NOT auto-minted for %s on %s: no mailer configured on this hub", customerID, occasion)
|
||||
case selfBindSkippedNoEmail:
|
||||
@@ -129,6 +131,36 @@ func (s *Server) autoMintSelfBindLink(customerID, email, occasion string) {
|
||||
}
|
||||
}
|
||||
|
||||
// selfBindSentEvent (R-509, v0.114.0) records WHEN and WHY a self-bind link went out, so the customer
|
||||
// page can say „Kapcsolódó link elküldve: <date> (<occasion>)". Hub-internal: stored, never dispatched.
|
||||
const selfBindSentEvent = "selfbind_link_sent"
|
||||
|
||||
func (s *Server) recordSelfBindSent(customerID, occasion string) {
|
||||
details, _ := json.Marshal(map[string]string{"occasion": occasion})
|
||||
if _, err := s.store.SaveEvent(customerID, selfBindSentEvent, "info", "Self-bind link e-mailed ("+occasion+")", string(details), "hub"); err != nil {
|
||||
s.logger.Printf("[WARN] self-bind: sent-record for %s not stored: %v", customerID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// autoMintSelfBindIfWaiting (R-509, operator decision A 2026-09-15) sends the link whenever an event
|
||||
// leaves a customer WAITING FOR A BOX: an e-mail set or changed on a customer with no bound host, and a
|
||||
// host delete that keeps the customer. Appliance registration is deliberately NOT a trigger — it knows
|
||||
// no customer (api/appliance.go). No host bound is re-checked here, so a customer who still has a box
|
||||
// is never mailed a pairing link.
|
||||
func (s *Server) autoMintSelfBindIfWaiting(customerID, occasion string) {
|
||||
cfg, err := s.store.GetCustomerConfig(customerID)
|
||||
if err != nil || cfg == nil {
|
||||
return
|
||||
}
|
||||
if h, herr := s.store.GetHostByCustomer(customerID); herr != nil || h != nil {
|
||||
if herr != nil {
|
||||
s.logger.Printf("[WARN] self-bind auto-send for %s on %s skipped: host lookup failed: %v", customerID, occasion, herr)
|
||||
}
|
||||
return
|
||||
}
|
||||
s.autoMintSelfBindLink(customerID, cfg.Email, occasion)
|
||||
}
|
||||
|
||||
// handleSelfBindLinkSend — POST /customers/{id}/selfbind-link. Mints a single-active capability token
|
||||
// for the customer and emails the public bind link. Honesty rules:
|
||||
// - F1: no registered email → nothing is minted, LOUD flash (a link no one can receive is useless).
|
||||
@@ -158,6 +190,7 @@ func (s *Server) handleSelfBindLinkSend(w http.ResponseWriter, r *http.Request,
|
||||
s.logger.Printf("[ERROR] self-bind link for %s: %v", customerID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
default: // selfBindSent (the no-mailer case was refused above)
|
||||
s.recordSelfBindSent(customerID, "operator button")
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=selfbind-sent#tab=setup", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user