Skip to content

fix: measure the SGA Spaces buffer in the timezone it is stored in (#87) - #89

Merged
pataniaeli merged 2 commits into
devfrom
fix/issue-87-spaces-buffer
Sep 9, 2026
Merged

fix: measure the SGA Spaces buffer in the timezone it is stored in (#87)#89
pataniaeli merged 2 commits into
devfrom
fix/issue-87-spaces-buffer

Conversation

@pataniaeli

Copy link
Copy Markdown
Collaborator

Closes #87. No migration.

What was wrong

With the requirement set to two hours it behaved like six in summer and seven in winter. That's why min_hours_advance_spaces is currently 0 in app_settings — the only way to stop it blocking legitimate bookings.

SGA Spaces stores times as Boston wall-clock digits with a Z on the end: a 6 PM booking is T18:00:00.000Z whatever the offset is that month. slotToIso sets UTC hours straight from the slot index, the modal reads them back with getUTCHours, and the confirmation email depends on it too — there's already a comment in space-booking-confirmed.ts saying UTC fields give the right local-time digits to pair with TZID=America/New_York.

Self-consistent, until something compares one of those values against a real instant. The check did exactly that — start_time against Date.now() — so every booking looked one UTC offset earlier than it was, and the requirement rejected anything inside minHours + 4 (or + 5 after the clocks go back).

This is also why the calendar disagreed with the server. The shading uses wallClockNow, already in the stored domain, so it correctly showed the slot as available — and then submitting failed.

The fix

"Now" is taken as Boston wall clock, in the same domain as the value it's compared against. Pinned to America/New_York rather than the server's clock, because Vercel runs in UTC and reading local fields there is the same bug with an extra step. Intl resolves the offset for the given instant, so EDT and EST both work without a table.

Also fixed: the same root cause in remaining-hours

It derived its Sun–Sat window from real UTC. UTC is already Sunday by 8 PM Saturday in EDT, so for the last four hours of the week a user's hours were counted against the next week and the remaining figure jumped. Out of scope for the issue as written, but fixing half a domain bug is worse than fixing none.

Verification

Ran the check both ways against fixed instants in each season, with a 2-hour requirement:

Booking is… EDT before EDT after EST before EST after
1h out rejected rejected ✓ rejected rejected ✓
2h out rejected allowed ✓ rejected allowed ✓
3h out rejected allowed ✓ rejected allowed ✓
5h out rejected allowed ✓ rejected allowed ✓
6h out allowed allowed rejected allowed ✓
7h out allowed allowed allowed allowed

Wall clock resolves to 11:46 from 15:46Z in September and 11:00 from 16:00Z in January — a hardcoded −4 would have been wrong for half the year.

After merging

min_hours_advance_spaces can go back to a real value; it's at 0 today as a workaround.

🤖 Generated with Claude Code

The advance-notice requirement rejected bookings that were far enough out. With
it set to two hours it behaved like six in summer and seven in winter, which is
why it is currently set to 0 in app_settings -- the only way to stop it blocking
legitimate bookings.

SGA Spaces stores times as Boston wall-clock digits with a Z on the end: a 6 PM
booking is T18:00:00.000Z whatever the offset is that month. slotToIso sets UTC
hours straight from the slot index, the modal reads them back with getUTCHours,
and the confirmation email depends on it too -- there is a comment in
space-booking-confirmed.ts saying UTC fields give the right local-time digits to
pair with TZID=America/New_York.

That is self-consistent until something compares one of those values against a
real instant. The check did exactly that: start_time against Date.now(). Every
booking therefore looked one UTC offset earlier than it really was, and the
requirement rejected anything inside minHours + 4, or + 5 once the clocks go
back.

Which is why the calendar disagreed with the server. The shading uses
wallClockNow, already in the stored domain, so it correctly showed the slot as
available -- and then submitting it failed.

"Now" is therefore taken as Boston wall clock, in the same domain as the value it
is compared against. Pinned to America/New_York rather than the server's clock,
because Vercel runs in UTC and reading local fields there is the same bug with an
extra step. Intl resolves the offset for the instant given, so EDT and EST both
work without a table.

Also fixes the same root cause in remaining-hours, which derived its Sun-Sat
window from real UTC. UTC is already Sunday by 8 PM on a Saturday in EDT, so for
the last four hours of the week a user's hours were counted against the next one
and the remaining figure jumped. Half a domain bug is worse than none.

Verified by running the check both ways against fixed instants in each season.
With a two-hour requirement in EDT, the old comparison rejected every booking up
to five hours out and the new one rejects only those under two; in EST the old
rejected up to six. Wall clock resolves to 11:46 from 15:46Z in September and to
11:00 from 16:00Z in January, so a hardcoded offset would have been wrong for
half the year.

min_hours_advance_spaces can go back to a real value once this ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
chambers Ready Ready Preview Sep 9, 2026 3:53pm UTC

The kiosk displays themselves were already right, and deliberately so: the page
compares local minutes-from-midnight against the stored value's UTC hours and
minutes, which are the wall-clock digits, so both sides are in the same domain.
There is a comment saying as much, and the date-flip trap is handled in the page
as well -- local date components rather than toISOString, which would roll over
at 8 PM EDT.

What was still armed is the server's fallback for a missing `date` parameter,
which took the UTC date. After 8 PM EDT that is already tomorrow, so a caller
without the parameter would be served the wrong day's bookings. The display page
always sends it, so nothing reaches this today -- but it is the same trap as the
booking buffer, and leaving it set for the next caller is how the buffer bug got
written in the first place.

Verified at 9:30 PM EDT: the kiosk still shows a 9-10 PM booking as in use, and
the two fallbacks disagree by a day exactly as expected -- UTC says the 9th,
Boston says the 8th.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pataniaeli

Copy link
Copy Markdown
Collaborator Author

Added 63704b9 — checked the displays, as asked.

The displays were already correct

Not by luck. The page compares like with like:

nowLocalMins                   = now.getHours() * 60 + now.getMinutes()      // kiosk local clock
minsFromMidnight(b.start_time) = d.getUTCHours() * 60 + d.getUTCMinutes()    // stored wall-clock digits

Both sides are in the same wall-clock domain, so "in use now" is right. There is already a comment saying so, and the date-flip trap is handled in the page too — local date components rather than toISOString, which would roll over at 8 PM EDT. The page always sends &date= built the same way.

One latent gap, now closed

The server's fallback for a missing date parameter took the UTC date. After 8 PM EDT that is already tomorrow, so a caller without the parameter gets the wrong day's bookings. The display page always sends it, so nothing reaches this today — but it is the identical trap, and leaving it armed for the next caller is exactly how the buffer bug got written.

Verified at 9:30 PM EDT

Real instant 2026-09-09T01:30:00Z
Boston wall clock 2026-09-08T21:30
Kiosk shows 9–10 PM booking as in use yes (1290 between 1260 and 1320) — unchanged
Old fallback date 2026-9-9
New fallback date 2026-9-8

So: nothing about the kiosk behaviour changes, and the one place that could have gone wrong no longer can.

@pataniaeli pataniaeli added the bug Something isn't working label Sep 9, 2026
@pataniaeli pataniaeli self-assigned this Sep 9, 2026
@pataniaeli pataniaeli linked an issue Sep 9, 2026 that may be closed by this pull request
@pataniaeli
pataniaeli merged commit 65b335f into dev Sep 9, 2026
4 checks passed
@pataniaeli
pataniaeli deleted the fix/issue-87-spaces-buffer branch September 9, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SGA Spaces buffer doesn't block correctly

1 participant