diff --git a/services/render/cmd/server/main.go b/services/render/cmd/server/main.go index 816c0d3..75af658 100644 --- a/services/render/cmd/server/main.go +++ b/services/render/cmd/server/main.go @@ -119,6 +119,7 @@ func main() { nodes.POST("", nodeH.Create) nodes.GET("/:node_id", nodeH.Get) nodes.PATCH("/:node_id", nodeH.Patch) + nodes.DELETE("/:node_id", nodeH.Delete) nodes.POST("/:node_id/restart", nodeH.Restart) nodes.POST("/:node_id/release", nodeH.Release) nodes.POST("/:node_id/close-ae", nodeH.CloseAE) diff --git a/services/render/internal/db/db.go b/services/render/internal/db/db.go index ddc880e..f3f17ff 100644 --- a/services/render/internal/db/db.go +++ b/services/render/internal/db/db.go @@ -145,6 +145,12 @@ func (s *Store) PatchNode(ctx context.Context, id uuid.UUID, req *models.NodePat return s.GetNodeByID(ctx, id) } +// DeleteNode permanently removes a render node. +func (s *Store) DeleteNode(ctx context.Context, id uuid.UUID) error { + _, err := s.pool.Exec(ctx, `DELETE FROM render.render_nodes WHERE id = $1`, id) + return err +} + func (s *Store) UpdateNodeHeartbeat(ctx context.Context, nodeID uuid.UUID, req *models.NodeHeartbeatRequest) error { _, err := s.pool.Exec(ctx, ` UPDATE render.render_nodes SET diff --git a/services/render/internal/handlers/nodes.go b/services/render/internal/handlers/nodes.go index 4c605cc..dfb9373 100644 --- a/services/render/internal/handlers/nodes.go +++ b/services/render/internal/handlers/nodes.go @@ -107,6 +107,20 @@ func (h *NodeHandler) Release(c *gin.Context) { 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")) diff --git a/src/app/api/admin/nodes/[nodeId]/route.ts b/src/app/api/admin/nodes/[nodeId]/route.ts new file mode 100644 index 0000000..29ad663 --- /dev/null +++ b/src/app/api/admin/nodes/[nodeId]/route.ts @@ -0,0 +1,9 @@ +import { type NextRequest } from "next/server"; +import { adminProxy } from "@/app/api/admin/_adminProxy"; + +export const runtime = "nodejs"; +interface Ctx { params: { nodeId: string } } + +export async function DELETE(req: NextRequest, { params }: Ctx) { + return adminProxy(req, `/v1/nodes/${params.nodeId}`, "DELETE"); +} diff --git a/src/components/admin/NodesTable.tsx b/src/components/admin/NodesTable.tsx index 1e466a7..233e431 100644 --- a/src/components/admin/NodesTable.tsx +++ b/src/components/admin/NodesTable.tsx @@ -55,6 +55,17 @@ export function NodesTable({ nodes }: { nodes: V2Node[] }) { } }; + const deleteNode = async (nodeId: string) => { + if (!confirm("این نود برای همیشه حذف شود؟")) return; + setLoading((p) => ({ ...p, [nodeId]: true })); + try { + await fetch(`/api/admin/nodes/${nodeId}`, { method: "DELETE" }); + router.refresh(); + } finally { + setLoading((p) => ({ ...p, [nodeId]: false })); + } + }; + const addNode = async () => { setSaving(true); setErr(null); const res = await fetch("/api/admin/nodes", { @@ -204,6 +215,13 @@ export function NodesTable({ nodes }: { nodes: V2Node[] }) { > {t("actionRelease")} +