Логін:
- core.users.username замість пошти як ідентифікатор: у мережевій
інсталяції половина акаунтів технічні (noc, monitoring, oncall) і
скриньки не мають узагалі. Пошта лишилась необов'язковим полем
- наявним користувачам логін виведено з пошти, збіги розведено
суфіксом: мовчки злити admin@a.com і admin@b.com в один логін —
це втрата акаунта, а не міграція
- сервер шукає за логіном і за поштою, тому звичка вводити email
нікого не відхиляє
Групи (модель Zabbix):
- групи хостів і групи доступу; права read/write/deny на групу хостів
- роль каже, що вільно робити; група — над якими хостами. Інженер над
філією та інженер над усією мережею мають однакову роль і різний
доступ
- хто не входить у жодну групу, групами не обмежений — свідомо не
по-заббіксівськи: там кожна нова інсталяція починається з питання
«чому порожньо»
- заборона перемагає дозвіл, інакше її обійти додаванням у сусідню групу
- фільтр накладається в самому запиті, а не після вибірки
Хости: додавання, редагування, м'яке видалення, прив'язка до груп,
фільтр за групою в таблиці.
Мапа: створення мапи з транслітерацією slug, інспектор вузла — підпис,
значок, розмір, ширина, колір рамки, закріплення.
Виправлено за скаргами:
- перемикач вилазив на 14px за трек: у ручки не заданий left, а
статичну позицію зсуває типове text-align: center у <button>
- сторінка правил не оновлювала «активних»: після повернення правила
алерти піднімаються наступним тіком движка, тобто ПІСЛЯ нашого
перечитування — бракувало підписки на живі події
- канал із секретом показувався як «секрету немає»
Живий прогін: eng у групі з read на «Доступ» бачить 2 хости замість 3,
обидва без запису, алертів 1 замість 3, редагування чужого хоста 403.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
До цього «веб» був одним екраном: логін, мапа й панель алертів. Усе,
що вміло API, доводилось викликати curl-ом.
Каркас:
- бічна навігація з мобільною шухлядою, індикатор зв'язку в шапці,
живий лічильник алертів
- пункт, на який немає права, не показується взагалі; пряме посилання
лишається робочим і дає пояснення з назвою потрібного права
- стартова сторінка залежить від ролі: глядача без прав на мапи вітати
відмовою — поганий перший екран
Сторінки: мапа, пристрої з пошуком і картками алертів, алерти з
фільтрами, правила зі створенням через форму, канали з кнопкою
перевірки, зонди, команда з керуванням ролями, профіль зі зміною
пароля.
DataTable на телефоні перестає бути таблицею: горизонтальний скрол на
375 px робить дані формально присутніми й фактично нечитабельними.
Знайдено роботою з живим UI:
- WebSocket жив усередині мапи, тому на решті сторінок живих оновлень
не було взагалі — лічильник алертів замерзав, щойно людина йшла з
мапи. З'єднання винесено в модуль-одинак, яким володіє оболонка
- сторінка пристроїв перечитувала все на кожну подію алерту; зі
злиттям сплеску й спільними алертами той самий сплеск коштує
2 запити замість дванадцяти
- канал із секретом показувався як «секрету немає»: прапорець ставився
лише в гілці розшифровки, а перелік для UI викликається без ключа
Перевірено в браузері проти повного стека: усі вісім сторінок із
живими даними, створення правила й користувача через форми, перевірка
каналу, обмеження глядача, мобільний вигляд на 375 px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Система вміла малювати мапу, але мовчала, коли щось падало. Схема alr.*
лежала готовою з Етапу 1 і повністю порожньою.
Движок:
- обчислює правила з icmp / interface / metric / no_data і не тримає
стану між тіками: вікно for_seconds — це запит по часу до TSDB, тож
перезапуск нічого не збиває
- дві семантики вікна: без agg умова має триматися всі виміри (антифлап),
з agg порівнюється агрегат
- кореляція за топологією: причина аварії — той, у кого лишився живий
сусід; хто оточений мертвими, той наслідок. Заради цього й будувалась
topo.links: інакше падіння маршрутизатора дає сорок сповіщень
- вікна обслуговування, ручне заглушення зі стелею в тиждень
- advisory-блокування на тік: кілька API за балансувальником безпечні
Доставка:
- Telegram, webhook, SMTP; токени в core.secrets тим самим кільцем, що
й паролі від обладнання
- тенант без маршрутів отримує все в усі придатні канали — підключили
Telegram, має працювати
- тиха година не глушить disaster
- вебхуки на внутрішні адреси заблоковано; для self-hosted знімається
прапорцем процесу, бо там ця мережа своя
UI: індикатор у шапці, панель зі списком, ack/mute/close, окремий фільтр
придушених, спільна шина подій замість другого WebSocket.
Знайдено живою роботою й виправлено:
- вимкнене або видалене правило лишало алерти сиротами назавжди
- перехід у suppressed не публікував події — UI дізнавався лише після
перезавантаження сторінки
- кнопки дій на телефоні були 26 px
20 нових тестів, go vet і tsc чисто. Живий прогін: поріг посередині
розділив два справжні пристрої, 5 доставок на 5 подій без повторів.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Досі доступ давав машинний токен зі змінної збірки — одні права на всіх
і жодного способу відрізнити, хто що зробив. Тепер продукт уміє впустити
людину.
Сервер:
- argon2id для паролів, власний HS256 JWT (15 хв) + refresh-сесія в
httpOnly-кукі на 30 днів з ротацією при кожному обміні
- Principal зводить людину й машинний токен до одного набору прав;
права читаються з БД на кожному запиті, а не з claims, щоб відкликана
роль не жила до кінця TTL
- 9 ендпоїнтів: auth/login|refresh|logout|password, me, team CRUD, roles
- netpulse-user — CLI для першого власника: публічна реєстрація в B2B
це дірка, а «перший через веб, поки нікого немає» — нечесна гонка
- міграція 0012: три RLS-політики винятку для шляху входу (без них
вхід неможливий за побудовою — щоб знайти користувача за email,
треба знати тенант, який відомий лише після пошуку) і login_attempts
для тротлінгу
Фронтенд:
- сторінка входу з вибором організації, access-токен у замиканні
модуля замість localStorage, тихе відновлення сесії по кукі
- один refresh на всі паралельні запити: інакше ротація зробила б усі,
крім першого, недійсними й викинула б людину на вхід
- дії без права не показуються; полотно нередаговане для глядача
- мобільний адаптив: висувна бічна панель, інспектор нижнім аркушем
11 нових тестів (37 у httpapi), go vet і tsc чисто, живий прогін з
8 кроків проти netpulse_it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POST /api/v1/maps/{id}/undo повертає полотно до попереднього знімка.
Відкат оформлюється як нова ревізія, а не відмотування лічильника:
інакше клієнт зі старим номером тихо перезаписав би відкочене.
Ідентифікатори вузлів зберігаються, тож ребра прив'язуються назад самі.
В UI: малювання зв'язку від краю вузла, видалення по Delete, кнопка
відкату. Намальоване рукою ребро не прив'язується до topo.links —
лінія на полотні це подання, а не факт про мережу.
Виправлено флак у тестах: TestSchedulerRunsTaskAndFillsCredentials
перевіряв канал статусів знімком, хоча SUCCEEDED надсилається вже після
запису в sink. Під навантаженням падав раз на п'ять; тепер 0 з 8.
Перевірено наживо: намальовано зв'язок -> відкат -> видалено вузол ->
відкат повернув той самий id, ребро й живий стан лінка.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PATCH /api/v1/maps/{id} з оптимістичним блокуванням за revision, плюс
створення, видалення й автопобудова з виявленої топології.
Усі скалярні поля патча — вказівники: перетягування шле лише x/y, і якби
відсутні поля означали порожні, кожен рух миші стирав би стиль, розмір і
прив'язку до пристрою.
Ребро може посилатися на вузол, створений тим же патчем, за client_id.
Той, хто спізнився з ревізією, отримує 409, а не тихо затирає чужу правку.
Знімок пишеться тією ж транзакцією, що й зміна, — інакше в історії лишався
б крок, якого в мапі немає.
Автопобудова ідемпотентна: повторний запуск не дублює вузлів і не скидає
ручну розкладку.
Тестами знайдено: revision <= $2 - $3 з двома нетипізованими параметрами
дає "operator is not unique: unknown - unknown" — потрібні явні касти.
Перевірено: 23 інтеграційні тести API (-race), плюс живий прогін проти
даних, зібраних агентом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Окремий процес від AgentService: у зондів і браузерів різні профілі
навантаження й периметри, спільний лише шар store.
Стан мапи віддається одним викликом разом із живими статусами — інакше
полотно малювалося б сірим і доганяло кольори сотнею дозапитів.
link_status виводиться з кінців лінка, а не читається з topo.links: цю
колонку ніхто не підтримує, а стан лінка це похідна величина. Живий
прогін показував "unknown" на цілком робочому каналі, поки не порахували.
Події для WebSocket пишуться тією ж транзакцією, що й зміна, яку
описують. Транспорт — опитування outbox, а не LISTEN/NOTIFY: NOTIFY не
переживає падіння підписника, а тут потрібна гарантія доставки.
Перевірено: 10 інтеграційних тестів проти живої БД, HTTP і WebSocket
(-race), плюс живий прогін агента, сервера й API разом проти snmpd.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Автовиявлення наповнювало inv.interfaces, але їх ніхто не опитував: на
мапі були лінки й не було трафіку. Тепер сервер формує snmp.if-чек зі
складу портів і штовхає його живій сесії як TaskDelta — без цього після
кожного нового комутатора були б години порожніх графіків.
Чек оновлюється, а не задвоюється: унікальний індекс core.checks включає
md5(params). Склад портів порівнюється як множина, бо порядок ключів у
jsonb не гарантований. Без SNMP-креденшела чек не створюється.
Живий прогін знайшов помилку: OID у запиті йшов без провідної крапки, а
pdu.Name повертається з нею — пошук у мапі мовчки не знаходив нічого, і
чек виглядав як "жоден інтерфейс не відповів". Канонізація тепер у
snmpx.Normalize, застосована з обох боків.
Перевірено наскрізь: виявлення -> автостворення чека -> справжні
HC-лічильники -> ts.if_counters, без жодного ручного кроку.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Спільний SNMP-транспорт винесено в snmpx: ним користуються два модулі.
Звіти автовиявлення йдуть окремим RPC, не телеметричним стрімом — вони
рідкі, великі й не прив'язані до моменту часу так, як метрики.
Живий прогін проти справжнього snmpd+lldpd знайшов дві помилки:
1. Префікс типу чека не збігався з ключем модуля (topo.discover при
плагіні topology). Агент маршрутизує задачі саме за префіксом, тож
зонд відхиляв би їх. Інваріант закріплено обмеженням у БД.
2. Унікальний індекс topo.neighbors схлопував ARP-сусідів: ключ не
включав MAC, а chassis_id/port_id в ARP немає взагалі.
Перевірено на живих даних: інвентар із ifTable, 2 ARP-сусіди, зіставлення
шлюзу за MAC (впевненість 90), зведений лінк із capacity 10 Гбіт/с.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Автентифікація зондів за токеном, побудова TaskPlan із детермінованим
schedule_offset, видача розшифрованих креденшелів із TTL, запис телеметрії
в гіпертаблиці, резолвер сусідів LLDP/CDP у topo.links, прийом конфігів.
Ізоляція тенантів робиться двічі — RLS плюс явний предикат tenant_id, бо
RLS не працює на гіпертаблях, а саме туди йде вся телеметрія.
Агент: -token і передача його в метаданих; MarkAllPending() перереєстровує
серії на початку сесії замість обнуляти нумерацію й губити буфер.
Перевірено на Debian 13 / PG 17.11 / TimescaleDB 2.29.1: 11 інтеграційних
тестів проти живої БД (-race), плюс живий прогін справжнього агента проти
справжнього сервера — телеметрія, статус пристрою, heartbeat у базі.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>