"use client"; import React, { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { apiFetch } from "@/lib/api"; import { formatTime, translateStatus, useCancelCitySweep, type ScraperSource, } from "@/app/scrapers/_components/ScraperPage"; // ── Types ────────────────────────────────────────────────────────────────── export interface ScrapeRunFull { run_id: number; source: string; run_type: string | null; status: string; params: Record | null; counters: Record | null; total_seen: number | null; new_count: number | null; started_at: string | null; finished_at: string | null; heartbeat_at: string | null; error_text: string | null; } interface RunsListResp { total: number; rows: ScrapeRunFull[]; } // ── Hook ─────────────────────────────────────────────────────────────────── const RUN_STATUS_ALL = ["", "running", "done", "failed", "cancelled", "zombie", "banned"] as const; type RunStatusFilter = (typeof RUN_STATUS_ALL)[number]; function useScraperRuns( source: ScraperSource, status: RunStatusFilter, limit = 20, ) { return useQuery({ queryKey: ["scrape-runs", source, status, limit], queryFn: () => { const qs = new URLSearchParams({ source, limit: String(limit) }); if (status) qs.set("status", status); return apiFetch( `/api/v1/admin/scrape/runs?${qs.toString()}`, ); }, refetchInterval: 8_000, staleTime: 0, retry: 1, }); } // ── Status badge ─────────────────────────────────────────────────────────── function RunStatusBadge({ status }: { status: string }) { let variant: "running" | "done" | "danger" | "neutral"; switch (status) { case "running": variant = "running"; break; case "done": variant = "done"; break; case "failed": case "banned": case "zombie": variant = "danger"; break; default: variant = "neutral"; } const styles: Record = { running: { background: "var(--accent-soft, #dbeafe)", color: "var(--accent, #1d4ed8)", }, done: { background: "var(--success-soft, #dcfce7)", color: "var(--success, #0a7a3a)", }, danger: { background: "var(--danger-soft, #fee2e2)", color: "var(--danger, #b3261e)", }, neutral: { background: "var(--bg-card-alt, #fafbfc)", color: "var(--fg-secondary, #5b6066)", }, }; return ( {translateStatus(status)} ); } // ── Counters cell ────────────────────────────────────────────────────────── function CountersCell({ row, }: { row: ScrapeRunFull; }) { const c = row.counters; if (!c) return ; const parts: string[] = []; const lots = c.lots_fetched != null ? `${c.lots_fetched} лотов` : null; const newC = row.new_count != null ? `+${row.new_count} new` : c.lots_inserted != null ? `+${c.lots_inserted} new` : null; const errs = c.errors_count != null && Number(c.errors_count) > 0 ? `${c.errors_count} err` : null; if (lots) parts.push(lots); if (newC) parts.push(newC); if (errs) parts.push(errs); return ( {parts.length > 0 ? parts.join(" · ") : "—"} ); } // ── RunsTable ────────────────────────────────────────────────────────────── interface RunsTableProps { source: ScraperSource; } export function RunsTable({ source }: RunsTableProps) { const [statusFilter, setStatusFilter] = useState(""); const qc = useQueryClient(); const runsQ = useScraperRuns(source, statusFilter); const cancelMut = useCancelCitySweep(source); function handleCancel(runId: number) { cancelMut.mutate(runId, { onSuccess: () => { void qc.invalidateQueries({ queryKey: ["scrape-runs", source, statusFilter], }); }, }); } return (

История прогонов

Последние 20 прогонов. Автообновление каждые 8 сек.

{/* Status filter */}
{RUN_STATUS_ALL.map((s) => ( ))}
{runsQ.isPending &&

Загрузка…

} {runsQ.isError && (

Ошибка загрузки: {runsQ.error.message} {" — "} API /scrape/runs ещё не задеплоен

)} {runsQ.data && runsQ.data.rows.length === 0 && (

Прогонов не найдено.

)} {runsQ.data && runsQ.data.rows.length > 0 && ( <>

Итого: {runsQ.data.total}

{runsQ.data.rows.map((r) => ( ))}
# Тип Статус Старт Финиш Лоты / Ошибки Действие
{r.run_id} {r.run_type ?? "sweep"} {formatTime(r.started_at)} {formatTime(r.finished_at)} {r.error_text && (
{r.error_text}
)}
{r.status === "running" && ( )}
)}
); }