hub v0.72.0 — R-70 + R-71c: offsite delivery-state detector, card, stuck event, R-39(a)-guarded self-heal restage
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKSN3gSg4TKVBBqkwW2djR
This commit is contained in:
@@ -913,6 +913,77 @@ func (s *Store) SaveReport(customerID string, reportJSON []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// reportOffsitePresence is the minimal parse for "does this controller report carry an offsite
|
||||
// status object" — the R-70 delivery-state signal. The report builder attaches `offsite` only when
|
||||
// the box actually has an offbox target configured, so presence == applied-on-the-box.
|
||||
type reportOffsitePresence struct {
|
||||
Offsite json.RawMessage `json:"offsite"`
|
||||
}
|
||||
|
||||
func reportHasOffsite(reportJSON string) bool {
|
||||
var p reportOffsitePresence
|
||||
if err := json.Unmarshal([]byte(reportJSON), &p); err != nil {
|
||||
return false // unparseable report → no offsite evidence
|
||||
}
|
||||
return len(p.Offsite) > 0 && string(p.Offsite) != "null"
|
||||
}
|
||||
|
||||
// LatestReportOffsitePresence reports whether the customer's most recent controller report exists
|
||||
// and whether it carries an offsite status object (R-70 detector input).
|
||||
func (s *Store) LatestReportOffsitePresence(customerID string) (found bool, receivedAt time.Time, hasOffsite bool, err error) {
|
||||
var recv, reportJSON string
|
||||
err = s.db.QueryRow(`SELECT received_at, report_json FROM reports WHERE customer_id = ? ORDER BY id DESC LIMIT 1`,
|
||||
customerID).Scan(&recv, &reportJSON)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, time.Time{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, time.Time{}, false, err
|
||||
}
|
||||
return true, parseSQLiteTime(recv), reportHasOffsite(reportJSON), nil
|
||||
}
|
||||
|
||||
// CountReportsOffsiteSince counts the customer's controller reports received strictly after `since`
|
||||
// (UTC) and how many of them carry an offsite status object (R-70 detector input: "N consecutive
|
||||
// reports since consume without offbox" == total>0 && withOffsite==0). Capped at 500 rows per call —
|
||||
// far beyond any detector threshold; the cap only bounds memory.
|
||||
func (s *Store) CountReportsOffsiteSince(customerID string, since time.Time) (total, withOffsite int, err error) {
|
||||
rows, err := s.db.Query(`SELECT report_json FROM reports WHERE customer_id = ? AND received_at > ? ORDER BY id LIMIT 500`,
|
||||
customerID, since.UTC().Format("2006-01-02 15:04:05"))
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var reportJSON string
|
||||
if err := rows.Scan(&reportJSON); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
total++
|
||||
if reportHasOffsite(reportJSON) {
|
||||
withOffsite++
|
||||
}
|
||||
}
|
||||
return total, withOffsite, rows.Err()
|
||||
}
|
||||
|
||||
// LastEventAt returns the created_at of the most recent event of the given type for a customer
|
||||
// (zero time when none). Durable across hub restarts — used as the cooldown/rate-limit source for
|
||||
// hub-emitted detector events (R-70/R-71c: a repeating pattern must surface as repeating events on
|
||||
// a bounded cadence, never as a silent retry loop OR a restart-reset flood).
|
||||
func (s *Store) LastEventAt(customerID, eventType string) (time.Time, error) {
|
||||
var createdAt string
|
||||
err := s.db.QueryRow(`SELECT created_at FROM events WHERE customer_id = ? AND event_type = ? ORDER BY id DESC LIMIT 1`,
|
||||
customerID, eventType).Scan(&createdAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return parseSQLiteTime(createdAt), nil
|
||||
}
|
||||
|
||||
// GetCustomers returns the latest report summary for each customer.
|
||||
func (s *Store) GetCustomers() ([]CustomerSummary, error) {
|
||||
rows, err := s.db.Query(`
|
||||
@@ -1249,6 +1320,47 @@ func (s *Store) ConsumeOneTimeSecret(customerID string) (string, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// OneTimeSecretInfo is the delivery-state metadata of a customer's one-time offsite secret —
|
||||
// timestamps ONLY, the value column is deliberately never selected (R-70 detector input; the
|
||||
// customer-scoped sibling of PBSDRHealStates' SecretUnconsumedFor).
|
||||
type OneTimeSecretInfo struct {
|
||||
CustomerID string
|
||||
CreatedAt time.Time
|
||||
ConsumedAt time.Time // zero = staged, not yet consumed
|
||||
}
|
||||
|
||||
// GetOneTimeSecretInfo returns the timestamps of a customer's one-time offsite secret row, or nil
|
||||
// when none exists. Never reads the value.
|
||||
func (s *Store) GetOneTimeSecretInfo(customerID string) (*OneTimeSecretInfo, error) {
|
||||
var createdAt string
|
||||
var consumedAt sql.NullString
|
||||
err := s.db.QueryRow(`SELECT created_at, consumed_at FROM one_time_secrets WHERE customer_id = ?`,
|
||||
customerID).Scan(&createdAt, &consumedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info := &OneTimeSecretInfo{CustomerID: customerID, CreatedAt: parseSQLiteTime(createdAt)}
|
||||
if consumedAt.Valid {
|
||||
info.ConsumedAt = parseSQLiteTime(consumedAt.String)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// SetOneTimeSecretTimesForTest back-dates a one-time secret's timestamps (SQLite datetime strings;
|
||||
// consumedAt "" leaves it NULL) so grace/age behavior is testable without sleeping. TEST-ONLY —
|
||||
// mirrors SetHostPBSSecretCreatedAtForTest.
|
||||
func (s *Store) SetOneTimeSecretTimesForTest(customerID, createdAt, consumedAt string) error {
|
||||
if consumedAt == "" {
|
||||
_, err := s.db.Exec(`UPDATE one_time_secrets SET created_at = ?, consumed_at = NULL WHERE customer_id = ?`, createdAt, customerID)
|
||||
return err
|
||||
}
|
||||
_, err := s.db.Exec(`UPDATE one_time_secrets SET created_at = ?, consumed_at = ? WHERE customer_id = ?`, createdAt, consumedAt, customerID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListCustomerConfigs returns all customer configurations ordered by ID.
|
||||
func (s *Store) ListCustomerConfigs() ([]CustomerConfig, error) {
|
||||
rows, err := s.db.Query(`
|
||||
|
||||
Reference in New Issue
Block a user