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:
2026-02-28 10:48:43 +01:00
parent ef5a27097d
commit 836c8474eb
9 changed files with 1666 additions and 10 deletions

View File

@@ -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()