Commit graph

23 commits

Author SHA1 Message Date
387980d655 Картка хоста на вкладках, графіки в шаблонах, пошукові списки
Форма хоста стала вкладками: Хост, Шаблони, Доступи, Збір конфігів,
Ручні перевірки. Інтервалів опитування тут більше немає — вони живуть
у шаблонах, і два джерела правди про одне число розійшлися б саме тоді,
коли треба швидко зрозуміти, чому хост опитується не так, як написано.
Новий хост отримує шаблон «Доступність (ICMP)», а не ручний чек.

Групи, шаблони й доступи обираються пошуком із чипами: список галочок
працює на пʼяти позиціях і перестає працювати на пʼятдесяти.

Графіки описуються в шаблоні (tpl.graphs): шість видів, жорсткі межі
осей, перелік ключів метрик замість посилань на елементи — графік має
право показувати й те, що прийшло з іншого шаблону. Графік без жодного
знайденого ряду не показується: порожня рамка з підписом — це обіцянка
даних, яких немає.

Знайдено живим прогоном і виправлено: обробник виділення на полотні
створював новий масив щорендеру й зациклював оновлення. Цикл зʼїдав
головний потік, і мертвими ставали всі сторінки, не лише мапа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 19:11:24 +03:00
5aba75ccc3 Мапи: режим редагування, доступи груп, іконки, форми вузлів
Редагування вмикається кнопкою. Полотно, яке рухається від випадкового
кліку, — найшвидший спосіб зіпсувати схему, на яку дивиться черговий.

Доступи за групами користувачів (topo.map_permissions): read, write або
deny, deny перекриває решту. Мапа без жодного запису доступна всім, хто
має maps:read — інакше кожна нова мапа була б невидимою до окремого
налаштування. Перевірено: обмежена мапа зникає зі списку, прямий GET
дає 403, PATCH у read-only мапу теж 403.

Виділення рамкою лівою кнопкою (панорамує середня або права), Ctrl і
Shift додають до виділення, груповий драг пише один патч замість
десяти ревізій. Вигляд застосовується до всіх виділених одразу.

Додавання хостів на мапу з пошуком і фільтром за групою; ті, що вже на
полотні, показані сірим. Нові розкладаються сіткою праворуч від
наявних.

Власні іконки (topo.icons): SVG/PNG/JPEG/WebP до 256 КБ у БД, а не у
файловій системі — інакше інсталяція з кількох процесів вимагала б
спільного тому, а бекап продукту перестав би бути бекапом бази. SVG
віддається з CSP і nosniff: інакше завантажена іконка стає збереженою
XSS проти власного ж інтерфейсу.

Форми вузла: картка, пігулка, крапка, картинка, без рамки. Підпис
праворуч, знизу, зверху або відсутній. У форми «картинка» стан показує
обвідка знизу, а не рамка навколо — рамка зʼїдає саму картинку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 18:44:09 +03:00
7fa8adc506 Дашборди, Esc/клік повз вікно, редагування каналів і протоколів
Дашборди: схема core.dashboards лежала з Етапу 1 без жодного рядка коду.
Сітка на 12 колонок, шість видів плиток (графік, число, шкала, список
алертів, сітка хостів, текст), автооновлення з інтервалом дашборда,
режим редагування. Права окремі від maps:* — дашборд збирає дані з
усього тенанта.

Esc і клік повз панель закривають будь-яке вікно. Слухач на document, бо
фокус може стояти де завгодно; закриття за mousedown, а не click, щоб
виділення тексту, доведене за межі вікна, не втрачало набране.

Канали сповіщень редагуються (раніше лише створювались і видалялись).
Протокол доступу змінюється — з вимогою ввести пароль заново, бо секрет
зашифрований під видом старого протоколу.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 18:30:28 +03:00
a4a44b1d76 Сесія переживає перезавантаження, однакові відступи, вигляд вузлів мапи
«Просить логін після кожного оновлення» — дві поломки в одному симптомі.

Перша: RotateSession читав email::text без COALESCE, а пошта стала
необов'язковою разом із входом за логіном. Користувач без адреси міг
увійти й не міг поновити сесію взагалі.

