From b7197a8679bdcf91a870e769944eb3c0809a3f08 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sat, 14 Feb 2026 22:21:18 +0100 Subject: [PATCH] Add version visibility and discovery controls to Web UI and API --- cmd/soundtouch-service/main.go | 21 ++++++-- docs/guides/SOUNDTOUCH-SERVICE.md | 1 + pkg/service/datastore/datastore.go | 2 + pkg/service/datastore/datastore_test.go | 14 +++-- pkg/service/handlers/handlers_setup.go | 60 ++++++++++++++++++--- pkg/service/handlers/server.go | 69 ++++++++++++++++++------- pkg/service/handlers/web/index.html | 8 +++ pkg/service/handlers/web/js/script.js | 24 ++++++++- 8 files changed, 165 insertions(+), 34 deletions(-) diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index d6c6aea..469b4f0 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -151,6 +151,12 @@ func main() { config.httpsServerURL = persisted.HTTPServerURL } + if persisted.DiscoveryInterval != "" { + if d, err := time.ParseDuration(persisted.DiscoveryInterval); err == nil { + config.discoveryInterval = d + } + } + config.redact = persisted.RedactLogs || config.redact config.logBody = persisted.LogBodies || config.logBody config.record = persisted.RecordInteractions || config.record @@ -167,6 +173,8 @@ func main() { sm := setup.NewManager(config.serverURL, ds, cm) server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record) server.SetHTTPServerURL(config.httpsServerURL) + server.SetVersionInfo(version, commit, date) + server.SetDiscoverySettings(config.discoveryInterval, persisted.DiscoveryDisabled) recorder := proxy.NewRecorder(config.dataDir) recorder.Redact = config.redact @@ -188,7 +196,7 @@ func main() { pyProxy := setupPythonProxy(config.targetURL, config.redact, config.logBody, recorder, server) - startDeviceDiscovery(server, config.discoveryInterval) + startDeviceDiscovery(server) r := setupRouter(server, pyProxy) @@ -392,11 +400,15 @@ func setupPythonProxy(targetURL string, redact, logBody bool, recorder *proxy.Re return pyProxy } -func startDeviceDiscovery(server *handlers.Server, interval time.Duration) { +func startDeviceDiscovery(server *handlers.Server) { go func() { for { - server.DiscoverDevices(context.Background()) - time.Sleep(interval) + currentInterval, disabled := server.GetDiscoverySettings() + if !disabled { + server.DiscoverDevices(context.Background()) + } + + time.Sleep(currentInterval) } }() } @@ -470,6 +482,7 @@ func setupRouter(server *handlers.Server, pyProxy *httputil.ReverseProxy) *chi.M r.Get("/ca.crt", server.HandleGetCACert) r.Get("/proxy-settings", server.HandleGetProxySettings) r.Post("/proxy-settings", server.HandleUpdateProxySettings) + r.Get("/version", server.HandleGetVersionInfo) r.Get("/devices/{deviceId}/events", server.HandleGetDeviceEvents) }) diff --git a/docs/guides/SOUNDTOUCH-SERVICE.md b/docs/guides/SOUNDTOUCH-SERVICE.md index e3b2ed1..88a3997 100644 --- a/docs/guides/SOUNDTOUCH-SERVICE.md +++ b/docs/guides/SOUNDTOUCH-SERVICE.md @@ -161,6 +161,7 @@ The service supports multiple ways to configure its behavior. When multiple sour | `LOG_PROXY_BODY` | `--log-bodies` | Log full request/response bodies | `false` | | `RECORD_INTERACTIONS` | `--record-interactions` | Record HTTP interactions to disk | `true` | | `DISCOVERY_INTERVAL` | `--discovery-interval` | Device discovery interval | `5m` | +| `DISCOVERY_DISABLED` | | Disable automated device discovery | `false` | ### Configuration Examples diff --git a/pkg/service/datastore/datastore.go b/pkg/service/datastore/datastore.go index 1f9752b..f94eae0 100644 --- a/pkg/service/datastore/datastore.go +++ b/pkg/service/datastore/datastore.go @@ -714,6 +714,8 @@ type Settings struct { RedactLogs bool `json:"redact_logs"` LogBodies bool `json:"log_bodies"` RecordInteractions bool `json:"record_interactions"` + DiscoveryInterval string `json:"discovery_interval,omitempty"` + DiscoveryDisabled bool `json:"discovery_disabled"` } // GetSettings retrieves the global service settings. diff --git a/pkg/service/datastore/datastore_test.go b/pkg/service/datastore/datastore_test.go index 6160e3b..68178be 100644 --- a/pkg/service/datastore/datastore_test.go +++ b/pkg/service/datastore/datastore_test.go @@ -376,9 +376,11 @@ func TestSettingsPersistence(t *testing.T) { ds := NewDataStore(tempDir) settings := Settings{ - ServerURL: "http://myserver:8000", - ProxyURL: "http://myproxy:8001", - LogBodies: true, + ServerURL: "http://myserver:8000", + ProxyURL: "http://myproxy:8001", + LogBodies: true, + DiscoveryInterval: "10m", + DiscoveryDisabled: true, } err = ds.SaveSettings(settings) @@ -397,4 +399,10 @@ func TestSettingsPersistence(t *testing.T) { if loaded.LogBodies != settings.LogBodies { t.Errorf("Expected LogBodies %v, got %v", settings.LogBodies, loaded.LogBodies) } + if loaded.DiscoveryInterval != settings.DiscoveryInterval { + t.Errorf("Expected DiscoveryInterval %s, got %s", settings.DiscoveryInterval, loaded.DiscoveryInterval) + } + if loaded.DiscoveryDisabled != settings.DiscoveryDisabled { + t.Errorf("Expected DiscoveryDisabled %v, got %v", settings.DiscoveryDisabled, loaded.DiscoveryDisabled) + } } diff --git a/pkg/service/handlers/handlers_setup.go b/pkg/service/handlers/handlers_setup.go index 0ba33bd..5b8e74b 100644 --- a/pkg/service/handlers/handlers_setup.go +++ b/pkg/service/handlers/handlers_setup.go @@ -6,6 +6,7 @@ import ( "log" "net/http" "os" + "time" "github.com/gesellix/bose-soundtouch/pkg/models" "github.com/gesellix/bose-soundtouch/pkg/service/datastore" @@ -95,12 +96,18 @@ func (s *Server) HandleGetDiscoveryStatus(w http.ResponseWriter, _ *http.Request func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") - serverURL, proxyURL, httpsServerURL := s.GetSettings() + s.mu.RLock() + serverURL, proxyURL, httpsServerURL := s.serverURL, s.proxyURL, s.httpsServerURL + discoveryInterval := s.discoveryInterval.String() + discoveryDisabled := s.discoveryDisabled + s.mu.RUnlock() - if err := json.NewEncoder(w).Encode(map[string]string{ - "server_url": serverURL, - "proxy_url": proxyURL, - "https_server_url": httpsServerURL, + if err := json.NewEncoder(w).Encode(map[string]interface{}{ + "server_url": serverURL, + "proxy_url": proxyURL, + "https_server_url": httpsServerURL, + "discovery_interval": discoveryInterval, + "discovery_disabled": discoveryDisabled, }); err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) return @@ -110,17 +117,31 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) { // HandleUpdateSettings updates the service settings. func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) { var settings struct { - ServerURL string `json:"server_url"` - ProxyURL string `json:"proxy_url"` + ServerURL string `json:"server_url"` + ProxyURL string `json:"proxy_url"` + DiscoveryInterval string `json:"discovery_interval"` + DiscoveryDisabled bool `json:"discovery_disabled"` } if err := json.NewDecoder(r.Body).Decode(&settings); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } + interval, err := time.ParseDuration(settings.DiscoveryInterval) + if err != nil && settings.DiscoveryInterval != "" { + http.Error(w, "Invalid discovery interval: "+err.Error(), http.StatusBadRequest) + return + } + s.mu.Lock() s.serverURL = settings.ServerURL + s.proxyURL = settings.ProxyURL + if settings.DiscoveryInterval != "" { + s.discoveryInterval = interval + } + + s.discoveryDisabled = settings.DiscoveryDisabled if s.sm != nil { s.sm.ServerURL = settings.ServerURL @@ -134,13 +155,15 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) { currentHTTPS := s.httpsServerURL log.Printf("Saving updated settings to %s/settings.json", s.ds.DataDir) - err := s.ds.SaveSettings(datastore.Settings{ + err = s.ds.SaveSettings(datastore.Settings{ ServerURL: s.serverURL, ProxyURL: s.proxyURL, HTTPServerURL: currentHTTPS, RedactLogs: currentRedact, LogBodies: currentLogBody, RecordInteractions: currentRecord, + DiscoveryInterval: s.discoveryInterval.String(), + DiscoveryDisabled: s.discoveryDisabled, }) s.mu.Unlock() @@ -491,6 +514,8 @@ func (s *Server) HandleUpdateProxySettings(w http.ResponseWriter, r *http.Reques // Persist to datastore // Access fields directly since we already hold the lock serverURL, proxyURL, httpsServerURL := s.serverURL, s.proxyURL, s.httpsServerURL + discoveryInterval := s.discoveryInterval.String() + discoveryDisabled := s.discoveryDisabled log.Printf("Saving updated proxy settings to %s/settings.json", s.ds.DataDir) err := s.ds.SaveSettings(datastore.Settings{ @@ -500,6 +525,8 @@ func (s *Server) HandleUpdateProxySettings(w http.ResponseWriter, r *http.Reques RedactLogs: s.proxyRedact, LogBodies: s.proxyLogBody, RecordInteractions: s.recordEnabled, + DiscoveryInterval: discoveryInterval, + DiscoveryDisabled: discoveryDisabled, }) s.mu.Unlock() @@ -651,3 +678,20 @@ func (s *Server) HandleTestConnection(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to encode response", http.StatusInternalServerError) } } + +// HandleGetVersionInfo returns version information for the service. +func (s *Server) HandleGetVersionInfo(w http.ResponseWriter, _ *http.Request) { + s.mu.RLock() + defer s.mu.RUnlock() + + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(map[string]string{ + "version": s.Version, + "commit": s.Commit, + "date": s.Date, + }); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } +} diff --git a/pkg/service/handlers/server.go b/pkg/service/handlers/server.go index c5b3856..1bc6ea6 100644 --- a/pkg/service/handlers/server.go +++ b/pkg/service/handlers/server.go @@ -15,32 +15,65 @@ import ( // Server handles HTTP requests for the SoundTouch service. type Server struct { - ds *datastore.DataStore - sm *setup.Manager - mu sync.RWMutex - serverURL string - proxyURL string - httpsServerURL string - discovering bool - proxyRedact bool - proxyLogBody bool - recordEnabled bool - recorder *proxy.Recorder + ds *datastore.DataStore + sm *setup.Manager + mu sync.RWMutex + serverURL string + proxyURL string + httpsServerURL string + discovering bool + proxyRedact bool + proxyLogBody bool + recordEnabled bool + discoveryInterval time.Duration + discoveryDisabled bool + recorder *proxy.Recorder + Version string + Commit string + Date string } // NewServer creates a new SoundTouch service server. func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, proxyRedact, proxyLogBody, recordEnabled bool) *Server { return &Server{ - ds: ds, - sm: sm, - serverURL: serverURL, - proxyURL: serverURL, - proxyRedact: proxyRedact, - proxyLogBody: proxyLogBody, - recordEnabled: recordEnabled, + ds: ds, + sm: sm, + serverURL: serverURL, + proxyURL: serverURL, + proxyRedact: proxyRedact, + proxyLogBody: proxyLogBody, + recordEnabled: recordEnabled, + discoveryInterval: 5 * time.Minute, } } +// SetVersionInfo sets the version information for the server. +func (s *Server) SetVersionInfo(version, commit, date string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.Version = version + s.Commit = commit + s.Date = date +} + +// SetDiscoverySettings sets the discovery settings for the server. +func (s *Server) SetDiscoverySettings(interval time.Duration, disabled bool) { + s.mu.Lock() + defer s.mu.Unlock() + + s.discoveryInterval = interval + s.discoveryDisabled = disabled +} + +// GetDiscoverySettings returns the current discovery settings. +func (s *Server) GetDiscoverySettings() (time.Duration, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.discoveryInterval, s.discoveryDisabled +} + // SetHTTPServerURL sets the external HTTPS URL of the service. func (s *Server) SetHTTPServerURL(url string) { s.mu.Lock() diff --git a/pkg/service/handlers/web/index.html b/pkg/service/handlers/web/index.html index 1901ca2..5db1cb7 100644 --- a/pkg/service/handlers/web/index.html +++ b/pkg/service/handlers/web/index.html @@ -89,6 +89,11 @@ (Upstream proxy URL) +
+ + + +
@@ -270,5 +275,8 @@
+ diff --git a/pkg/service/handlers/web/js/script.js b/pkg/service/handlers/web/js/script.js index 914edca..6ca59e1 100644 --- a/pkg/service/handlers/web/js/script.js +++ b/pkg/service/handlers/web/js/script.js @@ -8,6 +8,12 @@ async function fetchSettings() { if (settings.proxy_url) { document.getElementById('proxy-domain').value = settings.proxy_url; } + if (settings.discovery_interval) { + document.getElementById('discovery-interval').value = settings.discovery_interval; + } + if (settings.discovery_disabled !== undefined) { + document.getElementById('discovery-disabled').checked = settings.discovery_disabled; + } fetchProxySettings(); } catch (error) { console.error('Failed to fetch settings', error); @@ -46,7 +52,9 @@ async function updateProxySettings() { async function updateSettings() { const settings = { server_url: document.getElementById('target-domain').value, - proxy_url: document.getElementById('proxy-domain').value + proxy_url: document.getElementById('proxy-domain').value, + discovery_interval: document.getElementById('discovery-interval').value, + discovery_disabled: document.getElementById('discovery-disabled').checked }; const status = document.getElementById('settings-status'); status.innerText = 'Saving...'; @@ -208,10 +216,24 @@ async function startSync() { } } +async function fetchVersion() { + try { + const response = await fetch('/setup/version'); + const data = await response.json(); + const info = document.getElementById('version-info'); + if (info && data.version) { + info.innerText = `SoundTouch Toolkit ${data.version} (${data.commit}) - ${data.date}`; + } + } catch (error) { + console.error('Failed to fetch version info', error); + } +} + document.addEventListener('DOMContentLoaded', () => { fetchSettings(); fetchDevices(); triggerDiscovery(); + fetchVersion(); document.getElementById('sync-now-btn').onclick = startSync; });