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 = {