v0.295.0: a box that was off at its backup time catches up once (R-871, decision 109); the missed-backup banner (decision 110); a late daily timer after a host suspend is skipped
gates / gates (push) Successful in 31s

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-05 09:27:16 +02:00
parent e730a629fd
commit 635c33d381
24 changed files with 2074 additions and 18 deletions
+28
View File
@@ -329,3 +329,31 @@ func (s *MetricsStore) Prune(olderThan time.Duration) (int64, error) {
return total, nil
}
// SampleCount is how many system samples (one a minute) exist in [from, to) — the box's own record of whether it
// was on (R-871 banner: "the box was off at 02:30").
func (s *MetricsStore) SampleCount(from, to time.Time) (int, error) {
var n int
err := s.db.QueryRow(`SELECT COUNT(*) FROM system_metrics WHERE ts >= ? AND ts < ?`, from.Unix(), to.Unix()).Scan(&n)
return n, err
}
// HourSampleCounts returns the number of system samples per UTC hour start (unix seconds) in [from, to) — the
// banner's "when is this box usually on" pattern (R-871). At most 24×7 rows for a week.
func (s *MetricsStore) HourSampleCounts(from, to time.Time) (map[int64]int, error) {
rows, err := s.db.Query(`SELECT (ts / 3600) * 3600 AS h, COUNT(*) FROM system_metrics WHERE ts >= ? AND ts < ? GROUP BY h`, from.Unix(), to.Unix())
if err != nil {
return nil, err
}
defer rows.Close()
out := map[int64]int{}
for rows.Next() {
var h int64
var n int
if err := rows.Scan(&h, &n); err != nil {
return nil, err
}
out[h] = n
}
return out, rows.Err()
}