diff --git a/frontend/src/app/admin/layout.tsx b/frontend/src/app/admin/layout.tsx index 76659f44..8f4bad2c 100644 --- a/frontend/src/app/admin/layout.tsx +++ b/frontend/src/app/admin/layout.tsx @@ -9,7 +9,6 @@ const TABS = [ { href: "/admin/scrape/geo", label: "🗺 NSPD geo (rosreestr2coord)" }, { href: "/admin/scrape/objective", label: "ETL Objective" }, { href: "/admin/leads", label: "Лиды PRINZIP" }, - { href: "/admin/jobs/settings", label: "⚙️ Job Settings" }, ]; export default function AdminLayout({ diff --git a/frontend/src/app/admin/scrape/all/page.tsx b/frontend/src/app/admin/scrape/all/page.tsx index 30e8d5e7..c97150ef 100644 --- a/frontend/src/app/admin/scrape/all/page.tsx +++ b/frontend/src/app/admin/scrape/all/page.tsx @@ -4,6 +4,8 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { apiFetch } from "@/lib/api"; +import { BulkGeoPanel } from "@/components/admin/BulkGeoPanel"; +import { JobSettingsPanel } from "@/components/admin/JobSettingsPanel"; interface UnifiedRunRow { scraper_type: "kn" | "nspd" | "objective" | "nspd_geo"; @@ -351,6 +353,16 @@ export default function ScrapeAllAdminPage() { + {/* Job Settings */} +
+ +
+ + {/* Bulk geo backfill */} +
+ +
+ {/* Logs */}

diff --git a/frontend/src/components/admin/BulkGeoPanel.tsx b/frontend/src/components/admin/BulkGeoPanel.tsx new file mode 100644 index 00000000..527367f6 --- /dev/null +++ b/frontend/src/components/admin/BulkGeoPanel.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; + +import { apiFetch } from "@/lib/api"; + +interface BulkGeoResponse { + job_ids: number[]; + targets_total: number; + parallelism: number; + targets_per_job: number; +} + +interface BulkGeoError { + detail: string; +} + +export function BulkGeoPanel({ token }: { token: string }) { + const [parallelism, setParallelism] = useState(5); + const [result, setResult] = useState(null); + const [errorMsg, setErrorMsg] = useState(null); + + const bulkMutation = useMutation({ + mutationFn: () => + apiFetch("/api/v1/admin/scrape/geo/bulk", { + method: "POST", + headers: { "X-Admin-Token": token }, + body: JSON.stringify({ parallelism, thematic_id: 2 }), + }), + onSuccess: (data) => { + setResult(data); + setErrorMsg(null); + }, + onError: (err: unknown) => { + setResult(null); + const msg = err instanceof Error ? err.message : "Неизвестная ошибка"; + setErrorMsg(msg); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + bulkMutation.mutate(); + }; + + return ( +
+

Bulk Свердловская geo backfill

+

+ Запускает пакетное geo-обогащение участков без координат (thematic_id=2, + регион 66). Разбивает на N параллельных job-ов через{" "} + POST /api/v1/admin/scrape/geo/bulk. +

+ +
+ + + +
+ + {!token && ( +

+ Введите Admin Token выше для запуска. +

+ )} + + {errorMsg && ( +
+ Ошибка: {errorMsg} +
+ )} + + {result && ( +
+
+ Создано job-ов: {result.job_ids.length} +
+
+ Targets: {result.targets_total} участков ÷{" "} + {result.parallelism} воркеров ={" "} + ~{result.targets_per_job} на job +
+
+ job_ids: [{result.job_ids.join(", ")}] +
+
+ )} +
+ ); +} + +// ── Styles ────────────────────────────────────────────────────────────────── + +const cardStyle = { + background: "#fff", + border: "1px solid #e6e8ec", + borderRadius: 12, + padding: 20, +}; + +const sectionTitle = { + margin: "0 0 8px", + fontSize: 16, + fontWeight: 600 as const, +}; + +const labelStyle = { + fontSize: 12, + color: "#5b6066", + textTransform: "uppercase" as const, + letterSpacing: 0.4, +}; + +const numInput = { + padding: "8px 10px", + border: "1px solid #d1d5db", + borderRadius: 6, + fontSize: 14, + width: 80, + boxSizing: "border-box" as const, +}; + +const triggerBtn = { + padding: "8px 18px", + background: "#7c3aed", + color: "#fff", + border: "none", + borderRadius: 6, + fontSize: 14, + fontWeight: 600 as const, + cursor: "pointer", +}; + +const errorBox = { + marginTop: 16, + padding: "12px 16px", + background: "#fef2f2", + border: "1px solid #fecaca", + borderRadius: 8, + color: "#991b1b", + fontSize: 13, +}; + +const successBox = { + marginTop: 16, + padding: "12px 16px", + background: "#f0fdf4", + border: "1px solid #bbf7d0", + borderRadius: 8, + color: "#065f46", +}; diff --git a/frontend/src/app/admin/jobs/settings/page.tsx b/frontend/src/components/admin/JobSettingsPanel.tsx similarity index 89% rename from frontend/src/app/admin/jobs/settings/page.tsx rename to frontend/src/components/admin/JobSettingsPanel.tsx index fe8afafd..dfa77549 100644 --- a/frontend/src/app/admin/jobs/settings/page.tsx +++ b/frontend/src/components/admin/JobSettingsPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiFetch } from "@/lib/api"; @@ -42,7 +42,6 @@ interface RowDraft { extra_config_error: string | null; } -// Toast state type interface Toast { id: number; message: string; @@ -65,27 +64,13 @@ function formatIso(s: string): string { return new Date(s).toLocaleString("ru-RU"); } -export default function JobSettingsPage() { - const [token, setToken] = useState(() => - typeof window === "undefined" - ? "" - : (localStorage.getItem("admin_token") ?? ""), - ); - // Per-row drafts keyed by job_type +export function JobSettingsPanel({ token }: { token: string }) { const [drafts, setDrafts] = useState>({}); - // Which rows have the extra_config textarea expanded const [expanded, setExpanded] = useState>({}); const [toasts, setToasts] = useState([]); const toastSeqRef = useRef(0); const queryClient = useQueryClient(); - const saveToken = (v: string) => { - setToken(v); - if (typeof window !== "undefined") { - localStorage.setItem("admin_token", v); - } - }; - const addToast = useCallback((message: string, kind: "success" | "error") => { const id = ++toastSeqRef.current; setToasts((prev: Toast[]) => [...prev, { id, message, kind }]); @@ -106,7 +91,7 @@ export default function JobSettingsPage() { }); // Initialise drafts when settings data first arrives (or new job_types appear). - // Using useEffect for syncing external data → local draft state, per codebase pattern. + // Using useEffect for syncing external data -> local draft state. useEffect(() => { if (!settings.data) return; setDrafts((prev: Record) => { @@ -165,7 +150,6 @@ export default function JobSettingsPage() { const draft = drafts[job_type]; if (!draft) return; - // Parse extra_config JSON only when the textarea is expanded let extra_config: Record | undefined; if (expanded[job_type]) { try { @@ -248,37 +232,12 @@ export default function JobSettingsPage() { ))} -

Job Settings

-

- Централизованные настройки Celery jobs. Изменения применяются с - следующего beat-цикла. Manual trigger остаётся в{" "} - - /admin/scrape - - . -

- - {/* Token */}
- -
- - {/* Table */} -
+

Настройки jobs

+

+ Централизованные настройки Celery jobs. Изменения применяются с + следующего beat-цикла. +

{!token ? (

Введите Admin Token выше чтобы загрузить настройки. @@ -373,7 +332,7 @@ export default function JobSettingsPage() { /> - {/* queue_name — read-only (infra config) */} + {/* queue_name — read-only */} {row.queue_name} @@ -553,22 +512,10 @@ const cardStyle = { padding: 20, }; -const labelStyle = { - display: "block", - fontSize: 12, - color: "#5b6066", - marginBottom: 4, - textTransform: "uppercase" as const, - letterSpacing: 0.4, -}; - -const inputStyle = { - padding: "8px 10px", - border: "1px solid #d1d5db", - borderRadius: 6, - fontSize: 14, - width: "100%", - boxSizing: "border-box" as const, +const sectionTitle = { + margin: "0 0 8px", + fontSize: 16, + fontWeight: 600 as const, }; const compactInput = {