mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
feat(service): serve the web UI from soundtouch-service (refs #451)
Fold soundtouch-web into soundtouch-service as an additive mount, so a single process serves both the speaker/cloud-replacement API and the LAN control UI. No new auth and no opt-in flag: the web surface sits at the same LAN-trust tier as /setup (which -web already calls without credentials), and -web is LAN-only by nature. - newEmbeddedWebApp builds the web app with release metadata, a loopback ServiceURL (plain HTTP, no CA needed) for the TTS / Play URL proxy, and an initial discovery sweep. setupRouter gains the web app + discovery service and mounts the portable surface (MountWeb) additively: /api/control/* and /app/* (+ /app/static/*). The service keeps its own /, /health and /static; nothing collides. webApp is optional so the router unit tests that only exercise the service surface pass nil. - Manual devices with discovery off: the web app's ExtraDeviceHosts hook is pointed at the service datastore (ListAllDevices), and SeedExtraDevices (run from DiscoverDevices, i.e. at startup and on each /api/control/discover) registers them via the existing AddDeviceByHost. So speakers added via /setup show up in the UI even when periodic discovery is disabled. - The admin page at / now links to the player UI at /app; the speaker / JSON contract is unchanged. - Router golden file regenerated: the diff is purely the additive /api/control + /app routes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3693cfa65b
commit
99b3f5d0aa
@@ -129,7 +129,7 @@ func loadHTTPClientRequests(t *testing.T, dir string) [][2]string {
|
||||
// machine-checked companion to tests/integration/http-client/COVERAGE.md.
|
||||
func TestFrozenRouteContractCoverage(t *testing.T) {
|
||||
server := handlers.NewServer(nil, nil, "http://localhost:8000", true, true, true)
|
||||
r := setupRouter(server, nil)
|
||||
r := setupRouter(server, nil, nil, nil)
|
||||
|
||||
httpRequests := loadHTTPClientRequests(t, filepath.Join("..", "..", "tests", "integration", "http-client"))
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestDeprecatedRouteSignal(t *testing.T) {
|
||||
_ = ds.Initialize()
|
||||
|
||||
server := handlers.NewServer(ds, nil, "http://localhost:8000", true, false, false)
|
||||
r := setupRouter(server, nil)
|
||||
r := setupRouter(server, nil, nil, nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestDualRouteEquivalence(t *testing.T) {
|
||||
_ = ds.Initialize()
|
||||
|
||||
server := handlers.NewServer(ds, nil, "http://localhost:8000", true, false, false)
|
||||
r := setupRouter(server, nil)
|
||||
r := setupRouter(server, nil, nil, nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/logbuf"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/proxy"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/setup"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/soundtouchweb"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/spotify"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/stockholm"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -587,7 +588,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
r := setupRouter(server, stockholmHandler)
|
||||
// Embedded web UI (soundtouch-web): LAN control UI under /app, control
|
||||
// API under /api/control. Same LAN-trust tier as /setup, no auth.
|
||||
webApp, webDiscovery := newEmbeddedWebApp(config.serverURL, ds)
|
||||
|
||||
r := setupRouter(server, stockholmHandler, webApp, webDiscovery)
|
||||
|
||||
// Bind the listener before logging so we print the true
|
||||
// effective port (handles :0 and catches "address already
|
||||
@@ -1083,7 +1088,52 @@ func startDeviceDiscovery(server *handlers.Server) {
|
||||
}()
|
||||
}
|
||||
|
||||
func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) *chi.Mux {
|
||||
// newEmbeddedWebApp builds the soundtouch-web application for embedding in the
|
||||
// service router: release metadata from the build vars, a loopback ServiceURL
|
||||
// for the TTS / Play URL proxy (plain HTTP, no CA trust needed), and a device
|
||||
// seed from the service datastore so the UI shows manually-added speakers even
|
||||
// when network discovery is disabled. It also returns a discovery service and
|
||||
// kicks off an initial sweep.
|
||||
func newEmbeddedWebApp(serverURL string, ds *datastore.DataStore) (*soundtouchweb.WebApp, *discovery.UnifiedDiscoveryService) {
|
||||
webApp := soundtouchweb.NewWebApp()
|
||||
webApp.Version = version
|
||||
webApp.Commit = commit
|
||||
webApp.Date = date
|
||||
webApp.RepoURL = repoURL
|
||||
webApp.ServiceURL = strings.TrimRight(serverURL, "/")
|
||||
webApp.ExtraDeviceHosts = func() []string {
|
||||
devices, listErr := ds.ListAllDevices()
|
||||
if listErr != nil {
|
||||
log.Printf("web UI: failed to list devices from datastore: %v", listErr)
|
||||
return nil
|
||||
}
|
||||
|
||||
hosts := make([]string, 0, len(devices))
|
||||
for i := range devices {
|
||||
if devices[i].IPAddress != "" {
|
||||
hosts = append(hosts, devices[i].IPAddress)
|
||||
}
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
webDiscovery := soundtouchweb.NewDiscoveryService("")
|
||||
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
webApp.BroadcastDiscoveryStatus("starting", webApp.DeviceCount())
|
||||
webApp.DiscoverDevices(ctx, webDiscovery)
|
||||
webApp.BroadcastDiscoveryStatus("completed", webApp.DeviceCount())
|
||||
webApp.BroadcastDeviceList()
|
||||
}()
|
||||
|
||||
return webApp, webDiscovery
|
||||
}
|
||||
|
||||
func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler, webApp *soundtouchweb.WebApp, webDiscovery *discovery.UnifiedDiscoveryService) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// CleanPath collapses duplicate slashes ("//bmx/..." -> "/bmx/...") and
|
||||
@@ -1478,6 +1528,14 @@ func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) *
|
||||
mountSetupAPI(r)
|
||||
})
|
||||
|
||||
// Embedded web UI: control API under /api/control and the SPA under /app
|
||||
// (LAN-trust, like /setup). Additive — nothing here collides with the
|
||||
// service's own /, /health, or /static. Skipped when nil, e.g. unit tests
|
||||
// that only exercise the service surface.
|
||||
if webApp != nil {
|
||||
webApp.MountWeb(r, webDiscovery)
|
||||
}
|
||||
|
||||
if stockholmHandler != nil {
|
||||
stockholmHandler.Mount(r)
|
||||
}
|
||||
|
||||
@@ -13,13 +13,16 @@ import (
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/handlers"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/soundtouchweb"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func TestPrintRoutes(t *testing.T) {
|
||||
// Initialize a minimal server to get the router
|
||||
// Initialize a minimal server to get the router. Pass a web app so the
|
||||
// snapshot also captures the embedded soundtouch-web surface
|
||||
// (/api/control + /app); discovery is nil since we only register routes.
|
||||
server := handlers.NewServer(nil, nil, "http://localhost:8000", true, true, true)
|
||||
r := setupRouter(server, nil)
|
||||
r := setupRouter(server, nil, soundtouchweb.NewWebApp(), nil)
|
||||
|
||||
var routes []string
|
||||
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
@@ -128,7 +131,7 @@ func TestPUTRenameRoutesToLocalHandler(t *testing.T) {
|
||||
_ = ds.Initialize()
|
||||
|
||||
server := handlers.NewServer(ds, nil, "http://localhost:8000", false, false, false)
|
||||
r := setupRouter(server, nil)
|
||||
r := setupRouter(server, nil, nil, nil)
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
|
||||
@@ -33,6 +33,20 @@ GET /accounts/{account}/devices/{device}/presets handlers.(
|
||||
GET /accounts/{account}/devices/{device}/recents handlers.(*Server).HandleUnsupported-fm
|
||||
GET /accounts/{account}/full handlers.(*Server).HandleUnsupported-fm
|
||||
GET /accounts/{account}/sources handlers.(*Server).HandleUnsupported-fm
|
||||
GET /api/control/devices/ soundtouchweb.(*WebApp).HandleAPIDevices-fm
|
||||
GET /api/control/devices/{id}/ soundtouchweb.(*WebApp).HandleAPIDevice-fm
|
||||
GET /api/control/devices/{id}/action/{action} soundtouchweb.(*WebApp).HandleAPIControl-fm
|
||||
GET /api/control/devices/{id}/power-status soundtouchweb.(*WebApp).HandleDevicePowerStatus-fm
|
||||
GET /api/control/devices/{id}/recents soundtouchweb.(*WebApp).HandleDeviceRecents-fm
|
||||
GET /api/control/devices/{id}/ws soundtouchweb.(*WebApp).HandleDeviceWebSocket-fm
|
||||
GET /api/control/devices/{id}/zone/ soundtouchweb.(*WebApp).HandleGetZone-fm
|
||||
GET /api/control/providers/radiobrowser/search soundtouchweb.(*WebApp).HandleRadioBrowserSearch-fm
|
||||
GET /api/control/providers/tunein/navigate soundtouchweb.(*WebApp).HandleTuneInNavigate-fm
|
||||
GET /api/control/providers/tunein/navigate/* soundtouchweb.(*WebApp).HandleTuneInNavigate-fm
|
||||
GET /api/control/providers/tunein/search soundtouchweb.(*WebApp).HandleTuneInSearch-fm
|
||||
GET /api/control/providers/tunein/search/next soundtouchweb.(*WebApp).HandleTuneInSearchNext-fm
|
||||
GET /api/control/version soundtouchweb.(*WebApp).HandleAPIVersion-fm
|
||||
GET /api/control/ws soundtouchweb.(*WebApp).HandleWebSocket-fm
|
||||
GET /api/mgmt/accounts/ handlers.(*Server).HandleMgmtListAccounts-fm
|
||||
GET /api/mgmt/accounts/{accountId} handlers.(*Server).HandleMgmtAccountDetails-fm
|
||||
GET /api/mgmt/accounts/{accountId}/speakers handlers.(*Server).HandleMgmtListSpeakers-fm
|
||||
@@ -62,6 +76,14 @@ GET /api/setup/settings handlers.(
|
||||
GET /api/setup/summary/{deviceId} handlers.(*Server).HandleGetMigrationSummary-fm
|
||||
GET /api/setup/tts/config handlers.(*Server).HandleTTSConfig-fm
|
||||
GET /api/setup/version handlers.(*Server).HandleGetVersionInfo-fm
|
||||
GET /app soundtouchweb.(*WebApp).serveIndex-fm
|
||||
GET /app/device/* soundtouchweb.(*WebApp).serveIndex-fm
|
||||
GET /app/devices soundtouchweb.(*WebApp).serveIndex-fm
|
||||
GET /app/playurl soundtouchweb.(*WebApp).serveIndex-fm
|
||||
GET /app/radiobrowser soundtouchweb.(*WebApp).serveIndex-fm
|
||||
GET /app/static/* http.Handler.ServeHTTP-fm
|
||||
GET /app/tts soundtouchweb.(*WebApp).serveIndex-fm
|
||||
GET /app/tunein soundtouchweb.(*WebApp).serveIndex-fm
|
||||
GET /bmx-icons/* handlers.(*Server).HandleBmxIcons
|
||||
GET /bmx/registry/v1/services handlers.(*Server).HandleBMXRegistry-fm
|
||||
GET /bmx/registry/v1/servicesAvailability handlers.(*Server).HandleBMXServicesAvailability-fm
|
||||
@@ -153,6 +175,20 @@ POST /accounts/{account}/group handlers.(
|
||||
POST /accounts/{account}/group/ handlers.(*Server).HandleUnsupported-fm
|
||||
POST /accounts/{account}/group/{groupId} handlers.(*Server).HandleUnsupported-fm
|
||||
POST /alexa/certificate handlers.(*Server).HandleAlexaCertificate-fm
|
||||
POST /api/control/devices/{id}/action/{action} soundtouchweb.(*WebApp).HandleAPIControl-fm
|
||||
POST /api/control/devices/{id}/key/{key} soundtouchweb.(*WebApp).HandleDeviceKey-fm
|
||||
POST /api/control/devices/{id}/play soundtouchweb.(*WebApp).HandleDevicePlay-fm
|
||||
POST /api/control/devices/{id}/power soundtouchweb.(*WebApp).HandleDevicePower-fm
|
||||
POST /api/control/devices/{id}/providers/radiobrowser/play soundtouchweb.(*WebApp).HandlePlayRadioBrowser-fm
|
||||
POST /api/control/devices/{id}/providers/tts/play soundtouchweb.(*WebApp).HandleAPISpeakText-fm
|
||||
POST /api/control/devices/{id}/providers/tunein/play soundtouchweb.(*WebApp).HandlePlayTuneIn-fm
|
||||
POST /api/control/devices/{id}/providers/url/play soundtouchweb.(*WebApp).HandlePlayURL-fm
|
||||
POST /api/control/devices/{id}/volume/{volume} soundtouchweb.(*WebApp).HandleDirectVolumeControl-fm
|
||||
POST /api/control/devices/{id}/zone/add/{slaveId} soundtouchweb.(*WebApp).HandleZoneAdd-fm
|
||||
POST /api/control/devices/{id}/zone/dissolve soundtouchweb.(*WebApp).HandleZoneDissolve-fm
|
||||
POST /api/control/devices/{id}/zone/leave soundtouchweb.(*WebApp).HandleZoneLeave-fm
|
||||
POST /api/control/devices/{id}/zone/remove/{slaveId} soundtouchweb.(*WebApp).HandleZoneRemove-fm
|
||||
POST /api/control/discover soundtouchweb.(*WebApp).MountWeb
|
||||
POST /api/mgmt/accounts/{accountId}/language handlers.(*Server).HandleMgmtUpdateAccountLanguage-fm
|
||||
POST /api/mgmt/accounts/{accountId}/provider-settings handlers.(*Server).HandleMgmtUpdateAccountProviderSetting-fm
|
||||
POST /api/mgmt/amazon/confirm handlers.(*Server).HandleMgmtAmazonConfirm-fm
|
||||
|
||||
Reference in New Issue
Block a user