Phase 3 Step 6: Complete Scheduler Integration with Bug Fixes
Implemented complete scheduler integration with automatic schedule loading, orphaned scan cleanup, and conversion to local timezone for better UX. Backend Changes: - Added load_schedules_on_startup() to load enabled schedules on app start - Implemented cleanup_orphaned_scans() to handle crashed/interrupted scans - Converted scheduler from UTC to local system timezone throughout - Enhanced scheduler service with robust error handling and logging Frontend Changes: - Updated all schedule UI templates to display local time instead of UTC - Improved timezone indicators and user messaging - Removed confusing timezone converter (no longer needed) - Updated quick templates and help text for local time Bug Fixes: - Fixed critical timezone bug causing cron expressions to run at wrong times - Fixed orphaned scans stuck in 'running' status after system crashes - Improved time display clarity across all schedule pages All schedules now use local system time for intuitive scheduling.
This commit is contained in:
@@ -6,7 +6,7 @@ scan execution and future scheduled scanning capabilities.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
@@ -63,11 +63,13 @@ class SchedulerService:
|
||||
'misfire_grace_time': 60 # Allow 60 seconds for delayed starts
|
||||
}
|
||||
|
||||
# Create scheduler
|
||||
# Create scheduler with local system timezone
|
||||
# This allows users to schedule jobs using their local time
|
||||
# APScheduler will automatically use the system's local timezone
|
||||
self.scheduler = BackgroundScheduler(
|
||||
executors=executors,
|
||||
job_defaults=job_defaults,
|
||||
timezone='UTC'
|
||||
job_defaults=job_defaults
|
||||
# timezone defaults to local system timezone
|
||||
)
|
||||
|
||||
# Start scheduler
|
||||
@@ -90,6 +92,63 @@ class SchedulerService:
|
||||
logger.info("APScheduler shutdown complete")
|
||||
self.scheduler = None
|
||||
|
||||
def load_schedules_on_startup(self):
|
||||
"""
|
||||
Load all enabled schedules from database and register with APScheduler.
|
||||
|
||||
Should be called after init_scheduler() to restore scheduled jobs
|
||||
that were active when the application last shutdown.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If scheduler not initialized
|
||||
"""
|
||||
if not self.scheduler:
|
||||
raise RuntimeError("Scheduler not initialized. Call init_scheduler() first.")
|
||||
|
||||
# Import here to avoid circular imports
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from web.models import Schedule
|
||||
|
||||
try:
|
||||
# Create database session
|
||||
engine = create_engine(self.db_url)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
|
||||
try:
|
||||
# Query all enabled schedules
|
||||
enabled_schedules = (
|
||||
session.query(Schedule)
|
||||
.filter(Schedule.enabled == True)
|
||||
.all()
|
||||
)
|
||||
|
||||
logger.info(f"Loading {len(enabled_schedules)} enabled schedules on startup")
|
||||
|
||||
# Register each schedule with APScheduler
|
||||
for schedule in enabled_schedules:
|
||||
try:
|
||||
self.add_scheduled_scan(
|
||||
schedule_id=schedule.id,
|
||||
config_file=schedule.config_file,
|
||||
cron_expression=schedule.cron_expression
|
||||
)
|
||||
logger.info(f"Loaded schedule {schedule.id}: '{schedule.name}'")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to load schedule {schedule.id} ('{schedule.name}'): {str(e)}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
logger.info("Schedule loading complete")
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading schedules on startup: {str(e)}", exc_info=True)
|
||||
|
||||
def queue_scan(self, scan_id: int, config_file: str) -> str:
|
||||
"""
|
||||
Queue a scan for immediate background execution.
|
||||
@@ -142,9 +201,11 @@ class SchedulerService:
|
||||
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
# Create cron trigger from expression
|
||||
# Create cron trigger from expression using local timezone
|
||||
# This allows users to specify times in their local timezone
|
||||
try:
|
||||
trigger = CronTrigger.from_crontab(cron_expression)
|
||||
# timezone defaults to local system timezone
|
||||
except (ValueError, KeyError) as e:
|
||||
raise ValueError(f"Invalid cron expression '{cron_expression}': {str(e)}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user