Phase 2: schedules accept playlist_id, scheduler overrides device playlist

Schedule CRUD now includes playlist_id field. List queries join playlist name.
Scheduler tracks active overrides per device and reverts to original
playlist/layout when no schedule is active.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-04-11 22:07:36 -05:00
parent f35894d17a
commit 19a08ef5bc
2 changed files with 35 additions and 19 deletions

View file

@ -6,7 +6,7 @@ const { db } = require('../db/database');
// List schedules (filterable) // List schedules (filterable)
router.get('/', (req, res) => { router.get('/', (req, res) => {
const { device_id, start, end } = req.query; const { device_id, start, end } = req.query;
let sql = 'SELECT s.*, c.filename as content_name, w.name as widget_name FROM schedules s LEFT JOIN content c ON s.content_id = c.id LEFT JOIN widgets w ON s.widget_id = w.id WHERE s.user_id = ?'; let sql = 'SELECT s.*, c.filename as content_name, w.name as widget_name, p.name as playlist_name FROM schedules s LEFT JOIN content c ON s.content_id = c.id LEFT JOIN widgets w ON s.widget_id = w.id LEFT JOIN playlists p ON s.playlist_id = p.id WHERE s.user_id = ?';
const params = [req.user.id]; const params = [req.user.id];
if (device_id) { sql += ' AND s.device_id = ?'; params.push(device_id); } if (device_id) { sql += ' AND s.device_id = ?'; params.push(device_id); }
@ -20,10 +20,11 @@ router.get('/', (req, res) => {
// Get schedules for a device // Get schedules for a device
router.get('/device/:deviceId', (req, res) => { router.get('/device/:deviceId', (req, res) => {
const schedules = db.prepare(` const schedules = db.prepare(`
SELECT s.*, c.filename as content_name, w.name as widget_name SELECT s.*, c.filename as content_name, w.name as widget_name, p.name as playlist_name
FROM schedules s FROM schedules s
LEFT JOIN content c ON s.content_id = c.id LEFT JOIN content c ON s.content_id = c.id
LEFT JOIN widgets w ON s.widget_id = w.id LEFT JOIN widgets w ON s.widget_id = w.id
LEFT JOIN playlists p ON s.playlist_id = p.id
WHERE s.device_id = ? AND s.enabled = 1 WHERE s.device_id = ? AND s.enabled = 1
ORDER BY s.priority DESC, s.start_time ASC ORDER BY s.priority DESC, s.start_time ASC
`).all(req.params.deviceId); `).all(req.params.deviceId);
@ -42,10 +43,11 @@ router.get('/week', (req, res) => {
weekEnd.setDate(weekEnd.getDate() + 7); weekEnd.setDate(weekEnd.getDate() + 7);
const schedules = db.prepare(` const schedules = db.prepare(`
SELECT s.*, c.filename as content_name, w.name as widget_name SELECT s.*, c.filename as content_name, w.name as widget_name, p.name as playlist_name
FROM schedules s FROM schedules s
LEFT JOIN content c ON s.content_id = c.id LEFT JOIN content c ON s.content_id = c.id
LEFT JOIN widgets w ON s.widget_id = w.id LEFT JOIN widgets w ON s.widget_id = w.id
LEFT JOIN playlists p ON s.playlist_id = p.id
WHERE s.device_id = ? AND s.enabled = 1 WHERE s.device_id = ? AND s.enabled = 1
ORDER BY s.priority DESC, s.start_time ASC ORDER BY s.priority DESC, s.start_time ASC
`).all(device_id); `).all(device_id);
@ -61,7 +63,7 @@ router.get('/week', (req, res) => {
// Create schedule // Create schedule
router.post('/', (req, res) => { router.post('/', (req, res) => {
const { device_id, zone_id, content_id, widget_id, layout_id, title, start_time, end_time, const { device_id, zone_id, content_id, widget_id, layout_id, playlist_id, title, start_time, end_time,
timezone, recurrence, recurrence_end, priority, color } = req.body; timezone, recurrence, recurrence_end, priority, color } = req.body;
if (!device_id || !start_time || !end_time) { if (!device_id || !start_time || !end_time) {
@ -70,11 +72,11 @@ router.post('/', (req, res) => {
const id = uuidv4(); const id = uuidv4();
db.prepare(` db.prepare(`
INSERT INTO schedules (id, user_id, device_id, zone_id, content_id, widget_id, layout_id, title, INSERT INTO schedules (id, user_id, device_id, zone_id, content_id, widget_id, layout_id, playlist_id, title,
start_time, end_time, timezone, recurrence, recurrence_end, priority, color) start_time, end_time, timezone, recurrence, recurrence_end, priority, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, req.user.id, device_id, zone_id || null, content_id || null, widget_id || null, `).run(id, req.user.id, device_id, zone_id || null, content_id || null, widget_id || null,
layout_id || null, title || '', start_time, end_time, timezone || 'UTC', layout_id || null, playlist_id || null, title || '', start_time, end_time, timezone || 'UTC',
recurrence || null, recurrence_end || null, priority || 0, color || '#3B82F6'); recurrence || null, recurrence_end || null, priority || 0, color || '#3B82F6');
const schedule = db.prepare('SELECT * FROM schedules WHERE id = ?').get(id); const schedule = db.prepare('SELECT * FROM schedules WHERE id = ?').get(id);
@ -87,7 +89,7 @@ router.put('/:id', (req, res) => {
if (!schedule) return res.status(404).json({ error: 'Schedule not found' }); if (!schedule) return res.status(404).json({ error: 'Schedule not found' });
if (!['admin','superadmin'].includes(req.user.role) && schedule.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' }); if (!['admin','superadmin'].includes(req.user.role) && schedule.user_id !== req.user.id) return res.status(403).json({ error: 'Access denied' });
const fields = ['device_id', 'zone_id', 'content_id', 'widget_id', 'layout_id', 'title', const fields = ['device_id', 'zone_id', 'content_id', 'widget_id', 'layout_id', 'playlist_id', 'title',
'start_time', 'end_time', 'timezone', 'recurrence', 'recurrence_end', 'priority', 'enabled', 'color']; 'start_time', 'end_time', 'timezone', 'recurrence', 'recurrence_end', 'priority', 'enabled', 'color'];
const updates = []; const updates = [];
const values = []; const values = [];

View file

@ -9,6 +9,9 @@ function startScheduler(socketIo) {
console.log('Scheduler service started'); console.log('Scheduler service started');
} }
// Track which devices have a schedule override active so we can revert
const activeOverrides = new Map(); // deviceId -> { playlist_id, layout_id }
function evaluateSchedules() { function evaluateSchedules() {
const deviceNs = io?.of('/device'); const deviceNs = io?.of('/device');
if (!deviceNs) return; if (!deviceNs) return;
@ -18,27 +21,38 @@ function evaluateSchedules() {
for (const device of onlineDevices) { for (const device of onlineDevices) {
const schedules = db.prepare(` const schedules = db.prepare(`
SELECT s.*, c.filename, c.mime_type, c.filepath, c.file_size, c.remote_url, SELECT s.*
c.duration_sec as content_duration
FROM schedules s FROM schedules s
LEFT JOIN content c ON s.content_id = c.id
WHERE s.device_id = ? AND s.enabled = 1 WHERE s.device_id = ? AND s.enabled = 1
ORDER BY s.priority DESC ORDER BY s.priority DESC
`).all(device.id); `).all(device.id);
// Find currently active schedule
const active = schedules.find(s => isScheduleActiveNow(s, now)); const active = schedules.find(s => isScheduleActiveNow(s, now));
const override = activeOverrides.get(device.id);
let changed = false;
if (active && active.content_id) { if (active) {
// Check if this is different from current playback // Apply layout override if schedule has one
const currentLayout = device.layout_id; if (active.layout_id && active.layout_id !== device.layout_id) {
if (active.layout_id && active.layout_id !== currentLayout) { if (!override) activeOverrides.set(device.id, { layout_id: device.layout_id, playlist_id: device.playlist_id });
// Switch layout
db.prepare("UPDATE devices SET layout_id = ? WHERE id = ?").run(active.layout_id, device.id); db.prepare("UPDATE devices SET layout_id = ? WHERE id = ?").run(active.layout_id, device.id);
// Push updated playlist changed = true;
pushPlaylistToDevice(device.id, deviceNs);
} }
// Apply playlist override if schedule has one
if (active.playlist_id && active.playlist_id !== device.playlist_id) {
if (!override) activeOverrides.set(device.id, { layout_id: device.layout_id, playlist_id: device.playlist_id });
db.prepare("UPDATE devices SET playlist_id = ? WHERE id = ?").run(active.playlist_id, device.id);
changed = true;
} }
} else if (override) {
// No active schedule — revert to original playlist/layout
db.prepare("UPDATE devices SET playlist_id = ?, layout_id = ? WHERE id = ?")
.run(override.playlist_id, override.layout_id, device.id);
activeOverrides.delete(device.id);
changed = true;
}
if (changed) pushPlaylistToDevice(device.id, deviceNs);
} }
} }