From db21e1217c5c7b0804e76bfa6eea2c79b7c10d57 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 10 Sep 2026 19:54:16 +0700 Subject: [PATCH] feat(network-map): enhance service selection logic and UI updates Refactor the service selection mechanism to utilize a live reference for selected services, ensuring accurate display of service details. Update UI components to reflect changes in service selection, including dynamic updates for service metrics and paths. Additionally, introduce new utility functions for deduplicating flow rows across exporters in the backend, improving data handling in traffic flow analysis. - Updated service selection logic in NetworkMapPage component. - Enhanced UI to display live service data. - Added deduplication functions in traffic-flow-dedup module for improved data integrity. Co-authored-by: Cursor --- ...ss_36e77033-b276-4928-b2d4-f9d734df4686.md | 63 +++++++++++++ app/(main)/network-map/page.tsx | 67 +++++++------ .../src/services/traffic-flow-dedup.test.ts | 16 +++- backend/src/services/traffic-flow-dedup.ts | 26 +++++ .../services/traffic-flow-map-hops.test.ts | 94 +++++++++++++++++++ backend/src/services/traffic-flow-map-hops.ts | 42 ++++++++- tsconfig.tsbuildinfo | 2 +- 7 files changed, 274 insertions(+), 36 deletions(-) create mode 100644 .zcode/plans/plan-sess_36e77033-b276-4928-b2d4-f9d734df4686.md diff --git a/.zcode/plans/plan-sess_36e77033-b276-4928-b2d4-f9d734df4686.md b/.zcode/plans/plan-sess_36e77033-b276-4928-b2d4-f9d734df4686.md new file mode 100644 index 0000000..e49f887 --- /dev/null +++ b/.zcode/plans/plan-sess_36e77033-b276-4928-b2d4-f9d734df4686.md @@ -0,0 +1,63 @@ +# Локальные GeoLite2-базы (Country + ASN) для потоков по странам и ASN + +## Контекст + +Сейчас страна и ASN для netflow-потоков резолвятся через внешний RIPEstat API (`backend/src/services/traffic-flow-ripe.ts`): лимит 30 новых префиксов/мин, очередь на 90, кэш в PG `flow_ip_meta`. Новые IP «дозревают» с задержкой, IPv6 не покрывается (кэш индексируется только по IPv4). Локальные mmdb-базы дают мгновенный синхронный lookup всех IP без внешних вызовов. + +Решения (подтверждены): +- Источник — **MaxMind GeoLite2 через P3TERX-зеркало**: `https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb` и `.../GeoLite2-ASN.mmdb`. Без регистрации, ключей и tar-распаковки. Точность по стране у GeoLite2 и IPinfo паритетная (<1% ошибок у обоих, arXiv 2026); выбран P3TERX за надёжность зеркала (5.2k звёзд) и преемственность: текущий RIPE-путь и так читает GeoLite (`maxmind-geo-lite`), история в кэше остаётся консистентной. +- **RIPEstat остаётся fallback** (до первой загрузки баз / если lookup не дал результата). +- City-базу не качаем (lat/lng фронтенд не использует). + +## Изменения + +### 1. Зависимость +- `npm install -w mikrotik-manager-backend maxmind` — sync-чтение mmdb, встроенные TS-типы, без транзитивных зависимостей, Node 22 ок. + +### 2. Новый сервис `backend/src/services/traffic-flow-geoip.ts` +(по конвенциям окружения traffic-flow-*: контракт → маршрут → сервис, без БД-логики в маршрутах) +- Каталог: `backend/storage/geoip/` (конвенция `storage/backups`), файлы `GeoLite2-Country.mmdb`, `GeoLite2-ASN.mmdb`. +- `initGeoip()` — открыть ридеры best-effort при старте (из `index.ts` рядом с `startTrafficFlowListener`), независимо от настроек автообновления: файлы есть — работают. +- `lookupGeoip(ip): FlowIpMeta | null` — синхронно: `country.iso_code` (fallback `registered_country.iso_code`) с валидацией `isIsoCountry`, ASN = `autonomous_system_number`, holder = `autonomous_system_organization`; приватные IP → negative-запись как в RIPE (`isNonPublicIp`); в PG не пишем (lookup и так быстрый). IPv6 поддержан ридером. +- `resolveFlowIp(ip)` — фасад: `lookupGeoip(ip) ?? lookupRipeCached(ip)`; главный экспорт для потребителей. +- `geoipStatus()` — loaded, даты сборки баз (метаданные mmdb). Тест-хук `setGeoipReadersForTests`. Смена ридеров после обновления — атомарная замена ссылок. + +### 3. Коллектор `backend/src/services/geoip-update-collector.ts` +`collectGeoipUpdateOnce()` по образцу `certificate-renew-collector.ts`: +1. Conditional GET с ETag/If-None-Match из настроек → 304 = skip (фолбэк-сравнение: размер/содержимое). +2. Скачивание в `*.tmp` через глобальный `fetch` + AbortController с таймаутом (внешний HTTP из service-слоя — по правилу fastify-backend-drizzle). +3. Валидация: открыть ридер из tmp-файла, пробой 8.8.8.8 (страна US, ASN 15169). +4. `fs.rename` атомарная подмена, старый файл → `*.prev` (откат, если новый ридер не открылся). +5. Перезагрузка ридеров, статус в настройках; snapshot для `scheduler_runs` (checked/downloaded/skipped/bytes/error). + +### 4. Планировщик (`backend/src/services/scheduler.ts`) +- `JOB_KEYS` += `geoip_update`; case в `runSchedulerJobBody`; блок в `refreshScheduler()` по образцу `certificates_renew`: интервал `Math.max(6ч, updateIntervalSec*1000)`, по умолчанию 7 дней (upstream обновляется еженедельно) + немедленный первый запуск при включённой настройке. + +### 5. Схема и миграция +- `backend/src/db/schema.ts`: singleton `geoip_settings` — `enabled` (default true), `updateIntervalSec` (default 604800), `lastCheckAt`, `lastSuccessAt`, `lastError`, `countryBuildAt`, `asnBuildAt`, `etagsJson` (jsonb), `createdAt/updatedAt`. +- Миграция: `npm run db:generate` → файл в `backend/drizzle/`. + +### 6. API + контракты +- `packages/contracts/src/geoip.ts`: zod-схемы настроек/статуса (все входы — Zod, по правилам проекта). +- Новый `backend/src/routes/geoip.ts`, регистрация в `index.ts` с prefix `/api`: + - `GET /api/geoip` — настройки + статус (ready, даты сборки, последняя проверка/ошибка); + - `PUT /api/geoip` — сохранить настройки, затем `refreshScheduler()`; + - `POST /api/geoip/update` — запустить загрузку сейчас (409, если уже идёт; флаг-гард как в коллекторах). + +### 7. Интеграция в пайплайн (geoip-first, RIPE-fallback) +- `traffic-flow-engine.ts` (`queueParsedFlows`, ~строка 336): `lookupRipeCached` → `resolveFlowIp`. Логика misses не меняется: при готовом mmdb публичные IP (v4+v6) резолвятся сразу, очередь RIPE пустеет; до скачивания баз — прежнее поведение. +- Остальные вызовы `lookupRipeCached` → `resolveFlowIp` (grep: как минимум `traffic-flow-analytics.ts` ~258–271). +- `classifyFlowDst`/бренды не трогаем: holder из mmdb (org name) встаёт в существующие `HOLDER_BRANDS`-регулярки как есть. + +### 8. Frontend (по next-shadcn-production / ui-guardian: только переиспользование) +- Секция «GeoIP-базы (GeoLite2)» внутри существующей `components/traffic/netflow-settings-panel.tsx`: статус (готово/не скачано, даты сборки Country/ASN, последняя проверка, ошибка), тумблер автообновления, интервал, кнопка «Обновить сейчас» с индикатором. Только уже используемые в панели примитивы (Switch/Button/поля) — никаких новых визуальных паттернов и Card-shell. API-клиент через существующие http-хелперы. + +### 9. Хаускипинг, тесты, проверки +- `backend/.gitignore`: `storage/geoip/`. +- Тесты `backend/src/services/traffic-flow-geoip.test.ts` + скрипт `test:geoip` (по образцу `test:traffic-flow`): приоритет фасада (geoip hit → RIPE не зовётся; miss → fallback), negative на приватных IP, фильтрация EU/ZZ через `isIsoCountry`, коллектор с мокнутым fetch (304-skip, битый файл → подмены нет, `.prev` сохранён), dims по стране/ASN с засеянным ридером. +- Проверки после реализации (обязательно по правилам): типы/сборка бэка (`npm run build -w mikrotik-manager-backend`), типы фронта при правке UI (`npx tsc --noEmit`), `npm run test:geoip` и `test:traffic-flow`; предупреждения не игнорировать. +- Коммит: `feat(netflow): ` — новая пользовательская фича (мгновенные страна/ASN в потоках), по commit-messages-ru. +- README: короткий раздел о GeoIP; примечание, что в Docker `storage/geoip` ephemeral без тома — базы перекачаются после пересоздания контейнера (~17 МБ); при желании смонтировать volume. + +## Что это даёт +Страна и ASN появляются у потока мгновенно при ingest (включая IPv6), без ограничения скорости RIPE; dims `country`/`asn` в `flow_daily_dims`, аналитика (карта, топы, monthly) становятся полными сразу. Внешняя зависимость от stat.ripe.net остаётся только как fallback до первой загрузки баз. \ No newline at end of file diff --git a/app/(main)/network-map/page.tsx b/app/(main)/network-map/page.tsx index 51e5ae1..2baafef 100644 --- a/app/(main)/network-map/page.tsx +++ b/app/(main)/network-map/page.tsx @@ -1216,6 +1216,9 @@ export default function NetworkMapPage() { // ── Interaction ───────────────────────────────────────────────────────────── const [selected, setSelected] = useState(null) const [selectedService, setSelectedService] = useState(null) + const liveSelectedService = selectedService + ? (mapServices.find((s) => s.id === selectedService.id) ?? selectedService) + : null const [highlightedPath, setHighlightedPath] = useState<{ viaId: string; enId: string; serviceId: string } | null>(null) const [selWanIdx, setSelWanIdx] = useState(null) const [hoveredId, setHoveredId] = useState(null) @@ -2702,16 +2705,16 @@ export default function NetworkMapPage() { })()} - ) : selectedService ? ( + ) : liveSelectedService ? ( <>
- +
-

{selectedService.label}

+

{liveSelectedService.label}

- Конечный сервис · {selectedService.category} + Конечный сервис · {liveSelectedService.category}