ЖУРНАЛИ БЕЗ СТЕЛІ. На запитання «чому не пишемо в /var/log» правильна відповідь — «бо в контейнері це не той шар»: файл усередині зникає при перестворенні, невидимий для docker logs і вимагає власної ротації. Але за питанням стояла справжня вада: драйвер json-file був налаштований порожньо, тобто НЕ КРУТИВ НІЧОГО. Проксі за добу набрав 27 МБ; до повного диска були місяці, і першим ліг би Postgres. Тепер 10 МБ × 3 файли на службу — близько 250 МБ на інсталяцію, і драйвер міняється однією змінною (journald, syslog) для тих, кому потрібні справжні файли або чужий збирач. САМОПЕРЕВІРКА ЗОНДА чекала хвилину й одного разу вже дала хибне червоне: зонд зареєструвався, просто пізніше — перший старт припадає на найзавантаженішу мить установки. Тепер три хвилини з повідомленням кожні півхвилини: мовчазна пауза невідрізненна від зависання, а хибне червоне після успішної установки коштує години пошуку неіснуючої поломки. GITSTORE: TestDeleteBranchLocal кличе зовнішній git і не мав перевірки на його відсутність — падав у golang:1.25-alpine, тобто саме там, де його проганяють. Сусідній тест таку перевірку має. ЕТАП 13 у ROADMAP: реєстр образів, релізи, пакети. Записано, чому порядок саме такий (пакет без реєстру ставив би «зберіть самі») і чого треба досягти до першого тегу — оновлення з версії на версію не перевіряв ніхто, а ламається найчастіше саме воно. |
||
|---|---|---|
| .. | ||
| src | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| preview-nav.html | ||
| README.md | ||
| TESTING.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.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
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 (обидві вади виправлені):
- Ревізія мапи виросла з 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, але полотно його не малює.
- Графіків: історія метрик не віддається сервером.
- Кластеризації при віддаленні — поле в схемі є, реалізації немає.