0.1.52 Gateway

This commit is contained in:
ponzischeme89
2026-08-17 19:09:17 +12:00
parent d5a8d3ad88
commit 1da91e40a1
46 changed files with 1626 additions and 106 deletions
@@ -0,0 +1,149 @@
package api
import (
"encoding/json"
"net/http"
"time"
"github.com/ponzischeme89/memby/server/internal/adminevents"
"github.com/ponzischeme89/memby/server/internal/buildinfo"
"github.com/ponzischeme89/memby/server/internal/store"
)
// 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.
type adminGatewaySettingsResponse struct {
Settings store.GatewaySettings `json:"settings"`
Deployed deployedGatewaySettings `json:"deployed"`
Effective deployedGatewaySettings `json:"effective"`
// 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"`
// 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"`
}
func (s *Server) gatewaySettingsResponse() adminGatewaySettingsResponse {
settings := s.gatewaySettings.get()
return 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),
},
LogLevels: store.GatewayLogLevels,
Version: buildinfo.Version(),
}
}
func (s *Server) effectiveLogLevel() string {
if s.logLevel != nil {
return levelName(s.logLevel.Level())
}
return levelName(s.deployedLogLevel)
}
// handleAdminGatewaySettings serves the page and accepts a write on the same path, which
// is the shape the *arr policy routes already take.
func (s *Server) handleAdminGatewaySettings(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
writeJSON(w, http.StatusOK, s.gatewaySettingsResponse())
return
}
var req store.GatewaySettings
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8<<10)).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "malformed request body")
return
}
previous := s.gatewaySettings.get()
_, operator, _ := s.browserSession(r, adminSessionPurpose)
req.UpdatedBy = operator
stored, err := s.store.SetGatewaySettings(r.Context(), req)
if err != nil {
s.loggerFor(r.Context()).Error("gateway settings write failed", "error", err)
writeError(w, http.StatusInternalServerError, "could not save gateway settings")
return
}
// Applied here rather than waited for: the watcher would pick this up within thirty
// seconds, and an operator who has just turned debug logging on and gone to look at
// the log would spend that half minute believing it had not worked.
s.gatewaySettings.set(stored)
s.applyLogLevel(stored.LogLevel)
s.loggerFor(r.Context()).Info("gateway settings changed",
"timezone", s.householdTimezoneName(), "log_level", s.effectiveLogLevel(),
"session_idle_days", int(s.sessionIdleExpiry()/(24*time.Hour)),
"operator", operator)
if summary := gatewaySettingsChanges(previous, stored); summary != "" {
s.publishAdmin(r.Context(), adminevents.Event{
Type: adminevents.TypeSettingsChanged,
Severity: adminevents.SeverityWarning,
Title: "Gateway settings changed",
Summary: summary,
Actor: operator,
Link: "/admin/settings",
})
}
writeJSON(w, http.StatusOK, s.gatewaySettingsResponse())
}
// gatewaySettingsChanges is what the notification says, and returns empty when nothing
// moved. The console re-posts the whole document, so a save that changed nothing is an
// ordinary event — the preferenceChanges rule, for the same reason: a feed that records
// every press of Save is one nobody reads.
func gatewaySettingsChanges(before, after store.GatewaySettings) string {
changes := []string{}
if before.Timezone != after.Timezone {
changes = append(changes, "timezone")
}
if before.LogLevel != after.LogLevel {
changes = append(changes, "log level")
}
if before.SessionIdleDays != after.SessionIdleDays {
changes = append(changes, "session expiry")
}
if before.SonarrAlertMinutes != after.SonarrAlertMinutes {
changes = append(changes, "episode alert window")
}
if before.RadarrAlertMinutes != after.RadarrAlertMinutes {
changes = append(changes, "film alert window")
}
if before.EmbyHealthSeconds != after.EmbyHealthSeconds {
changes = append(changes, "Emby health probe")
}
switch len(changes) {
case 0:
return ""
case 1:
return "The gateway's " + changes[0] + " was changed"
default:
return "The gateway's " + joinPhrase(changes) + " were changed"
}
}
func joinPhrase(values []string) string {
switch len(values) {
case 0:
return ""
case 1:
return values[0]
}
out := ""
for index, value := range values[:len(values)-1] {
if index > 0 {
out += ", "
}
out += value
}
return out + " and " + values[len(values)-1]
}