feat: add scheduled scans (cron-like recurring scans)
- New `scheduled_scans` table with daily/weekly/monthly frequencies - asyncio background scheduler loop checks for due schedules every 60s - 6 REST endpoints: CRUD + toggle enabled + run-now - `scheduled_scan_id` FK added to scans table; migrated automatically - Frontend: Schedules page (list + create form), Schedules nav link, "Scheduled" badge on ScanDetails when scan was triggered by a schedule Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,29 @@ def _migrate_add_routes_unique_index(conn, verbose=True):
|
||||
print(" ✅ Migration complete: uq_routes_scan_dest index created")
|
||||
|
||||
|
||||
def _migrate_add_scheduled_scan_id_to_scans(conn, verbose=True):
|
||||
"""
|
||||
Migration: add scheduled_scan_id column to scans table.
|
||||
|
||||
Existing rows get NULL (manual scans). New column has no inline FK
|
||||
declaration because SQLite's ALTER TABLE ADD COLUMN doesn't support it;
|
||||
the relationship is enforced at the application level.
|
||||
"""
|
||||
cursor = conn.execute("PRAGMA table_info(scans)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if 'scheduled_scan_id' in columns:
|
||||
return # Already migrated
|
||||
|
||||
if verbose:
|
||||
print(" 🔄 Migrating scans table: adding scheduled_scan_id column...")
|
||||
|
||||
conn.execute("ALTER TABLE scans ADD COLUMN scheduled_scan_id INTEGER")
|
||||
conn.commit()
|
||||
|
||||
if verbose:
|
||||
print(" ✅ Migration complete: scheduled_scan_id column added to scans")
|
||||
|
||||
|
||||
def initialize_database(db_path=None, verbose=True):
|
||||
"""
|
||||
Initialize or migrate the database.
|
||||
@@ -212,6 +235,7 @@ def initialize_database(db_path=None, verbose=True):
|
||||
# Apply migrations before running schema
|
||||
_migrate_relax_country_constraint(conn, verbose)
|
||||
_migrate_add_routes_unique_index(conn, verbose)
|
||||
_migrate_add_scheduled_scan_id_to_scans(conn, verbose)
|
||||
|
||||
# Load and execute schema
|
||||
schema_sql = load_schema()
|
||||
|
||||
@@ -45,6 +45,9 @@ CREATE TABLE IF NOT EXISTS scans (
|
||||
seat_class TEXT DEFAULT 'economy',
|
||||
adults INTEGER DEFAULT 1 CHECK(adults > 0 AND adults <= 9),
|
||||
|
||||
-- FK to scheduled_scans (NULL for manual scans)
|
||||
scheduled_scan_id INTEGER,
|
||||
|
||||
-- Constraints across columns
|
||||
CHECK(end_date >= start_date),
|
||||
CHECK(routes_scanned <= total_routes OR total_routes = 0)
|
||||
@@ -61,6 +64,10 @@ CREATE INDEX IF NOT EXISTS idx_scans_status
|
||||
CREATE INDEX IF NOT EXISTS idx_scans_created_at
|
||||
ON scans(created_at DESC); -- For recent scans query
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_scans_scheduled_scan_id
|
||||
ON scans(scheduled_scan_id)
|
||||
WHERE scheduled_scan_id IS NOT NULL;
|
||||
|
||||
-- ============================================================================
|
||||
-- Table: routes
|
||||
-- Purpose: Store discovered routes with flight statistics
|
||||
@@ -244,7 +251,9 @@ ORDER BY created_at ASC;
|
||||
-- Initial Data: None (tables start empty)
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- Schema version tracking (for future migrations)
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -254,6 +263,64 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
||||
INSERT OR IGNORE INTO schema_version (version, description)
|
||||
VALUES (1, 'Initial web app schema with scans and routes tables');
|
||||
|
||||
-- ============================================================================
|
||||
-- Table: scheduled_scans
|
||||
-- Purpose: Define recurring scan schedules (daily / weekly / monthly)
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS scheduled_scans (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
-- Scan parameters (same as scans table)
|
||||
origin TEXT NOT NULL CHECK(length(origin) = 3),
|
||||
country TEXT NOT NULL CHECK(length(country) >= 2),
|
||||
window_months INTEGER NOT NULL DEFAULT 1
|
||||
CHECK(window_months >= 1 AND window_months <= 12),
|
||||
seat_class TEXT NOT NULL DEFAULT 'economy',
|
||||
adults INTEGER NOT NULL DEFAULT 1
|
||||
CHECK(adults > 0 AND adults <= 9),
|
||||
|
||||
-- Schedule definition
|
||||
frequency TEXT NOT NULL
|
||||
CHECK(frequency IN ('daily', 'weekly', 'monthly')),
|
||||
hour INTEGER NOT NULL DEFAULT 6
|
||||
CHECK(hour >= 0 AND hour <= 23),
|
||||
minute INTEGER NOT NULL DEFAULT 0
|
||||
CHECK(minute >= 0 AND minute <= 59),
|
||||
day_of_week INTEGER CHECK(day_of_week >= 0 AND day_of_week <= 6),
|
||||
day_of_month INTEGER CHECK(day_of_month >= 1 AND day_of_month <= 28),
|
||||
|
||||
-- State
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
label TEXT,
|
||||
last_run_at TIMESTAMP,
|
||||
next_run_at TIMESTAMP NOT NULL,
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Frequency-specific field requirements
|
||||
CHECK(
|
||||
(frequency = 'weekly' AND day_of_week IS NOT NULL) OR
|
||||
(frequency = 'monthly' AND day_of_month IS NOT NULL) OR
|
||||
(frequency = 'daily')
|
||||
)
|
||||
);
|
||||
|
||||
-- Fast lookup of due schedules (partial index on enabled rows only)
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduled_scans_next_run
|
||||
ON scheduled_scans(next_run_at)
|
||||
WHERE enabled = 1;
|
||||
|
||||
-- Auto-update updated_at on every PATCH
|
||||
CREATE TRIGGER IF NOT EXISTS update_scheduled_scans_timestamp
|
||||
AFTER UPDATE ON scheduled_scans
|
||||
FOR EACH ROW BEGIN
|
||||
UPDATE scheduled_scans SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
INSERT OR IGNORE INTO schema_version (version, description)
|
||||
VALUES (2, 'Add scheduled_scans table');
|
||||
|
||||
-- ============================================================================
|
||||
-- Verification Queries (for testing)
|
||||
-- ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user