import logging import re from fastapi import APIRouter, HTTPException from pydantic import BaseModel router = APIRouter() class Workflow(BaseModel): name: str steps: list @router.get("/list") async def get_automation_list(): """User automations plus the live scheduler jobs. This used to be a hardcoded `return []`, so the endpoint reported success while claiming the system had no automations — even though automation_service.load_automations() reads real rows from `custom_automations`, and the scheduler genuinely runs persisted jobs (mcu_suit_vitals, mcu_battlefield_intel, hf_keepalive_ping) out of the `apscheduler_jobs` table. Anything driven off this route saw an empty system. """ from backend.services.automation_service import load_automations, scheduler try: automations = load_automations() except Exception as exc: # a missing table must not 500 the whole panel automations = [] logging.warning("load_automations failed: %s", exc) jobs = [] try: for job in scheduler.get_jobs(): jobs.append({ "id": job.id, "name": getattr(job, "name", job.id), "trigger": str(job.trigger), "next_run_time": job.next_run_time.isoformat() if job.next_run_time else None, }) except Exception as exc: logging.warning("scheduler.get_jobs failed: %s", exc) return {"automations": automations, "scheduled_jobs": jobs, "counts": {"automations": len(automations), "scheduled_jobs": len(jobs)}} @router.post("/trigger/{id}") async def trigger_automation_route(id: str): from backend.services.automation_service import trigger_automation await trigger_automation(id) return {"status": "triggered"} @router.post("/pause_all") async def pause_all(): from backend.services.automation_service import pause_all_automations await pause_all_automations() return {"status": "paused"} @router.post("/resume_all") async def resume_all(): from backend.services.automation_service import resume_all_automations await resume_all_automations() return {"status": "resumed"} @router.post("/jobs/{job_id}/pause") async def pause_single_job(job_id: str): from backend.services.automation_service import pause_job # APScheduler raises JobLookupError for an unknown id. Unhandled, that # surfaced as a 500 "Internal Server Error" for what is really a client # asking about a job that does not exist — a 404. try: pause_job(job_id) except Exception as exc: if "No job by the id" in str(exc): raise HTTPException(status_code=404, detail=f"No job with id {job_id}") raise return {"status": f"job {job_id} paused"} @router.post("/jobs/{job_id}/resume") async def resume_single_job(job_id: str): from backend.services.automation_service import resume_job try: resume_job(job_id) except Exception as exc: if "No job by the id" in str(exc): raise HTTPException(status_code=404, detail=f"No job with id {job_id}") raise return {"status": f"job {job_id} resumed"} @router.delete("/jobs/{job_id}") async def delete_single_job(job_id: str): from backend.services.automation_service import delete_job delete_job(job_id) return {"status": f"job {job_id} deleted"} @router.post("/save") async def save_workflow(w: Workflow): """Persist a workflow. This previously returned {"status": "saved"} without writing anything — the Save action reported success and the workflow was gone on the next read. automation_service.save_automation() is the real write path into the `custom_automations` table that /list reads back. """ from backend.services.automation_service import save_automation if not (w.name or "").strip(): raise HTTPException(status_code=400, detail="name is required") auto_id = re.sub(r"[^a-z0-9]+", "-", w.name.strip().lower()).strip("-") or "workflow" try: save_automation( auto_id=auto_id, trigger_type="manual", trigger_data={}, action_type="workflow", action_data={"name": w.name, "steps": w.steps}, ) except Exception as exc: logging.exception("save_automation failed") raise HTTPException(status_code=500, detail=str(exc)) return {"status": "saved", "id": auto_id, "steps": len(w.steps)} @router.get("/history") async def get_history(limit: int = 100): # Part 18: real backing store — automation_service.execute_action records # every run (ok/error) into the automation_history table in the same # env-driven SQLite file as custom_automations. from backend.services.automation_service import load_history try: return load_history(limit) except Exception as exc: logging.exception("automation history read failed") raise HTTPException(status_code=500, detail=str(exc)) @router.post("/dry-run") async def dry_run_workflow(w: Workflow): """ Simulates workflow execution without side effects. Returns a preview of the steps and their expected mock results. """ import asyncio preview_steps = [] for i, step in enumerate(w.steps): # Determine a mock result based on step type if present step_type = step.get('type', 'unknown') if isinstance(step, dict) else 'unknown' mock_result = f"Mock result for {step_type}" if step_type == 'api_call': mock_result = "HTTP 200 OK (Mock)" elif step_type == 'script': mock_result = "Script execution simulated" elif step_type == 'agent': mock_result = "Agent reasoning simulated" preview_steps.append({ "step_index": i, "step_config": step, "expected_status": "success", "mock_result": mock_result }) # Simulate minor delay await asyncio.sleep(0.1) return { "status": "dry-run-complete", "total_steps": len(w.steps), "execution_preview": preview_steps }