package handlers import ( "net/http" "time" "github.com/flatrender/render-svc/internal/db" "github.com/flatrender/render-svc/internal/models" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type NodeHandler struct { store *db.Store } func NewNodeHandler(store *db.Store) *NodeHandler { return &NodeHandler{store: store} } // GET /v1/nodes func (h *NodeHandler) List(c *gin.Context) { region := c.Query("region") status := c.Query("status") nodes, err := h.store.ListNodes(c.Request.Context(), region, status) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } if nodes == nil { nodes = []*models.RenderNode{} } c.JSON(http.StatusOK, gin.H{"data": nodes}) } // POST /v1/nodes func (h *NodeHandler) Create(c *gin.Context) { var req models.NodeCreateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } node, err := h.store.CreateNode(c.Request.Context(), &req) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.JSON(http.StatusCreated, node) } // GET /v1/nodes/:node_id func (h *NodeHandler) Get(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 } node, err := h.store.GetNodeByID(c.Request.Context(), nodeID) if err != nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()}) return } c.JSON(http.StatusOK, node) } // PATCH /v1/nodes/:node_id func (h *NodeHandler) Patch(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.NodePatchRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } node, err := h.store.PatchNode(c.Request.Context(), nodeID, &req) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.JSON(http.StatusOK, node) } // POST /v1/nodes/:node_id/restart — Stub: in production this calls the node agent func (h *NodeHandler) Restart(c *gin.Context) { _, err := uuid.Parse(c.Param("node_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"}) return } c.Status(http.StatusAccepted) } // POST /v1/nodes/:node_id/release func (h *NodeHandler) Release(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 } if err := h.store.ReleaseNode(c.Request.Context(), nodeID); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.Status(http.StatusNoContent) } // DELETE /v1/nodes/:node_id func (h *NodeHandler) Delete(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 } if err := h.store.DeleteNode(c.Request.Context(), nodeID); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.Status(http.StatusNoContent) } // POST /v1/nodes/:node_id/close-ae — Stub: signals the node agent to kill AE process func (h *NodeHandler) CloseAE(c *gin.Context) { _, err := uuid.Parse(c.Param("node_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid node_id"}) return } c.Status(http.StatusNoContent) } // GET /v1/nodes/:node_id/health func (h *NodeHandler) Health(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 } node, err := h.store.GetNodeByID(c.Request.Context(), nodeID) if err != nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "node_id": node.ID, "recorded_at": node.LastHeartbeatAt, "status": node.Status, "cpu_pct": node.LastCPUPct, "ram_available_mb": node.LastRAMAvailableMB, "ae_running": node.AERunning, "current_job_id": node.CurrentJobID, "cache_used_gb": node.CacheUsedGB, }) } // GET /v1/nodes/:node_id/health/history func (h *NodeHandler) HealthHistory(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 } fromStr := c.Query("from") toStr := c.Query("to") from := time.Now().Add(-24 * time.Hour) to := time.Now() if fromStr != "" { if t, err := time.Parse(time.RFC3339, fromStr); err == nil { from = t } } if toStr != "" { if t, err := time.Parse(time.RFC3339, toStr); err == nil { to = t } } logs, err := h.store.ListNodeHealthHistory(c.Request.Context(), nodeID, from, to) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } if logs == nil { logs = []*models.NodeHealthLog{} } c.JSON(http.StatusOK, gin.H{"data": logs}) } // GET /v1/nodes/:node_id/crashes func (h *NodeHandler) Crashes(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 } crashes, err := h.store.ListNodeCrashes(c.Request.Context(), nodeID) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } if crashes == nil { crashes = []*models.NodeCrash{} } c.JSON(http.StatusOK, gin.H{"data": crashes}) } // GET /v1/node-updates func (h *NodeHandler) ListUpdates(c *gin.Context) { updates, err := h.store.ListNodeUpdates(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } if updates == nil { updates = []*models.NodeUpdate{} } c.JSON(http.StatusOK, gin.H{"data": updates}) } // POST /v1/node-updates/:update_id/rollout func (h *NodeHandler) Rollout(c *gin.Context) { updateID, err := uuid.Parse(c.Param("update_id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid update_id"}) return } var body struct { NodeIDs []uuid.UUID `json:"node_ids"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } if err := h.store.QueueUpdateRollout(c.Request.Context(), updateID, body.NodeIDs); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "internal_error", Message: err.Error()}) return } c.Status(http.StatusAccepted) }