Unify publish behavior: all edits go to draft, require explicit publish

Remove autoPublish from assignments.js and device-groups.js. All item
mutations (add, update, delete, reorder, copy) now call markDraft
regardless of which UI the edit comes from. Users must explicitly
click Publish to push changes to devices.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-04-13 21:22:44 -05:00
parent 436a3be7f6
commit f30d8b82cd
2 changed files with 12 additions and 61 deletions

View file

@ -3,37 +3,9 @@ const router = express.Router();
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const { db } = require('../db/database'); const { db } = require('../db/database');
// Auto-publish: snapshot current items and push to devices. // Mark playlist as draft (called after any item mutation)
// Device-detail edits always go live immediately. function markDraft(playlistId) {
// If deviceId is provided, pushes to that device only; otherwise pushes to all devices using this playlist. db.prepare("UPDATE playlists SET status = 'draft', updated_at = strftime('%s','now') WHERE id = ?").run(playlistId);
function autoPublish(playlistId, req, deviceId) {
const items = db.prepare(`
SELECT pi.content_id, pi.widget_id, pi.sort_order, pi.duration_sec,
COALESCE(c.filename, w.name) as filename, c.mime_type, c.filepath, c.file_size,
c.duration_sec as content_duration, c.remote_url,
w.name as widget_name, w.widget_type, w.config as widget_config
FROM playlist_items pi
LEFT JOIN content c ON pi.content_id = c.id
LEFT JOIN widgets w ON pi.widget_id = w.id
WHERE pi.playlist_id = ?
ORDER BY pi.sort_order ASC
`).all(playlistId);
db.prepare("UPDATE playlists SET status = 'published', published_snapshot = ?, updated_at = strftime('%s','now') WHERE id = ?")
.run(JSON.stringify(items), playlistId);
try {
const io = req?.app?.get('io');
if (!io) return;
const { buildPlaylistPayload } = require('../ws/deviceSocket');
if (!buildPlaylistPayload) return;
if (deviceId) {
io.of('/device').to(deviceId).emit('device:playlist-update', buildPlaylistPayload(deviceId));
} else {
const devices = db.prepare('SELECT id FROM devices WHERE playlist_id = ?').all(playlistId);
for (const d of devices) {
io.of('/device').to(d.id).emit('device:playlist-update', buildPlaylistPayload(d.id));
}
}
} catch (e) { /* silent */ }
} }
// Check device ownership for device-scoped routes // Check device ownership for device-scoped routes
@ -117,7 +89,7 @@ router.post('/device/:deviceId', (req, res) => {
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`).run(playlistId, content_id || null, widget_id || null, order, duration_sec); `).run(playlistId, content_id || null, widget_id || null, order, duration_sec);
autoPublish(playlistId, req, req.params.deviceId); markDraft(playlistId);
const item = db.prepare(`${ITEM_SELECT} WHERE pi.id = ?`).get(result.lastInsertRowid); const item = db.prepare(`${ITEM_SELECT} WHERE pi.id = ?`).get(result.lastInsertRowid);
res.status(201).json(item); res.status(201).json(item);
@ -145,7 +117,7 @@ router.put('/:id', (req, res) => {
updates.push("updated_at = strftime('%s','now')"); updates.push("updated_at = strftime('%s','now')");
values.push(req.params.id); values.push(req.params.id);
db.prepare(`UPDATE playlist_items SET ${updates.join(', ')} WHERE id = ?`).run(...values); db.prepare(`UPDATE playlist_items SET ${updates.join(', ')} WHERE id = ?`).run(...values);
autoPublish(item.playlist_id, req, null); markDraft(item.playlist_id);
} }
const updated = db.prepare(`${ITEM_SELECT} WHERE pi.id = ?`).get(req.params.id); const updated = db.prepare(`${ITEM_SELECT} WHERE pi.id = ?`).get(req.params.id);
@ -158,7 +130,7 @@ router.delete('/:id', (req, res) => {
if (!item) return res.status(404).json({ error: 'Item not found' }); if (!item) return res.status(404).json({ error: 'Item not found' });
db.prepare('DELETE FROM playlist_items WHERE id = ?').run(req.params.id); db.prepare('DELETE FROM playlist_items WHERE id = ?').run(req.params.id);
autoPublish(item.playlist_id, req, null); markDraft(item.playlist_id);
res.json({ success: true, content_id: item.content_id }); res.json({ success: true, content_id: item.content_id });
}); });
@ -180,7 +152,7 @@ router.post('/device/:deviceId/reorder', (req, res) => {
}); });
transaction(); transaction();
autoPublish(device.playlist_id, req, req.params.deviceId); markDraft(device.playlist_id);
const items = db.prepare(`${ITEM_SELECT} WHERE pi.playlist_id = ? ORDER BY pi.sort_order ASC`) const items = db.prepare(`${ITEM_SELECT} WHERE pi.playlist_id = ? ORDER BY pi.sort_order ASC`)
.all(device.playlist_id); .all(device.playlist_id);
@ -216,7 +188,7 @@ router.post('/device/:deviceId/copy-to/:targetDeviceId', (req, res) => {
}); });
transaction(); transaction();
autoPublish(targetPlaylistId, req, req.params.targetDeviceId); markDraft(targetPlaylistId);
res.json({ success: true, copied: sourceItems.length }); res.json({ success: true, copied: sourceItems.length });
}); });

