gendesign/tradein-mvp/frontend/src/components/scrapers/ScheduleControl.tsx
bot-backend dd339d3302
All checks were successful
CI / changes (pull_request) Successful in 4s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(tradein-frontend): единая scrapers-страница — табы Avito/Cian/Yandex, proxy-health+rotate, runs-история, консолидация триггеров, удалён cian-cookies редирект
2026-06-20 09:33:07 +03:00

320 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
import {
formatTime,
useSchedules,
useStartCitySweep,
useUpdateSchedule,
utcToMsk,
mskToUtc,
type ScheduleSourceKey,
type ScraperSource,
type SweepParamConfig,
} from "@/app/scrapers/_components/ScraperPage";
// ── ScheduleControl ────────────────────────────────────────────────────────
interface ScheduleControlProps {
source: ScraperSource;
scheduleSource: ScheduleSourceKey;
paramConfig: SweepParamConfig;
}
export function ScheduleControl({
source,
scheduleSource,
paramConfig,
}: ScheduleControlProps) {
const schedulesQ = useSchedules();
const updateScheduleMut = useUpdateSchedule();
const sweepMut = useStartCitySweep(source);
const [schEnabled, setSchEnabled] = useState(true);
const [schMskStart, setSchMskStart] = useState(5);
const [schMskEnd, setSchMskEnd] = useState(8);
const [schPages, setSchPages] = useState(
paramConfig.defaults.pages_per_anchor,
);
const [schTopN, setSchTopN] = useState(
paramConfig.defaults.detail_top_n ?? 0,
);
const [schDelay, setSchDelay] = useState(
paramConfig.defaults.request_delay_sec,
);
const [schRadius, setSchRadius] = useState(paramConfig.defaults.radius_m);
const [schEnrichHouses, setSchEnrichHouses] = useState(
paramConfig.defaults.enrich_houses ?? false,
);
const [schEnrichAddress, setSchEnrichAddress] = useState(
paramConfig.defaults.enrich_address ?? false,
);
useEffect(() => {
const sweep = schedulesQ.data?.find((s) => s.source === scheduleSource);
if (!sweep) return;
setSchEnabled(sweep.enabled);
setSchMskStart(utcToMsk(sweep.window_start_hour));
setSchMskEnd(utcToMsk(sweep.window_end_hour));
const p = sweep.default_params ?? {};
if (typeof p.pages_per_anchor === "number")
setSchPages(p.pages_per_anchor);
if (typeof p.detail_top_n === "number") setSchTopN(p.detail_top_n);
if (typeof p.request_delay_sec === "number")
setSchDelay(p.request_delay_sec);
if (typeof p.radius_m === "number") setSchRadius(p.radius_m);
if (typeof p.enrich_houses === "boolean")
setSchEnrichHouses(p.enrich_houses);
if (typeof p.enrich_address === "boolean")
setSchEnrichAddress(p.enrich_address);
}, [schedulesQ.data, scheduleSource]);
const currentSweep = schedulesQ.data?.find(
(s) => s.source === scheduleSource,
);
function handleSweepTrigger() {
const body: Record<string, unknown> = {
pages_per_anchor: schPages,
request_delay_sec: schDelay,
radius_m: schRadius,
};
if (paramConfig.hasDetailParams) {
body.detail_top_n = schTopN;
body.enrich_houses = schEnrichHouses;
}
if (paramConfig.hasEnrichAddress) {
body.enrich_address = schEnrichAddress;
}
sweepMut.mutate(body);
}
return (
<section className="scraper-section scraper-section--schedule">
<h2>Расписание и ручной запуск</h2>
{schedulesQ.isPending && (
<p className="scraper-hint">Загрузка расписания</p>
)}
{schedulesQ.isError && (
<p className="scraper-result scraper-result--error">
Ошибка загрузки: {schedulesQ.error.message}
</p>
)}
{/* Current status */}
{currentSweep && (
<div className="schedule-status" style={{ marginBottom: 16 }}>
<div className="schedule-status__row">
<span className="schedule-status__label">Статус:</span>
<span
className={`run-badge run-badge--${currentSweep.enabled ? "running" : "cancelled"}`}
>
{currentSweep.enabled ? "включено" : "отключено"}
</span>
</div>
<div className="schedule-status__row">
<span className="schedule-status__label">Окно (МСК):</span>
<span>
{String(utcToMsk(currentSweep.window_start_hour)).padStart(
2,
"0",
)}
:00
{String(utcToMsk(currentSweep.window_end_hour)).padStart(
2,
"0",
)}
:00
</span>
</div>
<div className="schedule-status__row">
<span className="schedule-status__label">Следующий:</span>
<span>{formatTime(currentSweep.next_run_at)}</span>
</div>
<div className="schedule-status__row">
<span className="schedule-status__label">Последний:</span>
<span>
{currentSweep.last_run_at
? formatTime(currentSweep.last_run_at)
: "—"}
{currentSweep.last_run_id &&
` (run #${currentSweep.last_run_id})`}
</span>
</div>
</div>
)}
{/* Schedule settings form */}
<form
className="scraper-form"
onSubmit={(e) => {
e.preventDefault();
const defaultParams: Record<string, unknown> = {
pages_per_anchor: schPages,
request_delay_sec: schDelay,
radius_m: schRadius,
};
if (paramConfig.hasDetailParams) {
defaultParams.detail_top_n = schTopN;
defaultParams.enrich_houses = schEnrichHouses;
}
if (paramConfig.hasEnrichAddress) {
defaultParams.enrich_address = schEnrichAddress;
}
updateScheduleMut.mutate({
source: scheduleSource,
payload: {
enabled: schEnabled,
window_start_hour: mskToUtc(schMskStart),
window_end_hour: mskToUtc(schMskEnd),
default_params: defaultParams,
},
});
}}
>
<label className="checkbox">
<input
type="checkbox"
checked={schEnabled}
onChange={(e) => setSchEnabled(e.target.checked)}
/>
Включено
</label>
<label>
Окно начала (МСК)
<input
type="number"
min={0}
max={23}
value={schMskStart}
onChange={(e) => setSchMskStart(parseInt(e.target.value, 10))}
/>
</label>
<label>
Окно окончания (МСК)
<input
type="number"
min={0}
max={23}
value={schMskEnd}
onChange={(e) => setSchMskEnd(parseInt(e.target.value, 10))}
/>
</label>
<label>
Pages/anchor
<input
type="number"
min={1}
max={10}
value={schPages}
onChange={(e) => setSchPages(parseInt(e.target.value, 10))}
/>
</label>
{paramConfig.hasDetailParams && (
<label>
Detail top-N
<input
type="number"
min={0}
max={30}
value={schTopN}
onChange={(e) => setSchTopN(parseInt(e.target.value, 10))}
/>
</label>
)}
<label>
Delay (сек)
<input
type="number"
step={0.5}
min={1}
max={60}
value={schDelay}
onChange={(e) => setSchDelay(parseFloat(e.target.value))}
/>
</label>
<label>
Радиус (м)
<input
type="number"
min={500}
max={5000}
step={100}
value={schRadius}
onChange={(e) => setSchRadius(parseInt(e.target.value, 10))}
/>
</label>
{paramConfig.hasDetailParams && (
<label className="checkbox">
<input
type="checkbox"
checked={schEnrichHouses}
onChange={(e) => setSchEnrichHouses(e.target.checked)}
/>
Enrich houses
</label>
)}
{paramConfig.hasEnrichAddress && (
<label className="checkbox">
<input
type="checkbox"
checked={schEnrichAddress}
onChange={(e) => setSchEnrichAddress(e.target.checked)}
/>
Enrich address
</label>
)}
<button type="submit" disabled={updateScheduleMut.isPending}>
{updateScheduleMut.isPending
? "Сохраняем…"
: "Сохранить расписание"}
</button>
</form>
{updateScheduleMut.isError && (
<div className="scraper-result scraper-result--error">
Ошибка: {updateScheduleMut.error.message}
</div>
)}
{updateScheduleMut.isSuccess && updateScheduleMut.data && (
<div className="scraper-result">
Расписание обновлено. Next run:{" "}
{formatTime(updateScheduleMut.data.next_run_at)}
</div>
)}
{/* Manual trigger */}
<div style={{ marginTop: 16, borderTop: "1px solid var(--border-soft, #eef0f3)", paddingTop: 16 }}>
<p
style={{
fontSize: "0.85rem",
color: "var(--fg-secondary, #5b6066)",
marginBottom: 10,
}}
>
Ручной запуск city sweep с параметрами выше:
</p>
<button
type="button"
onClick={handleSweepTrigger}
disabled={sweepMut.isPending}
>
{sweepMut.isPending ? "Запускаем…" : "Запустить sweep вручную"}
</button>
{sweepMut.isError && (
<div className="scraper-result scraper-result--error" style={{ marginTop: 8 }}>
Ошибка: {sweepMut.error.message}
</div>
)}
{sweepMut.isSuccess && sweepMut.data && (
<div className="scraper-result" style={{ marginTop: 8 }}>
<pre style={{ margin: 0, fontSize: "0.8rem" }}>
{JSON.stringify(sweepMut.data, null, 2)}
</pre>
</div>
)}
</div>
</section>
);
}