feat(traffic): разделить плоскости IPFIX и показать путь JH→EN
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Failing after 1m31s
Docker images / frontend-image (push) Successful in 2m36s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 42s
Docker images / publish-release (push) Skipped

Считать payload отдельно от overlay GRE/ESP и mesh; вкладка Пути и KPI Wire из счётчиков iface.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-07 11:39:14 +07:00
co-authored by Cursor
parent cb799da13a
commit 90c8c393e5
32 changed files with 1656 additions and 65 deletions
+43 -3
View File
@@ -32,6 +32,8 @@ import {
import { cn } from "@/lib/utils"
import { requestJson } from "@/shared/api/http-client"
import { downloadSystemDatabaseBackup, restoreSystemDatabaseBackup } from "@/shared/api/system-database"
import { purgeTrafficFlowData } from "@/shared/api/traffic-flow"
import { formatFlowPurgeResult, NetflowPurgeConfirm } from "@/components/traffic/netflow-purge-dialog"
import { toast } from "sonner"
interface ApiKey { id: string; name: string; prefix: string; created: string; last: string; scopes: string[] }
@@ -148,6 +150,8 @@ export default function SettingsPage() {
const [dbRestoreBusy, setDbRestoreBusy] = useState(false)
const [dbRestoreFile, setDbRestoreFile] = useState<File | null>(null)
const [dbRestoreDialogOpen, setDbRestoreDialogOpen] = useState(false)
const [dbPurgeOpen, setDbPurgeOpen] = useState(false)
const [dbPurgeBusy, setDbPurgeBusy] = useState(false)
// notifications
const [notifEmail, setNotifEmail] = useState(true)
@@ -266,6 +270,20 @@ export default function SettingsPage() {
}
}, [backendUrl, dbRestoreFile, systemDbAvailable])
const handleNetflowPurgeConfirm = useCallback(async () => {
if (!systemDbAvailable) return
setDbPurgeBusy(true)
try {
const result = await purgeTrafficFlowData(backendUrl)
toast.success(formatFlowPurgeResult(result))
setDbPurgeOpen(false)
} catch (e) {
toast.error(e instanceof Error ? e.message : "Не удалось сбросить NetFlow")
} finally {
setDbPurgeBusy(false)
}
}, [backendUrl, systemDbAvailable])
const renderContent = () => {
const ra = DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS
@@ -417,7 +435,7 @@ export default function SettingsPage() {
<OpsPanel
title="База данных приложения"
description="Резервная копия SQLite бекенда: серверы, мониторинг, оповещения, EvoBGP. На время операции планировщик сбора данных приостанавливается."
description="Резервная копия SQLite бекенда и сброс таблиц NetFlow. На время операции планировщик и коллектор IPFIX приостанавливаются. Preview: https://reui.io/preview/base/settings-16"
contentClassName="divide-y px-5"
>
{!systemDbAvailable && (
@@ -434,7 +452,7 @@ export default function SettingsPage() {
variant="outline"
size="sm"
className="h-8"
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy}
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy || dbPurgeBusy}
onClick={() => { void handleSystemDatabaseBackup() }}
>
{dbBackupBusy ? <LoaderCircleIcon className="size-4 animate-spin" /> : <DownloadIcon className="size-4" />}
@@ -450,7 +468,7 @@ export default function SettingsPage() {
variant="outline"
size="sm"
className="h-8"
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy}
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy || dbPurgeBusy}
onClick={() => setDbRestoreDialogOpen(true)}
>
<UploadIcon className="size-4" />
@@ -461,6 +479,21 @@ export default function SettingsPage() {
)}
</div>
</SettingRow>
<SettingRow
label="Сбросить данные NetFlow"
description="Удалит сессии и агрегаты из SQLite, затем VACUUM. Ключи WG и пиры не трогает"
>
<Button
variant="destructive"
size="sm"
className="h-8"
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy || dbPurgeBusy}
onClick={() => setDbPurgeOpen(true)}
>
{dbPurgeBusy ? <LoaderCircleIcon className="size-4 animate-spin" /> : <TrashIcon className="size-4" />}
{dbPurgeBusy ? "Сброс…" : "Сбросить"}
</Button>
</SettingRow>
</OpsPanel>
<OpsPanel
@@ -893,6 +926,13 @@ export default function SettingsPage() {
/>
)}
<NetflowPurgeConfirm
open={dbPurgeOpen}
busy={dbPurgeBusy}
onConfirm={() => { void handleNetflowPurgeConfirm() }}
onCancel={() => { if (!dbPurgeBusy) setDbPurgeOpen(false) }}
/>
<FileImportDialog
open={dbRestoreDialogOpen}
onOpenChange={setDbRestoreDialogOpen}
+16 -1
View File
@@ -103,9 +103,14 @@ function monthlyToAnalytics(m: FlowMonthlyDto): FlowAnalyticsDto {
services: m.services,
mapEdges: [],
conversationsList: [],
paths: [],
ifaces: [],
live: false,
degraded: false,
bytesPayload: m.bytes,
bytesOverlay: 0,
bytesMesh: 0,
bytesWire: 0,
}
}
@@ -806,6 +811,8 @@ export default function TrafficPage() {
const [flowAnalytics, setFlowAnalytics] = useState<FlowAnalyticsDto | null>(null)
const [flowIface, setFlowIface] = useState("__all__")
const [flowDedup, setFlowDedup] = useState(true)
const [flowExcludeMesh, setFlowExcludeMesh] = useState(true)
const [flowExcludeOverlay, setFlowExcludeOverlay] = useState(true)
const [overlayOpen, setOverlayOpen] = useState(false)
const [catalogServers, setCatalogServers] = useState<ServerRead[]>([])
const effectiveMode: GroupMode = groupMode
@@ -824,6 +831,8 @@ export default function TrafficPage() {
userId: flowScope === "users" ? selectedId : undefined,
iface: flowIface,
dedup: flowDedup,
excludeMesh: flowExcludeMesh,
excludeOverlay: flowExcludeOverlay,
})
const toLiveServer = (s: LiveTrafficServer): ServerTraffic => {
@@ -961,8 +970,10 @@ export default function TrafficPage() {
userId: flowScope === "users" ? selectedId : undefined,
iface: flowIface,
dedup: flowDedup,
excludeMesh: flowExcludeMesh,
excludeOverlay: flowExcludeOverlay,
}).then(setFlowAnalytics).catch(() => setFlowAnalytics(null))
}, [isLive, effectiveMode, selectedId, range, flowScope, flowIface, flowDedup, backendUrl])
}, [isLive, effectiveMode, selectedId, range, flowScope, flowIface, flowDedup, flowExcludeMesh, flowExcludeOverlay, backendUrl])
useEffect(() => {
setFlowIface("__all__")
@@ -1328,6 +1339,10 @@ export default function TrafficPage() {
onIface={setFlowIface}
dedup={flowDedup}
onDedup={setFlowDedup}
excludeMesh={flowExcludeMesh}
onExcludeMesh={setFlowExcludeMesh}
excludeOverlay={flowExcludeOverlay}
onExcludeOverlay={setFlowExcludeOverlay}
liveHint={displayedFlow?.live ? "live" : undefined}
emptyHint={flowEmptyHint(flowStats, collectorAlive)}
/>