View file

@ -93,30 +93,9 @@ function ensureDevicePlaylist(deviceId, userId) {
return playlistId; return playlistId;
} }
// Auto-publish: snapshot current items and push to device. // Mark playlist as draft (called after any item mutation)
// Group assign-content is the bulk equivalent of device-detail — always goes live immediately. function markDraft(playlistId) {
function autoPublish(playlistId, req, deviceId) { db.prepare("UPDATE playlists SET status = 'draft', updated_at = strftime('%s','now') WHERE id = ?").run(playlistId);
const items = db.prepare(`
SELECT pi.content_id, pi.widget_id, pi.sort_order, pi.duration_sec,
COALESCE(c.filename, w.name) as filename, c.mime_type, c.filepath, c.file_size,
c.duration_sec as content_duration, c.remote_url,
w.name as widget_name, w.widget_type, w.config as widget_config
FROM playlist_items pi
LEFT JOIN content c ON pi.content_id = c.id
LEFT JOIN widgets w ON pi.widget_id = w.id
WHERE pi.playlist_id = ?
ORDER BY pi.sort_order ASC
`).all(playlistId);
db.prepare("UPDATE playlists SET status = 'published', published_snapshot = ?, updated_at = strftime('%s','now') WHERE id = ?")
.run(JSON.stringify(items), playlistId);
try {
const io = req?.app?.get('io');
if (!io) return;
const { buildPlaylistPayload } = require('../ws/deviceSocket');
if (buildPlaylistPayload && deviceId) {
io.of('/device').to(deviceId).emit('device:playlist-update', buildPlaylistPayload(deviceId));
}
} catch (e) { /* silent */ }
} }
// Push playlist update to a device (used by assign-playlist which doesn't modify items) // Push playlist update to a device (used by assign-playlist which doesn't modify items)
@ -147,7 +126,7 @@ router.post('/:id/assign-content', requireGroupOwnership, (req, res) => {
const max = db.prepare('SELECT COALESCE(MAX(sort_order),0)+1 as next FROM playlist_items WHERE playlist_id = ?').get(playlistId); const max = db.prepare('SELECT COALESCE(MAX(sort_order),0)+1 as next FROM playlist_items WHERE playlist_id = ?').get(playlistId);
db.prepare('INSERT INTO playlist_items (playlist_id, content_id, sort_order, duration_sec) VALUES (?, ?, ?, ?)') db.prepare('INSERT INTO playlist_items (playlist_id, content_id, sort_order, duration_sec) VALUES (?, ?, ?, ?)')
.run(playlistId, content_id, max.next, duration_sec || 10); .run(playlistId, content_id, max.next, duration_sec || 10);
autoPublish(playlistId, req, m.device_id); markDraft(playlistId);
} }
}); });
transaction(); transaction();