Compare commits
8 Commits
fix/479-ha
...
burn/349-1
| Author | SHA1 | Date | |
|---|---|---|---|
| 6df40c52f3 | |||
| 479f5a615f | |||
| d7f4c0886e | |||
| aa1e460912 | |||
| f2aef27d2e | |||
| 9289734034 | |||
| 6fcd046e00 | |||
| 906c3a4259 |
25
cron/jobs.py
25
cron/jobs.py
@@ -656,6 +656,31 @@ def mark_job_run(job_id: str, success: bool, error: Optional[str] = None):
|
||||
save_jobs(jobs)
|
||||
|
||||
|
||||
def clear_job_error(job_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Clear stale error state for a job.
|
||||
|
||||
Resets last_status to 'ok', last_error to None, and
|
||||
records when the error was cleared. Useful after auth
|
||||
recovery when the job itself is healthy but stale error
|
||||
state persists.
|
||||
|
||||
Returns:
|
||||
Updated job dict, or None if not found.
|
||||
"""
|
||||
jobs = load_jobs()
|
||||
for job in jobs:
|
||||
if job["id"] == job_id:
|
||||
job["last_status"] = "ok"
|
||||
job["last_error"] = None
|
||||
job["error_cleared_at"] = _hermes_now().isoformat()
|
||||
save_jobs(jobs)
|
||||
return job
|
||||
save_jobs(jobs)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def advance_next_run(job_id: str) -> bool:
|
||||
"""Preemptively advance next_run_at for a recurring job before execution.
|
||||
|
||||
|
||||
@@ -93,6 +93,20 @@ def cron_list(show_all: bool = False):
|
||||
script = job.get("script")
|
||||
if script:
|
||||
print(f" Script: {script}")
|
||||
|
||||
# Show error state with staleness
|
||||
last_error = job.get("last_error")
|
||||
last_status = job.get("last_status")
|
||||
error_cleared_at = job.get("error_cleared_at")
|
||||
|
||||
if last_error:
|
||||
if error_cleared_at:
|
||||
print(color(f" Error (stale): {last_error[:100]}...", Colors.YELLOW))
|
||||
print(color(f" Cleared at: {error_cleared_at}", Colors.DIM))
|
||||
else:
|
||||
print(color(f" Error: {last_error[:100]}...", Colors.RED))
|
||||
elif last_status == "ok" and error_cleared_at:
|
||||
print(color(f" Status: ok (error cleared at {error_cleared_at})", Colors.GREEN))
|
||||
print()
|
||||
|
||||
from hermes_cli.gateway import find_gateway_pids
|
||||
@@ -221,8 +235,20 @@ def cron_edit(args):
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
def _job_action(action: str, job_id: str, success_verb: str, now: bool = False) -> int:
|
||||
if action == "run" and now:
|
||||
if action == "clear_error":
|
||||
result = _cron_api(action="clear_error", job_id=job_id)
|
||||
if not result.get("success"):
|
||||
print(color(f"Failed to clear error: {result.get('error', 'unknown error')}", Colors.RED))
|
||||
return 1
|
||||
job = result.get("job", {})
|
||||
name = job.get("name", job_id)
|
||||
print(color(f"Cleared stale error state for job '{name}'", Colors.GREEN))
|
||||
if job.get("error_cleared_at"):
|
||||
print(f" Cleared at: {job['error_cleared_at']}")
|
||||
return 0
|
||||
if action == "run" and now:
|
||||
# Synchronous execution — run job immediately and show result
|
||||
result = _cron_api(action="run_now", job_id=job_id)
|
||||
if not result.get("success"):
|
||||
@@ -291,10 +317,12 @@ def cron_command(args):
|
||||
if subcmd == "run":
|
||||
now = getattr(args, 'now', False)
|
||||
return _job_action("run", args.job_id, "Triggered", now=now)
|
||||
if subcmd == "clear-error":
|
||||
return _job_action("clear_error", args.job_id, "Cleared")
|
||||
|
||||
if subcmd in {"remove", "rm", "delete"}:
|
||||
return _job_action("remove", args.job_id, "Removed")
|
||||
|
||||
print(f"Unknown cron command: {subcmd}")
|
||||
print("Usage: hermes cron [list|create|edit|pause|resume|run|remove|status|tick]")
|
||||
print("Usage: hermes cron [list|create|edit|pause|resume|run|remove|clear-error|status|tick]")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -4576,6 +4576,9 @@ For more help on a command:
|
||||
cron_run.add_argument("job_id", help="Job ID to trigger")
|
||||
cron_run.add_argument("--now", action="store_true", help="Execute immediately and wait for result (clears stale errors)")
|
||||
|
||||
cron_clear_error = cron_subparsers.add_parser("clear-error", help="Clear stale error state for a job")
|
||||
cron_clear_error.add_argument("job_id", help="Job ID to clear error for")
|
||||
|
||||
cron_remove = cron_subparsers.add_parser("remove", aliases=["rm", "delete"], help="Remove a scheduled job")
|
||||
cron_remove.add_argument("job_id", help="Job ID to remove")
|
||||
|
||||
|
||||
@@ -200,7 +200,10 @@ def _format_job(job: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"state": job.get("state", "scheduled" if job.get("enabled", True) else "paused"),
|
||||
"paused_at": job.get("paused_at"),
|
||||
"paused_reason": job.get("paused_reason"),
|
||||
}
|
||||
} if job.get("error_cleared_at"):
|
||||
result["error_cleared_at"] = job["error_cleared_at"]
|
||||
|
||||
|
||||
if job.get("script"):
|
||||
result["script"] = job["script"]
|
||||
return result
|
||||
@@ -326,6 +329,13 @@ def cronjob(
|
||||
if result is None:
|
||||
return json.dumps({"success": False, "error": "Job not found"}, indent=2)
|
||||
return json.dumps(result, indent=2)
|
||||
if normalized == "clear_error":
|
||||
from cron.jobs import clear_job_error
|
||||
job = clear_job_error(job_id)
|
||||
if job is None:
|
||||
return json.dumps({"success": False, "error": "Job not found"}, indent=2)
|
||||
return json.dumps({"success": True, "job": _format_job(job)}, indent=2)
|
||||
|
||||
|
||||
if normalized == "update":
|
||||
updates: Dict[str, Any] = {}
|
||||
|
||||
Reference in New Issue
Block a user