feat(schedule): each business day remembers the schedule it was measured under #113

Closed
opened 2026-09-25 18:16:35 +00:00 by gabogg · 1 comment
Owner

Problem

Only the current weekly schedule is stored (occupancy_daily_schedule: one row per weekday, overwritten on edit). Anything that looks at a past business day (Closed Days #103, open-window Mean Dwell, dayparts, the Usual Weekday Baseline's matching-hours rule #105) resolves that day with today's schedule. The first schedule edit silently rewrites history:

  • Close Mondays from January → every past Monday becomes a Closed Day; past weeks lose their Monday visitors and comparisons change after the fact.
  • Reopen Mondays → past closed Mondays look like outages; past weeks turn partial.
  • Change Sunday hours → past Sundays get the wrong open window (dwell, daypart cuts), and #105's "same opening hours" rule matches days that did not share hours.

Dated exceptions (occupancy_holidays) are date-specific. Production has not edited its schedule since counting started (2026-09-03), so nothing is wrong yet.

Decisions (maintainer, 2026-09-25)

  1. Each business day remembers the schedule it was measured under. When a business day starts (at the reset), its schedule is resolved from the dated exception or the weekly schedule and recorded. From then on that day follows its record no matter what is edited: a change to the weekly schedule or to that day's exception mid-day takes effect from the next business day. No mid-day changes. The admin UI says so when editing.
  2. Recorded per day: open/closed, opening time, closing time, source (WEEKLY, EXCEPTION, STAMPED, MANUAL, plus room for an IMPORTED source, see 6), and the exception name when there was one (e.g. holiday name).
  3. Reset time is not recorded per day (adjacent days could overlap or leave gaps). Split out as #114. This issue documents in the admin and API docs that changing daily_reset_time re-cuts history.
  4. Days without a record (future days, any day the migration missed) fall back to the current schedule, as today. The source field shows the admin which days are real history.
  5. Migration button (stamp the current schedule onto recorded days): by default fills only days that have no record (safe to press twice); an explicit "overwrite this date range" option sits behind a confirmation.
  6. Per-day correction via a calendar selector: pick a past day and correct its record, including marking it as a holiday/exception with a name, closed, or with different hours. The day record is the history; holiday entries remain the tool for planning future days and are copied into a day's record when it starts. The model must leave room for a future retroactive import from HikCentral's recorded history, which will create day records (IMPORTED) for days before counting started and may mark past holidays; records can exist for any date, not only days with counted data.
  7. Calibration is not touched by corrections. Past days keep the calibration they were audited with; a correction never re-runs an audit or feeds old data into the current calibration (exit multiplier k).
  8. Admins only, and every stamp or correction is audit-logged: who, when, old values, new values, required reason (same pattern as trust toggles).
  9. Controls live in the schedule section of the admin (weekly schedule and exceptions, see #108); calibration may link to it.
  10. One reader for "the schedule of business day X": statistics, live operations (today's label and working hours) and calibration's open window all go through a single function that prefers the day record and falls back to the current schedule. This replaces the resolution in get_active_schedule_info_async / ScheduleCalendar (#110) so there is one rule app-wide.

Acceptance

  • Editing the weekly schedule or an exception never changes a started or past day's classification, hours, dwell or dayparts.
  • The migration button fills unrecorded days only, or overwrites a confirmed range; both are audit-logged.
  • A past day can be corrected from the calendar (closed, hours, holiday name) and statistics reflect it immediately; its calibration is unchanged.
  • Tests cover: frozen-at-start behaviour including a mid-day edit, fallback for unrecorded days, stamping idempotence, range overwrite, correction audit log, and one-reader consistency (statistics and live view agree for the same day).

Related: #103 (Closed Day), #105 (baseline matches opening hours), #108 (schedule exceptions naming), #109 (Closed Day calibration), #111 (ScheduleCalendar placement).

🤖 Generated with Claude Code

## Problem Only the **current** weekly schedule is stored (`occupancy_daily_schedule`: one row per weekday, overwritten on edit). Anything that looks at a past business day (Closed Days #103, open-window Mean Dwell, dayparts, the Usual Weekday Baseline's matching-hours rule #105) resolves that day with **today's** schedule. The first schedule edit silently rewrites history: - Close Mondays from January → every past Monday becomes a Closed Day; past weeks lose their Monday visitors and comparisons change after the fact. - Reopen Mondays → past closed Mondays look like outages; past weeks turn partial. - Change Sunday hours → past Sundays get the wrong open window (dwell, daypart cuts), and #105's "same opening hours" rule matches days that did not share hours. Dated exceptions (`occupancy_holidays`) are date-specific. Production has not edited its schedule since counting started (2026-09-03), so nothing is wrong yet. ## Decisions (maintainer, 2026-09-25) 1. **Each business day remembers the schedule it was measured under.** When a business day starts (at the reset), its schedule is resolved from the dated exception or the weekly schedule and recorded. From then on that day follows its record **no matter what is edited**: a change to the weekly schedule or to that day's exception mid-day takes effect from the next business day. No mid-day changes. The admin UI says so when editing. 2. **Recorded per day:** open/closed, opening time, closing time, source (`WEEKLY`, `EXCEPTION`, `STAMPED`, `MANUAL`, plus room for an `IMPORTED` source, see 6), and the exception name when there was one (e.g. holiday name). 3. **Reset time is not recorded per day** (adjacent days could overlap or leave gaps). Split out as #114. This issue documents in the admin and API docs that changing `daily_reset_time` re-cuts history. 4. **Days without a record** (future days, any day the migration missed) fall back to the current schedule, as today. The `source` field shows the admin which days are real history. 5. **Migration button** (stamp the current schedule onto recorded days): by default fills only days that have **no** record (safe to press twice); an explicit "overwrite this date range" option sits behind a confirmation. 6. **Per-day correction** via a calendar selector: pick a past day and correct its record, including marking it as a holiday/exception with a name, closed, or with different hours. The day record is the history; holiday entries remain the tool for planning future days and are copied into a day's record when it starts. The model must leave room for a **future retroactive import** from HikCentral's recorded history, which will create day records (`IMPORTED`) for days before counting started and may mark past holidays; records can exist for any date, not only days with counted data. 7. **Calibration is not touched by corrections.** Past days keep the calibration they were audited with; a correction never re-runs an audit or feeds old data into the current calibration (exit multiplier k). 8. **Admins only**, and every stamp or correction is audit-logged: who, when, old values, new values, required reason (same pattern as trust toggles). 9. **Controls live in the schedule section** of the admin (weekly schedule and exceptions, see #108); calibration may link to it. 10. **One reader for "the schedule of business day X"**: statistics, live operations (today's label and working hours) and calibration's open window all go through a single function that prefers the day record and falls back to the current schedule. This replaces the resolution in `get_active_schedule_info_async` / `ScheduleCalendar` (#110) so there is one rule app-wide. ## Acceptance - Editing the weekly schedule or an exception never changes a started or past day's classification, hours, dwell or dayparts. - The migration button fills unrecorded days only, or overwrites a confirmed range; both are audit-logged. - A past day can be corrected from the calendar (closed, hours, holiday name) and statistics reflect it immediately; its calibration is unchanged. - Tests cover: frozen-at-start behaviour including a mid-day edit, fallback for unrecorded days, stamping idempotence, range overwrite, correction audit log, and one-reader consistency (statistics and live view agree for the same day). Related: #103 (Closed Day), #105 (baseline matches opening hours), #108 (schedule exceptions naming), #109 (Closed Day calibration), #111 (`ScheduleCalendar` placement). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

Implementation note from the #122 review: once per-day schedule records exist, switch these call sites to prefer the day record:

  • ScheduleCalendar.opening_hours(day) (Usual Weekday Baseline's same-opening-hours rule, #105)
  • ScheduleCalendar.is_closed(day) (Closed Days, #103)
  • OccupancyManager.get_active_schedule_info_async (live schedule; uses the same opening_hours_by_schedule / is_open_by_schedule rules)

Until then the hours rule only excludes days when the target day is a dated exception with its own hours, because every regular candidate resolves to today's weekday hours.

🤖 Generated with Claude Code

Implementation note from the #122 review: once per-day schedule records exist, switch these call sites to prefer the day record: - `ScheduleCalendar.opening_hours(day)` (Usual Weekday Baseline's same-opening-hours rule, #105) - `ScheduleCalendar.is_closed(day)` (Closed Days, #103) - `OccupancyManager.get_active_schedule_info_async` (live schedule; uses the same `opening_hours_by_schedule` / `is_open_by_schedule` rules) Until then the hours rule only excludes days when the target day is a dated exception with its own hours, because every regular candidate resolves to today's weekday hours. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gabogg/hikcentral#113
No description provided.