Files
felhom-controller/controller/internal/web/missed_backup_banner.go
T

114 lines
3.9 KiB
Go

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
}