"use client"; import React, { useState, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { apiFetch } from "@/lib/api"; // ── Types (per contract) ─────────────────────────────────────────────────── interface PacingProvider { source: "avito" | "cian" | "yandex" | "generic"; interval_s: number; env_default_s: number; } interface PacingResp { providers: PacingProvider[]; } interface PacingUpdateResp { ok: boolean; source: string; interval_s: number; } // ── Hooks ────────────────────────────────────────────────────────────────── function usePacing() { return useQuery({ queryKey: ["scraper-pacing"], queryFn: () => apiFetch("/api/v1/admin/scraper/pacing"), staleTime: 30_000, retry: 1, }); } function useUpdatePacing(source: string) { const qc = useQueryClient(); return useMutation({ mutationFn: ({ interval_s }) => apiFetch( `/api/v1/admin/scraper/pacing/${encodeURIComponent(source)}`, { method: "PUT", body: JSON.stringify({ interval_s }), }, ), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["scraper-pacing"] }); }, }); } // ── Per-provider row ─────────────────────────────────────────────────────── interface PacingRowProps { provider: PacingProvider; } function PacingRow({ provider }: PacingRowProps) { const [value, setValue] = useState(String(provider.interval_s)); const updateMut = useUpdatePacing(provider.source); // Sync when upstream data refreshes useEffect(() => { setValue(String(provider.interval_s)); }, [provider.interval_s]); const numValue = parseFloat(value); const isValid = !isNaN(numValue) && numValue >= 0 && numValue <= 60; return ( {provider.source}
setValue(e.target.value)} aria-label={`Pacing interval for ${provider.source} (seconds)`} style={{ width: 72, padding: "3px 6px", fontSize: "0.85rem", border: "1px solid var(--border-card, #e6e8ec)", borderRadius: 6, }} /> setValue(e.target.value)} aria-label={`Pacing slider for ${provider.source}`} style={{ width: 120 }} /> с (по умолч. {provider.env_default_s} с — сбросится при рестарте)
{updateMut.isSuccess && updateMut.data.ok && ( {updateMut.data.interval_s} с — ок )} {updateMut.isSuccess && !updateMut.data.ok && ( не применено )} {updateMut.isError && ( {updateMut.error.message} )} ); } // ── PacingSection (exported) ─────────────────────────────────────────────── export function PacingSection() { const pacingQ = usePacing(); return (

Pacing (интервалы запросов)

Минимальный интервал между запросами скраппера для каждого провайдера. 0 = без искусственной паузы. Изменение вступает в силу немедленно; сбрасывается при рестарте контейнера (постоянное значение — через ENV).

{/* Graceful degradation: API not yet deployed */} {pacingQ.isError && (

pacing API недоступен {" — "} {pacingQ.error.message}

)} {pacingQ.isPending && (

Загрузка pacing…

)} {pacingQ.isSuccess && pacingQ.data.providers.length === 0 && (

Нет данных pacing от API.

)} {pacingQ.isSuccess && pacingQ.data.providers.length > 0 && ( {pacingQ.data.providers.map((p) => ( ))}
Провайдер Интервал (0–60 с) Действие
)}
); }