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
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:
@@ -0,0 +1,113 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/metrics"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/nightchain"
|
||||
)
|
||||
|
||||
// The missed-backup banner (R-871, `09` decision 110 — the operator's idea). The rule is the pure
|
||||
// nightchain.ComputeBanner; this file only gathers its inputs and renders. It hangs off executeTemplate like the
|
||||
// escrow bar (R-543), so it reaches every dashboard page of a logged-in household. Unlike that bar its dismissal is
|
||||
// DURABLE (the night ledger): closed until the NEXT missed backup time, gone by itself after a successful night.
|
||||
|
||||
// SetMissedBackupBanner wires the night ledger and the metrics record (nil ledger = no banner: backups off).
|
||||
func (s *Server) SetMissedBackupBanner(l *nightchain.Ledger, ms *metrics.MetricsStore) {
|
||||
s.nightLedger, s.metricsRecord = l, ms
|
||||
}
|
||||
|
||||
// missedBackupBanner computes the banner for now (nil = nothing to show).
|
||||
func (s *Server) missedBackupBanner(now time.Time) *nightchain.Banner {
|
||||
if s.nightLedger == nil || s.settings == nil {
|
||||
return nil
|
||||
}
|
||||
loc := budapestLocation()
|
||||
seeded, dumpOK, _, dismissed, _ := s.nightLedger.Snapshot()
|
||||
in := nightchain.BannerInput{
|
||||
Now: now, Window: s.effectiveBackupWindow(), Loc: loc,
|
||||
SeededAt: seeded, DBDumpOK: dumpOK, Dismissed: dismissed,
|
||||
}
|
||||
if s.backupMgr != nil && s.backupMgr.OffboxConfigured() {
|
||||
if t := s.settings.GetOffboxTarget(); t != nil && t.Enabled {
|
||||
in.OffsiteConfigured = true
|
||||
if ok, err := time.Parse(time.RFC3339, t.LastSuccess); err == nil {
|
||||
in.OffsiteOK = ok
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.metricsRecord != nil {
|
||||
ms := s.metricsRecord
|
||||
in.OnAtW = func(w time.Time) (bool, bool) {
|
||||
n, err := ms.SampleCount(w.Add(-5*time.Minute), w.Add(5*time.Minute))
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
return n > 0, true
|
||||
}
|
||||
if counts, err := ms.HourSampleCounts(now.Add(-7*24*time.Hour), now); err == nil {
|
||||
h := nightchain.HourDaysFrom(counts, loc)
|
||||
in.Hours = &h
|
||||
}
|
||||
}
|
||||
b := nightchain.ComputeBanner(in)
|
||||
if !b.Show {
|
||||
return nil
|
||||
}
|
||||
return &b
|
||||
}
|
||||
|
||||
// addMissedBackupBanner is called from executeTemplate for every authenticated page.
|
||||
func (s *Server) addMissedBackupBanner(data map[string]interface{}, r *http.Request) {
|
||||
if data == nil || r == nil || !s.hasAdminSession(r) {
|
||||
return
|
||||
}
|
||||
b := s.missedBackupBanner(time.Now())
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
lang := s.langFor(r)
|
||||
msg := s.i18n.Msg(lang, "banner.missed_backup.never")
|
||||
if !b.LastBackup.IsZero() {
|
||||
msg = s.i18n.Msgf(lang, "banner.missed_backup.last", b.DaysAgo)
|
||||
}
|
||||
if b.OffAt != "" {
|
||||
msg += " " + s.i18n.Msgf(lang, "banner.missed_backup.off_at", b.OffAt)
|
||||
}
|
||||
hint := s.i18n.Msg(lang, "banner.missed_backup.choose")
|
||||
if b.Suggest != "" {
|
||||
hint = s.i18n.Msgf(lang, "banner.missed_backup.suggest", b.Suggest)
|
||||
}
|
||||
data["MissedBackupBanner"] = true
|
||||
data["MissedBackupMessage"] = msg
|
||||
data["MissedBackupHint"] = hint
|
||||
data["MissedBackupBack"] = r.URL.Path
|
||||
if data["CSRFField"] == nil {
|
||||
data["CSRFField"] = s.csrfField(r)
|
||||
}
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] [web] missed-backup banner: rendered on %s (last=%s off_at=%q suggest=%q)", r.URL.Path,
|
||||
b.LastBackup.Format(time.RFC3339), b.OffAt, b.Suggest)
|
||||
}
|
||||
}
|
||||
|
||||
// missedBackupBannerDismissHandler (POST /backups/missed-banner/dismiss) closes the banner until the NEXT missed
|
||||
// backup time — durable, in the night ledger.
|
||||
func (s *Server) missedBackupBannerDismissHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if b := s.missedBackupBanner(time.Now()); b != nil && s.nightLedger != nil {
|
||||
s.nightLedger.DismissBanner(b.MissedAt)
|
||||
s.logger.Printf("[INFO] [web] missed-backup banner: closed by the household until the next missed backup time (after %s)",
|
||||
b.MissedAt.Format(time.RFC3339))
|
||||
}
|
||||
http.Redirect(w, r, redirectBackTo(r, "/launcher"), http.StatusFound)
|
||||
}
|
||||
|
||||
// budapestLocation is the backup window's clock.
|
||||
func budapestLocation() *time.Location {
|
||||
loc, err := time.LoadLocation("Europe/Budapest")
|
||||
if err != nil {
|
||||
return time.Local
|
||||
}
|
||||
return loc
|
||||
}
|
||||
Reference in New Issue
Block a user