"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import "@/components/trade-in/trade-in.css"; import { Topbar } from "@/components/trade-in/Topbar"; import { apiFetch } from "@/lib/api"; // ── Types ────────────────────────────────────────────────────────────────── interface ScrapeAroundResp { total_fetched: number; total_inserted: number; total_updated: number; by_source: Array<{ source: string; fetched: number; inserted: number; updated: number; }>; } interface TestAuthResp { authenticated: boolean; userId: number | null; reason: string | null; } interface ScraperSetting { source: string; request_delay_sec: number; description: string | null; updated_at: string | null; } interface ScraperSettingUpdate { request_delay_sec: number; } // ── Hooks ────────────────────────────────────────────────────────────────── function useScrapeAround() { return useMutation< ScrapeAroundResp, Error, { lat: number; lon: number; radius_m: number; multi_room_cian: boolean } >({ mutationFn: (input) => apiFetch("/api/v1/admin/scrape", { method: "POST", body: JSON.stringify({ ...input, sources: ["cian"] }), }), }); } function useTestAuth() { return useQuery({ queryKey: ["cian-test-auth"], queryFn: () => apiFetch("/api/v1/admin/scrape/cian/test-auth"), staleTime: 30_000, refetchInterval: 60_000, }); } function useScraperSettings() { return useQuery({ queryKey: ["scraper-settings"], queryFn: () => apiFetch("/api/v1/admin/scraper-settings"), staleTime: 30_000, }); } function useUpdateScraperSetting() { const qc = useQueryClient(); return useMutation< ScraperSetting, Error, { source: string; payload: ScraperSettingUpdate } >({ mutationFn: ({ source, payload }) => apiFetch(`/api/v1/admin/scraper-settings/${source}`, { method: "PUT", body: JSON.stringify(payload), }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["scraper-settings"] }); }, }); } // ── Result panel ─────────────────────────────────────────────────────────── interface ResultPanelProps { mut: { data?: TData; error: Error | null; isPending: boolean; isSuccess: boolean; }; } function ResultPanel({ mut }: ResultPanelProps) { if (mut.error) { return (
Ошибка: {mut.error.message}
); } if (mut.isSuccess && mut.data) { return (
{JSON.stringify(mut.data, null, 2)}
); } return null; } // ── Page ─────────────────────────────────────────────────────────────────── export default function CianScraperPage() { // Auth status const authQ = useTestAuth(); // 1. Search around const [lat, setLat] = useState("56.8400"); const [lon, setLon] = useState("60.6050"); const [radius, setRadius] = useState("1500"); const [multiRoom, setMultiRoom] = useState(false); const aroundMut = useScrapeAround(); // Global delay const settingsQ = useScraperSettings(); const updateSettingMut = useUpdateScraperSetting(); const [globalDelay, setGlobalDelay] = useState(5); useEffect(() => { const global = settingsQ.data?.find((s) => s.source === "global"); if (global !== undefined) { setGlobalDelay(global.request_delay_sec); } }, [settingsQ.data]); return ( <>

Cian скрапер — admin триггер

Ручной запуск sub-pipeline'ов CianScraper. Для debug и теста после изменений.

{/* Cookies callout */}

Stage 7: Cian Valuation требует загруженных cookies.{" "} Настроить cookies →

Статус сессии:{" "} {authQ.isPending && "проверяем…"} {authQ.error && ( ошибка проверки )} {authQ.data && ( {authQ.data.authenticated ? `авторизован (userId: ${authQ.data.userId ?? "—"})` : `не авторизован — ${authQ.data.reason ?? "неизвестная причина"}`} )}

{/* 1. Search around point */}

1. Search around (общий /admin/scrape)

Запускает Cian SERP pipeline для одного якоря: search → save_listings. Поддерживает multi_room режим (~4× лотов, отдельный запрос на каждое кол-во комнат).

{ e.preventDefault(); aroundMut.mutate({ lat: parseFloat(lat), lon: parseFloat(lon), radius_m: parseInt(radius, 10), multi_room_cian: multiRoom, }); }} >
{/* 2. Scrape detail by URL — NOT YET IMPLEMENTED BACKEND */}

2. Detail page enrichment

Парсинг конкретного объявления Cian по URL. Endpoint{" "} POST /api/v1/admin/scrape/cian-detail планируется в следующем PR. Пока используйте поиск по anchor (#1) для массового скрейпа.

Endpoint не реализован на backend. Запрос вернёт 404.
{/* 3. Newbuilding — NOT YET IMPLEMENTED BACKEND */}

3. Новостройки (ЖК scrape)

Endpoint POST /api/v1/admin/scrape/cian-newbuilding{" "} планируется. Для Yandex Недвижимость аналог уже доступен на вкладке Yandex.

Endpoint не реализован на backend. Запрос вернёт 404.
{/* 4. Valuation Calculator — NOT YET IMPLEMENTED BACKEND */}

4. Valuation Calculator (оценка квартиры)

Endpoint POST /api/v1/admin/scrape/cian-valuation{" "} планируется. Требует загруженных cookies (Stage 7).{" "} Настроить cookies →

Endpoint не реализован на backend. Запрос вернёт 404.
{/* 5. Global Request Delay */}

5. Глобальный задержка запросов (Global Request Delay)

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

{/* Per-source read-only list */} {settingsQ.isPending && (

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

)} {settingsQ.error && (

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

)} {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)}
)} {/* Global delay edit form */}
{ e.preventDefault(); updateSettingMut.mutate({ source: "global", payload: { request_delay_sec: globalDelay }, }); }} > setGlobalDelay(parseFloat(e.target.value))} style={{ width: "100%", gridColumn: "1 / -1" }} />
{updateSettingMut.error && (
Ошибка: {updateSettingMut.error.message}
)} {updateSettingMut.isSuccess && updateSettingMut.data && (
Delay обновлён: {updateSettingMut.data.request_delay_sec.toFixed(1)} сек{" "} (обновлено {formatTime(updateSettingMut.data.updated_at)})
)}
); } // ── Helpers ──────────────────────────────────────────────────────────────── function formatTime(iso: string | null): string { if (!iso) return "—"; try { return new Date(iso).toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }); } catch { return iso; } }