Commit graph

2 commits

Author SHA1 Message Date
ScreenTinker c5550f5bc9 feat: agency zone-grant issuance UI + reactive placement card (#73)
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>
2026-06-14 15:12:55 -05:00
ScreenTinker 289d54f4fa feat(api): zone-grant confinement for agency tokens - FK-anchored (#73)
Placement-as-grant, replacing the inferred auto-place idea. api_token_target_zones is an
ADDITIVE second table (does NOT touch the proven api_token_targets), structurally anchored:
a composite FK to api_token_targets(token_id, playlist_id) makes a zone grant orphan-
impossible and cascade away when the playlist grant is revoked - "narrow" is structural, not
conventional. zone_id FK -> layout_zones cascades on zone/layout delete.

Confinement (lib/agency-targets.resolveGrantedZone, called in the item-add): grants exist ->
the item MUST land in a granted zone (a body zone_id picks among grants, never escapes them);
none -> whole-playlist/full-screen as before. The item-add stamps the granted zone_id.

Bite-tested (6, all proven incl. neutralize->red on the confinement): granted YES; non-
granted/cross-playlist/ambiguous blocked; orphan-grant rejected by the FK; cascade on
playlist-grant revoke, on playlist delete, on zone/layout delete; and foreign_keys=ON
asserted (a cascade that no-ops because FKs are off is the trap). 153 suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:57:27 -05:00