Дашборди: схема core.dashboards лежала з Етапу 1 без жодного рядка коду. Сітка на 12 колонок, шість видів плиток (графік, число, шкала, список алертів, сітка хостів, текст), автооновлення з інтервалом дашборда, режим редагування. Права окремі від maps:* — дашборд збирає дані з усього тенанта. Esc і клік повз панель закривають будь-яке вікно. Слухач на document, бо фокус може стояти де завгодно; закриття за mousedown, а не click, щоб виділення тексту, доведене за межі вікна, не втрачало набране. Канали сповіщень редагуються (раніше лише створювались і видалялись). Протокол доступу змінюється — з вимогою ввести пароль заново, бо секрет зашифрований під видом старого протоколу. 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, але полотно його не малює.
- Графіків: історія метрик не віддається сервером.
- Кластеризації при віддаленні — поле в схемі є, реалізації немає.