a6acb0f762
gates / gates (push) Successful in 6m27s
Security review of 50e47e29: requested_at is the box's clock — a future value is cut to the hub's
now; CustomerDeliberatelyOff now needs every host of the customer held (one machine of two no
longer silences the other), and fails closed on a read error or no host. Both red-proofed.
95 lines
3.6 KiB
Go
95 lines
3.6 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"time"
|
|
)
|
|
|
|
// ── The household's deliberate power-off (R-998, documentation/architecture/14-host-power-off.md) ──────
|
|
//
|
|
// The agent reports `deliberate_off.requested_at` from the moment the household pressed „A gép kikapcsolása"
|
|
// until the box goes down; the next boot's reports do not carry it. The hub keeps the time on the host row.
|
|
// While it is set and younger than DeliberateOffHold, every silence alarm about that box is held: host and
|
|
// controller staleness, the backup deadlines, off-site and restore-test freshness, the OS alarms and the
|
|
// kernel notice. After the hold, the alarms fire as usual — a box nobody switched back on in two weeks is
|
|
// worth a look. Pinned by monitor/deliberate_off_test.go, api/deliberate_off_test.go and web/deliberate_off_view_test.go.
|
|
|
|
// DeliberateOffHold is how long a deliberate power-off silences the box's alarms (operator default, 14 days).
|
|
const DeliberateOffHold = 14 * 24 * time.Hour
|
|
|
|
// DeliberateOffHeld reports whether a deliberate-off time still holds the alarms at now.
|
|
func DeliberateOffHeld(at *time.Time, now time.Time) bool {
|
|
return at != nil && !at.IsZero() && now.Sub(*at) < DeliberateOffHold
|
|
}
|
|
|
|
func parseDeliberateOff(v string) *time.Time {
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
t, err := time.Parse(time.RFC3339, v)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &t
|
|
}
|
|
|
|
// SetHostDeliberateOff records the report's deliberate-off field. at == nil clears it (a report without the
|
|
// field: the box is in a new boot). It returns the previous value, so the caller can emit an event on a change.
|
|
func (s *Store) SetHostDeliberateOff(hostID string, at *time.Time) (prev *time.Time, err error) {
|
|
var old sql.NullString
|
|
if err := s.db.QueryRow(`SELECT deliberate_off_at FROM hosts WHERE host_id = ?`, hostID).Scan(&old); err != nil {
|
|
return nil, err
|
|
}
|
|
prev = parseDeliberateOff(old.String)
|
|
var val any
|
|
if at != nil {
|
|
val = at.UTC().Format(time.RFC3339)
|
|
}
|
|
if (prev == nil && at == nil) || (prev != nil && at != nil && prev.Equal(*at)) {
|
|
return prev, nil
|
|
}
|
|
_, err = s.db.Exec(`UPDATE hosts SET deliberate_off_at = ?, updated_at = datetime('now') WHERE host_id = ?`, val, hostID)
|
|
return prev, err
|
|
}
|
|
|
|
// HostDeliberateOffAt returns the host's deliberate-off time (nil = none).
|
|
func (s *Store) HostDeliberateOffAt(hostID string) *time.Time {
|
|
var v sql.NullString
|
|
if err := s.db.QueryRow(`SELECT deliberate_off_at FROM hosts WHERE host_id = ?`, hostID).Scan(&v); err != nil {
|
|
return nil
|
|
}
|
|
return parseDeliberateOff(v.String)
|
|
}
|
|
|
|
// CustomerDeliberatelyOff reports whether the customer is switched off by the household and still within the hold
|
|
// at now — the checkers keyed by customer use it. EVERY host of the customer must be held (security review
|
|
// 2026-10-13): one switched-off machine must not silence the alarms of the customer's other machine. A read error
|
|
// or a customer with no host is NOT held (fail closed: the alarm fires). The time is the newest request.
|
|
func (s *Store) CustomerDeliberatelyOff(customerID string, now time.Time) (time.Time, bool) {
|
|
rows, err := s.db.Query(`SELECT COALESCE(deliberate_off_at, '') FROM hosts WHERE customer_id = ?`, customerID)
|
|
if err != nil {
|
|
return time.Time{}, false
|
|
}
|
|
defer rows.Close()
|
|
var newest time.Time
|
|
n := 0
|
|
for rows.Next() {
|
|
var v string
|
|
if rows.Scan(&v) != nil {
|
|
return time.Time{}, false
|
|
}
|
|
n++
|
|
t := parseDeliberateOff(v)
|
|
if !DeliberateOffHeld(t, now) {
|
|
return time.Time{}, false
|
|
}
|
|
if t.After(newest) {
|
|
newest = *t
|
|
}
|
|
}
|
|
if rows.Err() != nil || n == 0 {
|
|
return time.Time{}, false
|
|
}
|
|
return newest, true
|
|
}
|