package handlers import ( "net/http" "strconv" "github.com/flatrender/render-svc/internal/db" "github.com/flatrender/render-svc/internal/middleware" "github.com/flatrender/render-svc/internal/models" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type RenderHandler struct { store *db.Store } func NewRenderHandler(store *db.Store) *RenderHandler { return &RenderHandler{store: store} } // GET /v1/renders func (h *RenderHandler) List(c *gin.Context) { userID := middleware.GetUserID(c) status := c.Query("status") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } jobs, total, err := h.store.ListJobs(c.Request.Context(), userID, status, page, pageSize) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } if jobs == nil { jobs = []*models.RenderJob{} } c.JSON(http.StatusOK, models.PagedResponse[*models.RenderJob]{ Data: jobs, Meta: models.PaginationMeta{ Page: page, PageSize: pageSize, Total: total, HasMore: int64(page*pageSize) < total, }, }) } // POST /v1/renders func (h *RenderHandler) Create(c *gin.Context) { userID := middleware.GetUserID(c) tenantID := middleware.GetTenantID(c) var req models.RenderJobCreateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } job, err := h.store.CreateJob(c.Request.Context(), userID, tenantID, &req) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.JSON(http.StatusCreated, job) } // GET /v1/renders/:job_id func (h *RenderHandler) Get(c *gin.Context) { userID := middleware.GetUserID(c) jobID, err := uuid.Parse(c.Param("job_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid job_id"}) return } job, err := h.store.GetJobByID(c.Request.Context(), jobID, userID) if err != nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()}) return } frames, _ := h.store.ListFrameJobs(c.Request.Context(), jobID) if frames == nil { frames = []*models.FrameJob{} } c.JSON(http.StatusOK, gin.H{ "id": job.ID, "saved_project_id": job.SavedProjectID, "name": job.Name, "step": job.Step, "render_progress": job.RenderProgress, "priority_queue": job.PriorityQueue, "price_type": job.PriceType, "paid_price_minor": job.PaidPriceMinor, "quality": job.Quality, "resolution": job.Resolution, "frame_rate": job.FrameRate, "duration_sec": job.DurationSec, "has_voiceover": job.HasVoiceover, "image_preview_b64": job.ImagePreviewB64, "failed_message": job.FailedMessage, "export_id": job.ExportID, "queued_at": job.QueuedAt, "started_at": job.StartedAt, "completed_at": job.CompletedAt, "retry_count": job.RetryCount, "repair_attempts": job.RepairAttempts, "frame_jobs": frames, }) } // POST /v1/renders/:job_id/cancel func (h *RenderHandler) Cancel(c *gin.Context) { userID := middleware.GetUserID(c) jobID, err := uuid.Parse(c.Param("job_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid job_id"}) return } cancelled, err := h.store.CancelJob(c.Request.Context(), jobID, userID) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "cancelled": cancelled, "refund_amount_minor": 0, }) } // POST /v1/renders/:job_id/retry func (h *RenderHandler) Retry(c *gin.Context) { userID := middleware.GetUserID(c) tenantID := middleware.GetTenantID(c) jobID, err := uuid.Parse(c.Param("job_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid job_id"}) return } original, err := h.store.GetJobByID(c.Request.Context(), jobID, userID) if err != nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()}) return } // Create a new job cloning the original config req := &models.RenderJobCreateRequest{ SavedProjectID: original.SavedProjectID, Quality: original.Quality, Resolution: original.Resolution, FrameRate: &original.FrameRate, } newJob, err := h.store.CreateJob(c.Request.Context(), userID, tenantID, req) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.JSON(http.StatusCreated, newJob) } // GET /v1/renders/:job_id/progress func (h *RenderHandler) Progress(c *gin.Context) { userID := middleware.GetUserID(c) jobID, err := uuid.Parse(c.Param("job_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid job_id"}) return } job, err := h.store.GetJobByID(c.Request.Context(), jobID, userID) if err != nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "job_id": job.ID, "step": job.Step, "progress": job.RenderProgress, "current_frame": nil, "total_frames": nil, "eta_seconds": nil, "preview_b64": job.ImagePreviewB64, "active_nodes": job.CurrentActiveNodes, "message": job.FailedMessage, }) } // GET /v1/renders/:job_id/logs func (h *RenderHandler) Logs(c *gin.Context) { // Logs are stored externally (MinIO/ELK). Return empty for now — node agents push logs elsewhere. c.JSON(http.StatusOK, gin.H{"logs": []any{}}) }