feat: add multi-user support for favorites management and room clock synchronization
- Implemented a new API endpoint for retrieving and managing user favorites in /api/users. - Added functionality for admins to edit the shared "main" user's favorites. - Created a one-shot DB smoke test script for verifying multi-user kiosk migrations. - Introduced a RoomClock class for synchronizing server time across clients using WebSocket.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
ensurePersonalRoom
|
||||
} from '../rooms.js';
|
||||
import { getStation } from '../stations.js';
|
||||
import { dispatchRoomCommand, hasDisplay } from '../ws.js';
|
||||
|
||||
export const router = Router();
|
||||
router.use(requireUser);
|
||||
@@ -59,3 +60,38 @@ router.delete('/:slug/members/:userId', (req, res) => {
|
||||
removeMember(room.id, Number(req.params.userId));
|
||||
res.json(listMembers(room.id));
|
||||
});
|
||||
|
||||
// Inject a playback intent for the room from outside the WS hub. Used by
|
||||
// smart-home/scripts/integrations: the request behaves like a synthetic
|
||||
// controller — the master (display) actually plays the audio and emits its
|
||||
// authoritative `state`, which the server rebroadcasts so every other peer
|
||||
// stays in sync. 409 when no display is connected, because without a master
|
||||
// there's no audio source to honour the intent.
|
||||
router.post('/:slug/command', (req, res) => {
|
||||
const room = getRoomBySlug(req.params.slug);
|
||||
if (!room) return res.status(404).json({ error: 'not found' });
|
||||
if (!isMember(room.id, req.user.id)) return res.status(403).json({ error: 'not a member' });
|
||||
|
||||
const action = String(req.body?.action || '');
|
||||
if (!['play', 'pause', 'stop', 'volume'].includes(action)) {
|
||||
return res.status(400).json({ error: 'action must be one of play|pause|stop|volume' });
|
||||
}
|
||||
const msg = { type: 'command', action };
|
||||
if (action === 'play') {
|
||||
const stationId = Number(req.body?.stationId);
|
||||
if (!Number.isFinite(stationId)) return res.status(400).json({ error: 'stationId required for play' });
|
||||
msg.stationId = stationId;
|
||||
} else if (action === 'volume') {
|
||||
const value = Number(req.body?.value);
|
||||
if (!Number.isFinite(value)) return res.status(400).json({ error: 'numeric value required for volume' });
|
||||
msg.value = Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
if (!hasDisplay(room.slug)) {
|
||||
return res.status(409).json({ error: 'no display connected to room', slug: room.slug });
|
||||
}
|
||||
|
||||
const result = dispatchRoomCommand(room, msg);
|
||||
if (!result) return res.status(400).json({ error: 'command rejected' });
|
||||
res.json({ ok: true, room: { id: room.id, slug: room.slug, name: room.name }, state: { ...result.state, station: result.station } });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user