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> |
||
|---|---|---|
| .. | ||
| src | ||
| index.html | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
NetPulse Web — мапа мережі
React + TypeScript + React Flow + Tailwind поверх server/API.md.
npm install
VITE_API_TOKEN=<токен> npm run dev # проксі на http://127.0.0.1:8080
npm run build
Dev-сервер проксіює /api (разом із WebSocket) на бекенд. Це знімає питання CORS і
робить адресу API однаковою в розробці й за зворотним проксі у проді.
Будова
src/
types.ts типи API — дзеркало server/API.md
api/client.ts REST, ApiError з isConflict / isPlanLimit
api/ws.ts WebSocket із реконектом і повторною підпискою
hooks/useLiveMap.ts стан полотна: завантаження, живі оновлення, запис
components/
MapCanvas.tsx React Flow: полотно, сітка, мінімапа
DeviceNode.tsx вузол, колір із статусу, RTT і втрати
TrafficEdge.tsx ребро: колір із порогів, точка рухається за завантаженням
App.tsx шапка, лічильники, зонди, інспектор вузла
Рішення, які варто розуміти
Сервер — джерело істини, але не під час перетягування. Поки вузол тягнуть, його
позицію диктує миша: інакше кожне оновлення по WebSocket смикало б вузол назад
під курсором. Синхронізація пропускає саме ті вузли, у яких dragging.
Зберігаємо на відпусканні, і лише якщо вузол зрушив. Один драг інакше перетворюється на сотню запитів і сотню ревізій. А React Flow вважає драгом будь-яке натискання, тож без перевірки зсуву простий клік писав би порожню ревізію: історія засмічується, а всі інші відкриті полотна дарма перечитують мапу.
Успішний патч застосовується до локального стану. Без цього вузол після перетягування лишався б на новому місці лише візуально — перша ж подія про зміну статусу перебудувала б список вузлів зі старими координатами й відкинула б його назад.
Конфлікт ревізій не приховується. 409 показує повідомлення й перечитує полотно:
мовчки затирати чужу правку гірше, ніж змусити повторити свою.
Швидкість анімації обернено пропорційна завантаженню. Точка біжить швидше, коли трафіку більше: стеля 0.6 с (далі рух зливається в смугу), підлога 6 с (щоб ледь завантажений канал не виглядав мертвим). При нульовому трафіку анімації немає взагалі — рухома точка на порожньому каналі показувала б рух, якого нема.
Стан з'єднання показується завжди. Мапа, що тихо замерзла після розриву WebSocket, виглядає точно так само, як мапа, де все гаразд, — і це найнебезпечніший стан моніторингу. Тому окремий індикатор і «N с тому» від останньої події.
Червоний лишається за обривом. Якщо ним підсвічувати ще й «невідомо», оператор перестане на нього реагувати.
Стан перевірки
tsc --noEmit і vite build — чисто (345 КБ JS, 112 КБ gzip).
Перевірено в живому браузері проти повного стека (агент + netpulse-server +
netpulse-api + справжній snmpd на стенді):
| Що видно | Результат |
|---|---|
| Вузли з даних автовиявлення | snmp-host і gateway, обидва зелені |
| RTT із живого ICMP | 0.05 мс і 1.04 мс, оновлюється |
| Ребро з портами | eth0 → ? (ARP не дає порт віддаленої сторони) |
| Завантаження каналу | <0.01% з 10.0 Гбіт/с — знаменник із ifHighSpeed |
| Індикатор з'єднання | «наживо, 1 с тому» |
| Лічильники й зонд | 2/2 працюють, probe-snmp linux/amd64, RSS 11.1 МБ |
| Кнопка «Добудувати з топології» | +2 вузли, +1 ребро з topo.links |
Знайдено під час роботи з живим UI (обидві вади виправлені):
- Ревізія мапи виросла з 2 до 5, поки я лише робив скріншоти: React Flow віддає
onNodeDragStopна будь-яке натискання, і кожен клік писав порожню ревізію. - Після збереження хук оновлював лише номер ревізії — тож перша ж подія
device.statusвідкидала щойно перетягнутий вузол на стару позицію.
Обмеження перевірки
Перетягування не вдалося відтворити автоматизацією. Синтетичні події вказівника
не запускають drag у React Flow (d3-drag не сприймає недовірені події без повного
потоку pointerdown → pointermove → pointerup від справжнього пристрою вводу).
Шлях «драг → PATCH → БД» покритий інтеграційними тестами сервера
(TestPatchMoveNodeKeepsOtherFields, TestPatchRevisionConflict), але саме
через UI він лишається неперевіреним автоматично — потрібен ручний клік.
Ребра не малюються, поки вкладка прихована. React Flow міряє вузли в
requestAnimationFrame, а він у прихованій вкладці не викликається; вузли при цьому
рендеряться, бо це звичайний DOM. Це поведінка бібліотеки, не помилка — але вона
робить скріншот у згорнутій панелі непридатним для перевірки.
Чого ще немає
- Входу: токен береться зі змінної збірки або
localStorage. - Малювання зв'язків мишею:
PATCHце вміє, UI ще ні. - Підкладок: план приміщення віддається API, але полотно його не малює.
- Графіків: історія метрик не віддається сервером.
- Кластеризації при віддаленні — поле в схемі є, реалізації немає.