""" TSCM Schedule Routes Handles /schedules/* endpoints for automated sweep scheduling. """ from __future__ import annotations import logging from datetime import datetime, timezone from typing import Any from flask import jsonify, request from routes.tscm import ( _get_schedule_timezone, _next_run_from_cron, _start_sweep_internal, tscm_bp, ) from utils.database import ( create_tscm_schedule, delete_tscm_schedule, get_all_tscm_schedules, get_tscm_schedule, update_tscm_schedule, ) logger = logging.getLogger('intercept.tscm') @tscm_bp.route('/schedules', methods=['GET']) def list_schedules(): """List all TSCM sweep schedules.""" enabled_param = request.args.get('enabled') enabled = None if enabled_param is not None: enabled = enabled_param.lower() in ('1', 'true', 'yes') schedules = get_all_tscm_schedules(enabled=enabled, limit=200) return jsonify({ 'status': 'success', 'count': len(schedules), 'schedules': schedules, }) @tscm_bp.route('/schedules', methods=['POST']) def create_schedule(): """Create a new sweep schedule.""" data = request.get_json() or {} name = (data.get('name') or '').strip() cron_expression = (data.get('cron_expression') or '').strip() sweep_type = data.get('sweep_type', 'standard') baseline_id = data.get('baseline_id') zone_name = data.get('zone_name') enabled = bool(data.get('enabled', True)) notify_on_threat = bool(data.get('notify_on_threat', True)) notify_email = data.get('notify_email') if not name: return jsonify({'status': 'error', 'message': 'Schedule name required'}), 400 if not cron_expression: return jsonify({'status': 'error', 'message': 'cron_expression required'}), 400 next_run = None if enabled: try: tz = _get_schedule_timezone(zone_name) next_local = _next_run_from_cron(cron_expression, datetime.now(tz)) next_run = next_local.astimezone(timezone.utc).isoformat() if next_local else None except Exception as e: return jsonify({'status': 'error', 'message': f'Invalid cron: {e}'}), 400 schedule_id = create_tscm_schedule( name=name, cron_expression=cron_expression, sweep_type=sweep_type, baseline_id=baseline_id, zone_name=zone_name, enabled=enabled, notify_on_threat=notify_on_threat, notify_email=notify_email, next_run=next_run, ) schedule = get_tscm_schedule(schedule_id) return jsonify({ 'status': 'success', 'message': 'Schedule created', 'schedule': schedule }) @tscm_bp.route('/schedules/', methods=['PUT', 'PATCH']) def update_schedule(schedule_id: int): """Update a sweep schedule.""" schedule = get_tscm_schedule(schedule_id) if not schedule: return jsonify({'status': 'error', 'message': 'Schedule not found'}), 404 data = request.get_json() or {} updates: dict[str, Any] = {} for key in ('name', 'cron_expression', 'sweep_type', 'baseline_id', 'zone_name', 'notify_email'): if key in data: updates[key] = data[key] if 'baseline_id' in updates and updates['baseline_id'] in ('', None): updates['baseline_id'] = None if 'enabled' in data: updates['enabled'] = 1 if data['enabled'] else 0 if 'notify_on_threat' in data: updates['notify_on_threat'] = 1 if data['notify_on_threat'] else 0 # Recalculate next_run when cron/zone/enabled changes if any(k in updates for k in ('cron_expression', 'zone_name', 'enabled')): if updates.get('enabled', schedule.get('enabled', 1)): cron_expr = updates.get('cron_expression', schedule.get('cron_expression', '')) zone_name = updates.get('zone_name', schedule.get('zone_name')) try: tz = _get_schedule_timezone(zone_name) next_local = _next_run_from_cron(cron_expr, datetime.now(tz)) updates['next_run'] = next_local.astimezone(timezone.utc).isoformat() if next_local else None except Exception as e: return jsonify({'status': 'error', 'message': f'Invalid cron: {e}'}), 400 else: updates['next_run'] = None if not updates: return jsonify({'status': 'error', 'message': 'No updates provided'}), 400 update_tscm_schedule(schedule_id, **updates) schedule = get_tscm_schedule(schedule_id) return jsonify({'status': 'success', 'schedule': schedule}) @tscm_bp.route('/schedules/', methods=['DELETE']) def delete_schedule(schedule_id: int): """Delete a sweep schedule.""" success = delete_tscm_schedule(schedule_id) if not success: return jsonify({'status': 'error', 'message': 'Schedule not found'}), 404 return jsonify({'status': 'success', 'message': 'Schedule deleted'}) @tscm_bp.route('/schedules//run', methods=['POST']) def run_schedule_now(schedule_id: int): """Trigger a scheduled sweep immediately.""" schedule = get_tscm_schedule(schedule_id) if not schedule: return jsonify({'status': 'error', 'message': 'Schedule not found'}), 404 result = _start_sweep_internal( sweep_type=schedule.get('sweep_type') or 'standard', baseline_id=schedule.get('baseline_id'), wifi_enabled=True, bt_enabled=True, rf_enabled=True, wifi_interface='', bt_interface='', sdr_device=None, verbose_results=False, ) if result.get('status') != 'success': status_code = result.pop('http_status', 400) return jsonify(result), status_code # Update schedule run timestamps cron_expr = schedule.get('cron_expression') or '' tz = _get_schedule_timezone(schedule.get('zone_name')) now_utc = datetime.now(timezone.utc) try: next_local = _next_run_from_cron(cron_expr, datetime.now(tz)) except Exception: next_local = None update_tscm_schedule( schedule_id, last_run=now_utc.isoformat(), next_run=next_local.astimezone(timezone.utc).isoformat() if next_local else None, ) return jsonify(result)