This commit is contained in:
ponzischeme89
2026-08-22 21:47:54 +12:00
parent d2f7f84cbf
commit 9a1f44da46
27 changed files with 452 additions and 110 deletions
+22 -15
View File
@@ -11,10 +11,10 @@ import (
)
// adminGatewaySettingsResponse is deliberately three things at once: what the operator has
// chosen, what the container was started with, and what is therefore in force. A settings
// page that showed only the first would leave every empty field looking like a value of
// nothing, when an empty field here means "whatever .env says" — and the operator has no
// other way to see what that is without opening a file on the NAS.
// chosen, the deployed or server defaults, and what is therefore in force. A settings page
// that showed only the first would leave every empty environment-backed field looking like
// a value of nothing, when it means "whatever .env says" — and the operator has no other
// way to see that without opening a file on the NAS.
type adminGatewaySettingsResponse struct {
Settings store.GatewaySettings `json:"settings"`
Deployed deployedGatewaySettings `json:"deployed"`
@@ -22,7 +22,8 @@ type adminGatewaySettingsResponse struct {
// LogLevels is the vocabulary rather than a list the console keeps its own copy of,
// for the reason the preference catalogue is served with the accounts page: a value
// the server would refuse must never be offerable.
LogLevels []string `json:"logLevels"`
LogLevels []string `json:"logLevels"`
NotificationDisplays []string `json:"notificationDisplays"`
// Version and Timezone name the process this page is about, so the page can identify
// the gateway it is changing without a second request.
Version string `json:"version"`
@@ -34,17 +35,19 @@ func (s *Server) gatewaySettingsResponse() adminGatewaySettingsResponse {
Settings: settings,
Deployed: s.deployedSettings(),
Effective: deployedGatewaySettings{
Timezone: s.householdTimezoneName(),
LogLevel: s.effectiveLogLevel(),
SessionIdleDays: int(s.sessionIdleExpiry() / (24 * time.Hour)),
SonarrAlertMinutes: int(s.sonarrAlertWindow() / time.Minute),
RadarrAlertMinutes: int(s.radarrAlertWindow() / time.Minute),
EmbyHealthSeconds: int(s.embyHealthInterval() / time.Second),
SlowRequestMillis: effectiveSlowRequestMillis(s.slowRequestThreshold()),
LibrarySyncMinutes: int(s.LibrarySyncInterval() / time.Minute),
Timezone: s.householdTimezoneName(),
LogLevel: s.effectiveLogLevel(),
SessionIdleDays: int(s.sessionIdleExpiry() / (24 * time.Hour)),
SonarrAlertMinutes: int(s.sonarrAlertWindow() / time.Minute),
RadarrAlertMinutes: int(s.radarrAlertWindow() / time.Minute),
NotificationDisplay: s.notificationDisplay(),
EmbyHealthSeconds: int(s.embyHealthInterval() / time.Second),
SlowRequestMillis: effectiveSlowRequestMillis(s.slowRequestThreshold()),
LibrarySyncMinutes: int(s.LibrarySyncInterval() / time.Minute),
},
LogLevels: store.GatewayLogLevels,
Version: buildinfo.Version(),
LogLevels: store.GatewayLogLevels,
NotificationDisplays: store.GatewayNotificationDisplays,
Version: buildinfo.Version(),
}
}
@@ -94,6 +97,7 @@ func (s *Server) handleAdminGatewaySettings(w http.ResponseWriter, r *http.Reque
s.loggerFor(r.Context()).Info("gateway settings changed",
"timezone", s.householdTimezoneName(), "log_level", s.effectiveLogLevel(),
"notification_display", s.notificationDisplay(),
"session_idle_days", int(s.sessionIdleExpiry()/(24*time.Hour)),
"operator", operator)
if summary := gatewaySettingsChanges(previous, stored); summary != "" {
@@ -130,6 +134,9 @@ func gatewaySettingsChanges(before, after store.GatewaySettings) string {
if before.RadarrAlertMinutes != after.RadarrAlertMinutes {
changes = append(changes, "film alert window")
}
if before.NotificationDisplay != after.NotificationDisplay {
changes = append(changes, "notification display")
}
if before.EmbyHealthSeconds != after.EmbyHealthSeconds {
changes = append(changes, "Emby health probe")
}
+33 -19
View File
@@ -125,6 +125,17 @@ func (s *Server) radarrAlertWindow() time.Duration {
s.cfg.RadarrAlertWindow)
}
// notificationDisplay is the household-wide banner policy. The store normalises the
// cached value, and this fallback keeps tests and a not-yet-primed server safe too.
func (s *Server) notificationDisplay() string {
switch display := s.gatewaySettings.get().NotificationDisplay; display {
case store.NotificationDisplayEverywhere, store.NotificationDisplayOff:
return display
default:
return store.NotificationDisplayHomeOnly
}
}
// slowRequestThreshold is the line between a request that logs a breakdown and one that
// does not. Switched off it returns a duration no request can exceed rather than zero,
// because zero would mean *every* request carried one — the opposite of what the operator
@@ -166,18 +177,20 @@ func overrideWindow(value int, unit, deployed time.Duration) time.Duration {
}
}
// 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.
// deployedGatewaySettings describes the defaults a cleared field falls back to: deployed
// values for environment-backed settings, and Home only for the native notification
// policy. Deliberately not the same shape as the stored document: 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"`
SlowRequestMillis int `json:"slowRequestMillis"`
LibrarySyncMinutes int `json:"librarySyncMinutes"`
Timezone string `json:"timezone"`
LogLevel string `json:"logLevel"`
SessionIdleDays int `json:"sessionIdleDays"`
SonarrAlertMinutes int `json:"sonarrAlertMinutes"`
RadarrAlertMinutes int `json:"radarrAlertMinutes"`
NotificationDisplay string `json:"notificationDisplay"`
EmbyHealthSeconds int `json:"embyHealthSeconds"`
SlowRequestMillis int `json:"slowRequestMillis"`
LibrarySyncMinutes int `json:"librarySyncMinutes"`
}
func (s *Server) deployedSettings() deployedGatewaySettings {
@@ -186,14 +199,15 @@ func (s *Server) deployedSettings() deployedGatewaySettings {
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),
SlowRequestMillis: int(s.cfg.SlowRequestThreshold / time.Millisecond),
LibrarySyncMinutes: int(s.cfg.SyncInterval / time.Minute),
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),
NotificationDisplay: store.NotificationDisplayHomeOnly,
EmbyHealthSeconds: int(s.cfg.EmbyHealthInterval / time.Second),
SlowRequestMillis: int(s.cfg.SlowRequestThreshold / time.Millisecond),
LibrarySyncMinutes: int(s.cfg.SyncInterval / time.Minute),
}
}
@@ -54,6 +54,17 @@ func TestGatewaySettingsChangesReportsOnlyWhatMoved(t *testing.T) {
}
}
func TestNotificationDisplayDefaultsToHomeOnly(t *testing.T) {
s := &Server{}
if got := s.notificationDisplay(); got != store.NotificationDisplayHomeOnly {
t.Fatalf("unset notification display = %q, want %q", got, store.NotificationDisplayHomeOnly)
}
s.gatewaySettings.set(store.GatewaySettings{NotificationDisplay: store.NotificationDisplayEverywhere})
if got := s.notificationDisplay(); got != store.NotificationDisplayEverywhere {
t.Fatalf("notification display = %q, want everywhere", got)
}
}
func TestLevelNameCoversTheVocabulary(t *testing.T) {
for _, level := range store.GatewayLogLevels {
if got := levelName(parseTestLevel(t, level)); got != level {
+3 -1
View File
@@ -109,9 +109,10 @@ func (s *Server) handleServiceStatus(w http.ResponseWriter, r *http.Request, ses
message = store.DefaultMaintenanceMessage
}
alerts := []clientAlert{}
notificationDisplay := s.notificationDisplay()
// Nothing to celebrate while the service is down, and the client is showing the
// maintenance screen anyway.
if !state.Enabled {
if !state.Enabled && notificationDisplay != store.NotificationDisplayOff {
published, sonarr := s.publishedAlerts(r.Context()), s.sonarrAiredAlerts(r.Context())
if len(published) > 0 || len(sonarr) > 0 {
prefs, err := s.notificationPreferencesFor(r.Context(), sess.EmbyUserID)
@@ -136,6 +137,7 @@ func (s *Server) handleServiceStatus(w http.ResponseWriter, r *http.Request, ses
"quietTime": quiet.Active,
"message": message,
"alerts": alerts,
"notificationDisplay": notificationDisplay,
"compatible": compatible,
"compatibilityMessage": compatibilityMessage,
"clientVersion": clientVersion(r),