feat: config hot-reload — save restarts services automatically

This commit is contained in:
Claus Lohmar 2026-08-06 14:37:02 +01:00
parent e790855f69
commit 02ee23281c
4 changed files with 39 additions and 3 deletions

13
api.go
View file

@ -138,6 +138,19 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
}
}
// handleConfigReload restarts internal services with the current config.
// POST /api/config/reload
func (s *Server) handleConfigReload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Error: "method not allowed"})
return
}
if s.onReload != nil {
s.onReload()
}
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: "reloaded"})
}
// handleStream proxies MJPEG streams from go2rtc to avoid cross-origin issues.
// GET /stream/{cam_id}?type=sub (default: sub stream via go2rtc)
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {

19
main.go
View file

@ -77,6 +77,25 @@ func main() {
// Start the HTTP server (main port, with auth).
app.server, _ = NewServer(cfg)
app.server.onReload = func() {
log.Println("Config reload — restarting services...")
if app.go2rtc != nil { app.go2rtc.Stop() }
if app.snapshots != nil { app.snapshots.StopAll() }
if app.recorder != nil { app.recorder.StopAll() }
// Restart with the updated config from the server's in-memory copy.
liveCfg := app.server.appConfig
if liveCfg.Go2RTC.Enabled {
app.go2rtc = NewGo2RTCManager(liveCfg.Go2RTC)
app.go2rtc.Start(liveCfg.Cameras)
}
snaps := NewSnapshotEngine(liveCfg.Storage)
snaps.StartAll(liveCfg.Cameras)
app.snapshots = snaps
rec := NewRecorderManager(liveCfg.Storage)
rec.StartAll(liveCfg.Cameras)
app.recorder = rec
log.Println("Config reload — services restarted.")
}
errCh := make(chan error, 1)
go func() {
errCh <- app.server.ListenAndServe()

View file

@ -378,14 +378,16 @@ document.getElementById('btn-save').addEventListener('click', async () => {
});
const j = await r.json();
if (j.success) {
// Reload cameras from the server (in-memory config is now updated).
// Reload cameras from the server.
await loadCameras();
renderLiveGrid();
renderPlaybackCameras();
renderCameraCards();
status.textContent = '✅ Cameras saved! Switching to Live Wall...';
status.textContent = '✅ Saved! Restarting services...';
status.style.color = 'var(--green)';
setTimeout(() => switchTab('live'), 800);
// Trigger backend reload of cameras.
try { await fetch(API + '/config/reload', { method: 'POST' }); } catch(e) {}
setTimeout(() => switchTab('live'), 1500);
} else {
status.textContent = '❌ Save failed: ' + j.error;
status.style.color = 'var(--red)';

View file

@ -28,6 +28,7 @@ type Server struct {
appConfig *Config
mu sync.RWMutex
sessions *SessionStore
onReload func() // called when config is saved via API
}
// NewServer creates and configures the HTTP server.
@ -116,6 +117,7 @@ func (s *Server) registerRoutes() {
s.mux.HandleFunc("/api/cameras", s.handleCameras)
s.mux.HandleFunc("/api/cameras/", s.handleCameraByID)
s.mux.HandleFunc("/api/config", s.handleConfig)
s.mux.HandleFunc("/api/config/reload", s.handleConfigReload)
s.mux.HandleFunc("/api/scan", s.handleScan)
s.mux.HandleFunc("/api/status", s.handleStatus)
s.mux.HandleFunc("/api/recordings", s.handleRecordings)