"""/api/cron — reads echo-core/cron/jobs.json (flat schema).""" import json from datetime import datetime import constants def _parse_cron_time(expr): """Extract a display-time string from a cron expression. Echo-core cron strings are already Bucharest local time (Lane B scheduler sets tz=Europe/Bucharest), so NO UTC→local conversion. """ parts = expr.split() if len(parts) < 2: return expr[:15] minute, hour = parts[0], parts[1] if minute.isdigit() and (hour.isdigit() or '-' in hour): if '-' in hour: hour = hour.split('-')[0] try: return f"{int(hour):02d}:{int(minute):02d}" except ValueError: return expr[:15] return expr[:15] def _iso_to_epoch_ms(iso_str): """Convert an ISO 8601 datetime string to epoch ms. Returns 0 on failure.""" if not iso_str: return 0 try: dt = datetime.fromisoformat(iso_str.replace('Z', '+00:00')) return int(dt.timestamp() * 1000) except (ValueError, TypeError): return 0 class CronHandlers: """Mixin for /api/cron.""" def handle_cron_status(self): """Get enabled cron jobs from echo-core/cron/jobs.json (flat schema). Output shape preserved for the frontend: id, name, time, schedule, ranToday, lastStatus, lastRunAtMs, nextRunAtMs. """ try: jobs_file = constants.BASE_DIR / 'cron' / 'jobs.json' if not jobs_file.exists(): self.send_json({'jobs': [], 'error': 'No jobs file found'}) return all_jobs = json.loads(jobs_file.read_text()) if not isinstance(all_jobs, list): self.send_json({'jobs': [], 'error': 'Unexpected jobs.json shape'}) return today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) today_start_ms = today_start.timestamp() * 1000 jobs = [] for job in all_jobs: if not job.get('enabled', False): continue name = job.get('name', '') expr = job.get('cron', '') last_run_iso = job.get('last_run') next_run_iso = job.get('next_run') last_status = job.get('last_status', 'unknown') last_run_ms = _iso_to_epoch_ms(last_run_iso) next_run_ms = _iso_to_epoch_ms(next_run_iso) or None ran_today = last_run_ms >= today_start_ms jobs.append({ 'id': name, # echo-core has no separate id; use name 'name': name, 'time': _parse_cron_time(expr), 'schedule': expr, 'ranToday': ran_today, 'lastStatus': last_status if ran_today else None, 'lastRunAtMs': last_run_ms, 'nextRunAtMs': next_run_ms, }) jobs.sort(key=lambda j: j['time']) self.send_json({ 'jobs': jobs, 'total': len(jobs), 'ranToday': sum(1 for j in jobs if j['ranToday']), }) except Exception as e: self.send_json({'error': str(e)}, 500)