From ebfd29a0a6cee8b121f18dcc19747bff11914186 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 23 May 2026 15:36:07 +0000 Subject: [PATCH] feat(tradein-ui): Cian scraper admin page + global request delay control (#486) --- .../frontend/src/app/scrapers/cian/page.tsx | 443 +++++++++++++++++- 1 file changed, 435 insertions(+), 8 deletions(-) diff --git a/tradein-mvp/frontend/src/app/scrapers/cian/page.tsx b/tradein-mvp/frontend/src/app/scrapers/cian/page.tsx index e239e18a..b53229ab 100644 --- a/tradein-mvp/frontend/src/app/scrapers/cian/page.tsx +++ b/tradein-mvp/frontend/src/app/scrapers/cian/page.tsx @@ -1,24 +1,451 @@ "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 скрапер

-

В разработке.

-
-

- Cian SERP refactor merged (PR #450), но admin UI ещё не подключён. - Используй cron-scrape.sh или прямой curl к{" "} - POST /api/v1/admin/scrape с{" "} - sources: ["cian"]. +

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) => ( + + + + + + + ))} + +
SourceDelay (сек)ОписаниеОбновлено
+ {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; + } +}