fix: address critical issues in PR #75 agent scheduling feature

This commit fixes several issues identified in the agent scheduling
feature from PR #75:

Frontend Fixes:
- Add day boundary handling in timeUtils.ts for timezone conversions
- Add utcToLocalWithDayShift/localToUTCWithDayShift functions
- Add shiftDaysForward/shiftDaysBackward helpers for bitfield adjustment
- Update ScheduleModal to correctly adjust days_of_week when crossing
  day boundaries during UTC conversion (fixes schedules running on
  wrong days for users in extreme timezones like UTC+9)

Backend Fixes:
- Add MAX_SCHEDULES_PER_PROJECT (50) limit to prevent resource exhaustion
- Wire up crash recovery callback in scheduler_service._start_agent()
- Convert schedules.py endpoints to use context manager for DB sessions
- Fix race condition in override creation with atomic delete-then-create
- Replace deprecated datetime.utcnow with datetime.now(timezone.utc)
- Add DB-level CHECK constraints for Schedule model fields

Files Modified:
- api/database.py: Add _utc_now helper, CheckConstraint imports, constraints
- progress.py: Replace deprecated datetime.utcnow
- server/routers/schedules.py: Add context manager, schedule limits
- server/services/assistant_database.py: Replace deprecated datetime.utcnow
- server/services/scheduler_service.py: Wire crash recovery, fix race condition
- ui/src/components/ScheduleModal.tsx: Use day shift functions
- ui/src/lib/timeUtils.ts: Add day boundary handling functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-22 08:35:57 +02:00
parent 44e333d034
commit 0736b5ec6b
7 changed files with 226 additions and 72 deletions

View File

@@ -368,6 +368,14 @@ class SchedulerService:
logger.info(f"Agent already running for {project_name}, skipping scheduled start")
return
# Register crash callback to enable auto-restart during scheduled windows
async def on_status_change(status: str):
if status == "crashed":
logger.info(f"Crash detected for {project_name}, attempting recovery")
await self.handle_crash_during_window(project_name, project_dir)
manager.add_status_callback(on_status_change)
logger.info(
f"Starting agent for {project_name} "
f"(schedule {schedule.id}, yolo={schedule.yolo_mode}, concurrency={schedule.max_concurrency})"
@@ -382,6 +390,8 @@ class SchedulerService:
logger.info(f"✓ Agent started successfully for {project_name}")
else:
logger.error(f"✗ Failed to start agent for {project_name}: {msg}")
# Remove callback if start failed
manager.remove_status_callback(on_status_change)
async def _stop_agent(self, project_name: str, project_dir: Path):
"""Stop the agent for a project."""
@@ -457,7 +467,10 @@ class SchedulerService:
def _create_override_for_active_schedules(
self, project_name: str, project_dir: Path, override_type: str
):
"""Create overrides for all active schedule windows."""
"""Create overrides for all active schedule windows.
Uses atomic delete-then-create pattern to prevent race conditions.
"""
from api.database import Schedule, ScheduleOverride, create_database
try:
@@ -479,17 +492,20 @@ class SchedulerService:
# Calculate window end time
window_end = self._calculate_window_end(schedule, now)
# Check if override already exists
existing = db.query(ScheduleOverride).filter(
# Atomic operation: delete any existing overrides of this type
# and create a new one in the same transaction
deleted = db.query(ScheduleOverride).filter(
ScheduleOverride.schedule_id == schedule.id,
ScheduleOverride.override_type == override_type,
ScheduleOverride.expires_at > now,
).first()
).delete()
if existing:
continue
if deleted:
logger.debug(
f"Removed {deleted} existing '{override_type}' override(s) "
f"for schedule {schedule.id}"
)
# Create override
# Create new override
override = ScheduleOverride(
schedule_id=schedule.id,
override_type=override_type,