Друга: ротація не переживала подвійного обміну. React у режимі розробки
виконує ефекти двічі, дві вкладки в проді дають ту саму гонку: перший
запит відкликав токен, другий приносив уже відкликаний. Тепер
core.sessions.replaced_by тримає ланцюг, вікно 30 секунд — вистачає на
гонку від того самого клієнта й замало для реального повтору.

PageBody один на всіх сторінках: відступи й прокрутка більше не
залежать від того, хто писав сторінку. Заміряно — рівно 16 px скрізь.

Мапа: видалення (ендпоїнт існував із Етапу 4, кнопки не було) з
підтвердженням, і вигляд вузла — форма (картка/пігулка/крапка), товщина
рамки, заливка, приховування підпису й цифр. Аварія й попередження
світяться ореолом: рамка іншого кольору не помітна периферійним зором.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 18:02:18 +03:00
05adce6e7b Сторінка метрик: графіки замість psql
Свій SVG-графік замість recharts: із сотні кілобайт бібліотеки потрібна
одна ламана, а SVG ще й масштабується без переобчислення на ресайз.

Ряди групуються за одиницею виміру — відсотки й біти на секунду на
спільній осі дають пряму лінію біля нуля. Пропуск у даних розриває
лінію, а не з'єднує сусідів прямою.

Перевірено наживо: 6 годин читаються з сирих даних (крок 72 с),
тиждень — з роллапу 5m (крок 2016 с), підписи портів і одиниці на місці.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 17:33:15 +03:00
cd8d4c62ed Історія метрик, шаблони будь-яких перевірок, маршрути правил
Метрики збиралися в ts.samples і не показувалися ніде — побачити
зібране можна було лише через psql. Додано GET /devices/{id}/series і
/metrics: джерело (сирі дані, 5m, 1h) обирається за потрібним кроком,
бакетизація в БД, пропуск у даних лишається пропуском, а не лінією
через діру.

Знайдено живим прогоном: зонд працює рівно годину. CredentialTTL —
година, Credentials() свідомо не віддає прострочені (щоб не блокувати
облікові записи на пристроях), а поновлення не просив ніхто:
CredentialRequest є в контракті з Етапу 2, сервер його обробляє, агент
не надсилає. Будь-яка інсталяція припиняла збирати SNMP через годину
після старту й мовчала про це.

Шаблон описує перевірки будь-якого типу, не лише OID. Пачкою в один PDU
збираються тільки snmp.get; решта — елемент на чек, слід у
core.checks.template_item_key. Вбудований шаблон «Доступність (ICMP)».
Імпорт/експорт глобальний і поштучний, свій формат замість Zabbix-YAML.

Спільний розклад бекапів із перевизначенням на хості: прапорець
follows_default, а не порівняння значень — власний розклад може
випадково збігтися зі спільним.

Правило саме каже, куди йде його алерт: канали, тихі години, групи
хостів, повідомлення про відновлення. Канали правила перекривають
маршрути повністю.

Доступи до обладнання отримали свою сторінку: SSH-паролі й
SNMP-community заводяться, змінюються й видаляються з вебу. Секрет
назовні не повертається ніколи.

