screentinker/server/scripts/demo-schedule.js
ScreenTinker 2ccf3264a9
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
feat(scheduling): per-item schedule blocks (#74 dayparting, #75 auto-expire)
Each playlist item can carry schedule blocks (active days, start/end
time-of-day, optional start/end dates). An item plays when the screen's
local "now" matches at least one block; an item with no blocks always
plays. #74 covers time-of-day/day-of-week windows including overnight
wrap; #75 covers inclusive date ranges (auto-expiry). Evaluation is
on-device, so dayparting and expiry work offline.

- Shared evaluator contract: shared/schedule-vectors.json (39 vectors —
  DST US+AU, overnight-wrap anchoring, timezone correctness, date
  boundaries). Canonical JS evaluator in server/lib/schedule-eval.js;
  Kotlin and Tizen ports kept in lockstep by drift guards (Tizen byte-diff
  test, Kotlin JUnit reads the shared JSON, new android-test CI job).
- All three players (web, Android, Tizen) filter by schedule against their
  own clock, idle with a "Nothing scheduled" message + 30s re-check when
  everything is filtered, and fail open on any evaluator error.
- Editor: per-item schedule modal + row badge in the playlist editor;
  client validation mirrors the server; editing marks the playlist draft.
- Part B (behaviour change): device/group schedule overrides now evaluate
  in each device's effective timezone instead of server-local time.
- Device detail shows the reported timezone + a clock-skew warning.
- i18n for en/es/fr/de/pt across all new strings (namespaced itemsched.*
  to avoid colliding with the device-schedule calendar's schedule.*).
- CHANGELOG documents the feature, the Part B change, the fail-open
  guarantee, and the scheduled-single-video re-render tradeoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:46:41 -05:00

56 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
// Local, headless demonstration of per-item scheduling (#74 dayparting + #75 expiry).
// node scripts/demo-schedule.js
//
// Builds a 3-item playlist and shows, using the REAL shared evaluator
// (server/lib/schedule-eval.js) and the same "next active item" rule the three
// players use, exactly which items rotate at four moments. No server or browser
// needed - this is the deterministic proof. Live web-player repro steps are printed
// at the end (and in the feature report).
const { isItemActiveNow } = require('../lib/schedule-eval');
const TZ = 'Australia/Sydney'; // Bold Media's zone; set as the device timezone override
// 'Yesterday' relative to the demo's reference day (Fri 2026-06-12 in TZ) is 06-11;
// the expired item ends 06-10 so it is dead on the 12th and 13th.
const playlist = [
{ id: 'A', label: 'Dayparted promo (Mon-Fri 09:00-17:00)', schedules: [{ days: [1, 2, 3, 4, 5], start: '09:00', end: '17:00', start_date: null, end_date: null }] },
{ id: 'B', label: 'Expired sale (ended 2026-06-10)', schedules: [{ days: [0, 1, 2, 3, 4, 5, 6], start: '00:00', end: '24:00', start_date: null, end_date: '2026-06-10' }] },
{ id: 'C', label: 'Filler (no schedule, always)', schedules: [] },
];
function rotationAt(utcIso) {
const active = playlist.filter(it => isItemActiveNow(it.schedules, utcIso, TZ));
return active.length ? active.map(it => it.id).join(' -> ') : '(idle: "Nothing scheduled right now")';
}
const scenarios = [
['INSIDE the daypart window (Fri 10:00 local)', '2026-06-12T00:00:00Z'],
['Window JUST opened (Fri 09:00 local)', '2026-06-11T23:00:00Z'],
['OUTSIDE the window (Fri 20:00 local)', '2026-06-12T10:00:00Z'],
['Weekend, window closed (Sat 10:00 local)', '2026-06-13T00:00:00Z'],
];
console.log('\n Per-item scheduling demo — device timezone = ' + TZ + '\n');
for (const it of playlist) console.log(' ' + it.id + ' ' + it.label);
console.log('\n ' + 'Moment'.padEnd(48) + 'Items that rotate');
console.log(' ' + '-'.repeat(48) + '-----------------');
for (const [label, utc] of scenarios) {
console.log(' ' + label.padEnd(48) + rotationAt(utc));
}
console.log('\n Notes:');
console.log(' - B (expired) never appears on any day after 2026-06-10. (#75)');
console.log(' - Inside the window: filler C + dayparted A rotate. Outside: C only. (#74)');
console.log(' - The players re-evaluate at each item boundary, and re-check every 30s');
console.log(' while idle, so A appears within 30s of 09:00 local — controllable on a');
console.log(' test screen via the device timezone override.\n');
console.log(' Live web-player repro:');
console.log(' 1. cd server && DATA_DIR=/tmp/st-demo SELF_HOSTED=true node server.js');
console.log(' 2. Dashboard -> create a playlist with 3 items (any content); on item A open the');
console.log(' clock icon and add a block for the next few minutes in your screen\'s local time,');
console.log(' on item B set an end date of yesterday, leave C unscheduled. Publish.');
console.log(' 3. Device detail -> set Timezone to ' + TZ + ' (or your zone) to control "local now".');
console.log(' 4. Open /player on the paired screen: B never shows; outside A\'s window only C');
console.log(' plays; within 30s of A\'s window opening, A joins the rotation.\n');