"use client"; import { useEffect, useState } from "react"; import "@/components/trade-in/trade-in.css"; import { Topbar } from "@/components/trade-in/Topbar"; import { API_BASE_URL } from "@/lib/api"; import { formatTime, translateStatus, useScraperSettings, useSchedules, useUpdateScraperSetting, useUpdateSchedule, utcToMsk, type ScheduleConfig, type ScheduleSourceKey, } from "@/app/scrapers/_components/ScraperPage"; // ── Global delay section ─────────────────────────────────────────────────── function GlobalDelaySetting() { const settingsQ = useScraperSettings(); const updateMut = useUpdateScraperSetting(); const globalSetting = settingsQ.data?.find((s) => s.source === "global"); const [delayInput, setDelayInput] = useState("0"); const [initialized, setInitialized] = useState(false); useEffect(() => { if (globalSetting && !initialized) { setDelayInput(String(globalSetting.request_delay_sec)); setInitialized(true); } }, [globalSetting, initialized]); return (

Глобальная задержка запросов (нижний порог)

Глобальный делэй — нижний порог для всех скрапперов (max с per-source defaults). Если 0 — только per-source defaults. Изменение вступает в силу немедленно без перезапуска процесса. Для per-source задержек зайдите на страницу конкретного скраппера.

{settingsQ.isPending && (

Загрузка настроек…

)} {settingsQ.error && (

Ошибка загрузки: {settingsQ.error.message}

)} {globalSetting && (
Текущий global delay: {globalSetting.request_delay_sec} сек
{globalSetting.updated_at && (
Обновлено: {formatTime(globalSetting.updated_at)}
)}
)} {/* Per-source read-only table */} {settingsQ.data && settingsQ.data.length > 0 && (

Все per-source задержки (read-only здесь, редактировать на страницах провайдеров):

{settingsQ.data.map((s) => ( ))}
Source Delay (сек) Описание Обновлено
{s.source} {s.request_delay_sec.toFixed(1)} {s.description ?? "—"} {formatTime(s.updated_at)}
)}
{ e.preventDefault(); updateMut.mutate({ source: "global", request_delay_sec: parseFloat(delayInput), }); }} > setDelayInput(e.target.value)} style={{ width: "100%", gridColumn: "1 / -1" }} />
{updateMut.error && (
Ошибка: {updateMut.error.message}
)} {updateMut.isSuccess && updateMut.data && (
Global delay обновлён:{" "} {Number(updateMut.data.request_delay_sec).toFixed(1)} сек {" "} (обновлено {formatTime(updateMut.data.updated_at)})
)}
); } // ── Schedules summary table ──────────────────────────────────────────────── const SCHEDULE_SOURCE_LABELS: Record = { avito_city_sweep: "Avito city sweep", cian_city_sweep: "Cian city sweep", yandex_city_sweep: "Yandex city sweep", }; const SCHEDULE_SOURCES: ScheduleSourceKey[] = [ "avito_city_sweep", "cian_city_sweep", "yandex_city_sweep", ]; function SchedulesSummary() { const schedulesQ = useSchedules(); const updateMut = useUpdateSchedule(); function handleToggle(schedule: ScheduleConfig) { updateMut.mutate({ source: schedule.source as ScheduleSourceKey, payload: { enabled: !schedule.enabled, window_start_hour: schedule.window_start_hour, window_end_hour: schedule.window_end_hour, default_params: schedule.default_params as Record, }, }); } // Build ordered rows from known sources + any extras const knownRows = SCHEDULE_SOURCES.map((src) => schedulesQ.data?.find((s) => s.source === src), ).filter((s): s is ScheduleConfig => s !== undefined); const extraRows = schedulesQ.data?.filter( (s) => !SCHEDULE_SOURCES.includes(s.source as ScheduleSourceKey), ) ?? []; const rows = [...knownRows, ...extraRows]; return (

Сводная таблица расписаний

Быстрое включение/отключение всех cron-расписаний. Параметры и окна настраиваются на страницах провайдеров. Автообновление каждые 30 сек.

{schedulesQ.isPending &&

Загрузка…

} {schedulesQ.error && (

Ошибка загрузки: {schedulesQ.error.message}

)} {rows.length > 0 && ( {rows.map((s) => { const mskStart = utcToMsk(s.window_start_hour); const mskEnd = utcToMsk(s.window_end_hour); const label = SCHEDULE_SOURCE_LABELS[s.source] ?? s.source; return ( ); })}
Source Статус Окно (МСК) Следующий запуск Последний запуск Последний run Вкл/выкл
{label} {s.enabled ? "включено" : "отключено"} {String(mskStart).padStart(2, "0")}:00– {String(mskEnd).padStart(2, "0")}:00 МСК {formatTime(s.next_run_at)} {formatTime(s.last_run_at)} {s.last_run_id ? ( #{s.last_run_id} ) : ( "—" )}
)} {rows.length === 0 && !schedulesQ.isPending && !schedulesQ.error && (

Расписания не найдены. Они создаются автоматически при первом сохранении на странице провайдера.

)} {updateMut.error && (
Ошибка обновления: {updateMut.error.message}
)} {updateMut.isSuccess && (
Расписание {translateStatus( updateMut.data.enabled ? "running" : "cancelled", )}.
)}
); } // ── Provider cards ───────────────────────────────────────────────────────── const PROVIDERS = [ { key: "avito", name: "Avito", href: "/scrapers/avito", description: "City sweep, houses catalog, detail enrichment, IMV evaluation", }, { key: "cian", name: "Cian", href: "/scrapers/cian", description: "City sweep, detail enrichment, новостройки ЖК, cookies", }, { key: "yandex", name: "Yandex Realty", href: "/scrapers/yandex", description: "City sweep, detail, ЖК landing, valuation", }, ]; function ProviderCards() { return (

Провайдеры

{PROVIDERS.map((p) => (

{p.name}

{p.description}

))}
); } // ── Page ─────────────────────────────────────────────────────────────────── export default function ScrapersHubPage() { return ( <>

Скрапперы — хаб

Центр управления всеми скрапперами: глобальные настройки задержки, сводная таблица расписаний, ссылки на admin-триггеры провайдеров.

); }