refactor(security): убрать UI ввода admin token (backend не требует с PR #437)
- Удалить token state + localStorage.setItem("admin_token") из всех admin pages
- Удалить X-Admin-Token header из всех apiFetch вызовов
- Убрать enabled: !!token из useQuery — fix «Загрузка…» forever для новых users
- Убрать token prop из BulkGeoPanel, JobSettingsPanel, ScrapeLogsPanel
- Удалить !token guards и UI input fields для ввода токена
- queryKey: убрать token из ключей (scrape-runs, scrape-queue, etc.)
This commit is contained in:
parent
a2147872ed
commit
df34ad7bb5
9 changed files with 69 additions and 483 deletions
|
|
@ -68,11 +68,6 @@ interface LeadsStats {
|
|||
}
|
||||
|
||||
export default function AdminLeadsPage() {
|
||||
const [token] = useState<string>(() =>
|
||||
typeof window === "undefined"
|
||||
? ""
|
||||
: (localStorage.getItem("admin_token") ?? ""),
|
||||
);
|
||||
const [statusFilter, setStatusFilter] = useState<string>("");
|
||||
const [sourceFilter, setSourceFilter] = useState<string>("");
|
||||
const [convertedFilter, setConvertedFilter] = useState<string>("");
|
||||
|
|
@ -81,36 +76,24 @@ export default function AdminLeadsPage() {
|
|||
const limit = 50;
|
||||
|
||||
const stats = useQuery({
|
||||
queryKey: ["admin-leads-stats", token],
|
||||
queryFn: () =>
|
||||
apiFetch<LeadsStats>("/api/v1/admin/leads/stats?months=12", {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token,
|
||||
queryKey: ["admin-leads-stats"],
|
||||
queryFn: () => apiFetch<LeadsStats>("/api/v1/admin/leads/stats?months=12"),
|
||||
});
|
||||
|
||||
const funnelBySource = useQuery({
|
||||
queryKey: ["admin-leads-funnel-source", token],
|
||||
queryKey: ["admin-leads-funnel-source"],
|
||||
queryFn: () =>
|
||||
apiFetch<FunnelSourceRow[]>(
|
||||
"/api/v1/admin/leads/funnel/by-source?months=12",
|
||||
{
|
||||
headers: { "X-Admin-Token": token },
|
||||
},
|
||||
),
|
||||
enabled: !!token,
|
||||
});
|
||||
|
||||
const funnelMonthly = useQuery({
|
||||
queryKey: ["admin-leads-funnel-monthly", token],
|
||||
queryKey: ["admin-leads-funnel-monthly"],
|
||||
queryFn: () =>
|
||||
apiFetch<FunnelMonthRow[]>(
|
||||
"/api/v1/admin/leads/funnel/monthly?months=24",
|
||||
{
|
||||
headers: { "X-Admin-Token": token },
|
||||
},
|
||||
),
|
||||
enabled: !!token,
|
||||
});
|
||||
|
||||
const sankeyOption = useMemo(() => {
|
||||
|
|
@ -239,18 +222,13 @@ export default function AdminLeadsPage() {
|
|||
}, [funnelMonthly.data]);
|
||||
|
||||
const sources = useQuery({
|
||||
queryKey: ["admin-leads-sources", token],
|
||||
queryFn: () =>
|
||||
apiFetch<string[]>("/api/v1/admin/leads/sources", {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token,
|
||||
queryKey: ["admin-leads-sources"],
|
||||
queryFn: () => apiFetch<string[]>("/api/v1/admin/leads/sources"),
|
||||
});
|
||||
|
||||
const leads = useQuery({
|
||||
queryKey: [
|
||||
"admin-leads",
|
||||
token,
|
||||
statusFilter,
|
||||
sourceFilter,
|
||||
convertedFilter,
|
||||
|
|
@ -265,11 +243,8 @@ export default function AdminLeadsPage() {
|
|||
if (sourceFilter) qs.set("source", sourceFilter);
|
||||
if (convertedFilter !== "") qs.set("converted", convertedFilter);
|
||||
if (search) qs.set("search", search);
|
||||
return apiFetch<LeadsResponse>(`/api/v1/admin/leads/?${qs.toString()}`, {
|
||||
headers: { "X-Admin-Token": token },
|
||||
});
|
||||
return apiFetch<LeadsResponse>(`/api/v1/admin/leads/?${qs.toString()}`);
|
||||
},
|
||||
enabled: !!token,
|
||||
});
|
||||
|
||||
const fmtMoney = (v: number | null) =>
|
||||
|
|
@ -279,18 +254,6 @@ export default function AdminLeadsPage() {
|
|||
? `${(v / 1e6).toFixed(2)} М ₽`
|
||||
: v.toLocaleString("ru");
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<p style={{ color: "#9a6700", fontSize: 13 }}>
|
||||
Введите Admin Token на странице{" "}
|
||||
<Link href="/admin/scrape" style={{ color: "#1d4ed8" }}>
|
||||
Скрапер
|
||||
</Link>{" "}
|
||||
— он сохранится для всех админ-страниц.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (stats.isError || leads.isError) {
|
||||
const err = String(stats.error ?? leads.error ?? "");
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useState } from "react";
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { cardStyle, inputStyle, labelStyle, td, th } from "@/lib/adminStyles";
|
||||
import { cardStyle, labelStyle, td, th } from "@/lib/adminStyles";
|
||||
import { BulkGeoPanel } from "@/components/admin/BulkGeoPanel";
|
||||
import { JobSettingsPanel } from "@/components/admin/JobSettingsPanel";
|
||||
|
||||
|
|
@ -67,46 +67,31 @@ function diffSecs(a: string | null, b: string | null): string {
|
|||
}
|
||||
|
||||
export default function ScrapeAllAdminPage() {
|
||||
const [token, setToken] = useState<string>(() =>
|
||||
typeof window === "undefined"
|
||||
? ""
|
||||
: (localStorage.getItem("admin_token") ?? ""),
|
||||
);
|
||||
const [filter, setFilter] = useState<
|
||||
"all" | "kn" | "nspd" | "objective" | "nspd_geo"
|
||||
>("all");
|
||||
const [logsRunId, setLogsRunId] = useState<number | "">("");
|
||||
|
||||
const saveToken = (v: string) => {
|
||||
setToken(v);
|
||||
localStorage.setItem("admin_token", v);
|
||||
};
|
||||
|
||||
const runs = useQuery({
|
||||
queryKey: ["unified-runs", token, filter],
|
||||
queryKey: ["unified-runs", filter],
|
||||
queryFn: () => {
|
||||
const q =
|
||||
filter !== "all" ? `?scraper_type=${filter}&limit=30` : "?limit=30";
|
||||
return apiFetch<UnifiedRunRow[]>(`/api/v1/admin/scrape/all/runs${q}`, {
|
||||
headers: { "X-Admin-Token": token },
|
||||
});
|
||||
return apiFetch<UnifiedRunRow[]>(`/api/v1/admin/scrape/all/runs${q}`);
|
||||
},
|
||||
enabled: !!token,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const logs = useQuery({
|
||||
queryKey: ["unified-logs", token, filter, logsRunId],
|
||||
queryKey: ["unified-logs", filter, logsRunId],
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams({ limit: "100" });
|
||||
if (filter !== "all") params.set("scraper_type", filter);
|
||||
if (logsRunId !== "") params.set("run_id", String(logsRunId));
|
||||
return apiFetch<UnifiedLogRow[]>(
|
||||
`/api/v1/admin/scrape/all/logs?${params}`,
|
||||
{ headers: { "X-Admin-Token": token } },
|
||||
);
|
||||
},
|
||||
enabled: !!token,
|
||||
refetchInterval: 8000,
|
||||
});
|
||||
|
||||
|
|
@ -146,19 +131,6 @@ export default function ScrapeAllAdminPage() {
|
|||
аудита.
|
||||
</p>
|
||||
|
||||
<section style={cardStyle}>
|
||||
<label style={{ display: "block" }}>
|
||||
<span style={labelStyle}>Admin Token</span>
|
||||
<input
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(e) => saveToken(e.target.value)}
|
||||
placeholder="SCRAPE_ADMIN_TOKEN"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{/* Сводка-плитки */}
|
||||
<section
|
||||
style={{
|
||||
|
|
@ -356,12 +328,12 @@ export default function ScrapeAllAdminPage() {
|
|||
|
||||
{/* Job Settings */}
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<JobSettingsPanel token={token} />
|
||||
<JobSettingsPanel />
|
||||
</div>
|
||||
|
||||
{/* Bulk geo backfill */}
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<BulkGeoPanel token={token} />
|
||||
<BulkGeoPanel />
|
||||
</div>
|
||||
|
||||
{/* Logs */}
|
||||
|
|
|
|||
|
|
@ -122,25 +122,14 @@ function reqPerSec(job: CadastreJob): string {
|
|||
// Query keys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const jobsKey = (token: string) => ["admin", "cadastre", "jobs", token];
|
||||
const jobKey = (token: string, id: number) => [
|
||||
"admin",
|
||||
"cadastre",
|
||||
"job",
|
||||
id,
|
||||
token,
|
||||
];
|
||||
const jobsKey = () => ["admin", "cadastre", "jobs"];
|
||||
const jobKey = (id: number) => ["admin", "cadastre", "job", id];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function CadastreScrapeAdminPage() {
|
||||
const [token, setToken] = useState<string>(() =>
|
||||
typeof window === "undefined"
|
||||
? ""
|
||||
: (localStorage.getItem("admin_token") ?? ""),
|
||||
);
|
||||
const [scope, setScope] = useState<ScopeOption>("pilot");
|
||||
const [name, setName] = useState("");
|
||||
const [quartersText, setQuartersText] = useState("");
|
||||
|
|
@ -152,35 +141,23 @@ export default function CadastreScrapeAdminPage() {
|
|||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const saveToken = (v: string) => {
|
||||
setToken(v);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("admin_token", v);
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Queries
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const jobs = useQuery({
|
||||
queryKey: jobsKey(token),
|
||||
queryKey: jobsKey(),
|
||||
queryFn: () =>
|
||||
apiFetch<CadastreJob[]>("/api/v1/admin/cadastre/jobs?limit=30", {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token,
|
||||
apiFetch<CadastreJob[]>("/api/v1/admin/cadastre/jobs?limit=30"),
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
// Per-job detail (only fetched when row is expanded)
|
||||
const expandedJob = useQuery({
|
||||
queryKey: jobKey(token, expandedId ?? 0),
|
||||
queryKey: jobKey(expandedId ?? 0),
|
||||
queryFn: () =>
|
||||
apiFetch<CadastreJob>(`/api/v1/admin/cadastre/jobs/${expandedId}`, {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token && expandedId !== null,
|
||||
apiFetch<CadastreJob>(`/api/v1/admin/cadastre/jobs/${expandedId}`),
|
||||
enabled: expandedId !== null,
|
||||
refetchInterval: expandedId !== null ? 5000 : false,
|
||||
});
|
||||
|
||||
|
|
@ -204,17 +181,13 @@ export default function CadastreScrapeAdminPage() {
|
|||
"/api/v1/admin/cadastre/jobs",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Admin-Token": token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setLastResp(data);
|
||||
void queryClient.invalidateQueries({ queryKey: jobsKey(token) });
|
||||
void queryClient.invalidateQueries({ queryKey: jobsKey() });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -222,11 +195,11 @@ export default function CadastreScrapeAdminPage() {
|
|||
mutationFn: (id: number) =>
|
||||
apiFetch<{ cancelled: boolean }>(
|
||||
`/api/v1/admin/cadastre/jobs/${id}/cancel`,
|
||||
{ method: "POST", headers: { "X-Admin-Token": token } },
|
||||
{ method: "POST" },
|
||||
),
|
||||
onSuccess: (_data, id) => {
|
||||
void queryClient.invalidateQueries({ queryKey: jobKey(token, id) });
|
||||
void queryClient.invalidateQueries({ queryKey: jobsKey(token) });
|
||||
void queryClient.invalidateQueries({ queryKey: jobKey(id) });
|
||||
void queryClient.invalidateQueries({ queryKey: jobsKey() });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -234,11 +207,11 @@ export default function CadastreScrapeAdminPage() {
|
|||
mutationFn: (id: number) =>
|
||||
apiFetch<{ resumed: boolean }>(
|
||||
`/api/v1/admin/cadastre/jobs/${id}/resume`,
|
||||
{ method: "POST", headers: { "X-Admin-Token": token } },
|
||||
{ method: "POST" },
|
||||
),
|
||||
onSuccess: (_data, id) => {
|
||||
void queryClient.invalidateQueries({ queryKey: jobKey(token, id) });
|
||||
void queryClient.invalidateQueries({ queryKey: jobsKey(token) });
|
||||
void queryClient.invalidateQueries({ queryKey: jobKey(id) });
|
||||
void queryClient.invalidateQueries({ queryKey: jobsKey() });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -248,7 +221,6 @@ export default function CadastreScrapeAdminPage() {
|
|||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!token) return;
|
||||
if (scope === "manual_list" && !quartersText.trim()) {
|
||||
alert("Для Manual list укажите хотя бы один квартал");
|
||||
return;
|
||||
|
|
@ -662,22 +634,8 @@ export default function CadastreScrapeAdminPage() {
|
|||
незавершённых targets.
|
||||
</p>
|
||||
|
||||
{/* Admin Token */}
|
||||
<section style={cardStyle}>
|
||||
<label style={{ display: "block" }}>
|
||||
<span style={labelStyle}>Admin Token</span>
|
||||
<input
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(e) => saveToken(e.target.value)}
|
||||
placeholder="SCRAPE_ADMIN_TOKEN"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{/* Create job form */}
|
||||
<section style={{ ...cardStyle, marginTop: 16 }}>
|
||||
<section style={cardStyle}>
|
||||
<h3 style={sectionTitle}>Запуск bulk harvest</h3>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ display: "grid", gap: 12 }}>
|
||||
|
|
@ -760,7 +718,7 @@ export default function CadastreScrapeAdminPage() {
|
|||
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createJob.isPending || !token}
|
||||
disabled={createJob.isPending}
|
||||
style={primaryBtn}
|
||||
>
|
||||
{createJob.isPending ? "Создание…" : "Запустить"}
|
||||
|
|
@ -787,11 +745,7 @@ export default function CadastreScrapeAdminPage() {
|
|||
{/* Jobs table */}
|
||||
<section style={{ ...cardStyle, marginTop: 16 }}>
|
||||
<h3 style={sectionTitle}>Cadastre jobs (top 30, refresh 5 с)</h3>
|
||||
{!token ? (
|
||||
<p style={{ color: "#5b6066", fontSize: 13 }}>
|
||||
Введите Admin Token чтобы видеть список jobs.
|
||||
</p>
|
||||
) : jobs.isLoading ? (
|
||||
{jobs.isLoading ? (
|
||||
<p style={{ color: "#5b6066" }}>Загрузка…</p>
|
||||
) : jobs.isError ? (
|
||||
<p style={{ color: "#b3261e" }}>
|
||||
|
|
|
|||
|
|
@ -58,11 +58,6 @@ function diffSecs(a: string | null, b: string | null): string {
|
|||
}
|
||||
|
||||
export default function GeoScrapeAdminPage() {
|
||||
const [token, setToken] = useState<string>(() =>
|
||||
typeof window === "undefined"
|
||||
? ""
|
||||
: (localStorage.getItem("admin_token") ?? ""),
|
||||
);
|
||||
const [name, setName] = useState("");
|
||||
const [sourceKind, setSourceKind] = useState<
|
||||
"manual_list" | "rosreestr_pending"
|
||||
|
|
@ -89,18 +84,9 @@ export default function GeoScrapeAdminPage() {
|
|||
const [lastResp, setLastResp] = useState<EnqueueResp | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const saveToken = (v: string) => {
|
||||
setToken(v);
|
||||
localStorage.setItem("admin_token", v);
|
||||
};
|
||||
|
||||
const jobs = useQuery({
|
||||
queryKey: ["geo-jobs", token],
|
||||
queryFn: () =>
|
||||
apiFetch<GeoJob[]>("/api/v1/admin/scrape/geo/jobs?limit=30", {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token,
|
||||
queryKey: ["geo-jobs"],
|
||||
queryFn: () => apiFetch<GeoJob[]>("/api/v1/admin/scrape/geo/jobs?limit=30"),
|
||||
refetchInterval: 4000,
|
||||
});
|
||||
|
||||
|
|
@ -122,7 +108,6 @@ export default function GeoScrapeAdminPage() {
|
|||
: null;
|
||||
return apiFetch<EnqueueResp>("/api/v1/admin/scrape/geo/jobs", {
|
||||
method: "POST",
|
||||
headers: { "X-Admin-Token": token, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: name || null,
|
||||
job_kind: jobKind,
|
||||
|
|
@ -144,7 +129,7 @@ export default function GeoScrapeAdminPage() {
|
|||
mutationFn: (id: number) =>
|
||||
apiFetch<{ cancelled: boolean }>(
|
||||
`/api/v1/admin/scrape/geo/jobs/${id}/cancel`,
|
||||
{ method: "POST", headers: { "X-Admin-Token": token } },
|
||||
{ method: "POST" },
|
||||
),
|
||||
onSuccess: () =>
|
||||
void queryClient.invalidateQueries({ queryKey: ["geo-jobs"] }),
|
||||
|
|
@ -154,7 +139,7 @@ export default function GeoScrapeAdminPage() {
|
|||
mutationFn: (id: number) =>
|
||||
apiFetch<{ resumed: boolean }>(
|
||||
`/api/v1/admin/scrape/geo/jobs/${id}/resume`,
|
||||
{ method: "POST", headers: { "X-Admin-Token": token } },
|
||||
{ method: "POST" },
|
||||
),
|
||||
onSuccess: () =>
|
||||
void queryClient.invalidateQueries({ queryKey: ["geo-jobs"] }),
|
||||
|
|
@ -173,21 +158,8 @@ export default function GeoScrapeAdminPage() {
|
|||
продолжит с pending targets.
|
||||
</p>
|
||||
|
||||
<section style={cardStyle}>
|
||||
<label style={{ display: "block" }}>
|
||||
<span style={labelStyle}>Admin Token</span>
|
||||
<input
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(e) => saveToken(e.target.value)}
|
||||
placeholder="SCRAPE_ADMIN_TOKEN"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{/* Форма создания нового job */}
|
||||
<section style={{ ...cardStyle, marginTop: 16 }}>
|
||||
<section style={cardStyle}>
|
||||
<h3 style={sectionTitle}>🚀 Новый bulk geo-job</h3>
|
||||
<div style={{ display: "grid", gap: 12 }}>
|
||||
<label>
|
||||
|
|
@ -331,7 +303,7 @@ export default function GeoScrapeAdminPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => enqueue.mutate()}
|
||||
disabled={enqueue.isPending || !token}
|
||||
disabled={enqueue.isPending}
|
||||
style={primaryBtn}
|
||||
>
|
||||
{enqueue.isPending ? "Создание…" : "🚀 Запустить bulk-job"}
|
||||
|
|
@ -542,7 +514,6 @@ export default function GeoScrapeAdminPage() {
|
|||
</section>
|
||||
|
||||
<ScrapeLogsPanel
|
||||
token={token}
|
||||
scraperType="nspd_geo"
|
||||
title="Журнал NSPD geo (per-target)"
|
||||
subtitle="из nspd_geo_log · обновляется каждые 3 сек"
|
||||
|
|
|
|||
|
|
@ -94,11 +94,6 @@ function diffSecs(a: string | null, b: string | null): string {
|
|||
}
|
||||
|
||||
export default function ObjectiveEtlAdminPage() {
|
||||
const [token, setToken] = useState<string>(() =>
|
||||
typeof window === "undefined"
|
||||
? ""
|
||||
: (localStorage.getItem("admin_token") ?? ""),
|
||||
);
|
||||
const [overrideSqlitePath, setOverrideSqlitePath] = useState("");
|
||||
const [lastTaskId, setLastTaskId] = useState<string | null>(null);
|
||||
const [syncGroupsCsv, setSyncGroupsCsv] = useState("");
|
||||
|
|
@ -108,39 +103,26 @@ export default function ObjectiveEtlAdminPage() {
|
|||
const [showEtl, setShowEtl] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const saveToken = (v: string) => {
|
||||
setToken(v);
|
||||
localStorage.setItem("admin_token", v);
|
||||
};
|
||||
|
||||
const coverage = useQuery({
|
||||
queryKey: ["objective-coverage", token],
|
||||
queryKey: ["objective-coverage"],
|
||||
queryFn: () =>
|
||||
apiFetch<ObjectiveCoverage>("/api/v1/admin/scrape/objective/coverage", {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token,
|
||||
apiFetch<ObjectiveCoverage>("/api/v1/admin/scrape/objective/coverage"),
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const runs = useQuery({
|
||||
queryKey: ["objective-runs", token],
|
||||
queryKey: ["objective-runs"],
|
||||
queryFn: () =>
|
||||
apiFetch<ObjectiveRunRow[]>(
|
||||
"/api/v1/admin/scrape/objective/runs?limit=20",
|
||||
{ headers: { "X-Admin-Token": token } },
|
||||
),
|
||||
enabled: !!token,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const cfg = useQuery({
|
||||
queryKey: ["objective-config", token],
|
||||
queryKey: ["objective-config"],
|
||||
queryFn: () =>
|
||||
apiFetch<ObjectiveConfig>("/api/v1/admin/scrape/objective/config", {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token,
|
||||
apiFetch<ObjectiveConfig>("/api/v1/admin/scrape/objective/config"),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
|
|
@ -155,10 +137,6 @@ export default function ObjectiveEtlAdminPage() {
|
|||
mutationFn: () =>
|
||||
apiFetch<ObjectiveTriggerResp>("/api/v1/admin/scrape/objective", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Admin-Token": token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ sqlite_path: overrideSqlitePath || null }),
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
|
|
@ -178,10 +156,6 @@ export default function ObjectiveEtlAdminPage() {
|
|||
"/api/v1/admin/scrape/objective/sync-our",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Admin-Token": token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ groups: groups.length ? groups : null }),
|
||||
},
|
||||
);
|
||||
|
|
@ -196,10 +170,6 @@ export default function ObjectiveEtlAdminPage() {
|
|||
mutationFn: (patch: Partial<ObjectiveConfig>) =>
|
||||
apiFetch<ObjectiveConfig>("/api/v1/admin/scrape/objective/config", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Admin-Token": token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(patch),
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
|
|
@ -223,22 +193,8 @@ export default function ObjectiveEtlAdminPage() {
|
|||
только как bootstrap-fallback (свёрнут ниже).
|
||||
</p>
|
||||
|
||||
{/* Token */}
|
||||
<section style={cardStyle}>
|
||||
<label style={{ display: "block" }}>
|
||||
<span style={labelStyle}>Admin Token (X-Admin-Token)</span>
|
||||
<input
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(e) => saveToken(e.target.value)}
|
||||
placeholder="SCRAPE_ADMIN_TOKEN — сохранится в localStorage"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{/* Coverage */}
|
||||
<section style={{ ...cardStyle, marginTop: 16 }}>
|
||||
<section style={cardStyle}>
|
||||
<h3 style={sectionTitle}>📦 Состояние PG (источник истины)</h3>
|
||||
{coverage.isLoading && (
|
||||
<p style={{ color: "#5b6066", fontSize: 13 }}>Загрузка…</p>
|
||||
|
|
@ -504,7 +460,7 @@ export default function ObjectiveEtlAdminPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => saveCfg.mutate(cfgDraft)}
|
||||
disabled={saveCfg.isPending || !token}
|
||||
disabled={saveCfg.isPending}
|
||||
style={primaryBtn}
|
||||
>
|
||||
{saveCfg.isPending ? "Сохранение…" : "💾 Сохранить настройки"}
|
||||
|
|
@ -580,7 +536,7 @@ export default function ObjectiveEtlAdminPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => syncOur.mutate()}
|
||||
disabled={syncOur.isPending || !token}
|
||||
disabled={syncOur.isPending}
|
||||
style={primaryBtn}
|
||||
>
|
||||
{syncOur.isPending ? "Запуск…" : "🚀 Запустить наш sync"}
|
||||
|
|
@ -642,7 +598,7 @@ export default function ObjectiveEtlAdminPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => trigger.mutate()}
|
||||
disabled={trigger.isPending || !token}
|
||||
disabled={trigger.isPending}
|
||||
style={secondaryBtn}
|
||||
>
|
||||
{trigger.isPending ? "Запуск…" : "Bootstrap ETL"}
|
||||
|
|
@ -799,7 +755,6 @@ export default function ObjectiveEtlAdminPage() {
|
|||
</section>
|
||||
|
||||
<ScrapeLogsPanel
|
||||
token={token}
|
||||
scraperType="objective"
|
||||
title="Журнал Objective ETL"
|
||||
subtitle="фильтр scraper_type=objective · обновляется каждые 3 сек"
|
||||
|
|
|
|||
|
|
@ -26,13 +26,6 @@ interface TriggerResp {
|
|||
developers: string[] | null;
|
||||
}
|
||||
|
||||
interface CatalogTriggerResp {
|
||||
task_id: string;
|
||||
region_code: number;
|
||||
max_objects: number | null;
|
||||
force: boolean;
|
||||
}
|
||||
|
||||
interface QueueTask {
|
||||
worker: string;
|
||||
task_id: string;
|
||||
|
|
@ -74,30 +67,20 @@ interface FailureRow {
|
|||
}
|
||||
|
||||
export default function ScrapeAdminPage() {
|
||||
const [token, setToken] = useState<string>(() =>
|
||||
typeof window === "undefined"
|
||||
? ""
|
||||
: (localStorage.getItem("admin_token") ?? ""),
|
||||
);
|
||||
const [region, setRegion] = useState(66);
|
||||
const [devs, setDevs] = useState("");
|
||||
const [fetchFlats, setFetchFlats] = useState(true);
|
||||
const [extras, setExtras] = useState(true);
|
||||
const [downloadPhotos, setDownloadPhotos] = useState(false);
|
||||
const [forceRun, setForceRun] = useState(false);
|
||||
const [catalogMaxObjects, setCatalogMaxObjects] = useState<number | "">("");
|
||||
const [showFailures, setShowFailures] = useState(false);
|
||||
const [failuresRunId, setFailuresRunId] = useState<number | "">("");
|
||||
const [logsRunId, setLogsRunId] = useState<number | "">("");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const runs = useQuery({
|
||||
queryKey: ["scrape-runs", token],
|
||||
queryFn: () =>
|
||||
apiFetch<RunRow[]>("/api/v1/admin/scrape/runs?limit=20", {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token,
|
||||
queryKey: ["scrape-runs"],
|
||||
queryFn: () => apiFetch<RunRow[]>("/api/v1/admin/scrape/runs?limit=20"),
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
|
|
@ -105,7 +88,6 @@ export default function ScrapeAdminPage() {
|
|||
mutationFn: () =>
|
||||
apiFetch<TriggerResp>("/api/v1/admin/scrape/kn", {
|
||||
method: "POST",
|
||||
headers: { "X-Admin-Token": token },
|
||||
body: JSON.stringify({
|
||||
region_code: region,
|
||||
developers: devs
|
||||
|
|
@ -129,27 +111,6 @@ export default function ScrapeAdminPage() {
|
|||
},
|
||||
});
|
||||
|
||||
const catalogTrigger = useMutation({
|
||||
mutationFn: (force: boolean) =>
|
||||
apiFetch<CatalogTriggerResp>("/api/v1/admin/scrape/kn-catalog-objects", {
|
||||
method: "POST",
|
||||
headers: { "X-Admin-Token": token },
|
||||
body: JSON.stringify({
|
||||
region_code: region,
|
||||
max_objects: catalogMaxObjects === "" ? null : catalogMaxObjects,
|
||||
force,
|
||||
}),
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
console.log("[scrape] catalog queued:", data);
|
||||
queryClient.invalidateQueries({ queryKey: ["scrape-runs"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["scrape-queue"] });
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error("[scrape] catalog failed:", err);
|
||||
},
|
||||
});
|
||||
|
||||
const releaseLock = useMutation({
|
||||
mutationFn: () =>
|
||||
apiFetch<{
|
||||
|
|
@ -158,7 +119,6 @@ export default function ScrapeAdminPage() {
|
|||
released: boolean;
|
||||
}>("/api/v1/admin/scrape/release-lock", {
|
||||
method: "POST",
|
||||
headers: { "X-Admin-Token": token },
|
||||
body: JSON.stringify({
|
||||
region_code: region,
|
||||
developers: devs
|
||||
|
|
@ -180,12 +140,8 @@ export default function ScrapeAdminPage() {
|
|||
});
|
||||
|
||||
const queue = useQuery({
|
||||
queryKey: ["scrape-queue", token],
|
||||
queryFn: () =>
|
||||
apiFetch<QueueStatus>("/api/v1/admin/scrape/queue", {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token,
|
||||
queryKey: ["scrape-queue"],
|
||||
queryFn: () => apiFetch<QueueStatus>("/api/v1/admin/scrape/queue"),
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
|
||||
|
|
@ -201,7 +157,6 @@ export default function ScrapeAdminPage() {
|
|||
"/api/v1/admin/scrape/revoke",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "X-Admin-Token": token },
|
||||
body: JSON.stringify({ task_id, terminate }),
|
||||
},
|
||||
),
|
||||
|
|
@ -232,30 +187,25 @@ export default function ScrapeAdminPage() {
|
|||
};
|
||||
|
||||
const logs = useQuery({
|
||||
queryKey: ["scrape-logs", logsRunId, token],
|
||||
queryKey: ["scrape-logs", logsRunId],
|
||||
queryFn: () => {
|
||||
const q =
|
||||
logsRunId !== "" ? `?run_id=${logsRunId}&limit=200` : "?limit=200";
|
||||
return apiFetch<LogRow[]>(`/api/v1/admin/scrape/logs${q}`, {
|
||||
headers: { "X-Admin-Token": token },
|
||||
});
|
||||
return apiFetch<LogRow[]>(`/api/v1/admin/scrape/logs${q}`);
|
||||
},
|
||||
enabled: !!token,
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
|
||||
const failures = useQuery({
|
||||
queryKey: ["scrape-failures", failuresRunId, token],
|
||||
queryKey: ["scrape-failures", failuresRunId],
|
||||
queryFn: () => {
|
||||
const q =
|
||||
failuresRunId !== ""
|
||||
? `?run_id=${failuresRunId}&limit=100`
|
||||
: "?limit=100";
|
||||
return apiFetch<FailureRow[]>(`/api/v1/admin/scrape/failures${q}`, {
|
||||
headers: { "X-Admin-Token": token },
|
||||
});
|
||||
return apiFetch<FailureRow[]>(`/api/v1/admin/scrape/failures${q}`);
|
||||
},
|
||||
enabled: !!token && showFailures,
|
||||
enabled: showFailures,
|
||||
refetchInterval: showFailures ? 5000 : false,
|
||||
});
|
||||
|
||||
|
|
@ -265,11 +215,6 @@ export default function ScrapeAdminPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const saveToken = (v: string) => {
|
||||
setToken(v);
|
||||
localStorage.setItem("admin_token", v);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 style={{ marginTop: 0, fontSize: 18 }}>
|
||||
|
|
@ -281,16 +226,6 @@ export default function ScrapeAdminPage() {
|
|||
</p>
|
||||
|
||||
<section style={cardStyle}>
|
||||
<label style={{ display: "block", marginBottom: 12 }}>
|
||||
<span style={labelStyle}>Admin Token</span>
|
||||
<input
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(e) => saveToken(e.target.value)}
|
||||
placeholder="SCRAPE_ADMIN_TOKEN"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
<div style={{ display: "flex", gap: 16, flexWrap: "wrap" }}>
|
||||
<label>
|
||||
<span style={labelStyle}>Регион</span>
|
||||
|
|
@ -382,9 +317,8 @@ export default function ScrapeAdminPage() {
|
|||
>
|
||||
<button
|
||||
onClick={() => trigger.mutate()}
|
||||
disabled={!token || trigger.isPending}
|
||||
disabled={trigger.isPending}
|
||||
style={primaryBtn}
|
||||
title={!token ? "Введите Admin Token выше" : undefined}
|
||||
>
|
||||
{trigger.isPending ? "Запускаю…" : "Запустить сейчас"}
|
||||
</button>
|
||||
|
|
@ -398,24 +332,11 @@ export default function ScrapeAdminPage() {
|
|||
releaseLock.mutate();
|
||||
}
|
||||
}}
|
||||
disabled={!token || releaseLock.isPending}
|
||||
disabled={releaseLock.isPending}
|
||||
style={secondaryBtn}
|
||||
title="Удалить scrape:kn:lock:<region>:<devs> в Redis"
|
||||
>
|
||||
{releaseLock.isPending ? "Снимаю…" : "🔓 Снять Redis-lock"}
|
||||
</button>
|
||||
{!token ? (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 12,
|
||||
color: "#9a6700",
|
||||
fontSize: 13,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
>
|
||||
← введите Admin Token выше
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{releaseLock.isSuccess ? (
|
||||
<div
|
||||
|
|
@ -487,118 +408,6 @@ export default function ScrapeAdminPage() {
|
|||
) : null}
|
||||
</section>
|
||||
|
||||
<section style={{ ...cardStyle, marginTop: 16 }}>
|
||||
<h2 style={{ margin: 0, fontSize: 16 }}>📦 Catalog-object (SSR)</h2>
|
||||
<p style={{ color: "#5b6066", fontSize: 13, marginTop: 6 }}>
|
||||
Заполняет wall_type, energy_eff, ceiling_height_m, parking_*,
|
||||
playground_*, scores из публичной страницы каталога DOM.РФ. По
|
||||
умолчанию пропускает объекты, обновлённые сегодня.
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
alignItems: "flex-end",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
<span style={labelStyle}>max_objects (пусто = 300)</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={2000}
|
||||
value={catalogMaxObjects}
|
||||
onChange={(e) =>
|
||||
setCatalogMaxObjects(
|
||||
e.target.value === "" ? "" : Number(e.target.value),
|
||||
)
|
||||
}
|
||||
placeholder="300"
|
||||
style={{ ...inputStyle, width: 140 }}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => catalogTrigger.mutate(false)}
|
||||
disabled={!token || catalogTrigger.isPending}
|
||||
style={primaryBtn}
|
||||
title="Грузим только новые + не обновлённые сегодня"
|
||||
>
|
||||
{catalogTrigger.isPending && catalogTrigger.variables === false
|
||||
? "Запускаю…"
|
||||
: "Грузить (skip today)"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (
|
||||
confirm(
|
||||
"Загрузить ВСЕ объекты последнего snapshot (force=true)? Игнорирует skip-today, нагрузка на DOM.РФ выше.",
|
||||
)
|
||||
) {
|
||||
catalogTrigger.mutate(true);
|
||||
}
|
||||
}}
|
||||
disabled={!token || catalogTrigger.isPending}
|
||||
style={{
|
||||
...secondaryBtn,
|
||||
color: "#b3261e",
|
||||
borderColor: "#fecaca",
|
||||
}}
|
||||
title="Force: грузит все объекты независимо от catalog_scraped_at"
|
||||
>
|
||||
{catalogTrigger.isPending && catalogTrigger.variables === true
|
||||
? "Запускаю…"
|
||||
: "⚡ Загрузить ВСЕ (force)"}
|
||||
</button>
|
||||
</div>
|
||||
{catalogTrigger.isError ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: 12,
|
||||
background: "#fef2f2",
|
||||
border: "1px solid #fecaca",
|
||||
borderRadius: 6,
|
||||
color: "#991b1b",
|
||||
fontSize: 13,
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
<strong>Ошибка:</strong> {String(catalogTrigger.error)}
|
||||
</div>
|
||||
) : null}
|
||||
{catalogTrigger.isSuccess ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: 12,
|
||||
background: "#f0fdf4",
|
||||
border: "1px solid #bbf7d0",
|
||||
borderRadius: 6,
|
||||
color: "#065f46",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<strong>✓ Catalog-object queued.</strong> task_id={" "}
|
||||
<code
|
||||
style={{
|
||||
background: "#dcfce7",
|
||||
padding: "2px 6px",
|
||||
borderRadius: 3,
|
||||
}}
|
||||
>
|
||||
{catalogTrigger.data.task_id}
|
||||
</code>{" "}
|
||||
· регион {catalogTrigger.data.region_code} · force={" "}
|
||||
{String(catalogTrigger.data.force)} · max_objects={" "}
|
||||
{catalogTrigger.data.max_objects ?? "default"}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section style={{ ...cardStyle, marginTop: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -620,11 +429,7 @@ export default function ScrapeAdminPage() {
|
|||
) : null}
|
||||
</h2>
|
||||
</div>
|
||||
{!token ? (
|
||||
<p style={{ color: "#5b6066", fontSize: 13 }}>
|
||||
Введите Admin Token чтобы видеть состояние очереди.
|
||||
</p>
|
||||
) : queue.isLoading ? (
|
||||
{queue.isLoading ? (
|
||||
<p style={{ color: "#5b6066" }}>Загрузка…</p>
|
||||
) : queue.isError ? (
|
||||
<p style={{ color: "#b3261e" }}>Ошибка: {String(queue.error)}</p>
|
||||
|
|
@ -859,11 +664,7 @@ export default function ScrapeAdminPage() {
|
|||
style={{ ...inputStyle, width: 130 }}
|
||||
/>
|
||||
</div>
|
||||
{!token ? (
|
||||
<p style={{ color: "#5b6066", fontSize: 13 }}>
|
||||
Введите Admin Token чтобы видеть лог.
|
||||
</p>
|
||||
) : logs.isLoading ? (
|
||||
{logs.isLoading ? (
|
||||
<p style={{ color: "#5b6066" }}>Загрузка…</p>
|
||||
) : logs.isError ? (
|
||||
<p style={{ color: "#b3261e" }}>Ошибка: {String(logs.error)}</p>
|
||||
|
|
@ -937,7 +738,6 @@ export default function ScrapeAdminPage() {
|
|||
/>
|
||||
<button
|
||||
onClick={() => setShowFailures((v) => !v)}
|
||||
disabled={!token}
|
||||
style={secondaryBtn}
|
||||
>
|
||||
{showFailures ? "Скрыть" : "Показать"}
|
||||
|
|
@ -1035,11 +835,7 @@ export default function ScrapeAdminPage() {
|
|||
|
||||
<section style={{ ...cardStyle, marginTop: 16 }}>
|
||||
<h2 style={{ marginTop: 0, fontSize: 16 }}>Последние прогоны</h2>
|
||||
{!token ? (
|
||||
<p style={{ color: "#5b6066" }}>
|
||||
Введите admin token чтобы увидеть журнал.
|
||||
</p>
|
||||
) : runs.isLoading ? (
|
||||
{runs.isLoading ? (
|
||||
<p>Загрузка…</p>
|
||||
) : runs.isError ? (
|
||||
<p style={{ color: "#b3261e" }}>Ошибка: {String(runs.error)}</p>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const THEMATIC_OPTIONS = [
|
|||
|
||||
// ── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function BulkGeoPanel({ token }: { token: string }) {
|
||||
export function BulkGeoPanel() {
|
||||
const [parallelism, setParallelism] = useState(5);
|
||||
const [thematicIds, setThematicIds] = useState<number[]>([2]);
|
||||
const [source, setSource] = useState<"rosreestr_pending" | "all_in_region">(
|
||||
|
|
@ -62,7 +62,6 @@ export function BulkGeoPanel({ token }: { token: string }) {
|
|||
};
|
||||
return apiFetch<BulkGeoResponse>("/api/v1/admin/scrape/geo/bulk", {
|
||||
method: "POST",
|
||||
headers: { "X-Admin-Token": token },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
},
|
||||
|
|
@ -179,9 +178,9 @@ export function BulkGeoPanel({ token }: { token: string }) {
|
|||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!token || bulkMutation.isPending}
|
||||
disabled={bulkMutation.isPending}
|
||||
style={
|
||||
!token || bulkMutation.isPending
|
||||
bulkMutation.isPending
|
||||
? { ...triggerBtn, opacity: 0.6 }
|
||||
: triggerBtn
|
||||
}
|
||||
|
|
@ -193,12 +192,6 @@ export function BulkGeoPanel({ token }: { token: string }) {
|
|||
</div>
|
||||
</form>
|
||||
|
||||
{!token && (
|
||||
<p style={{ color: "#a16207", fontSize: 13, marginTop: 12 }}>
|
||||
Введите Admin Token выше для запуска.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{errorMsg && (
|
||||
<div style={errorBox}>
|
||||
<strong>Ошибка:</strong> {errorMsg}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ function formatIso(s: string): string {
|
|||
return new Date(s).toLocaleString("ru-RU");
|
||||
}
|
||||
|
||||
export function JobSettingsPanel({ token }: { token: string }) {
|
||||
export function JobSettingsPanel() {
|
||||
const [drafts, setDrafts] = useState<Record<string, RowDraft>>({});
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
|
@ -82,12 +82,8 @@ export function JobSettingsPanel({ token }: { token: string }) {
|
|||
}, []);
|
||||
|
||||
const settings = useQuery({
|
||||
queryKey: ["job-settings", token],
|
||||
queryFn: () =>
|
||||
apiFetch<JobSettingRead[]>("/api/v1/admin/jobs/settings", {
|
||||
headers: { "X-Admin-Token": token },
|
||||
}),
|
||||
enabled: !!token,
|
||||
queryKey: ["job-settings"],
|
||||
queryFn: () => apiFetch<JobSettingRead[]>("/api/v1/admin/jobs/settings"),
|
||||
});
|
||||
|
||||
// Initialise drafts when settings data first arrives (or new job_types appear).
|
||||
|
|
@ -117,7 +113,6 @@ export function JobSettingsPanel({ token }: { token: string }) {
|
|||
}) =>
|
||||
apiFetch<JobSettingRead>(`/api/v1/admin/jobs/settings/${job_type}`, {
|
||||
method: "PUT",
|
||||
headers: { "X-Admin-Token": token },
|
||||
body: JSON.stringify(update),
|
||||
}),
|
||||
onSuccess: (data: JobSettingRead) => {
|
||||
|
|
@ -238,11 +233,7 @@ export function JobSettingsPanel({ token }: { token: string }) {
|
|||
Централизованные настройки Celery jobs. Изменения применяются с
|
||||
следующего beat-цикла.
|
||||
</p>
|
||||
{!token ? (
|
||||
<p style={{ color: "#5b6066", fontSize: 13 }}>
|
||||
Введите Admin Token выше чтобы загрузить настройки.
|
||||
</p>
|
||||
) : settings.isLoading ? (
|
||||
{settings.isLoading ? (
|
||||
<p style={{ color: "#5b6066" }}>Загрузка…</p>
|
||||
) : settings.isError ? (
|
||||
<p style={{ color: "#b3261e" }}>Ошибка: {String(settings.error)}</p>
|
||||
|
|
@ -483,7 +474,7 @@ export function JobSettingsPanel({ token }: { token: string }) {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => handleSave(row.job_type)}
|
||||
disabled={saving || !token}
|
||||
disabled={saving}
|
||||
style={
|
||||
saving ? { ...saveBtn, opacity: 0.6 } : saveBtn
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ interface LogRow {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
/** Admin token (для X-Admin-Token header). */
|
||||
token: string;
|
||||
/** scraper_type filter — kn / nspd / nspd_geo / objective. */
|
||||
scraperType: "kn" | "nspd" | "nspd_geo" | "objective";
|
||||
/** Имя поля по которому фильтруем run в запросе (по умолчанию `run_id`). */
|
||||
|
|
@ -40,7 +38,6 @@ interface Props {
|
|||
* фильтром по `scraper_type` и опциональным `run_id`. Auto-refetch каждые 3 сек.
|
||||
*/
|
||||
export default function ScrapeLogsPanel({
|
||||
token,
|
||||
scraperType,
|
||||
runIdParam = "run_id",
|
||||
limit = 200,
|
||||
|
|
@ -51,7 +48,7 @@ export default function ScrapeLogsPanel({
|
|||
const [runIdFilter, setRunIdFilter] = useState<number | "">("");
|
||||
|
||||
const logs = useQuery({
|
||||
queryKey: ["scrape-logs", scraperType, runIdFilter, token],
|
||||
queryKey: ["scrape-logs", scraperType, runIdFilter],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({ scraper_type: scraperType });
|
||||
params.set("limit", String(limit));
|
||||
|
|
@ -60,10 +57,8 @@ export default function ScrapeLogsPanel({
|
|||
}
|
||||
return apiFetch<LogRow[]>(
|
||||
`/api/v1/admin/scrape/all/logs?${params.toString()}`,
|
||||
{ headers: { "X-Admin-Token": token } },
|
||||
);
|
||||
},
|
||||
enabled: !!token,
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
|
||||
|
|
@ -109,11 +104,7 @@ export default function ScrapeLogsPanel({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{!token ? (
|
||||
<p style={{ color: "#5b6066", fontSize: 13 }}>
|
||||
Введите Admin Token чтобы видеть лог.
|
||||
</p>
|
||||
) : logs.isLoading ? (
|
||||
{logs.isLoading ? (
|
||||
<p style={{ color: "#5b6066" }}>Загрузка…</p>
|
||||
) : logs.isError ? (
|
||||
<p style={{ color: "#b3261e" }}>Ошибка: {String(logs.error)}</p>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue