П'ять паралельних задач. Найцінніше в них — не можливості, а знайдене.
0069 БІЛІНГ. Аудит 0009 показав, що перевірка ліміту не спрацювала б
жодного разу: isPlanLimit шукала слово «ліміт», а тригер писав
"device limit reached" англійською. Перше ж досягнення стелі дало б
клієнту 500 замість пояснення. Плюс три діри: тригер лише на INSERT
(стеля в 15 обходилась за чотири дії через архів), max_maps/max_agents/
max_users не перевіряло ніщо — тобто рівно те, чим відрізняються плани,
і license_keys була закрита політикою tenant_isolation з 0011, хоча
tenant_id там NULLABLE навмисно: головний сценарій self-hosted був
недосяжний.
Після закінчення ліцензії не вимикається нічого — замерзає лише ріст.
Моніторинг, що перестав моніторити через несплачений рахунок, це
аварія в мережі клієнта, спричинена нами.
0070 SLA. Джерелом обрано ts.icmp_1h, а не device_status_history:
остання не вміє сказати «ми не знали» — перехід пишеться лише при
зміні стану, тож доба мовчання зонда виглядає як доба роботи. Час
розкладено на чотири частини, і «немає даних» не додається ні до чого;
замість вибору між двома брехнями звіт каже, яку частку періоду він
бачив. Закритий період тримає тригер, а не домовленість у Go.
0071 ВІДПОВІДНІСТЬ. 20 правил, кожне прив'язане до родини: об'єднаний
вираз, що покриває Cisco й не покриває MikroTik, дав би «0 порушень» і
сховав сліпу пляму. Вендор не входить у перелік, доки для нього немає
зразка конфігу в тесті. TestBuiltinRulesAreNotAlwaysGreen вимагає, щоб
у кожного правила був конфіг, де воно спрацювало, І де ні.
ПІСОЧНИЦЯ УСТАНОВНИКА — та сама установка в ізольованому проєкті
compose. Знайшла дві справжні вади з трьох спроб:
* healthcheck бази ходив unix-сокетом, а споживачі по TCP. При
первинній ініціалізації Postgres слухає лише сокет — compose
вважав базу здоровою, migrate отримував connection refused. На
створеній базі цієї фази немає, тож вада чекала на першого клієнта;
* у білому переліку модулів API не було traps і filecfg — зонд із
приймачем трапів неможливо було зареєструвати взагалі.
ТЕСТИ СТОРІНОК: 137 → 252. Мережевий шар, права доступу, незворотні
дії, фільтри з адресного рядка. Підмінюється лише fetch і WebSocket —
api/client.ts працює справжній.
|
||
|---|---|---|
| .. | ||
| src | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| 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, але полотно його не малює.
- Графіків: історія метрик не віддається сервером.
- Кластеризації при віддаленні — поле в схемі є, реалізації немає.