mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-17 11:42:40 -06:00
Issuance (on the proven seam): - tokens.js create + PUT /:id/targets accept per-playlist zone grants (target_zones), inserted into api_token_target_zones inside the same transaction as the playlist grants (FK requires the parent, so order matters and is correct). - Issuance validation (the mirror of runtime confinement): grantableZoneIds() - can grant ONLY a zone the playlist's layout actually feeds; can't grant one it doesn't have or one from another playlist's layout. Bite-tested. PUT re-designate stays atomic: delete parent rows -> zone grants cascade out (no manual child delete). - settings.js: checking a designated playlist reveals its grantable zones (GET /api/playlists/:id/zones, JWT); leave unchecked = whole-playlist. i18n across all 5 locales. Card: - GET /api/agency/playlists/:playlistId/layout (rides router.param - confined; a non- designated playlist -> 403, asserted). "Your zone" = the GRANTED zones. Retired the token-wide /layouts (the per-playlist card replaces the disconnected lump). - Portal card reacts to the playlist selector: pick a playlist -> its layout renders, the granted zone highlighted with px size, siblings as context. Full suite + agency bite-suite green (154). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
2.3 KiB
JavaScript
50 lines
2.3 KiB
JavaScript
'use strict';
|
|
|
|
// #73: layout GEOMETRY for an agency token's designated playlists. DEVICE-FREE BY
|
|
// CONSTRUCTION: the only path used is playlist_items.zone_id -> layout_zones -> layouts.
|
|
// It never references devices / device_groups / schedules, so no fleet data (device names,
|
|
// locations, IPs, screen sizes, topology) can leak - it's structurally absent, not filtered.
|
|
// Confined to THIS token's designated playlists (t.token_id) in its bound workspace.
|
|
// Returns layout canvas size + ALL zones' geometry (no zone CONTENT) + which zones this
|
|
// token feeds. Bite-tested in test/agency-layouts.test.js.
|
|
function listLayoutGeometry(db, tokenId, workspaceId, playlistId = null) {
|
|
// Distinct layouts that this token's designated playlists feed (via their items' zones).
|
|
// Optional playlistId narrows to ONE designated playlist (the per-playlist card).
|
|
const layouts = db.prepare(`
|
|
SELECT DISTINCT l.id, l.name, l.width, l.height
|
|
FROM api_token_targets t
|
|
JOIN playlists p ON p.id = t.playlist_id AND p.workspace_id = ?
|
|
JOIN playlist_items pi ON pi.playlist_id = p.id AND pi.zone_id IS NOT NULL
|
|
JOIN layout_zones lz ON lz.id = pi.zone_id
|
|
JOIN layouts l ON l.id = lz.layout_id
|
|
WHERE t.token_id = ?${playlistId ? ' AND p.id = ?' : ''}
|
|
ORDER BY l.name
|
|
`).all(...(playlistId ? [workspaceId, tokenId, playlistId] : [workspaceId, tokenId]));
|
|
|
|
// All zones of a layout - GEOMETRY ONLY (no content, no device data lives here anyway).
|
|
const zonesStmt = db.prepare(`
|
|
SELECT id, name, x_percent, y_percent, width_percent, height_percent,
|
|
z_index, zone_type, fit_mode, background_color, sort_order
|
|
FROM layout_zones WHERE layout_id = ? ORDER BY sort_order, z_index
|
|
`);
|
|
// Which zones of a given layout THIS token actually feeds.
|
|
const feedsStmt = db.prepare(`
|
|
SELECT DISTINCT pi.zone_id
|
|
FROM api_token_targets t
|
|
JOIN playlist_items pi ON pi.playlist_id = t.playlist_id AND pi.zone_id IS NOT NULL
|
|
JOIN layout_zones lz ON lz.id = pi.zone_id
|
|
WHERE t.token_id = ? AND lz.layout_id = ?
|
|
`);
|
|
|
|
return layouts.map(l => ({
|
|
id: l.id,
|
|
name: l.name,
|
|
width: l.width,
|
|
height: l.height,
|
|
zones: zonesStmt.all(l.id),
|
|
feeds_zone_ids: feedsStmt.all(tokenId, l.id).map(r => r.zone_id),
|
|
}));
|
|
}
|
|
|
|
module.exports = { listLayoutGeometry };
|