package api import ( "context" "net/http" "sync" "time" "github.com/ponzischeme89/memby/server/internal/store" ) // maintenanceState caches the operator switch in memory so the hot path never queries // Postgres, while Postgres stays the source of truth across restarts. type maintenanceState struct { mu sync.RWMutex value store.Maintenance } func (m *maintenanceState) get() store.Maintenance { m.mu.RLock() defer m.mu.RUnlock() return m.value } func (m *maintenanceState) set(value store.Maintenance) { m.mu.Lock() defer m.mu.Unlock() m.value = value } // LoadMaintenance primes the cached switch. Called at boot, and after every toggle. func (s *Server) LoadMaintenance(ctx context.Context) error { state, err := s.store.Maintenance(ctx) if err != nil { return err } s.maintenance.set(state) if state.Enabled { s.log.Warn("starting in maintenance mode", "message", state.Message) } return nil } // WatchMaintenance re-reads the switch periodically, so a change made directly in the // database (or by another instance) is picked up without a restart. func (s *Server) WatchMaintenance(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.LoadMaintenance(ctx); err != nil { s.log.Warn("maintenance refresh failed", "error", err) } } } } // maintenanceGate turns the whole client API off, independently of Emby. // // 503 with a machine-readable `maintenance: true` so the TV can show the operator's // message rather than a generic network error. Admin routes and health checks are // deliberately outside this gate — you need them most while the app is down. func (s *Server) maintenanceGate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { state := s.maintenance.get() if !state.Enabled { next.ServeHTTP(w, r) return } message := state.Message if message == "" { message = store.DefaultMaintenanceMessage } // Retry-After keeps well-behaved clients from hammering a service that has // already said it is unavailable. w.Header().Set("Retry-After", "300") writeJSON(w, http.StatusServiceUnavailable, map[string]any{ "error": message, "maintenance": true, "message": message, }) }) }