mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
refactor(web): move the app-wide socket to /api/control/ws (refs #451)
Move the web UI's app-wide event stream (device list, discovery status,
per-device status updates) from top-level /ws to /api/control/ws. It is
the read/event half of the control surface, so it belongs under the same
namespace as the rest of the web API (the per-device socket already sits
at /api/control/devices/{id}/ws). The bundled app.js WebSocket URL is
updated in lockstep.
This brings soundtouch-web's entire HTTP surface under two clean subtrees
(/api/control/* for the API, /app/* for the SPA), so folding -web into
-service becomes a near-additive mount.
mount_test.go now asserts /api/control/ws is registered and top-level /ws
is gone.
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
d9581dc10f
commit
3a038b0129
@@ -498,7 +498,7 @@ func TestHandleWebSocket_InvalidUpgrade(t *testing.T) {
|
||||
app := createTestApp()
|
||||
|
||||
// Test without proper WebSocket headers (should fail gracefully)
|
||||
req := httptest.NewRequest("GET", "/ws", nil)
|
||||
req := httptest.NewRequest("GET", "/api/control/ws", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// This will fail because it's not a real WebSocket upgrade, but should not panic
|
||||
|
||||
@@ -19,20 +19,22 @@ func (app *WebApp) Mount(r chi.Router, discoveryService *discovery.UnifiedDiscov
|
||||
subFS, _ := fs.Sub(StaticFS, "static")
|
||||
r.Get("/static/*", http.StripPrefix("/static", http.FileServer(http.FS(subFS))).ServeHTTP)
|
||||
|
||||
// WebSocket endpoint
|
||||
r.Get("/ws", app.HandleWebSocket)
|
||||
|
||||
// Health / liveness
|
||||
r.Get("/health", app.HandleHealth)
|
||||
|
||||
// Player / control API. Per #451 this is the post-merge canonical shape:
|
||||
// device-scoped actions nest under devices/{id}/, so every direct child of
|
||||
// /api/control is a literal namespace (version, discover, devices,
|
||||
// /api/control is a literal namespace (version, ws, discover, devices,
|
||||
// providers) — no static-vs-param sibling, so routing never depends on
|
||||
// chi's static-over-param precedence.
|
||||
r.Route("/api/control", func(r chi.Router) {
|
||||
r.Get("/version", app.HandleAPIVersion)
|
||||
|
||||
// App-wide event stream: device list, discovery status, per-device
|
||||
// status updates. The read/event half of the control surface (the
|
||||
// per-device socket lives at devices/{id}/ws).
|
||||
r.Get("/ws", app.HandleWebSocket)
|
||||
|
||||
r.Post("/discover", func(w http.ResponseWriter, r *http.Request) {
|
||||
app.HandleAPIDiscover(w, r)
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@ func TestMountControlAPIShape(t *testing.T) {
|
||||
|
||||
var apiRoutes []string
|
||||
|
||||
registered := map[string]bool{}
|
||||
|
||||
walkErr := chi.Walk(r, func(_, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
|
||||
registered[route] = true
|
||||
|
||||
if strings.HasPrefix(route, "/api/") {
|
||||
apiRoutes = append(apiRoutes, route)
|
||||
}
|
||||
@@ -44,15 +48,12 @@ func TestMountControlAPIShape(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
registered := make(map[string]bool, len(apiRoutes))
|
||||
for _, route := range apiRoutes {
|
||||
registered[route] = true
|
||||
}
|
||||
|
||||
// The provider infix (#451): browsable providers expose global browse
|
||||
// routes; every provider play nests under devices/{id}/providers/.
|
||||
// routes; every provider play nests under devices/{id}/providers/. The
|
||||
// app-wide socket moved from top-level /ws to /api/control/ws.
|
||||
mustExist := []string{
|
||||
"/api/control/version",
|
||||
"/api/control/ws",
|
||||
"/api/control/providers/tunein/search",
|
||||
"/api/control/providers/radiobrowser/search",
|
||||
"/api/control/devices/{id}/providers/tunein/play",
|
||||
@@ -66,8 +67,10 @@ func TestMountControlAPIShape(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The pre-infix flat paths are gone.
|
||||
// The pre-infix flat paths are gone, and the app-wide socket no longer
|
||||
// sits at top-level /ws.
|
||||
mustNotExist := []string{
|
||||
"/ws",
|
||||
"/api/control/tunein/search",
|
||||
"/api/control/radiobrowser/search",
|
||||
"/api/control/devices/{id}/play-url",
|
||||
|
||||
@@ -91,7 +91,7 @@ function App() {
|
||||
.catch(err => console.error('Failed to fetch version:', err));
|
||||
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
|
||||
const ws = new WebSocket(`${protocol}//${location.host}/api/control/ws`);
|
||||
let reconnectTimer;
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
|
||||
Reference in New Issue
Block a user