# NetPulse Web — мапа мережі React + TypeScript + React Flow + Tailwind поверх [server/API.md](../server/API.md). ```bash npm install VITE_API_TOKEN=<токен> npm run dev # проксі на http://127.0.0.1:8080 npm run build npm test # vitest run ``` Що саме покривають тести і — головне — **що вони НЕ покривають**, описано в [TESTING.md](TESTING.md). Другий перелік там важливіший за перший: без нього зелений прогін легко прийняти за доказ справності всього вебу. 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** (обидві вади виправлені): 1. Ревізія мапи виросла з 2 до 5, поки я лише робив скріншоти: React Flow віддає `onNodeDragStop` на будь-яке натискання, і кожен клік писав порожню ревізію. 2. Після збереження хук оновлював лише номер ревізії — тож перша ж подія `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, але полотно його не малює. - **Графіків**: історія метрик не віддається сервером. - **Кластеризації при віддаленні** — поле в схемі є, реалізації немає.