fix(geoip): сохранять черновик и показывать частичный результат обновления

This commit is contained in:
Denozordec committed 2026-10-03 12:13:25 +07:00
1 parent f0d9f488e6
commit 57e7dcf990
6 files changed
+185 -127

No files matched your search

+3 -125
View File
@@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { CodeExportSheet, type CodeExportFormat } from "@/components/reui-kit/code-export-sheet"
import type { TrafficFlowSettingsDto } from "@mmapp/contracts/traffic-flow"
import type { GeoipStatusDto } from "@mmapp/contracts/geoip"
import { GeoipSettingsSection } from "./geoip-settings-section"
import {
generateTrafficFlowKeys,
getTrafficFlowHostFiles,
@@ -18,7 +18,6 @@ import {
purgeTrafficFlowData,
putTrafficFlowSettings,
} from "@/shared/api/traffic-flow"
import { getGeoipStatus, putGeoipSettings, runGeoipUpdateNow } from "@/shared/api/geoip"
import { formatFlowPurgeResult, NetflowPurgeConfirm } from "@/components/traffic/netflow-purge-dialog"
import { KeyRoundIcon, DownloadIcon, InfoIcon } from "lucide-react"
@@ -31,127 +30,6 @@ const HOST_STEPS = [
"Проверка: wg show · ss -ulnp | grep 4739 · в этой панели — last datagram.",
]
function fmtDate(iso: string | null | undefined): string {
return iso ? new Date(iso).toLocaleString("ru-RU") : "—"
}
function GeoipSettingsSection({ backendUrl }: { backendUrl: string }) {
const [status, setStatus] = useState<GeoipStatusDto | null>(null)
const [busy, setBusy] = useState(false)
const [updating, setUpdating] = useState(false)
const [autoOn, setAutoOn] = useState(true)
const [intervalHours, setIntervalHours] = useState("168")
const load = useCallback(async () => {
const s = await getGeoipStatus(backendUrl)
setStatus(s)
setAutoOn(s.settings.enabled)
setIntervalHours(String(Math.round(s.settings.updateIntervalSec / 3600)))
}, [backendUrl])
useEffect(() => {
void load().catch((e: unknown) => {
toast.error(e instanceof Error ? e.message : "Не удалось загрузить GeoIP")
})
}, [load])
async function handleSave() {
setBusy(true)
try {
const hours = Math.min(720, Math.max(6, Number.parseInt(intervalHours, 10) || 168))
const res = await putGeoipSettings(backendUrl, {
enabled: autoOn,
updateIntervalSec: hours * 3600,
})
setStatus(res.status)
toast.success("Настройки GeoIP сохранены")
} catch (e) {
toast.error(e instanceof Error ? e.message : "Не удалось сохранить")
} finally {
setBusy(false)
}
}
async function handleUpdateNow() {
setUpdating(true)
try {
const res = await runGeoipUpdateNow(backendUrl)
if (res.ok || res.snapshot.downloaded > 0 || res.snapshot.skippedUnchanged > 0) {
toast.success(
res.snapshot.downloaded > 0
? `Скачано баз: ${res.snapshot.downloaded} (${(res.snapshot.bytes / 1024 / 1024).toFixed(1)} МБ)`
: "Базы актуальны, скачивание не требуется",
)
} else {
toast.error(res.snapshot.errors.join("; ") || "Обновление не выполнено")
}
await load()
} catch (e) {
toast.error(e instanceof Error ? e.message : "Не удалось обновить базы")
} finally {
setUpdating(false)
}
}
return (
<OpsPanel
title="GeoIP-базы (GeoLite2)"
description="Локальные mmdb MaxMind GeoLite2 с зеркала P3TERX: страна и ASN каждого потока при ingest — мгновенно, включая IPv6, без лимитов RIPEstat."
headerRight={
<div className="flex items-center gap-2">
<Badge variant={status?.countryLoaded ? "success" : "secondary"}>
country {status?.countryLoaded ? "ok" : "нет"}
</Badge>
<Badge variant={status?.asnLoaded ? "success" : "secondary"}>
asn {status?.asnLoaded ? "ok" : "нет"}
</Badge>
</div>
}
contentClassName="px-5 py-4 flex flex-col gap-4"
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="flex items-center gap-3">
<FormToggle checked={autoOn} onChange={setAutoOn} />
<span className="text-sm">Автообновление</span>
</div>
<FormField label="Интервал проверки (часов)" hint="Upstream обновляется еженедельно; минимум 6 ч">
<Input
className="font-mono"
value={intervalHours}
onChange={(e) => setIntervalHours(e.target.value)}
inputMode="numeric"
disabled={!autoOn}
/>
</FormField>
</div>
<p className="text-xs text-muted-foreground">
Сборка Country: {fmtDate(status?.settings.countryBuildAt)} · ASN: {fmtDate(status?.settings.asnBuildAt)}
{" · "}последняя проверка: {fmtDate(status?.settings.lastCheckAt)}
{status?.settings.lastError ? ` · ошибка: ${status.settings.lastError}` : ""}
</p>
<p className="text-xs text-muted-foreground">
Каталог: <span className="font-mono">{status?.dir || "storage/geoip"}</span>. До загрузки баз
и при промахе lookup страна/ASN берутся из RIPEstat, как раньше.
</p>
<div className="flex flex-wrap gap-2">
<Button size="sm" disabled={busy || updating} onClick={() => { void handleSave() }}>
Сохранить GeoIP
</Button>
<Button size="sm" variant="outline" disabled={busy || updating} onClick={() => { void handleUpdateNow() }}>
<DownloadIcon className={updating ? "size-4 animate-spin" : "size-4"} />
{updating ? "Обновление…" : "Обновить сейчас"}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Данные: MaxMind GeoLite2 (CC BY-SA 4.0), зеркало P3TERX/GeoLite.mmdb.
</p>
</OpsPanel>
)
}
function NetflowSettingsPanel({
backendUrl,
enabled,
@@ -194,9 +72,9 @@ function NetflowSettingsPanel({
}, [backendUrl, enabled])
useEffect(() => {
void load().catch((e: unknown) => {
queueMicrotask(() => { void load().catch((e: unknown) => {
toast.error(e instanceof Error ? e.message : "Не удалось загрузить NetFlow")
})
}) })
}, [load])
async function handleSave() {