Дрібниці за скаргами: відступи в картках шаблонів, українська множина,
ручний ввід інтервалу опитування, підтвердження видалення з описом
наслідків замість «Ви впевнені?», помітні кнопки видалення замість
сірого ✕ у кутку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 17:21:50 +03:00
2de1894fd5 Етап 6: шаблони опитування + звірка планів зонда
Схема tpl.* (шаблон → елементи → прив'язка до хоста), реконсиляція в
core.checks, REST, редактор у вебі, чотири вбудовані шаблони SNMP.

Елемент шаблону — одна метрика; у чеки вони групуються за (шаблон, тип,
інтервал) в один snmp.get. Сотня окремих чеків замість однієї пачки —
це сотня SNMP-сесій там, де досить кількох PDU. Позначка template_id у
core.checks дає реконсиляції право власності: без неї відв'язування
шаблону не знало б, що прибирати.

Звірка планів раз на 5 секунд — те, чого бракувало весь час. Чеки міняє
REST-процес, живу сесію зонда тримає AgentService; досі будь-яка зміна
доїжджала до зонда лише при обриві зв'язку, тобто ніколи.

Знайдено живими прогонами й виправлено:
- креденшели не їхали разом із планом, і хост, приписаний зонду після
  його підключення, падав на кожній задачі з «немає SNMP-креденшелів»;
- форма хоста відв'язувала зонд: поле починалося порожнім, підпис
  обіцяв «не змінювати», сервер трактував порожнє буквально;
- hrProcessorLoad.1 у базовому шаблоні — здогадка, а не адреса: на
  net-snmp «No Such Instance».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:07:54 +03:00
436eff343d Етап 7: планувальник бекапів за cron
Збір конфігів перестав залежати від того, чи згадає людина натиснути
кнопку.

Свій парсер cron (internal/cronx) замість залежності: бітові маски
uint64 на поле, пошук наступного запуску покроково по хвилинах із
запобіжником у чотири роки. Правило dom/dow — об'єднання, як у справжнього
cron. Неможливий розклад (30 лютого) чесно відмовляє замість зациклення.

Планувальник тікає раз на хвилину під advisory-блокуванням, тож у
кластері розклад розкручує рівно один екземпляр. Спершу переноситься
next_backup_at, потім ставиться завдання: падіння між кроками коштує
одного пропущеного бекапу, зворотний порядок дав би нескінченну чергу.

Форма розкладу: чотири пресети плюс довільний cron. Виправлено помилку,
через яку пункт «свій розклад…» нічого не робив — обробник select
відсікав порожнє значення, тобто саме той випадок, заради якого пункт
існує.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:54:07 +03:00
cdf5f1fb54 Візуальний diff конфігів
Збір працював, але подивитись на зібране було ніде: жодної сторінки, а
lines_added завжди 0 — порівняння ніхто не рахував.

difftext: власне порядкове порівняння з ділянками й контекстом.
Бібліотека принесла б підтримку слів, символів і кольорів у терміналі —
десяток речей, які тут не знадобляться.

- спільний початок і кінець відкидаються до основного алгоритму: у
  конфігах змінюється кілька рядків із тисячі, і квадратична таблиця
  будувалася б там, де досить порівняти десяток
- понад чотири мільйони клітинок дають truncated і грубу заміну блоку:
  точність там нічого не дає, а чесна позначка краща за правдоподібний,
  але вигаданий diff
- сусідні зміни зливаються в одну ділянку, інакше контекст дублюється

Результат кешується в ncm.diffs — diff двох версій незмінний назавжди.
Підсумок +N/−M заразом дозаписується у version, щоб список історії не
розшифровував два тіла на кожен рядок.

UI: сторінка «Конфіги» — хости зліва, історія й diff справа. Порівняння
з попередньою версією відкривається одразу: питання завжди одне — що
змінилось цього разу.

Перевірено наживо: друга версія стенду дала @@ −8,3 +8,4 @@ з одним
доданим рядком і контекстом, лічильник +1 −0 дозаписався, повний текст
на 292 байти читається.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:30:30 +03:00
3816e2cfcf Етап 7: збір конфігів запрацював наскрізно
Профілі були даними без виконавця. Тепер ланцюг замкнено: черга в БД →
диспетчер у AgentService → зонд по SSH/Telnet → вивантаження стрімом →
звірка sha256 і дедуплікація.

Агент (internal/ncmx):
- усе побудовано навколо пошуку промпту: у консолі немає ані коду
  завершення, ані довжини відповіді
- промпт шукається лише в хвості 512 байтів, інакше "banner motd #"
  обривав би збір на середині конфігу
- дедлайн на паузу між байтами, а не на всю операцію: збір із шасі
  триває хвилини, а тиша означає завислий пристрій або хибний regex
- ключі SSH обладнання не звіряються свідомо: залізо міняє їх з кожною
  прошивкою, і known_hosts на сотні пристроїв означав би не збирати
  конфіги зовсім

Сервер: черга ncm.jobs із FOR UPDATE SKIP LOCKED (два екземпляри не
надішлють одне завдання двічі), диспетчер, прибирання завислих.

Знайдено живим прогоном:
- сервер ігнорував поле encoding: агент стискав gzip і рахував sha256
  від оригіналу, сервер рахував від стиснених байтів і відхиляв кожен
  бекап. Поле в контракті з Етапу 2, реалізації не було — інтеграційний
  тест користувався "none" і повз дірку проходив
- промпт обрізався не по рядку: шаблон [>#]\s*$ ловить лише символ, і
  в конфізі лишалось ім'я пристрою окремим рядком
- у конфіг потрапляли escape-послідовності bash і подвоєні \r\r\n від
  PTY: 295 байтів / 12 рядків замість 286 / 10. Після виправлення —
  285 / 10, різниця лише у фінальному переводі рядка

Живий прогін проти стенду: success, конфіг 285 байтів, повторний збір —
unchanged без другої версії.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:02:50 +03:00
ee3f653bd8 Каталог команд збору конфігу як власні дані
Команди для зчитування конфігу тепер живуть у db/profiles/catalog.json —
147 платформ, 67 вендорів. Міграція з нього породжується збіркою:
два описи одного й того самого розійшлися б із першою ж правкою, і
невідомо було б, який справжній. build.py --check звіряє, чи міграція
не відстала.

Каталог — код, а не дані клієнта: однаковий для всіх інсталяцій,
переглядається в code review, їде з релізом. Тенант при цьому може
завести власний профіль через tenant_id — вбудовані лишаються
недоторканими.

Промпт і вимкнення пейджера тримаються раз на родину CLI, а не в
кожному профілі: bdcom, arista, brocade і ще з десяток говорять
діалектом Cisco, h3c і 3com — діалектом Huawei. Це різниця між правкою
в одному місці й правкою в сорока.

Прибрано разовий імпортер db/import разом із його залежністю від
зовнішнього формату. Слово NOC лишилось тільки там, де воно означає
центр керування мережею (NOC-екран, NOC TV) — це термін із ТЗ.

Лічильники в шапці міграції обчислюються: зашите «148 платформ»
розійшлося зі згенерованими 147 одразу.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 02:46:23 +03:00
f162dcab19 Імпорт профілів збору конфігу з NOC Project
147 профілів на 67 вендорів у ncm.profiles: Cisco, Huawei, Juniper,
MikroTik, Eltex, D-Link, HP, Brocade, Extreme, Alcatel, Allied Telesis,
Qtech, ZTE, BDCOM та інші.

Розбір через ast, без виконання: NOC-скрипт тягне половину свого
фреймворку, і імпортувати його означало б або принести весь NOC, або
підсунути заглушки, які мовчки змінюють поведінку.

З кожного get_config.py береться перша команда збору, гілка startup,
strip_first_lines і регекси платформ. Альтернативи лишаються в
коментарі. Шум (exit, sh, changeto system, terminal width 200)
відсіюється явним списком: Cisco.ASA після фільтра дає рівно
more system:running-config, Juniper.JUNOSe — рівно
show running-configuration замість чотирьох команд із сусідніх гілок.

Пейджер додається лише мережевому CLI: Eltex SMG і TAU знімають конфіг
через cat, і terminal datadump у bash просто впав би.

Важливо: команди — з NOC, а промпти й вимкнення пейджера — ні. У дампі
всі __init__.py порожні (359 із 364), а NOC тримає pattern_prompt саме
там. Вони проставлені за родиною вендора з типових значень і потребують
перевірки на живому залізі.

Не витягнулись 23 з 170, і не через помилку: 21 знімає конфіг по HTTP
(камери, відеокодери, MikroTik SwOS, HP iLO2), 2 — порожні заглушки в
самому NOC.

Виконувати профілі поки нікому: агентського модуля ncm (SSH/Telnet)
немає, це Етап 7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 02:34:55 +03:00
48848bd3a5 Опитування хоста з форми й повне редагування користувачів
Форма створення хоста збирала назву, адресу, тип і групи — і не
збирала головного. Хост, доданий через UI, не опитувався взагалі:
жодного рядка в core.checks. Наявні хости на стенді працювали лише
тому, що їхні перевірки засіяні через SQL.

Опитування:
- POST /api/v1/devices приймає перевірки одразу; новий хост у формі
  починає з icmp.ping — єдиної перевірки, яка працює будь-де без
  налаштування
- поля параметрів будуються з params_schema, яку віддає сервер, а не
  з захардкодженого списку: інакше кожен новий тип від плагіна вимагав
  би перезбирання фронтенду
- правка йде за id, а не перестворенням: унікальний індекс включає
  md5(params), тож зміна параметрів створила б другу перевірку того
  самого типу
- доступи (SNMP-community, SSH) шифруються тим самим кільцем, що й
  секрети каналів, і прив'язуються до хоста

Користувачі:
- PATCH /api/v1/team/{id} міняє роль, логін, пошту, імʼя й пароль одним
  запитом; зміна пароля відкликає всі сесії
- профіль редагується лише в того, хто працює тільки в цій організації:
  core.users глобальна, і адмін філії не має міняти пароль тому, хто
  тим самим акаунтом заходить у сусідню — той навіть не дізнався б.
  Спроба дає 409 shared_user

Знайдено при написанні: core.plugins не має колонки enabled — активація
на тенанта живе в core.plugin_installs, і вона порожня, тому базові
плагіни доступні без явного встановлення.

Живий прогін: 8 типів із 9, три перевірки записались, правка інтервалу
не задвоїла, зміна пароля пустила новим і відхилила старий, зміна ролі
собі — 403.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 02:22:56 +03:00
96692ec9df Вхід за логіном, групи хостів і права доступу
Логін:
- 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>
2026-08-24 02:00:04 +03:00
36f29e23ec Повноцінний веб: навігація і вісім сторінок
До цього «веб» був одним екраном: логін, мапа й панель алертів. Усе,
що вміло API, доводилось викликати curl-ом.

Каркас:
- бічна навігація з мобільною шухлядою, індикатор зв'язку в шапці,
  живий лічильник алертів
- пункт, на який немає права, не показується взагалі; пряме посилання
  лишається робочим і дає пояснення з назвою потрібного права
- стартова сторінка залежить від ролі: глядача без прав на мапи вітати
  відмовою — поганий перший екран

Сторінки: мапа, пристрої з пошуком і картками алертів, алерти з
фільтрами, правила зі створенням через форму, канали з кнопкою
перевірки, зонди, команда з керуванням ролями, профіль зі зміною
пароля.

DataTable на телефоні перестає бути таблицею: горизонтальний скрол на
375 px робить дані формально присутніми й фактично нечитабельними.

Знайдено роботою з живим UI:
- WebSocket жив усередині мапи, тому на решті сторінок живих оновлень
  не було взагалі — лічильник алертів замерзав, щойно людина йшла з
  мапи. З'єднання винесено в модуль-одинак, яким володіє оболонка
- сторінка пристроїв перечитувала все на кожну подію алерту; зі
  злиттям сплеску й спільними алертами той самий сплеск коштує
  2 запити замість дванадцяти
- канал із секретом показувався як «секрету немає»: прапорець ставився
  лише в гілці розшифровки, а перелік для UI викликається без ключа

Перевірено в браузері проти повного стека: усі вісім сторінок із
живими даними, створення правила й користувача через форми, перевірка
каналу, обмеження глядача, мобільний вигляд на 375 px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 01:17:57 +03:00
215bf8c4db Етап 9: алерти й сповіщення
Система вміла малювати мапу, але мовчала, коли щось падало. Схема 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>
2026-08-15 11:09:19 +03:00
a4faf31fcd Етап 5: користувачі, вхід і права
Досі доступ давав машинний токен зі змінної збірки — одні права на всіх
і жодного способу відрізнити, хто що зробив. Тепер продукт уміє впустити
людину.

Сервер:
- 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>
2026-08-15 10:24:58 +03:00
bee55be3d1 Етап 4: редактор мапи — зв'язки, видалення, відкат
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>
2026-08-14 21:46:18 +03:00
8bf2902522 Етап 3: запис у мапу — редактор полотна
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>
2026-08-14 15:58:16 +03:00
328850be06 Етап 3: REST/WebSocket API для UI
Окремий процес від 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>
2026-08-14 15:43:02 +03:00
aaf067a46b Етап 2: сервер сам заводить snmp.if-чеки з виявлених інтерфейсів
Автовиявлення наповнювало 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>
2026-08-14 15:10:02 +03:00
ad7672c32f Етап 2: модуль topology — LLDP/CDP/ARP/FDB та інвентар портів
Спільний 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>
2026-08-14 14:42:01 +03:00
8a92ef8a45 Етап 2: серверна сторона AgentService
Автентифікація зондів за токеном, побудова 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>
2026-08-14 04:13:40 +03:00