feat(dashboard): rewrite /api/cron for echo-core flat schema
This commit is contained in:
@@ -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
|
import json
|
||||||
from datetime import datetime, timezone as dt_timezone
|
from datetime import datetime
|
||||||
from pathlib import Path
|
|
||||||
from zoneinfo import ZoneInfo
|
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:
|
class CronHandlers:
|
||||||
"""Mixin for /api/cron."""
|
"""Mixin for /api/cron."""
|
||||||
|
|
||||||
def handle_cron_status(self):
|
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:
|
try:
|
||||||
jobs_file = Path.home() / '.clawdbot' / 'cron' / 'jobs.json'
|
jobs_file = constants.BASE_DIR / 'cron' / 'jobs.json'
|
||||||
if not jobs_file.exists():
|
if not jobs_file.exists():
|
||||||
self.send_json({'jobs': [], 'error': 'No jobs file found'})
|
self.send_json({'jobs': [], 'error': 'No jobs file found'})
|
||||||
return
|
return
|
||||||
|
|
||||||
data = json.loads(jobs_file.read_text())
|
all_jobs = json.loads(jobs_file.read_text())
|
||||||
all_jobs = data.get('jobs', [])
|
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 = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
today_start_ms = today_start.timestamp() * 1000
|
today_start_ms = today_start.timestamp() * 1000
|
||||||
@@ -27,46 +64,25 @@ class CronHandlers:
|
|||||||
if not job.get('enabled', False):
|
if not job.get('enabled', False):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
schedule = job.get('schedule', {})
|
name = job.get('name', '')
|
||||||
expr = schedule.get('expr', '')
|
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()
|
last_run_ms = _iso_to_epoch_ms(last_run_iso)
|
||||||
if len(parts) >= 2:
|
next_run_ms = _iso_to_epoch_ms(next_run_iso) or None
|
||||||
minute = parts[0]
|
ran_today = last_run_ms >= today_start_ms
|
||||||
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')
|
|
||||||
|
|
||||||
jobs.append({
|
jobs.append({
|
||||||
'id': job.get('id'),
|
'id': name, # echo-core has no separate id; use name
|
||||||
'name': job.get('name'),
|
'name': name,
|
||||||
'agentId': job.get('agentId'),
|
'time': _parse_cron_time(expr),
|
||||||
'time': time_str,
|
|
||||||
'schedule': expr,
|
'schedule': expr,
|
||||||
'ranToday': ran_today,
|
'ranToday': ran_today,
|
||||||
'lastStatus': last_status if ran_today else None,
|
'lastStatus': last_status if ran_today else None,
|
||||||
'lastRunAtMs': last_run,
|
'lastRunAtMs': last_run_ms,
|
||||||
'nextRunAtMs': state.get('nextRunAtMs'),
|
'nextRunAtMs': next_run_ms,
|
||||||
})
|
})
|
||||||
|
|
||||||
jobs.sort(key=lambda j: j['time'])
|
jobs.sort(key=lambda j: j['time'])
|
||||||
|
|||||||
Reference in New Issue
Block a user