diff --git a/services/gateway/cmd/server/main.go b/services/gateway/cmd/server/main.go index 6824558..9f7ac0a 100644 --- a/services/gateway/cmd/server/main.go +++ b/services/gateway/cmd/server/main.go @@ -137,6 +137,7 @@ func main() { v1.Any("/nodes/*path", apiRL, auth, render.Handler()) v1.Any("/node-fonts/*path", apiRL, auth, render.Handler()) v1.Any("/admin-exports/*path", apiRL, auth, render.Handler()) + v1.Any("/admin-renders", apiRL, auth, render.Handler()) v1.Any("/node-updates/*path", apiRL, auth, render.Handler()) // ── Notification Service ────────────────────────────────────────────────── diff --git a/services/render/cmd/server/main.go b/services/render/cmd/server/main.go index f0b7c86..526be4a 100644 --- a/services/render/cmd/server/main.go +++ b/services/render/cmd/server/main.go @@ -145,6 +145,9 @@ func main() { nodeFonts.DELETE("/:id", fontH.Delete) } + // ── Render queue (admin: all users' jobs) ───────────────────────────────── + v1.GET("/admin-renders", auth, admin, renderH.AdminList) + // ── Exports management (admin: all users' rendered videos) ──────────────── adminExports := v1.Group("/admin-exports", auth, admin) { diff --git a/services/render/internal/db/renders_admin.go b/services/render/internal/db/renders_admin.go new file mode 100644 index 0000000..41db298 --- /dev/null +++ b/services/render/internal/db/renders_admin.go @@ -0,0 +1,45 @@ +package db + +import ( + "context" + "fmt" + + "github.com/flatrender/render-svc/internal/models" +) + +const jobCols = `id, tenant_id, user_id, saved_project_id, original_project_id, + project_name, title, name, external_job_id, priority_queue::text, priority_score, + step::text, render_progress, convert_progress, image_preview_b64, + price_type::text, paid_price_minor, discount_code, support_flatrender, + mode, quality::text, resolution, r_height, frame_rate, is_60_fps, + duration_sec, export_duration_sec, + has_music, has_sfx, has_voiceover, music_volume, sfx_volume, voiceover_volume, + render_node_count, current_active_nodes, region, tell_me_when_done, + retry_count, max_retries, repair_attempts, failed_message, failed_at_step::text, + export_id, task_start_date, queued_at, started_at, completed_at, created_at, updated_at` + +// ListAllJobs returns render jobs across all users (admin view), optional status filter. +func (s *Store) ListAllJobs(ctx context.Context, status string, page, pageSize int) ([]*models.RenderJob, int64, error) { + where := "" + args := []any{} + idx := 1 + if status != "" { + where = fmt.Sprintf(" WHERE step::text = $%d", idx) + args = append(args, status) + idx++ + } + + var total int64 + _ = s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM render.render_jobs"+where, args...).Scan(&total) + + args = append(args, pageSize, (page-1)*pageSize) + q := "SELECT " + jobCols + " FROM render.render_jobs" + where + + fmt.Sprintf(" ORDER BY created_at DESC LIMIT $%d OFFSET $%d", idx, idx+1) + rows, err := s.pool.Query(ctx, q, args...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + jobs, err := scanJobs(rows) + return jobs, total, err +} diff --git a/services/render/internal/handlers/renders_admin.go b/services/render/internal/handlers/renders_admin.go new file mode 100644 index 0000000..2186840 --- /dev/null +++ b/services/render/internal/handlers/renders_admin.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/flatrender/render-svc/internal/models" + "github.com/gin-gonic/gin" +) + +// GET /v1/admin-renders — all users' render jobs (admin), optional ?status= filter. +func (h *RenderHandler) AdminList(c *gin.Context) { + status := c.Query("status") + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + if page < 1 { + page = 1 + } + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "30")) + if pageSize < 1 || pageSize > 100 { + pageSize = 30 + } + jobs, total, err := h.store.ListAllJobs(c.Request.Context(), 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}, + }) +} diff --git a/src/app/[locale]/admin/renders/page.tsx b/src/app/[locale]/admin/renders/page.tsx index ebb6d73..c085976 100644 --- a/src/app/[locale]/admin/renders/page.tsx +++ b/src/app/[locale]/admin/renders/page.tsx @@ -23,8 +23,8 @@ export type V2RenderJob = { }; interface V2RenderList { - items: V2RenderJob[]; - total: number; + data: V2RenderJob[]; + meta?: { total?: number }; } export default async function AdminRendersPage({ @@ -33,10 +33,11 @@ export default async function AdminRendersPage({ searchParams: { step?: string }; }) { const step = searchParams.step ?? ""; - const qs = step ? `?step=${step}&pageSize=50` : "?pageSize=50"; - const data = await adminGet(`/v1/renders${qs}`); - const jobs = data?.items ?? []; - const total = data?.total ?? 0; + // Admin endpoint → all users' jobs (not just the caller's). + const qs = step ? `?status=${step}&page_size=50` : "?page_size=50"; + const data = await adminGet(`/v1/admin-renders${qs}`); + const jobs = data?.data ?? []; + const total = data?.meta?.total ?? 0; const t = await getTranslations("auto.appAdminRendersPage"); const steps = ["Queued", "Preparing", "Rendering", "Uploading", "Done", "Failed", "Cancelled"];