mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
Two independent multi-zone bugs, plus operator-facing warnings, i18n, and regression tests guarding the data contracts. Bug 1 — per-item mute was a no-op end to end: - GET /api/devices/:id dropped the `muted` column from its assignments SELECT, so the dashboard toggle never reflected state (the muted=false case in particular). Column restored to the device payload. - Android player now honours the per-item mute flag for YouTube (initial state + live via the IFrame JS API). Bug 2 — items whose zone_id belongs to a different layout were silently dropped: - Player fallback (web + Android): an orphaned zone_id is recovered into the largest zone instead of vanishing, with telemetry. - server/lib/zone-validate.js is the single source of truth for the orphan rule (zone not in the device's active layout); used by the device payload (per-item `orphan` flag + `active_layout_zones`) and the device list (`orphan_count`). - Assign-time hardening: a stale zone_id (not in the device's active layout) is cleared to null on POST/PUT rather than persisted as a new orphan. - scripts/find-orphan-zone-items.js: read-only sweep for existing orphans. Dashboard warnings (operator-facing, never on the live player): - Per-item badge + reassign affordance, device-list glance, preview banner. - Graceful degradation: the zone selector falls back to /api/layouts/:id so it can't vanish on a stale payload. i18n: orphan-zone strings added to en/es/fr/de/pt/it (hi falls back by design; count strings interpolate through tn()). Tests: server/test/device-zone-contract.test.js adds 5 regression tests for the data contracts above (muted true/false round-trip, active_layout_zones, orphan flag + count, orphan-clears-on-reassign, assign-time clearing). 172/172 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
3 KiB
JavaScript
67 lines
3 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* Report-only audit: find playlist_items whose zone_id is NOT a zone in the
|
|
* device's ACTIVE layout — i.e. orphaned cross-layout assignments. Un-patched
|
|
* players silently drop these; patched players (this branch) route them to the
|
|
* largest zone and emit a "zone" device-log warning. This script only REPORTS;
|
|
* it never mutates. Run it against a COPY of the prod DB.
|
|
*
|
|
* node scripts/find-orphan-zone-items.js [path/to/remote_display.db]
|
|
*
|
|
* Exit code is always 0 (it's a report); the count is printed.
|
|
*/
|
|
const path = require('path');
|
|
let Database;
|
|
try {
|
|
Database = require('better-sqlite3');
|
|
} catch (e) {
|
|
// Resolve from the server's node_modules when run from the repo root.
|
|
Database = require(path.join(__dirname, '..', 'server', 'node_modules', 'better-sqlite3'));
|
|
}
|
|
|
|
const dbPath = process.argv[2] || path.join(__dirname, '..', 'server', 'db', 'remote_display.db');
|
|
const db = new Database(dbPath, { readonly: true });
|
|
|
|
// One row per (device, zoned item). A playlist shared by N devices is checked
|
|
// against EACH device's layout, since the same item can be valid for one device
|
|
// and orphaned for another.
|
|
const rows = db.prepare(`
|
|
SELECT d.id AS device_id, d.name AS device_name,
|
|
d.layout_id AS device_layout, dl.name AS device_layout_name,
|
|
pi.id AS item_id, pi.zone_id,
|
|
c.filename, c.mime_type,
|
|
lz.layout_id AS zone_layout, zl.name AS zone_layout_name, lz.name AS zone_name
|
|
FROM devices d
|
|
JOIN playlist_items pi ON pi.playlist_id = d.playlist_id
|
|
LEFT JOIN content c ON c.id = pi.content_id
|
|
LEFT JOIN layout_zones lz ON lz.id = pi.zone_id
|
|
LEFT JOIN layouts dl ON dl.id = d.layout_id
|
|
LEFT JOIN layouts zl ON zl.id = lz.layout_id
|
|
WHERE pi.zone_id IS NOT NULL
|
|
`).all();
|
|
|
|
// Orphan = the item's zone doesn't exist any more, OR it belongs to a different
|
|
// layout than the device is actually rendering.
|
|
const orphans = rows.filter(r => !r.zone_layout || r.zone_layout !== r.device_layout);
|
|
|
|
if (!orphans.length) {
|
|
console.log(`No orphaned zone assignments found in ${dbPath}.`);
|
|
db.close();
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log(`Found ${orphans.length} orphaned playlist_item(s) in ${dbPath}`);
|
|
console.log(`(zone_id references a zone that is NOT in the device's active layout):\n`);
|
|
for (const o of orphans) {
|
|
const sid = s => (s || '').slice(0, 8);
|
|
const where = o.zone_layout
|
|
? `zone "${o.zone_name}" lives in layout "${o.zone_layout_name}" (${sid(o.zone_layout)})`
|
|
: `zone_id no longer exists`;
|
|
console.log(` device "${o.device_name}" (${sid(o.device_id)}) active layout "${o.device_layout_name || '—'}" (${sid(o.device_layout)})`);
|
|
console.log(` item #${o.item_id} ${o.filename || '?'} [${o.mime_type || '?'}] zone_id=${sid(o.zone_id)} -> ${where}`);
|
|
}
|
|
console.log(`\nReport only — nothing changed. Un-patched players drop these; patched players`);
|
|
console.log(`route them to the largest zone and log a "zone" warning. Use the hardening`);
|
|
console.log(`(remap-on-duplicate / validate-on-assign) to stop new ones being created.`);
|
|
db.close();
|