// Package server exposes a read-only HTTP status endpoint for the daemon: // the live encode progress, the pending input queue, and recent log events. package server import ( _ "embed" "encoding/json" "net/http" "path/filepath" "av1dae/internal/logger" "av1dae/internal/settings" "av1dae/internal/status" "av1dae/internal/watcher" ) //go:embed index.html var indexHTML []byte //go:embed settings.html var settingsHTML []byte type Server struct { tracker *status.Tracker log *logger.Logger store *settings.Store inputDir string } func New(tracker *status.Tracker, log *logger.Logger, store *settings.Store, inputDir string) *Server { return &Server{tracker: tracker, log: log, store: store, inputDir: inputDir} } // Handler returns the mux for all status routes. Phase 3 adds "/" (the HTML // dashboard) to this same mux. func (s *Server) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/status", s.handleStatus) mux.HandleFunc("/settings", s.handleSettingsPage) mux.HandleFunc("/api/settings", s.handleAPISettings) mux.HandleFunc("/", s.handleIndex) return mux } func (s *Server) handleSettingsPage(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(settingsHTML) } // handleAPISettings serves the current settings (GET) and saves new ones (PUT). // Validation lives in settings.Store.Set; a bad payload returns 400. func (s *Server) handleAPISettings(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(s.store.Get()) case http.MethodPut: var in settings.Settings if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest) return } if err := s.store.Set(in); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } s.log.SetRetention(in.LogRetentionDays) s.log.Info("Settings updated via web UI") w.WriteHeader(http.StatusNoContent) default: w.Header().Set("Allow", "GET, PUT") http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(indexHTML) } type statusResponse struct { Current status.Snapshot `json:"current"` Queue []string `json:"queue"` Recent []logger.LogEntry `json:"recent"` } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { queue := []string{} for _, f := range watcher.InputFiles(s.inputDir) { queue = append(queue, filepath.Base(f)) } recent, err := s.log.RecentLogs(50) if err != nil { http.Error(w, "reading logs", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(statusResponse{ Current: s.tracker.Snapshot(), Queue: queue, Recent: recent, }) }