gates / gates (push) Successful in 6m40s
hosts.deliberate_off_at follows the agent's deliberate_off field (set, then cleared by the first report of a new boot; one info event each way). While held, every silence alarm is held: host and node staleness, the 05:00 backup deadlines, off-site and restore-test freshness, the OS alarms and the kernel notice. After 14 days the usual alarms fire once, naming the power-off. Pages show KIKAPCSOLVA and the time. Consequence tests with controls; each guard red-proofed.
88 lines
3.2 KiB
Go
88 lines
3.2 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 any of the customer's hosts is deliberately off and still within the
|
|
// hold at now — the checkers keyed by customer use it. The time is the newest such request.
|
|
func (s *Store) CustomerDeliberatelyOff(customerID string, now time.Time) (time.Time, bool) {
|
|
rows, err := s.db.Query(`SELECT deliberate_off_at FROM hosts WHERE customer_id = ? AND deliberate_off_at IS NOT NULL`, customerID)
|
|
if err != nil {
|
|
return time.Time{}, false
|
|
}
|
|
defer rows.Close()
|
|
var newest time.Time
|
|
held := false
|
|
for rows.Next() {
|
|
var v string
|
|
if rows.Scan(&v) != nil {
|
|
continue
|
|
}
|
|
if t := parseDeliberateOff(v); DeliberateOffHeld(t, now) {
|
|
held = true
|
|
if t.After(newest) {
|
|
newest = *t
|
|
}
|
|
}
|
|
}
|
|
return newest, held
|
|
}
|