Commit graph

59 commits

Author SHA1 Message Date
bee55be3d1 Етап 4: редактор мапи — зв'язки, видалення, відкат
POST /api/v1/maps/{id}/undo повертає полотно до попереднього знімка.
Відкат оформлюється як нова ревізія, а не відмотування лічильника:
інакше клієнт зі старим номером тихо перезаписав би відкочене.
Ідентифікатори вузлів зберігаються, тож ребра прив'язуються назад самі.

В UI: малювання зв'язку від краю вузла, видалення по Delete, кнопка
відкату. Намальоване рукою ребро не прив'язується до topo.links —
лінія на полотні це подання, а не факт про мережу.

Виправлено флак у тестах: TestSchedulerRunsTaskAndFillsCredentials
перевіряв канал статусів знімком, хоча SUCCEEDED надсилається вже після
запису в sink. Під навантаженням падав раз на п'ять; тепер 0 з 8.

Перевірено наживо: намальовано зв'язок -> відкат -> видалено вузол ->
відкат повернув той самий id, ребро й живий стан лінка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 21:46:18 +03:00
3de5643b98 Етап 4: фронтенд — мапа мережі в браузері
React Flow поверх наявного API: вузли фарбуються статусом, ребра
кольором із порогів, точка на ребрі біжить зі швидкістю, оберненою до
завантаження каналу. WebSocket дає живі статуси й util, реконект
повертає підписку сам.

Робота з живим UI знайшла дві вади:

1. Ревізія мапи росла від самих кліків — React Flow віддає
   onNodeDragStop на будь-яке натискання, і кожен клік писав порожню
   ревізію, змушуючи інші відкриті полотна перечитуватись.
2. Після збереження хук оновлював лише номер ревізії, тож наступна
   подія device.status відкидала щойно перетягнутий вузол назад.

Перевірено в браузері проти повного стека зі справжнім snmpd: два
зелені вузли з живим RTT, ребро eth0 з util і capacity 10 Гбіт/с,
індикатор "наживо", кнопка автопобудови з topo.links.

Драг через автоматизацію відтворити не вдалося — синтетичні події не
запускають d3-drag; цей шлях покритий тестами сервера.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:28:37 +03:00
8bf2902522 Етап 3: запис у мапу — редактор полотна
PATCH /api/v1/maps/{id} з оптимістичним блокуванням за revision, плюс
створення, видалення й автопобудова з виявленої топології.

Усі скалярні поля патча — вказівники: перетягування шле лише x/y, і якби
відсутні поля означали порожні, кожен рух миші стирав би стиль, розмір і
прив'язку до пристрою.

Ребро може посилатися на вузол, створений тим же патчем, за client_id.
Той, хто спізнився з ревізією, отримує 409, а не тихо затирає чужу правку.
Знімок пишеться тією ж транзакцією, що й зміна, — інакше в історії лишався
б крок, якого в мапі немає.

Автопобудова ідемпотентна: повторний запуск не дублює вузлів і не скидає
ручну розкладку.

Тестами знайдено: revision <= $2 - $3 з двома нетипізованими параметрами
дає "operator is not unique: unknown - unknown" — потрібні явні касти.

Перевірено: 23 інтеграційні тести API (-race), плюс живий прогін проти
даних, зібраних агентом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:58:16 +03:00
328850be06 Етап 3: REST/WebSocket API для UI
Окремий процес від AgentService: у зондів і браузерів різні профілі
навантаження й периметри, спільний лише шар store.

Стан мапи віддається одним викликом разом із живими статусами — інакше
полотно малювалося б сірим і доганяло кольори сотнею дозапитів.

link_status виводиться з кінців лінка, а не читається з topo.links: цю
колонку ніхто не підтримує, а стан лінка це похідна величина. Живий
прогін показував "unknown" на цілком робочому каналі, поки не порахували.

Події для WebSocket пишуться тією ж транзакцією, що й зміна, яку
описують. Транспорт — опитування outbox, а не LISTEN/NOTIFY: NOTIFY не
переживає падіння підписника, а тут потрібна гарантія доставки.

Перевірено: 10 інтеграційних тестів проти живої БД, HTTP і WebSocket
(-race), плюс живий прогін агента, сервера й API разом проти snmpd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:43:02 +03:00
aaf067a46b Етап 2: сервер сам заводить snmp.if-чеки з виявлених інтерфейсів
Автовиявлення наповнювало inv.interfaces, але їх ніхто не опитував: на
мапі були лінки й не було трафіку. Тепер сервер формує snmp.if-чек зі
складу портів і штовхає його живій сесії як TaskDelta — без цього після
кожного нового комутатора були б години порожніх графіків.

