screentinker/server/lib/device-timezone.js
ScreenTinker 0030acc526 Store a schedule in the timezone its screen runs in
Creation and playback disagreed about which clock a schedule's hours are on.
The player evaluated blocks in the device's zone — an operator override, else
whatever the player's OS reported. Creation defaulted to a bare 'UTC', because
the dialog never asked for a zone and the server filled the silence with one.

So hours typed as "09:00 to 17:00" were stored as UTC and evaluated somewhere
else. For anyone outside UTC the schedule was correct and appeared to do
nothing, opening hours later than intended, with nothing on screen to explain
why. A user in Asia/Tokyo hit exactly this and reported it as "I added
something and it didn't appear".

Both sides now resolve through lib/device-timezone, so they cannot drift: an
explicit device override wins, then the OS-reported zone, then null. A legacy
'UTC' override counts as unset, since that was the old default rather than a
deliberate choice and a genuine UTC deployment is indistinguishable from an
unconfigured one.

A new schedule inherits its target's zone — the device's, or for a group its
leader's, falling back to the oldest member that reports one. A zone named
explicitly by the caller still wins; this only fills the silence. A target that
has never reported one still lands on UTC, which is the previous behaviour made
explicit rather than assumed.

The dialog now states which clock the hours are on, and says so differently when
that clock is not the operator's own. Stating it is the other half of the fix:
the server can pick the right zone, but the user still has to be able to see it.

Tests pin both directions and, most importantly, that creation and playback
resolve identically from the same device row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 09:43:46 -05:00

28 lines
1.4 KiB
JavaScript

'use strict';
// The IANA zone a device's schedule blocks are evaluated in.
//
// This is the SINGLE definition, shared by the two places that must agree:
// - ws/deviceSocket.js — evaluating which schedule block is active right now
// - routes/schedules.js — choosing the zone a NEW schedule is stored in
//
// They previously disagreed. Playback resolved the device's zone, while creation
// defaulted to a bare 'UTC' because the dialog never asked. A user in any non-UTC
// zone typed wall-clock hours, got UTC, and watched a screen that was correctly
// showing nothing — with no visible cue that the hours meant something else.
// Observed in the wild: a schedule set 09:00-17:00 by a user in Asia/Tokyo, stored
// as UTC, which would not open until 18:00 their time.
//
// Precedence: an explicit operator override wins, then whatever the player's OS
// last reported, then null. 'UTC' as an override is treated as "unset" because
// that is the historical default value, not a deliberate choice — a real
// UTC deployment is indistinguishable from an unconfigured one, and defaulting to
// the reported zone is the safer of the two readings.
function effectiveDeviceTz(device) {
if (!device) return null;
const override = device.timezone && device.timezone !== 'UTC' ? device.timezone : null;
return override || device.reported_timezone || null;
}
module.exports = { effectiveDeviceTz };