diff --git a/dashboard/handlers/cron.py b/dashboard/handlers/cron.py index 18dc6e3..61d7f84 100644 --- a/dashboard/handlers/cron.py +++ b/dashboard/handlers/cron.py @@ -1,23 +1,60 @@ -"""/api/cron endpoint — currently reads clawdbot jobs.json (rewritten next commit).""" +"""/api/cron — reads echo-core/cron/jobs.json (flat schema).""" import json -from datetime import datetime, timezone as dt_timezone -from pathlib import Path -from zoneinfo import ZoneInfo +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 cron jobs status from ~/.clawdbot/cron/jobs.json (legacy schema).""" + """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 = Path.home() / '.clawdbot' / 'cron' / 'jobs.json' + jobs_file = constants.BASE_DIR / 'cron' / 'jobs.json' if not jobs_file.exists(): self.send_json({'jobs': [], 'error': 'No jobs file found'}) return - data = json.loads(jobs_file.read_text()) - all_jobs = data.get('jobs', []) + 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 @@ -27,46 +64,25 @@ class CronHandlers: if not job.get('enabled', False): continue - schedule = job.get('schedule', {}) - expr = schedule.get('expr', '') + 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') - parts = expr.split() - if len(parts) >= 2: - minute = parts[0] - hour = parts[1] - if minute.isdigit() and (hour.isdigit() or '-' in hour): - if '-' in hour: - hour_start, _ = hour.split('-') - hour = hour_start - try: - bucharest = ZoneInfo('Europe/Bucharest') - utc_dt = datetime.now(dt_timezone.utc).replace( - hour=int(hour), minute=int(minute), second=0, microsecond=0, - ) - local_dt = utc_dt.astimezone(bucharest) - time_str = f"{local_dt.hour:02d}:{local_dt.minute:02d}" - except Exception: - time_str = f"{int(hour):02d}:{int(minute):02d}" - else: - time_str = expr[:15] - else: - time_str = expr[:15] - - state = job.get('state', {}) - last_run = state.get('lastRunAtMs', 0) - ran_today = last_run >= today_start_ms - last_status = state.get('lastStatus', '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': job.get('id'), - 'name': job.get('name'), - 'agentId': job.get('agentId'), - 'time': time_str, + '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, - 'nextRunAtMs': state.get('nextRunAtMs'), + 'lastRunAtMs': last_run_ms, + 'nextRunAtMs': next_run_ms, }) jobs.sort(key=lambda j: j['time'])