0.1.52 Gateway
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/logging"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
// gatewaySettingsState caches the operator's overrides in memory, the way maintenance is
|
||||
// cached: several of these are read on the request path — the household's timezone is
|
||||
// read by every home response — and none of them is worth a query.
|
||||
type gatewaySettingsState struct {
|
||||
mu sync.RWMutex
|
||||
value store.GatewaySettings
|
||||
}
|
||||
|
||||
func (g *gatewaySettingsState) get() store.GatewaySettings {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
return g.value
|
||||
}
|
||||
|
||||
func (g *gatewaySettingsState) set(value store.GatewaySettings) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.value = value
|
||||
}
|
||||
|
||||
// LoadGatewaySettings primes the cache and applies the settings that live in the running
|
||||
// process rather than being read where they are used. Called at boot and after a write.
|
||||
func (s *Server) LoadGatewaySettings(ctx context.Context) error {
|
||||
if s.store == nil {
|
||||
return nil
|
||||
}
|
||||
settings, err := s.store.GatewaySettings(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.gatewaySettings.set(settings)
|
||||
s.applyLogLevel(settings.LogLevel)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WatchGatewaySettings re-reads the overrides periodically, so a change made directly in
|
||||
// the database — or by another instance — is picked up without a restart. The same reason
|
||||
// WatchMaintenance exists.
|
||||
func (s *Server) WatchGatewaySettings(ctx context.Context, interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := s.LoadGatewaySettings(ctx); err != nil {
|
||||
s.log.Warn("gateway settings refresh failed",
|
||||
"component", "settings", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// applyLogLevel moves the running process's log level. It is a no-op on a gateway wired
|
||||
// without a level variable — every unit test in this package — and an empty override
|
||||
// restores the level the container was started with, which is what makes clearing the
|
||||
// setting in the console a real undo rather than a value the operator has to remember.
|
||||
func (s *Server) applyLogLevel(level string) {
|
||||
if s.logLevel == nil {
|
||||
return
|
||||
}
|
||||
if level == "" {
|
||||
s.logLevel.Set(s.deployedLogLevel)
|
||||
return
|
||||
}
|
||||
s.logLevel.Set(logging.ParseLevel(level))
|
||||
}
|
||||
|
||||
// --- effective values -------------------------------------------------------
|
||||
//
|
||||
// Each of these is "the override, or what was deployed". They are the only readers of the
|
||||
// cached document, so a setting is added by adding one of these beside its config field
|
||||
// rather than by teaching every call site that an override exists.
|
||||
|
||||
// householdLocation is the household's idea of what day it is. Everything that groups by
|
||||
// a local day reads it: the schedule rows, the hero rotation, sign-in history and the
|
||||
// first-use notification.
|
||||
func (s *Server) householdLocation() *time.Location {
|
||||
if name := s.gatewaySettings.get().Timezone; name != "" {
|
||||
if location, err := time.LoadLocation(name); err == nil {
|
||||
return location
|
||||
}
|
||||
}
|
||||
if s.cfg.SonarrLocation != nil {
|
||||
return s.cfg.SonarrLocation
|
||||
}
|
||||
return time.Local
|
||||
}
|
||||
|
||||
// householdTimezoneName is what the console and the sign-in history print. It names the
|
||||
// zone in force rather than the one deployed, so a page cannot claim a grouping that is
|
||||
// not the one the rows were grouped by.
|
||||
func (s *Server) householdTimezoneName() string {
|
||||
return s.householdLocation().String()
|
||||
}
|
||||
|
||||
func (s *Server) sessionIdleExpiry() time.Duration {
|
||||
if days := s.gatewaySettings.get().SessionIdleDays; days > 0 {
|
||||
return time.Duration(days) * 24 * time.Hour
|
||||
}
|
||||
return s.cfg.SessionIdleExpiry
|
||||
}
|
||||
|
||||
func (s *Server) sonarrAlertWindow() time.Duration {
|
||||
return overrideWindow(s.gatewaySettings.get().SonarrAlertMinutes, time.Minute,
|
||||
s.cfg.SonarrAlertWindow)
|
||||
}
|
||||
|
||||
func (s *Server) radarrAlertWindow() time.Duration {
|
||||
return overrideWindow(s.gatewaySettings.get().RadarrAlertMinutes, time.Minute,
|
||||
s.cfg.RadarrAlertWindow)
|
||||
}
|
||||
|
||||
func (s *Server) embyHealthInterval() time.Duration {
|
||||
return overrideWindow(s.gatewaySettings.get().EmbyHealthSeconds, time.Second,
|
||||
s.cfg.EmbyHealthInterval)
|
||||
}
|
||||
|
||||
// overrideWindow reads one of the three settings that can be switched off: a negative
|
||||
// value is off, zero is "whatever was deployed", anything else is the override in the
|
||||
// given unit.
|
||||
func overrideWindow(value int, unit, deployed time.Duration) time.Duration {
|
||||
switch {
|
||||
case value < 0:
|
||||
return 0
|
||||
case value > 0:
|
||||
return time.Duration(value) * unit
|
||||
default:
|
||||
return deployed
|
||||
}
|
||||
}
|
||||
|
||||
// deployedGatewaySettings describes what the container was started with, so the console
|
||||
// can show the value a cleared field falls back to. Deliberately not the same shape as
|
||||
// the overrides: these are facts, not choices, and nothing may write them back.
|
||||
type deployedGatewaySettings struct {
|
||||
Timezone string `json:"timezone"`
|
||||
LogLevel string `json:"logLevel"`
|
||||
SessionIdleDays int `json:"sessionIdleDays"`
|
||||
SonarrAlertMinutes int `json:"sonarrAlertMinutes"`
|
||||
RadarrAlertMinutes int `json:"radarrAlertMinutes"`
|
||||
EmbyHealthSeconds int `json:"embyHealthSeconds"`
|
||||
}
|
||||
|
||||
func (s *Server) deployedSettings() deployedGatewaySettings {
|
||||
timezone := ""
|
||||
if s.cfg.SonarrLocation != nil {
|
||||
timezone = s.cfg.SonarrLocation.String()
|
||||
}
|
||||
return deployedGatewaySettings{
|
||||
Timezone: timezone,
|
||||
LogLevel: levelName(s.deployedLogLevel),
|
||||
SessionIdleDays: int(s.cfg.SessionIdleExpiry / (24 * time.Hour)),
|
||||
SonarrAlertMinutes: int(s.cfg.SonarrAlertWindow / time.Minute),
|
||||
RadarrAlertMinutes: int(s.cfg.RadarrAlertWindow / time.Minute),
|
||||
EmbyHealthSeconds: int(s.cfg.EmbyHealthInterval / time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func levelName(level slog.Level) string {
|
||||
switch {
|
||||
case level < slog.LevelDebug:
|
||||
return "trace"
|
||||
case level < slog.LevelInfo:
|
||||
return "debug"
|
||||
case level < slog.LevelWarn:
|
||||
return "info"
|
||||
case level < slog.LevelError:
|
||||
return "warn"
|
||||
default:
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user