diff --git a/services/render/internal/db/renders_admin.go b/services/render/internal/db/renders_admin.go index 41db298..0702167 100644 --- a/services/render/internal/db/renders_admin.go +++ b/services/render/internal/db/renders_admin.go @@ -41,5 +41,47 @@ func (s *Store) ListAllJobs(ctx context.Context, status string, page, pageSize i } defer rows.Close() jobs, err := scanJobs(rows) + if err != nil { + return nil, 0, err + } + s.attachUserInfo(ctx, jobs) return jobs, total, err } + +// attachUserInfo fills UserName/UserEmail on each job via one cross-schema lookup +// against identity.users (same Postgres DB). Best-effort — failures leave names blank. +func (s *Store) attachUserInfo(ctx context.Context, jobs []*models.RenderJob) { + if len(jobs) == 0 { + return + } + ids := make([]string, 0, len(jobs)) + seen := map[string]bool{} + for _, j := range jobs { + id := j.UserID.String() + if !seen[id] { + seen[id] = true + ids = append(ids, id) + } + } + rows, err := s.pool.Query(ctx, + `SELECT id::text, COALESCE(full_name,''), COALESCE(email,'') FROM identity.users WHERE id = ANY($1)`, + ids) + if err != nil { + return + } + defer rows.Close() + type ui struct{ name, email string } + m := map[string]ui{} + for rows.Next() { + var id, name, email string + if rows.Scan(&id, &name, &email) == nil { + m[id] = ui{name, email} + } + } + for _, j := range jobs { + if info, ok := m[j.UserID.String()]; ok { + j.UserName = info.name + j.UserEmail = info.email + } + } +} diff --git a/services/render/internal/models/models.go b/services/render/internal/models/models.go index 46761af..40b2cf6 100644 --- a/services/render/internal/models/models.go +++ b/services/render/internal/models/models.go @@ -180,6 +180,9 @@ type RenderJob struct { CompletedAt *time.Time `json:"completed_at,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` + // Admin-list enrichment (set by the handler via a cross-schema lookup, not scanned). + UserName string `json:"user_name,omitempty"` + UserEmail string `json:"user_email,omitempty"` } type FrameJob struct { diff --git a/src/app/[locale]/admin/renders/page.tsx b/src/app/[locale]/admin/renders/page.tsx index c085976..67de706 100644 --- a/src/app/[locale]/admin/renders/page.tsx +++ b/src/app/[locale]/admin/renders/page.tsx @@ -10,6 +10,9 @@ export type V2RenderJob = { id: string; saved_project_id: string; user_id: string; + user_name?: string; + user_email?: string; + project_name?: string | null; status: string; step: string; progress: number; @@ -17,6 +20,7 @@ export type V2RenderJob = { resolution: string; frame_rate: number; node_id: string | null; + export_id?: string | null; error_message: string | null; created_at: string; updated_at: string; @@ -24,20 +28,26 @@ export type V2RenderJob = { interface V2RenderList { data: V2RenderJob[]; - meta?: { total?: number }; + meta?: { total?: number; page?: number; page_size?: number; has_more?: boolean }; } +const PAGE_SIZE = 30; + export default async function AdminRendersPage({ searchParams, }: { - searchParams: { step?: string }; + searchParams: { step?: string; page?: string }; }) { const step = searchParams.step ?? ""; + const page = Math.max(1, Number(searchParams.page) || 1); // 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 params = new URLSearchParams({ page: String(page), page_size: String(PAGE_SIZE) }); + if (step) params.set("status", step); + const data = await adminGet(`/v1/admin-renders?${params}`); const jobs = data?.data ?? []; const total = data?.meta?.total ?? 0; + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + const stepQs = step ? `&step=${step}` : ""; const t = await getTranslations("auto.appAdminRendersPage"); const steps = ["Queued", "Preparing", "Rendering", "Uploading", "Done", "Failed", "Cancelled"]; @@ -88,6 +98,22 @@ export default async function AdminRendersPage({ + + {totalPages > 1 && ( +
+ {page > 1 ? ( + قبلی + ) : ( + قبلی + )} + صفحهٔ {page.toLocaleString("fa-IR")} از {totalPages.toLocaleString("fa-IR")} + {page < totalPages ? ( + بعدی + ) : ( + بعدی + )} +
+ )} ); } diff --git a/src/components/admin/RenderQueueTable.tsx b/src/components/admin/RenderQueueTable.tsx index 8db6d9b..109787e 100644 --- a/src/components/admin/RenderQueueTable.tsx +++ b/src/components/admin/RenderQueueTable.tsx @@ -75,10 +75,12 @@ export function RenderQueueTable({ jobs }: { jobs: V2RenderJob[] }) { {t("colJobId")} {t("colProject")} + کاربر {t("colStep")} {t("colProgress")} {t("colQuality")} {t("colNode")} + خروجی {t("colCreated")} {t("colActions")} @@ -94,8 +96,17 @@ export function RenderQueueTable({ jobs }: { jobs: V2RenderJob[] }) { {job.id.slice(0, 12)}… - - {job.saved_project_id.slice(0, 12)}… + + {job.project_name?.trim() || job.saved_project_id.slice(0, 8) + "…"} + + + + {job.user_name?.trim() || job.user_email || job.user_id.slice(0, 8) + "…"} + @@ -126,6 +137,18 @@ export function RenderQueueTable({ jobs }: { jobs: V2RenderJob[] }) { {job.node_id ? job.node_id.slice(0, 8) + "…" : "—"} + + {job.export_id ? ( + + ▶ مشاهده + + ) : ( + + )} + {relativeTime(job.created_at)}