feat(desktop): make cron jobs the first-class sidebar entity

Redesign the cron surface around jobs (not run sessions), following
power-user patterns (GitHub Actions / Airflow / Dagu): master → detail → output.

Sidebar "Cron jobs" section:
- jobs with a state pip + live next-run countdown
- click toggles an inline run-history peek; a run opens its chat (active run highlighted)
- hover: trigger-now + manage (open the Cron page)
- capped at 50 with a "50+" badge

Cron page: de-nested from a collapse-in-row accordion to master/detail —
job list + the selected job's schedule, actions, and run history.

Backend: GET /api/cron/jobs/{id}/runs lists a job's run sessions.

Share STATE_DOT/jobState across both surfaces; drop dead code/keys.
This commit is contained in:
Brooklyn Nicholson
2026-06-06 14:04:11 -05:00
parent ad0f6db151
commit 471a5fc5c9
16 changed files with 866 additions and 210 deletions
+47
View File
@@ -5650,6 +5650,53 @@ async def get_cron_job(job_id: str, profile: Optional[str] = None):
return job
@app.get("/api/cron/jobs/{job_id}/runs")
async def list_cron_job_runs(job_id: str, profile: Optional[str] = None, limit: int = 20):
"""Run sessions produced by a cron job, newest first.
Cron runs are stored as ordinary sessions whose id is
``cron_{job_id}_{timestamp}`` (see cron/scheduler.run_job). A job's history
is therefore every session whose id carries that prefix; ``source='cron'``
narrows it and the id substring binds it to this job. Powers the run-history
list under each job in the desktop cron detail. Same row shape as
``/api/sessions`` so the frontend can reuse SessionInfo.
"""
selected = profile or _find_cron_job_profile(job_id)
# job_id may be a human name; resolve to the canonical id used in run-session ids.
canonical = job_id
if selected:
job = _call_cron_for_profile(selected, "get_job", job_id)
if job and job.get("id"):
canonical = str(job["id"])
try:
limit_n = max(1, min(int(limit), 100))
except (TypeError, ValueError):
limit_n = 20
db = _open_session_db_for_profile(selected)
try:
runs = db.list_sessions_rich(
source="cron",
id_query=f"cron_{canonical}_",
limit=limit_n,
offset=0,
order_by_last_active=True,
)
now = time.time()
for s in runs:
s["is_active"] = (
s.get("ended_at") is None
and (now - s.get("last_active", s.get("started_at", 0))) < 300
)
s["archived"] = bool(s.get("archived"))
if selected:
s["profile"] = selected
return {"runs": runs, "limit": limit_n}
finally:
db.close()
@app.post("/api/cron/jobs")
async def create_cron_job(body: CronJobCreate, profile: str = "default"):
try: