Netpulse_SasS/web
byrsapty c7fecfba0b
All checks were successful
CI / hygiene (push) Successful in 8s
CI / web (push) Successful in 55s
CI / server (push) Successful in 1m56s
CI / agent (push) Successful in 2m5s
Згортання меню, лого з фавіконом, вирівнювання полів із підказкою
МЕНЮ. Шість груп, разом 1246 px при вікні 720 — довше за екран. Тепер
згортаються, стан живе в localStorage як перелік ЗГОРНУТИХ (не
розгорнутих: інакше кожна нова група приїхала б до наявних користувачів
схованою). Ключ групи окремо від заголовка — підписи тут уже
переписували, і якби ключем був заголовок, кожне переписування слова
тихо скидало б налаштування всім.

Група з активною сторінкою розгортається сама, але рівно одна — вибір
для решти не чіпається. Правка стану на ЗМІНІ шляху, а не на кожному
рендері, тож згорнути активну групу теж можна.

Дві наявні поломки мобільного вигляду, знайдені дорогою: рейка
вмикалась класами md:, а розгалуження заголовок/риска було на JS і про
md: не знало — людина згортала панель на моніторі, і в шухляді на
телефоні ЗНИКАЛИ ВСІ назви розділів.

ЛОГО. Пульс на лінії зв'язку: вузол → сплеск → провал → норма → вузол.
Два вузли по краях — ключова деталь: без них це іконка «activity» з
будь-якого безкоштовного набору, з ними кардіограма стає лінією між
двома точками мережі. SVG кодом, не растр: слідує темі через
currentColor, важить сотні байтів.

Три РІЗНІ малюнки, а не один масштабований: повне лого, знак, і
спрощений для 16 px усередині favicon.ico (зібраний власним
кодувальником на zlib, без сторонніх інструментів). Пульсація вузла —
опційна й типово вимкнена: у шапці вона конкурувала б зі справжнім
показником зв'язку, який стоїть поруч.

ПОЛЯ З ПІДКАЗКОЮ. Field малює підказку під контролем, тож у ряду з
вирівнюванням по низу колонка з підказкою вища — її підпис і поле
спливають угору, а гола кнопка з'їжджає вниз. Видно було в словнику
трапів; та сама вада знайшлась у редакторі фільтра портів.

Ряди вирівняно по верху, а кнопки без підпису загорнуто у FieldGap —
порожній підпис такої самої висоти. Відступ у пікселях полагодив би
один ряд і роз'їхався б від зміни розміру шрифту.

Перевірено очима в живому Chrome: іконки віддаються (4/4, правильні
типи й розміри), лого 64×64 на сторінці входу, праве коло справді
sky-400 і пульсує лише там.
2026-08-27 22:18:17 +03:00
..
public Згортання меню, лого з фавіконом, вирівнювання полів із підказкою 2026-08-27 22:18:17 +03:00
src Згортання меню, лого з фавіконом, вирівнювання полів із підказкою 2026-08-27 22:18:17 +03:00
index.html Згортання меню, лого з фавіконом, вирівнювання полів із підказкою 2026-08-27 22:18:17 +03:00
package-lock.json Дві сесії роботи: 0058–0068, розгортання однією командою, тести 2026-08-27 17:32:49 +03:00
package.json Дві сесії роботи: 0058–0068, розгортання однією командою, тести 2026-08-27 17:32:49 +03:00
README.md Дві сесії роботи: 0058–0068, розгортання однією командою, тести 2026-08-27 17:32:49 +03:00
TESTING.md Білінг, SLA, вбудовані правила, пісочниця установника, тести сторінок 2026-08-27 21:17:23 +03:00
tsconfig.json Етап 4: фронтенд — мапа мережі в браузері 2026-08-14 16:28:37 +03:00
vite.config.ts Етап 4: фронтенд — мапа мережі в браузері 2026-08-14 16:28:37 +03:00
vitest.config.ts Дві сесії роботи: 0058–0068, розгортання однією командою, тести 2026-08-27 17:32:49 +03:00

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
npm test                                # vitest run

Що саме покривають тести і — головне — що вони НЕ покривають, описано в 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, але полотно його не малює.
  • Графіків: історія метрик не віддається сервером.
  • Кластеризації при віддаленні — поле в схемі є, реалізації немає.