feat(schedule_type): minute-precision windows + variable maintenance length
Lifts the two hard restrictions in PR #18: * window bounds were `int hour` (0-23) → now `int minutes-since-UTC-midnight` (0-1439) * maintenance window was exactly 1 hour → now any duration in [1, 180] minutes ((maint_to - maint_from) mod 1440) ## Schema migration (additive) `_migrate_schema()` detects legacy "hours" rows (any row where MAX of the 6 window columns is ≤ 23) and multiplies each column by 60 to convert to minutes. Idempotent — post-conversion values are well above 23 so the guard never fires twice. ## Touched surfaces - `models/schedule_type.py` — column comments updated; new `compute_maintenance_duration()` helper (returns 1-1440 min, treats from==to as 1440 which is then rejected by validator) - `schemas/schedule_type.py` — `*_from`/`*_to` upper bound 23 → 1440; `_validate_maintenance_window` accepts 1-180min duration; response includes derived `maintenance_duration_minutes` - `schemas/schedule_type_special_slot.py` — `minute_in_window` max 59→179; `estimated_duration` max 60→180 - `routers/schedule_type.py` — PATCH re-validates merged maintenance pair (partial updates can put the row into an invalid combo the pydantic single-field validator can't catch); `_attach_derived` populates the new response field - `routers/schedule_type_special_slot.py` — `_validate_fits_window` now takes the parent's maintenance duration instead of hard-coded 60 - `routers/calendar.py` — `_scheduled_inside_window` arg renamed hour→min; the maintenance-window guard error message formats HH:MM not HH:00 - `services/special_slot_materialiser.py` — materialised `scheduled_at` derived from `(maint_from_min + tpl.minute_in_window)` with hour/minute split 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
29
app/main.py
29
app/main.py
@@ -400,9 +400,9 @@ def _migrate_schema():
|
||||
db.execute(text("ALTER TABLE agents ADD COLUMN schedule_type_id INTEGER NULL"))
|
||||
|
||||
# --- schedule_types: add maintenance_from / maintenance_to ---
|
||||
# Default 8:00–9:00 UTC for existing rows; the 1-hour-window
|
||||
# invariant is enforced at the schema level for any NEW rows by
|
||||
# the pydantic ScheduleTypeCreate validator.
|
||||
# Default 8:00–9:00 UTC for existing rows; the maintenance
|
||||
# duration invariant (1-180min) is enforced at the schema
|
||||
# level for any NEW rows by ScheduleTypeCreate validator.
|
||||
if _has_table(db, "schedule_types"):
|
||||
if not _has_column(db, "schedule_types", "maintenance_from"):
|
||||
db.execute(text(
|
||||
@@ -413,6 +413,29 @@ def _migrate_schema():
|
||||
"ALTER TABLE schedule_types ADD COLUMN maintenance_to INT NOT NULL DEFAULT 9"
|
||||
))
|
||||
|
||||
# --- minutes-since-midnight migration (PR #21+) ---
|
||||
# The 6 schedule_type window columns used to hold *hours*
|
||||
# (0-23). PR #21 changed semantics to *minutes since UTC
|
||||
# midnight* (0-1439). Detect the legacy regime by checking
|
||||
# if ANY row has all 6 values ≤ 23 — if so, multiply each
|
||||
# by 60 to convert. Idempotent: post-conversion values are
|
||||
# all ≥ 0 and usually well above 23, so guard never fires
|
||||
# twice.
|
||||
row = db.execute(text(
|
||||
"SELECT MAX(GREATEST(work_from, work_to, entertainment_from, entertainment_to, maintenance_from, maintenance_to)) AS m "
|
||||
"FROM schedule_types"
|
||||
)).fetchone()
|
||||
if row is not None and row.m is not None and row.m <= 23:
|
||||
db.execute(text(
|
||||
"UPDATE schedule_types SET "
|
||||
" work_from = work_from * 60, "
|
||||
" work_to = work_to * 60, "
|
||||
" entertainment_from = entertainment_from * 60, "
|
||||
" entertainment_to = entertainment_to * 60, "
|
||||
" maintenance_from = maintenance_from * 60, "
|
||||
" maintenance_to = maintenance_to * 60"
|
||||
))
|
||||
|
||||
# --- time_slots: admin-locked + special_slot pointer ---
|
||||
if _has_table(db, "time_slots"):
|
||||
if not _has_column(db, "time_slots", "is_admin_locked"):
|
||||
|
||||
Reference in New Issue
Block a user