package handlers import ( "net/http" "github.com/flatrender/render-svc/internal/db" "github.com/flatrender/render-svc/internal/models" "github.com/flatrender/render-svc/internal/notifier" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type InternalHandler struct { store *db.Store notifier *notifier.Client // may be nil — notifications are best-effort } func NewInternalHandler(store *db.Store, n *notifier.Client) *InternalHandler { return &InternalHandler{store: store, notifier: n} } // completeRequest is the body for POST .../complete type completeRequest struct { ExportID *uuid.UUID `json:"export_id"` } // failRequest is the body for POST .../fail type failRequest struct { Reason string `json:"reason" binding:"required"` AtStep string `json:"at_step"` // optional: which render step failed } // POST /v1/internal/render/jobs/:job_id/complete func (h *InternalHandler) Complete(c *gin.Context) { 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 } var req completeRequest _ = c.ShouldBindJSON(&req) // export_id is optional job, err := h.store.CompleteJob(c.Request.Context(), jobID, req.ExportID) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } // Fire notification if the user requested it (tell_me_when_done) if h.notifier != nil && job.TellMeWhenDone { jobName := "" if job.Name != nil { jobName = *job.Name } else if job.Title != nil { jobName = *job.Title } h.notifier.NotifyRenderDone(c.Request.Context(), job.UserID, job.TenantID, job.ID, job.ExportID, jobName) } c.JSON(http.StatusOK, gin.H{"status": "done", "job_id": job.ID}) } // POST /v1/internal/render/jobs/:job_id/fail func (h *InternalHandler) Fail(c *gin.Context) { 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 } var req failRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } atStep := req.AtStep if atStep == "" { atStep = "Rendering" } job, err := h.store.FailJob(c.Request.Context(), jobID, req.Reason, atStep) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } // Notify user of failure if h.notifier != nil { jobName := "" if job.Name != nil { jobName = *job.Name } else if job.Title != nil { jobName = *job.Title } h.notifier.NotifyRenderFailed(c.Request.Context(), job.UserID, job.TenantID, job.ID, jobName, req.Reason) } c.JSON(http.StatusOK, gin.H{"status": "failed", "job_id": job.ID}) } // POST /v1/internal/nodes/:node_id/heartbeat func (h *InternalHandler) Heartbeat(c *gin.Context) { nodeID, err := uuid.Parse(c.Param("node_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"}) return } var req models.NodeHeartbeatRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } req.NodeID = nodeID if err := h.store.UpdateNodeHeartbeat(c.Request.Context(), nodeID, &req); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "next_heartbeat_in_sec": 5, "pending_commands": []any{}, }) } // POST /v1/internal/nodes/:node_id/online func (h *InternalHandler) Online(c *gin.Context) { nodeID, err := uuid.Parse(c.Param("node_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"}) return } var req models.NodeOnlineRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } if err := h.store.UpdateNodeOnline(c.Request.Context(), nodeID, &req); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.Status(http.StatusOK) } // POST /v1/internal/render/jobs/:job_id/frames func (h *InternalHandler) FrameProgress(c *gin.Context) { 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 } var req models.FrameProgressRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } if err := h.store.UpdateFrameProgress(c.Request.Context(), jobID, &req); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.Status(http.StatusNoContent) } // POST /v1/internal/render/jobs/:job_id/crash func (h *InternalHandler) Crash(c *gin.Context) { 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 } var req models.CrashReportRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } if err := h.store.InsertCrash(c.Request.Context(), req.NodeID, jobID, &req); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "action_recommended": "ResetAndRestart", "reassigned_to_node_id": nil, }) } // POST /v1/internal/render/jobs/:job_id/replica-ready func (h *InternalHandler) ReplicaReady(c *gin.Context) { _, err := uuid.Parse(c.Param("job_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid job_id"}) return } var req models.ReplicaReadyRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } // In production: update job step to TemplateCache → JsxGen, signal next pipeline phase c.Status(http.StatusNoContent) } // POST /v1/internal/nodes/:node_id/cache-update func (h *InternalHandler) CacheUpdate(c *gin.Context) { nodeID, err := uuid.Parse(c.Param("node_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"}) return } var req models.CacheUpdateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } if err := h.store.UpdateNodeCache(c.Request.Context(), nodeID, &req); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.Status(http.StatusNoContent) }