"""Read-only status history for restart-safe background jobs.""" from __future__ import annotations import time from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse from core.database import background_job_artifact_path, background_job_get, background_job_list router = APIRouter() def _public_job(job: dict) -> dict: payload = dict(job) payload.pop("fingerprint", None) payload.pop("created", None) payload.pop("updated", None) result = payload.get("result") if isinstance(result, dict): result = dict(result) if isinstance(result.get("artifact"), dict) and result["artifact"].get("name"): result["artifact"] = {**result["artifact"], "url": f"/api/jobs/{job.get('job_id')}/artifacts/{result['artifact']['name']}"} if isinstance(result.get("artifacts"), list): result["artifacts"] = [ {**item, "url": f"/api/jobs/{job.get('job_id')}/artifacts/{item['name']}"} if isinstance(item, dict) and item.get("name") else item for item in result["artifacts"] ] payload["result"] = result payload["elapsed"] = round(time.time() - float(job.get("created") or time.time()), 1) return payload @router.get("/api/jobs") async def jobs_list(kind: str = "", limit: int = 50): return {"jobs": [_public_job(job) for job in background_job_list(kind, limit) if job]} @router.get("/api/jobs/{job_id}") async def jobs_get(job_id: str): job = background_job_get(job_id) if job is None: raise HTTPException(404, "Unknown or expired job") return _public_job(job) @router.get("/api/jobs/{job_id}/artifacts/{name}") async def jobs_artifact(job_id: str, name: str): path = background_job_artifact_path(job_id, name) if path is None: raise HTTPException(404, "Unknown or expired job artifact") media_type = "application/octet-stream" suffix = path.suffix.lower() if suffix == ".wav": media_type = "audio/wav" elif suffix == ".mp3": media_type = "audio/mpeg" elif suffix == ".png": media_type = "image/png" elif suffix in {".jpg", ".jpeg"}: media_type = "image/jpeg" return FileResponse(path, media_type=media_type, headers={"Cache-Control": "private, no-store"})