Skip to content

SOT: covers that run past midnight, and clocks that say which zone they're in

A partial cover on an overnight stream can now be recorded for the hours after midnight — it used to be refused. Every time field in the Scheduling Hub's cover and session dialogs now names the timezone it is in, and the Overview board reads the same timezone as the calendar beside it.

Covering the hours after midnight (SOT managers)

A stream that runs 8:00 PM to 2:00 AM crosses two days. If a substitute took over at midnight, Cover from 00:00 to 02:00 was refused with "the cover window must fall within the session's stream window" — the Hub was reading both times as the stream's first day, so it was asking to record a cover for the previous night. The only way through was to lie about the hours or skip the record entirely, and a cover that is never recorded is a cover that is never paid.

Both times now take their day from the shift itself, so a time after midnight lands on the second day where it belongs.

  • The dialog names the shift it is bounding you to, at the top: Shift: Oct 17 · 8:00 PM – 2:00 AM (GMT+8).
  • Once both times are set it shows the window you are about to save — Covering Oct 18 · 12:00 AM – 2:00 AM (GMT+8) — so a cover that rolls past midnight shows the day it landed on rather than leaving you to work it out.
  • A time genuinely outside the shift is now caught in the dialog, naming the field and leaving your times on screen to correct, instead of coming back as a failed save.

Guide: Covering a position (full or partial)

Every clock says which timezone it is

A session records the timezone it was scheduled in, and that is the timezone its times are shown and entered in. The cover dialog never said so — two bare Cover from / Cover to fields read as your own wall clock, which they are not.

  • The cover dialog now says Times are in Manila (GMT+8) under the fields.
  • When the calendar's Viewing in is set to a different zone, each time line gains the same times in that zone beneath it — Los Angeles (GMT-7): 9:00 AM–11:00 AM — in the cover dialog and the session detail alike.

This is a label, not a change of behaviour: what you type still means the session's timezone, and the session still records the zone it was scheduled in.

Guide: Reading the calendar in another timezone

The Overview board follows the calendar's timezone

Scheduling Hub → Overview used to cut each session's day by that session's own timezone, while the calendar next to it read every session through the one Viewing in zone. A session could sit in one week on the calendar and another on the board.

The board now reads the Viewing in zone too, and says so under the week: Days cut in Manila (GMT+8).

Hours may move between weeks on this board for any session that was scheduled in a timezone other than the one you are viewing in. Nothing was recorded wrongly before and nothing has been changed — the board is now answering the same question as the calendar.

Guide: Overview board

Technical changes

For developers and whoever deploys. None of this needs a guide.

Data model

  • No new tables, columns or constraints. Nothing stored changes: StreamSession keeps its own timezone, and StreamSessionAssignment.cover_start_at / cover_end_at keep holding instants. The midnight bug was entirely in how the browser turned a clock face into an instant.

Behaviour

  • GET /api/management/sot/scheduling/overview/ takes a new optional tz query parameter (IANA name). compute_overview(week_start, timezone_name) buckets every session through that one zone instead of each session's own, and the response echoes it back as timezone. Default is Asia/Manila, matching the Hub's own default view zone.
  • An unknown tz is a 400, not a silent fall back to Manila — the same stance the session write path takes. A board quietly cut on the wrong zone reports the wrong week's hours and says nothing.
  • cover_position in management/sot/scheduling/services.py is unchanged. Its "cover window must fall within the session's stream window" check was always right; the frontend was sending a window a day early. The dialog now applies the same two rules client-side first, so the server check becomes a backstop rather than the first thing the user hears.
  • New frontend helper timeWithinWindow(time, windowStart, windowEnd, zone) in src/lib/utils/timezone.ts. It tries the clock on both of the window's candidate days and returns the one inside it, falling back to the nearer miss so the caller's own bounds check can report a near miss. It replaces the old "derive both bounds from the start date, roll only the end" logic, which is what put a post-midnight cover 24 hours early.

Deploy

  • No migrations. No new environment variables, no new workers, no one-off commands. Frontend-only for the cover dialogs; the Overview change needs both sides, and an older frontend against the new backend simply omits tz and gets the Manila default.

Tests

  • management/sot/scheduling/tests/test_scheduling.py — test_partial_cover_after_midnight_on_an_overnight_shift pins the instants the fixed dialog sends; three more cover the tz parameter, the Manila default and its echo, and the fact that the same session buckets onto a different day in UTC than in Manila.
  • The existing OverviewTests now pass their zone explicitly (TZ = "UTC"). Their helper builds UTC sessions at 20:00, which is the next day in Manila — two of them failed against the new default until they said which zone they meant.
  • timeWithinWindow has no automated test: frontend/ has no test runner. It was exercised by hand over overnight, same-day and a US spring-forward night.