go-git, чистий Go без cgo. Голий репозиторій на тенанта, гілка на пристрій за ідентифікатором (ім'я змінюють, історія не має від цього розсипатись), файл за іменем. Однаковий вміст нового коміту не створює. netpulse-gitsync переливає накопичену історію й відтворює втрачений репозиторій із бази — тіла конфігів там і так лежать зашифрованими. У вебі з'явився вибір версії, з якою порівнювати: сервер це вмів (?from=), інтерфейс — ні. Дорогою виправлено чотири тести, які CI запустив уперше з базою. Серед них справжня помилка: машинний токен не міг читати мапи — ACL мап отримував порожній рядок замість uuid і давав 500. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| index.html | ||
| package-lock.json | ||
| 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. Це поведінка бібліотеки, не помилка — але вона
робить скріншот у згорнутій панелі непридатним для перевірки.
Редагування
| Дія | Як |
|---|---|
| Пересунути вузол | перетягнути; зберігається на відпусканні |
| Намалювати зв'язок | тягнути від краю вузла до іншого |
| Видалити | виділити й натиснути Delete або Backspace |
| Відкотити | кнопка «↶ Відкотити» в шапці |
| Добудувати з топології | кнопка в шапці; не чіпає ручну розкладку |
Намальоване рукою ребро свідомо створюється без прив'язки до topo.links: лінія
на полотні — це подання, а не факт про мережу. Автовиявлення прив'яже її саме, коли
справді побачить цей зв'язок.
Видалення вузла окремо не перелічує його ребра: на полотні їх прибирає React Flow, у базі — каскад зовнішнього ключа.
Чого ще немає
- Входу: токен береться зі змінної збірки або
localStorage. - Підкладок: план приміщення віддається API, але полотно його не малює.
- Графіків: історія метрик не віддається сервером.
- Кластеризації при віддаленні — поле в схемі є, реалізації немає.