"use client"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { apiFetch } from "@/lib/api"; // ── Types ────────────────────────────────────────────────────────────────── export interface ProviderHealth { source: string; proxy_host: string | null; proxy_port: number | null; rotate_supported: boolean; current_ip: string | null; } export interface BrowserHealth { reachable: boolean; browsers: Record; } export interface ScraperHealthResp { fetch_mode: string; browser: BrowserHealth; providers: ProviderHealth[]; } interface RotateIpResp { ok: boolean; new_ip: string | null; reason: string | null; } // ── Hooks ────────────────────────────────────────────────────────────────── export function useScraperHealth() { return useQuery({ queryKey: ["scraper-health"], queryFn: () => apiFetch("/api/v1/admin/scraper/health"), refetchInterval: 30_000, staleTime: 25_000, retry: 1, }); } export function useRotateIp(source: string) { const qc = useQueryClient(); return useMutation({ mutationFn: () => apiFetch( `/api/v1/admin/scraper/${encodeURIComponent(source)}/rotate-ip`, { method: "POST" }, ), onSuccess: () => { qc.invalidateQueries({ queryKey: ["scraper-health"] }); }, }); } // ── SystemSection ────────────────────────────────────────────────────────── function FetchModeBadge({ mode }: { mode: string }) { const isBrowser = mode === "browser"; return ( {mode} ); } function BrowserBadge({ reachable }: { reachable: boolean }) { return ( {reachable ? "доступен" : "недоступен"} ); } interface ProviderRowProps { provider: ProviderHealth; } function ProviderProxyRow({ provider }: ProviderRowProps) { const rotateMut = useRotateIp(provider.source); const proxyStr = provider.proxy_host && provider.proxy_port ? `${provider.proxy_host}:${provider.proxy_port}` : "—"; return ( {provider.source} {proxyStr} {provider.current_ip ?? "—"} {provider.rotate_supported ? "rotate" : "статический"} {provider.rotate_supported ? ( ) : ( )} {rotateMut.isSuccess && rotateMut.data.ok && ( {rotateMut.data.new_ip ?? "обновлён"} )} {rotateMut.isSuccess && !rotateMut.data.ok && ( {rotateMut.data.reason ?? "ошибка"} )} {rotateMut.isError && ( {rotateMut.error.message} )} ); } export function SystemHealthSection() { const healthQ = useScraperHealth(); return (

Система

Статус fetch-режима, browser-сервиса и прокси провайдеров. Автообновление каждые 30 сек.

{healthQ.isPending && (

Загрузка состояния системы…

)} {healthQ.isError && (

Ошибка загрузки health: {healthQ.error.message} {" — "} API /scraper/health ещё не задеплоен (будет после merge backend PR)

)} {healthQ.data && ( <> {/* Fetch mode + browser status row */}
Режим:
Browser-сервис:
{Object.entries(healthQ.data.browser.browsers).map( ([name, ok]) => (
{name}:
), )}
{/* Per-provider proxy table */} {healthQ.data.providers.length > 0 && ( {healthQ.data.providers.map((p) => ( ))}
Провайдер Прокси (host:port) Текущий IP Режим Действие
)} )}
); }