Files
Bose-SoundTouch/pkg/service/soundtouchweb/static/js/api.js
T
Tobias GesellchenandClaude Opus 4.8 b861c11d37 feat(web): remove devices from the player UI (refs #451)
The merge of soundtouch-web into soundtouch-service was asymmetric:
manual device *adds* propagated to the player UI (HandleAddManualDevice
notifies, the hook re-seeds + broadcasts), but *removals* did not. The
datastore-removal handler never notified, and the web registry's sync
only ever added entries — its map was append-only, so a removed device
lingered in the player UI until restart.

This adds the missing removal path:

- DELETE /api/control/devices/{id} (HandleDeleteDevice). The registry is
  keyed by host/IP; the datastore by device ID (MAC), so the handler
  resolves one to the other via the connection's DeviceInfo, cascades to
  the datastore through a new RemoveDeviceHook (embedded build only),
  prunes the in-memory entry, and broadcasts the updated list.
- WebApp.RemoveDevice prunes the registry and stops the per-device
  goroutines (status poller + WebSocket reconnect loop) via a new
  done-channel + Close() on DeviceConnection — previously both ran for
  the life of the process.
- Server.RemoveDeviceByID extracts the cross-account lookup + remove from
  HandleRemoveDevice and now fires notifyDevicesChanged, so the admin
  Devices tab removal also propagates to the player UI.
- Player UI: a quiet per-card Remove control (visible on hover), a
  confirm dialog, optimistic prune, and a note that a still-online
  device may reappear after the next discovery scan (honest v1 — no
  ignore-list).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:08:26 +02:00

60 lines
3.2 KiB
JavaScript

const JSON_HEADERS = { 'Content-Type': 'application/json' };
async function req(url, opts = {}) {
const r = await fetch(url, opts);
return r.json();
}
export const api = {
devices: () => req('/api/control/devices'),
device: (id) => req(`/api/control/devices/${id}`),
removeDevice: (id) => req(`/api/control/devices/${id}`, { method: 'DELETE' }),
discover: () => req('/api/control/discover', { method: 'POST' }),
key: (id, key) => req(`/api/control/devices/${id}/key/${key}`, { method: 'POST' }),
volume: (id, level) => req(`/api/control/devices/${id}/volume/${level}`, { method: 'POST' }),
bass: (id, level) => req(`/api/control/devices/${id}/action/bass`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({ level }),
}),
power: (id) => req(`/api/control/devices/${id}/power`, { method: 'POST' }),
recents: (id) => req(`/api/control/devices/${id}/recents`),
zone: (id) => req(`/api/control/devices/${id}/zone`),
zoneAdd: (masterId, slaveId) => req(`/api/control/devices/${masterId}/zone/add/${slaveId}`, { method: 'POST' }),
zoneRemove: (masterId, slaveId) => req(`/api/control/devices/${masterId}/zone/remove/${slaveId}`, { method: 'POST' }),
zoneDissolve: (id) => req(`/api/control/devices/${id}/zone/dissolve`, { method: 'POST' }),
zoneLeave: (id) => req(`/api/control/devices/${id}/zone/leave`, { method: 'POST' }),
play: (id, item) => req(`/api/control/devices/${id}/play`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify(item),
}),
tuneInBrowse: (path) => req(path ? `/api/control/providers/tunein/navigate/${path}` : '/api/control/providers/tunein/navigate'),
tuneInSearch: (q) => req(`/api/control/providers/tunein/search?q=${encodeURIComponent(q)}`),
tuneInSearchNext: (cursor) => req(`/api/control/providers/tunein/search/next?cursor=${encodeURIComponent(cursor)}`),
control: (id, action, presetId) => req(`/api/control/devices/${id}/action/${action}?id=${presetId}`),
storePreset: (id, slotId) => req(`/api/control/devices/${id}/action/storepreset?id=${slotId}`),
selectSource: (id, source, account) => req(`/api/control/devices/${id}/action/source?name=${encodeURIComponent(source)}&account=${encodeURIComponent(account || '')}`),
tuneInPlay: (deviceId, item) => req(`/api/control/devices/${deviceId}/providers/tunein/play`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify(item),
}),
radioBrowserSearch: (q) => req(`/api/control/providers/radiobrowser/search?q=${encodeURIComponent(q)}`),
radioBrowserPlay: (deviceId, item) => req(`/api/control/devices/${deviceId}/providers/radiobrowser/play`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify(item),
}),
playURL: (deviceId, url, name, imageUrl, serviceUrl) => req(`/api/control/devices/${deviceId}/providers/url/play`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({ url, name, imageUrl, serviceUrl }),
}),
speak: (deviceId, text) => req(`/api/control/devices/${deviceId}/providers/tts/play`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({ text }),
}),
};