Чек оновлюється, а не задвоюється: унікальний індекс core.checks включає
md5(params). Склад портів порівнюється як множина, бо порядок ключів у
jsonb не гарантований. Без SNMP-креденшела чек не створюється.

Живий прогін знайшов помилку: OID у запиті йшов без провідної крапки, а
pdu.Name повертається з нею — пошук у мапі мовчки не знаходив нічого, і
чек виглядав як "жоден інтерфейс не відповів". Канонізація тепер у
snmpx.Normalize, застосована з обох боків.

Перевірено наскрізь: виявлення -> автостворення чека -> справжні
HC-лічильники -> ts.if_counters, без жодного ручного кроку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:10:02 +03:00
ad7672c32f Етап 2: модуль topology — LLDP/CDP/ARP/FDB та інвентар портів
Спільний SNMP-транспорт винесено в snmpx: ним користуються два модулі.
Звіти автовиявлення йдуть окремим RPC, не телеметричним стрімом — вони
рідкі, великі й не прив'язані до моменту часу так, як метрики.

Живий прогін проти справжнього snmpd+lldpd знайшов дві помилки:

1. Префікс типу чека не збігався з ключем модуля (topo.discover при
   плагіні topology). Агент маршрутизує задачі саме за префіксом, тож
   зонд відхиляв би їх. Інваріант закріплено обмеженням у БД.
2. Унікальний індекс topo.neighbors схлопував ARP-сусідів: ключ не
   включав MAC, а chassis_id/port_id в ARP немає взагалі.

Перевірено на живих даних: інвентар із ifTable, 2 ARP-сусіди, зіставлення
шлюзу за MAC (впевненість 90), зведений лінк із capacity 10 Гбіт/с.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:42:01 +03:00
8a92ef8a45 Етап 2: серверна сторона AgentService
Автентифікація зондів за токеном, побудова TaskPlan із детермінованим
schedule_offset, видача розшифрованих креденшелів із TTL, запис телеметрії
в гіпертаблиці, резолвер сусідів LLDP/CDP у topo.links, прийом конфігів.

Ізоляція тенантів робиться двічі — RLS плюс явний предикат tenant_id, бо
RLS не працює на гіпертаблях, а саме туди йде вся телеметрія.

Агент: -token і передача його в метаданих; MarkAllPending() перереєстровує
серії на початку сесії замість обнуляти нумерацію й губити буфер.

Перевірено на Debian 13 / PG 17.11 / TimescaleDB 2.29.1: 11 інтеграційних
тестів проти живої БД (-race), плюс живий прогін справжнього агента проти
справжнього сервера — телеметрія, статус пристрою, heartbeat у базі.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 04:13:40 +03:00
c8075ee2a6 Етап 2: Go-агент — планувальник, буфер, сесія, модулі ICMP і SNMP
Модулі вкомпільовані (без .so): один бінарник на Alpine, Windows і роутер.
Креденшели беруться на момент виконання, бо мають TTL. Розклад вирівняний
по сітці інтервалу, тому переживає рестарт. Буфер обмежений і за кількістю,
і за пам'яттю; при переповненні викидає найстаріше, зміни статусу — останніми.

Перевірено на Debian 13 / Go 1.25: go vet чисто, go test -race усі пакети ok.
Релізний бінарник 12 МБ, базовий RSS 11.6 МБ при бюджеті 30 МБ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 03:46:46 +03:00
15d22a8d8a Етап 1-2: схема БД та protobuf-контракт агент-сервер
Схема PostgreSQL 16+/TimescaleDB: 11 міграцій, 7 схем, топологія
(neighbors -> links -> maps -> nodes/edges), time-series з CAGG,
NCM, alerting, білінг з entitlements, RLS.

Контракт agent<->server: 6 proto-файлів, gRPC, інтернування серій,
at-least-once з ack, чанкування конфігів.

Перевірено на стенді Debian 13 / PG 17.11 / TimescaleDB 2.29.1:
міграції + 8 функціональних перевірок схеми, buf lint + 5 наскрізних
gRPC-тестів контракту.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 03:28:40 +03:00