0.1.52 Gateway
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// GatewaySettingsKey is the app_settings row backing the gateway's own settings.
|
||||
const GatewaySettingsKey = "gateway_settings"
|
||||
|
||||
// GatewaySettings is the handful of server-level decisions an operator can change
|
||||
// without a redeployment.
|
||||
//
|
||||
// Every field is an *override* of the value the container was started with, and the zero
|
||||
// value means "whatever was deployed". That is the whole design: `.env` remains the
|
||||
// configuration — it is what a fresh container comes up with, what deploy-server.ps1
|
||||
// writes and what an operator locked out of the console still has — and this row is a
|
||||
// runtime amendment to it, so the console can always show the deployed value beside the
|
||||
// one in force and an operator can put a setting back by clearing it rather than by
|
||||
// remembering what it used to be.
|
||||
//
|
||||
// A window that can legitimately be *off* therefore needs a value distinct from "not
|
||||
// overridden", which is why the two alert windows and the health interval take -1 for off
|
||||
// and 0 for deployed. A plain zero would make "turn this off" indistinguishable from
|
||||
// "leave it alone", and the operator would find the setting quietly ignored.
|
||||
type GatewaySettings struct {
|
||||
// Timezone is an IANA name. It decides the household's local day, which is what the
|
||||
// schedule rows, the hero rotation, the sign-in history and the first-use
|
||||
// notifications are all grouped by.
|
||||
Timezone string `json:"timezone"`
|
||||
// LogLevel is one of trace, debug, info, warn, error. It takes effect immediately on
|
||||
// the running process, which is the point of it being here: turning debug on to watch
|
||||
// something happen is worth nothing if it costs a restart of the thing being watched.
|
||||
LogLevel string `json:"logLevel"`
|
||||
// SessionIdleDays is how long a television may go unused before it is signed out.
|
||||
SessionIdleDays int `json:"sessionIdleDays"`
|
||||
// SonarrAlertMinutes and RadarrAlertMinutes are how long a "just aired" or "new movie
|
||||
// added" banner stays on offer to a television that was switched off at the time.
|
||||
SonarrAlertMinutes int `json:"sonarrAlertMinutes"`
|
||||
RadarrAlertMinutes int `json:"radarrAlertMinutes"`
|
||||
// EmbyHealthSeconds is how often the reachability probe asks Emby whether it is there.
|
||||
EmbyHealthSeconds int `json:"embyHealthSeconds"`
|
||||
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
UpdatedBy string `json:"updatedBy,omitempty"`
|
||||
}
|
||||
|
||||
// GatewaySettingsOff is the value a duration-like override takes to mean "switched off",
|
||||
// as distinct from the zero that means "use the deployed value".
|
||||
const GatewaySettingsOff = -1
|
||||
|
||||
// GatewayLogLevels is the vocabulary the console offers, in order of severity. It is here
|
||||
// rather than in the API because normalisation has to refuse a level nothing can parse,
|
||||
// and refusing is worth nothing if the list it refuses against lives somewhere else.
|
||||
var GatewayLogLevels = []string{"trace", "debug", "info", "warn", "error"}
|
||||
|
||||
func normalizeGatewaySettings(settings GatewaySettings) GatewaySettings {
|
||||
settings.Timezone = strings.TrimSpace(settings.Timezone)
|
||||
if settings.Timezone != "" {
|
||||
// A timezone that will not load is dropped rather than stored: the alternative is
|
||||
// a row that every later read has to fail on, and the failure would surface as a
|
||||
// household whose idea of "today" quietly reverted with nothing saying why.
|
||||
if _, err := time.LoadLocation(settings.Timezone); err != nil {
|
||||
settings.Timezone = ""
|
||||
}
|
||||
}
|
||||
settings.LogLevel = strings.ToLower(strings.TrimSpace(settings.LogLevel))
|
||||
if settings.LogLevel != "" && !containsString(GatewayLogLevels, settings.LogLevel) {
|
||||
settings.LogLevel = ""
|
||||
}
|
||||
settings.SessionIdleDays = clampOverride(settings.SessionIdleDays, 1, 3650, false)
|
||||
settings.SonarrAlertMinutes = clampOverride(settings.SonarrAlertMinutes, 1, 24*60, true)
|
||||
settings.RadarrAlertMinutes = clampOverride(settings.RadarrAlertMinutes, 1, 7*24*60, true)
|
||||
settings.EmbyHealthSeconds = clampOverride(settings.EmbyHealthSeconds, 10, 3600, true)
|
||||
return settings
|
||||
}
|
||||
|
||||
// clampOverride keeps 0 meaning "deployed" and, where the setting can be switched off,
|
||||
// keeps every negative number meaning off rather than only -1 — an operator typing -5 has
|
||||
// said the same thing, and storing it verbatim would produce a second value with the same
|
||||
// meaning that every reader would have to know about.
|
||||
func clampOverride(value, low, high int, offAllowed bool) int {
|
||||
if value == 0 {
|
||||
return 0
|
||||
}
|
||||
if value < 0 {
|
||||
if offAllowed {
|
||||
return GatewaySettingsOff
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return max(low, min(value, high))
|
||||
}
|
||||
|
||||
func containsString(values []string, value string) bool {
|
||||
for _, candidate := range values {
|
||||
if candidate == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Store) GatewaySettings(ctx context.Context) (GatewaySettings, error) {
|
||||
var raw []byte
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT value FROM app_settings WHERE key = $1`, GatewaySettingsKey).Scan(&raw)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return GatewaySettings{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return GatewaySettings{}, fmt.Errorf("store: read gateway settings: %w", err)
|
||||
}
|
||||
var settings GatewaySettings
|
||||
if err := json.Unmarshal(raw, &settings); err != nil {
|
||||
return GatewaySettings{}, fmt.Errorf("store: decode gateway settings: %w", err)
|
||||
}
|
||||
return normalizeGatewaySettings(settings), nil
|
||||
}
|
||||
|
||||
// SetGatewaySettings writes the overrides and returns what was actually stored, because
|
||||
// normalisation can refuse a value and the console must show what is in force rather than
|
||||
// what was typed.
|
||||
func (s *Store) SetGatewaySettings(
|
||||
ctx context.Context, settings GatewaySettings,
|
||||
) (GatewaySettings, error) {
|
||||
settings = normalizeGatewaySettings(settings)
|
||||
settings.UpdatedAt = time.Now().UTC()
|
||||
raw, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
return GatewaySettings{}, err
|
||||
}
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
INSERT INTO app_settings (key, value, updated_at)
|
||||
VALUES ($1, $2::jsonb, now())
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
|
||||
GatewaySettingsKey, string(raw))
|
||||
if err != nil {
|
||||
return GatewaySettings{}, fmt.Errorf("store: write gateway settings: %w", err)
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
Reference in New Issue
Block a user