"use client" import { useCallback, useEffect, useRef, useState } from "react" import type { GeoipStatusDto } from "@mmapp/contracts/geoip" import { can } from "@/lib/auth" import { getGeoipStatus, putGeoipSettings, runGeoipUpdateNow } from "@/shared/api/geoip" import { OpsPanel } from "@/components/ops-panel" import { FormField, FormToggle } from "@/components/form-kit" import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert" import { Badge } from "@/components/reui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { AlertCircleIcon, DownloadIcon } from "lucide-react" const message = (error: unknown) => error instanceof Error ? error.message : "Не удалось выполнить операцию GeoIP" const date = (value: string | null | undefined) => value ? new Date(value).toLocaleString("ru-RU") : "—" export function GeoipSettingsSection({ backendUrl }: { backendUrl: string }) { return } function GeoipSettingsContent({ backendUrl }: { backendUrl: string }) { const [status, setStatus] = useState(null) const [receivedAt, setReceivedAt] = useState(null) const [readError, setReadError] = useState(null) const [opError, setOpError] = useState(null) const [needsReview, setNeedsReview] = useState(false) const [outcome, setOutcome] = useState(null) const [readBusy, setReadBusy] = useState(false) const [busy, setBusy] = useState(false) const [autoOn, setAutoOn] = useState(false) const [intervalHours, setIntervalHours] = useState("") const [dirty, setDirty] = useState(false) const dirtyRef = useRef(false) const lock = useRef(false) const reader = useRef<{ active: boolean; revision: number; controller?: AbortController }>({ active: true, revision: 0 }) useEffect(() => { const state = reader.current; state.active = true return () => { state.active = false; state.revision++; state.controller?.abort() } }, []) const load = useCallback(async () => { if (!reader.current.active || lock.current || !can("mm:traffic:read")) return const revision = ++reader.current.revision; reader.current.controller?.abort() const controller = new AbortController(); reader.current.controller = controller const current = () => reader.current.active && reader.current.revision === revision && !controller.signal.aborted setReadBusy(true) try { const data = await getGeoipStatus(backendUrl, { signal: controller.signal }) if (!current()) return setStatus(data); setReadError(null); setReceivedAt(new Date().toISOString()) if (!dirtyRef.current) { setAutoOn(data.settings.enabled); setIntervalHours(String(data.settings.updateIntervalSec / 3600)) } } catch (error) { if (current()) setReadError(message(error)) } finally { if (current()) setReadBusy(false) } }, [backendUrl]) useEffect(() => { queueMicrotask(() => { void load() }) }, [load]) const writable = can("mm:traffic:write") && Boolean(status) && !readError function begin() { if (!reader.current.active || lock.current || !writable || status?.running) return false lock.current = true; reader.current.revision++; reader.current.controller?.abort(); setReadBusy(false); setBusy(true); setOpError(null); setOutcome(null) return true } function finish() { lock.current = false; if (reader.current.active) setBusy(false) } async function save() { const hours = Number(intervalHours) if (!/^(?:\d+)(?:\.\d+)?$/.test(intervalHours) || !Number.isSafeInteger(hours * 3600) || hours < 6 || hours > 720) { setOpError("Укажите интервал от 6 до 720 часов с точностью до секунды"); return } if (!dirty || !begin()) return try { const data = await putGeoipSettings(backendUrl, { enabled: autoOn, updateIntervalSec: hours * 3600 }) if (!reader.current.active) return setStatus(data.status); setReceivedAt(new Date().toISOString()); dirtyRef.current = false; setDirty(false) setOutcome("Сохранение настроек GeoIP подтверждено") } catch (error) { if (reader.current.active) setOpError(message(error)) } finally { finish() } } async function update() { if (needsReview || !begin()) return try { const data = await runGeoipUpdateNow(backendUrl) if (!reader.current.active) return const snap = data.snapshot setOutcome(snap.skipped ? "Прогон GeoIP пропущен" : data.ok ? "Обновление GeoIP подтверждено" : "Обновление GeoIP завершилось частично или с ошибками") if (snap.errors.length || snap.fatalError) setOpError([snap.fatalError, ...snap.errors].filter(Boolean).join("; ")) setOutcome(prev => prev + `. Проверено: ${snap.checked}; скачано: ${snap.downloaded}; без изменений: ${snap.skippedUnchanged}; байт: ${snap.bytes}.`) } catch (error) { if (reader.current.active) { setNeedsReview(true); setOpError(message(error) + ". Результат операции неизвестен; обновите статус перед повтором.") } } finally { finish() } if (reader.current.active) await load() } const markDirty = () => { dirtyRef.current = true; setDirty(true) } return
Country: {status ? status.countryLoaded ? "загружена" : "не загружена" : "—"}ASN: {status ? status.asnLoaded ? "загружена" : "не загружена" : "—"}
{!can("mm:traffic:read") ?

Нет доступа к данным GeoIP

:

{readBusy ? "Загрузка GeoIP…" : status ? "Статус GeoIP получен: " + date(receivedAt) : "Статус GeoIP не получен"}

} {readError ? : null} {opError ? : null} {needsReview ?

Проверьте журнал планировщика и состояние баз. Повторное обновление запустит новую операцию.

: null} {outcome ?

{outcome}

: null}
{ markDirty(); setAutoOn(value) }} />Автообновление{!status ? ": —" : ""}
{ markDirty(); setIntervalHours(event.target.value) }} inputMode="decimal" disabled={!writable || busy} />

Сборка Country: {date(status?.settings.countryBuildAt)} · ASN: {date(status?.settings.asnBuildAt)} · проверка: {date(status?.settings.lastCheckAt)}{status?.settings.lastError ? " · ошибка: " + status.settings.lastError : ""}

Каталог: {status?.dir ?? "—"}. До загрузки базы и при отсутствии записи используется RIPEstat.

Данные: MaxMind GeoLite2 (CC BY-SA 4.0), зеркало P3TERX/GeoLite.mmdb.

}