hub: box presence from the wait channel + delete a switched-off box at once (R-30, D2)

Slice 1: intent.Hub records one start/end per GET /api/v1/wait request and answers
Presence(customer, now): connected (hold open, or one started < 333 s = 243 s cadence + 90 s
grace ago), not connected since T, or unknown (hub up < 333 s; in memory only). The host page
shows "Box connection". One DEBUG line per presence change.

Slice 2 (operator ruling D2, 2026-10-08, 09 §3 decision 186): a host that is online by its report
clock but whose box has had no wait-channel connection for >= 360 s may be deleted at once after
the tick "I checked: the box is off". Presence is re-read at POST time; the tick alone, unknown
presence, a connected box or a shorter gap keep today's 409. The delete logs the operator channel
and saves one host_deleted_box_off event. RESET and the customer-delete cascade are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-08 14:28:42 +02:00
parent e6184353db
commit d604e624a3
9 changed files with 685 additions and 12 deletions
+5
View File
@@ -48,6 +48,11 @@ func (h *Handler) handleWait(w http.ResponseWriter, r *http.Request) {
return
}
// R-30: box presence — recorded ONCE per request (intent.Wait below runs once per heartbeat
// window), and ended on every exit path.
h.intentHub.MarkWaitStart(customerID)
defer h.intentHub.MarkWaitEnd(customerID)
var lastSeen uint64
if s := r.URL.Query().Get("gen"); s != "" {
if v, err := strconv.ParseUint(s, 10, 64); err == nil {