635c33d381
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
114 lines
3.9 KiB
Go
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
|
|
}
|