Add version visibility and discovery controls to Web UI and API

This commit is contained in:
Tobias Gesellchen
2026-02-14 23:03:53 +01:00
parent 5bfc24b7fb
commit b7197a8679
8 changed files with 165 additions and 34 deletions
+17 -4
View File
@@ -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)
})
+1
View File
@@ -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
+2
View File
@@ -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.
+11 -3
View File
@@ -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)
}
}
+52 -8
View File
@@ -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
}
}
+51 -18
View File
@@ -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()
+8
View File
@@ -89,6 +89,11 @@
<input type="text" id="proxy-domain" placeholder="http://192.168.x.x:8000" style="width: 300px;">
<span style="font-size: 0.8em; color: #666;">(Upstream proxy URL)</span>
</div>
<div style="margin-bottom: 20px;">
<label for="discovery-interval">Discovery Interval:</label>
<input type="text" id="discovery-interval" placeholder="5m" style="width: 100px;">
<label style="margin-left: 15px;"><input type="checkbox" id="discovery-disabled"> Disable Automated Discovery</label>
</div>
<div style="margin-bottom: 20px;">
<button onclick="updateSettings()">Save Settings</button>
<span id="settings-status" style="margin-left: 10px; font-size: 0.9em;"></span>
@@ -270,5 +275,8 @@
</div>
<script src="/web/js/script.js"></script>
<footer style="margin-top: 50px; padding: 20px; border-top: 1px solid #eee; font-size: 0.8em; color: #888; text-align: center;">
<span id="version-info">SoundTouch Toolkit</span>
</footer>
</body>
</html>
+23 -1
View File
@@ -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;
});