Три вади, знайдені рецензією, яких щасливий шлях показати не міг:
* outcome='sent' писався до доставки; помилка читання каналів клала в
кеш порожню мапу й з'їдала всі сходинки кабінету за тік — усі зі
слідом «надіслано». Канали тепер читаються до просування стану,
журнал пишеться після доставки, з правдою.
* UPDATE не мав stopped_at IS NULL — підтвердження алерту посеред
партії не рятувало людину від дзвінка.
* час брався раз на партію.
Плюс суміжне: UpdateRule не гасив алертів вимкненого правила, сервер
домислював enabled на оновленні, channel_ids сходинок не звірялись із
каналами кабінету (зокрема чужого).
І scripts/dbtest.sh — тести проти бази перестали мовчки пропускатись.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Перший справжній прогін на живій мережі дав 18 порушень із 28: типові
SNMP-community на всіх шести хостах, telnet на керуванні на чотирьох,
паролі відкритим і зворотним текстом. Механізм працює — тому з
результатом тепер треба щось робити.
РЕДАГУВАННЯ. Вбудовані правила замкнені на те, що визначає ПИТАННЯ
(name, kind, pattern, config_type) і відкриті на політику кабінету
(enabled, severity, selector, remediation). Причина замка — доказ:
тест читає зразки з міграції й показує для кожного конфіг, де він
мусить спрацювати і де не мусить. Переписаний руками зразок цього
доказу не має, а значок «вбудоване» лишається — у звіті для аудитора
рядок означав би вже не те, що в довіднику. Для правок є копія.
Перевірка зразка на живому конфізі ДО збереження: віддає рядки з
номерами й окремо розрізняє «конфігу немає» від «нічого не знайшов».
Для правил «не має бути» нуль збігів підсвічується: це те саме, що
показало б правило з опискою.
ЗНАХІДКИ РЕЦЕНЗІЇ — всі пʼять підтверджені:
1. Перше збереження будь-якого вбудованого правила стирало результати.
Селектор порівнювався в базі, але порівнювались різні представлення
одного значення: міграція кладе {}, Go марширує сім ключів із null.
Тепер порівняння за ЗНАЧЕННЯМ у Go, колонка канонізується сама.
2. CSV приймав ін’єкцію формул — у клітинку йде сирий рядок конфігу, а
файл відкриває аудитор. Одне місце екранування на всі три звіти:
дублювати захист у трьох файлах означає забути його в четвертому.
3. Знахідки вимкнених правил і зниклих хостів лишались назавжди й
рахувались як чинні. Три заслони: фільтр у списку, прибирання при
прогоні, і звіт їх не рахує.
4. Лічильники в списку правил рахувались по всіх хостах повз права —
інженер філії бачив «5 з 12», а в знахідках дві. Тепер це одне
число, а не два.
5. Доказ перевірки зразка лишався на екрані після правки зразка — тобто
ручка робила протилежне до задуманого в мить найвищої довіри.
Продукт показував syslog З ПРИСТРОЇВ, аудит, черги, стенограми команд —
усе про мережу, і нічого про себе. Щоб дізнатись, на що лається сам
NetPulse, треба було заходити по ssh.
Буфер у памяті, не в базі: журнал, що пише в Postgres, замовкає рівно
тоді, коли ляже Postgres — у найцікавіший момент. Стеля в БАЙТАХ
(8 МіБ на процес), а не в рядках: один рядок із текстом SQL-помилки
буває довшим за сотню звичайних, тож «5000 рядків» означало б
непередбачувані десятки мегабайтів.
Два процеси — один перелік із позначкою джерела, а не дві вкладки:
людина знає симптом («о третій ночі перестали йти сповіщення»), а не
те, який із двох процесів за це відповідає.
Маскування — наявним gitstore.Scrub, не своїм: паролі DSN, матеріал
DEK, секрет підпису сесій. Це другий рубіж — відомі шляхи вже почищені
в місці народження, але кільце робить журнал видимим у браузері й
вивантажуваним у файл, що піде в тікет.
Межі написані НА СТОРІНЦІ, а не лише в документації: журнал не
переживає перезапуску й не покаже причини падіння бази. Поруч —
скільки записів витіснено: без цього числа не відрізнити «нічого не
сталося» від «сталося стільки, що початок уже не влазить».
Палітру приглушували тоді, коли мініатюра малювала РИСКИ. Для суцільної
риски контраст і є тією «жирністю» — тьмяніша риска виглядає тоншою.
Для дрібної ЛІТЕРИ все навпаки: вона складається зі штрихів в один
піксель, і кожен відсоток контрасту, який у неї забрали, це відсоток
розбірливості. Тьмяний шрифт такого розміру читається саме як
«нечіткий», хоча жодного розмиття в ньому немає.
Одна й та сама зміна лікує риски й калічить текст. Малюємо текст —
контраст потрібен.
Шрифт 5 px (округлюється до цілого пікселя пристрою), рядок 6 px.
Чіткість дрібного шрифту тримається на двох речах, і обидві про
піксельну сітку, а не про розмір. Позицію рядка вирівняли минулого
разу; бракувало другого — САМОГО РОЗМІРУ.
5.5 px при щільності екрана 1.25 це 6.875 фізичного пікселя: кожна
риска літери потрапляє між пікселями й розмазується. Тепер розмір
округлюється до цілого фізичного (5.6 px, що дає рівно 7), і штрихи
стають різкими без утрати розміру.
Саме тому подальше зменшення «на око» не працювало б: винен був не
розмір, а те, що він не лягав на сітку.
Менше — на прохання. Чіткіше — виправленням того, чого бракувало з
самого початку: полотно масштабоване на dpr, тож координата 9 при
масштабі 1.25 дає 11.25 фізичного пікселя, і браузер розмазує літеру
між двома. На дрібному шрифті це читається як «нечітко», хоча винен не
розмір, а півпікселя зсуву.
Тепер позиції округлюються до цілого пристроєвого пікселя. Розмиття
зникає без жодної втрати, і саме тому шість пікселів тепер працюють
там, де раніше не вистачало семи.
Скільки не підбирай товщину, колір і щільність — прямокутник не стає
літерою. Арифметика: щоб текст читався, символу треба 5-6 пікселів;
при 120 символах у смузі 180 px виходило 1.4. Читабельним він не міг
стати за жодних налаштувань, і три попередні спроби це лікували
симптоматично.
Тепер canvas друкує ПОЧАТОК рядка шрифтом 7 px — найменшим, на якому
моноширинний ще лишається літерами (на 6 зникають внутрішні просвіти в
e, a, o). Ширина символа вимірюється, а не рахується формулою: у різних
системах підставляється різний моноширинний шрифт.
Для конфігу початок рядка й несе структуру: відступ і ключове слово
стоять зліва, а хвіст з адресами не читався б однаково. Табуляція
розгортається в пробіли — у canvas вона не має ширини, і блоки JunOS
з їхали б до лівого краю.
Рядків у вікні стало близько вісімдесяти замість двохсот. Це обмін, а
не втрата: двісті нечитабельних показують силует файла, вісімдесят
читабельних показують, ДЕ ти в ньому.
Запасний шлях прямокутниками лишився для порівняння версій, де рядки
беруться з різниці, а не з файла.
Дві причини мазанини, і обидві не там, де я спершу шукав.
ШИРИНА. 280 символів рядка тиснулись у 180 пікселів — 0.64 пікселя на
символ. Проміжок між словами виходив менший за піксель і зникав при
округленні, тож рядки ставали суцільними рисами. Тепер 120 символів:
1.4 пікселя на символ, проміжок тримається цілим пікселем. Хвости
довгих рядків на карту не потрапляють — для мережевих конфігів це
майже нічого не коштує, бо відступ і ключове слово стоять на початку.
ЯСКРАВІСТЬ. На майже чорному тлі світло-сірий штрих читається як
жирний навіть в один піксель: око бачить не товщину, а контраст. Саме
тому попереднє зменшення висоти штриха мазанини не прибрало.
Палітра на крок тьмяніша, різниця між видами рядків збережена.
Скарга: «жирний, зливається, текст не видно — просто лінії». На конфігах
із довгими схожими рядками (ZTE на 33 тисячі) це перетворювалось на сіру
масу.
Причина не в кольорі й не в товщині шрифту як такій: світлий текст на
майже чорному тлі Windows малює помітно товщим, ніж він є. Типове
лікування — -webkit-font-smoothing: antialiased — на Windows у Chrome не
працює взагалі, воно лише для macOS. Тому зроблено те, що там справді
діє: рядок 22 px замість 20 і невеликий міжлітерний просвіт.
Розмір лишився 12 px навмисно: на 33 тисячах рядків кожен зайвий піксель
висоти — це ще один екран прокрутки.
ROW_H оновлено разом із класом: воно мусить збігатися з розміткою, бо
за ним рахується віртуальна прокрутка й позиція рамки на мінікарті.
ФАЙЛИ В /var/log/netpulse. rsyslog читає journald і розкладає по файлах
на службу. Драйвер docker лишається journald, а не syslog: syslog
віддав би рядки назовні й нічого не лишив докеру, тож `docker logs`
замовк би назавжди — і мовчання виглядало б як «служба нічого не пише».
Тепер працюють усі три шляхи: docker logs, journalctl, файли.
Дорогою три власні помилки, кожна виглядала як «rsyslog не працює»:
1. Умова матчила $!CONTAINER_NAME — метадані журналу. journalctl їх
бачить, а до правил rsyslog вони доходять не завжди. Каталог
створювався й лишався порожнім.
2. Тег «netpulse/{{.Name}}» здавався охайнішим, але rsyslog обриває
programname на скісній: для ВСІХ контейнерів вона ставала просто
«netpulse». Тег тепер — саме ім'я контейнера, воно й так має
префікс проєкту.
3. Шаблон `%msg:::sp-if-no-1st-sp,drop-last-lf%` давав ПОРОЖНІЙ
текст: файли були, рядки були, слів не було. Журнал виглядав
робочим і не містив нічого — найгірший різновид поломки.
Конфігурації в deploy/, щоб їхали клієнтам, а не лишались разовим
налаштуванням одного сервера.
МІНІКАРТА. Штрих рядка був 2 px при 3 px на рядок — просвіт в один
піксель. На дробовому масштабі екрана (1.25, 1.5 — тобто на більшості
ноутбуків) він губився при округленні, і рядки злипались у суцільну
пляму: мінікарта показувала не форму конфігу, а сірий прямокутник.
Тепер штрих 1 px, просвіт удвічі товщий за нього й переживає будь-яке
округлення. Малюнок став блідішим — це правильний бік розміну: на
мінікарту дивляться, щоб побачити структуру, а не прочитати текст.
МЕНЮ. Шість груп, разом 1246 px при вікні 720 — довше за екран. Тепер
згортаються, стан живе в localStorage як перелік ЗГОРНУТИХ (не
розгорнутих: інакше кожна нова група приїхала б до наявних користувачів
схованою). Ключ групи окремо від заголовка — підписи тут уже
переписували, і якби ключем був заголовок, кожне переписування слова
тихо скидало б налаштування всім.
Група з активною сторінкою розгортається сама, але рівно одна — вибір
для решти не чіпається. Правка стану на ЗМІНІ шляху, а не на кожному
рендері, тож згорнути активну групу теж можна.
Дві наявні поломки мобільного вигляду, знайдені дорогою: рейка
вмикалась класами md:, а розгалуження заголовок/риска було на JS і про
md: не знало — людина згортала панель на моніторі, і в шухляді на
телефоні ЗНИКАЛИ ВСІ назви розділів.
ЛОГО. Пульс на лінії зв'язку: вузол → сплеск → провал → норма → вузол.
Два вузли по краях — ключова деталь: без них це іконка «activity» з
будь-якого безкоштовного набору, з ними кардіограма стає лінією між
двома точками мережі. SVG кодом, не растр: слідує темі через
currentColor, важить сотні байтів.
Три РІЗНІ малюнки, а не один масштабований: повне лого, знак, і
спрощений для 16 px усередині favicon.ico (зібраний власним
кодувальником на zlib, без сторонніх інструментів). Пульсація вузла —
опційна й типово вимкнена: у шапці вона конкурувала б зі справжнім
показником зв'язку, який стоїть поруч.
ПОЛЯ З ПІДКАЗКОЮ. Field малює підказку під контролем, тож у ряду з
вирівнюванням по низу колонка з підказкою вища — її підпис і поле
спливають угору, а гола кнопка з'їжджає вниз. Видно було в словнику
трапів; та сама вада знайшлась у редакторі фільтра портів.
Ряди вирівняно по верху, а кнопки без підпису загорнуто у FieldGap —
порожній підпис такої самої висоти. Відступ у пікселях полагодив би
один ряд і роз'їхався б від зміни розміру шрифту.
Перевірено очима в живому Chrome: іконки віддаються (4/4, правильні
типи й розміри), лого 64×64 на сторінці входу, праве коло справді
sky-400 і пульсує лише там.
ЖУРНАЛИ БЕЗ СТЕЛІ. На запитання «чому не пишемо в /var/log» правильна
відповідь — «бо в контейнері це не той шар»: файл усередині зникає при
перестворенні, невидимий для docker logs і вимагає власної ротації.
Але за питанням стояла справжня вада: драйвер json-file був
налаштований порожньо, тобто НЕ КРУТИВ НІЧОГО. Проксі за добу набрав
27 МБ; до повного диска були місяці, і першим ліг би Postgres.
Тепер 10 МБ × 3 файли на службу — близько 250 МБ на інсталяцію, і
драйвер міняється однією змінною (journald, syslog) для тих, кому
потрібні справжні файли або чужий збирач.
САМОПЕРЕВІРКА ЗОНДА чекала хвилину й одного разу вже дала хибне
червоне: зонд зареєструвався, просто пізніше — перший старт припадає
на найзавантаженішу мить установки. Тепер три хвилини з повідомленням
кожні півхвилини: мовчазна пауза невідрізненна від зависання, а хибне
червоне після успішної установки коштує години пошуку неіснуючої
поломки.
GITSTORE: TestDeleteBranchLocal кличе зовнішній git і не мав перевірки
на його відсутність — падав у golang:1.25-alpine, тобто саме там, де
його проганяють. Сусідній тест таку перевірку має.
ЕТАП 13 у ROADMAP: реєстр образів, релізи, пакети. Записано, чому
порядок саме такий (пакет без реєстру ставив би «зберіть самі») і чого
треба досягти до першого тегу — оновлення з версії на версію не
перевіряв ніхто, а ламається найчастіше саме воно.
П'ять паралельних задач. Найцінніше в них — не можливості, а знайдене.
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 працює справжній.
Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.
ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ
0058 подієві алерти: syslog, ncm, compliance спрацьовують у мить
події; правило з нереалізованим джерелом більше не зберігається
мовчки
0059 snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
описуються шаблоном, а не Go
0060 відкат конфігу: план як різниця, маскування паролів із підписом
плану, обов'язковий контрольний збір, verifying при обриві
0061 кнопки Telegram: довге опитування, авторизація не з callback_data
0062 аудит і архів хостів; тест на AST, що падає на ключі без назви
0063 RLS: три ролі, окремий пул для фонових тактів
0064 строки зберігання даних і сторінка сховища
0065 приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
переклад v1→v2 за RFC 3584 дає правильний OID
0066 ескалації сповіщень
0067 алерт про вичерпання диска
0068 поля заливки конфігу переїхали в каталог профілів
Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.
ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ
netpulse установник: одна команда замість 18 змінних і
593 рядків інструкції
RLS з першого запуску нова інсталяція під політиками одразу;
RLS-EXISTING-INSTALL.md лишається тільки для
старих інсталяцій
.forgejo + CI раннер не зареєстрований
Ці три перевірені компіляцією й міркуванням, але не виконанням.
ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ
Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.
Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.
Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
Знайдено на живому ZTE C320. Вхід чекає «Username:» з дедлайном читання
у дві секунди, а дедлайн сокета липкий: він лишався на всі наступні
читання.
Наслідок подвійний. Довгий конфіг обривався на середині помилкою
«read tcp: i/o timeout» — мережевою там, де мережа ні до чого. А коли
ще й запрошення не збігалось зі зразком, той самий дедлайн спрацьовував
раніше за власний таймер, і замість «не дочекались запрошення» людина
бачила ту саму мережеву помилку.
Щоб це побачити, довелося спершу полагодити діагностику: стенограма
починалась після входу, тобто після того місця, де все зупинялось.
Тепер розмова входу пишеться теж — без пароля, лише те, що надіслав
пристрій.
Плюс вбудований профіль zte-zxan: наявні профілі ZTE чекають «>» або
«]», а OLT показує «ZXAN#». Окремий профіль, бо в Comware трапляються
рядки з самої решітки, і зразок із «#» обірвав би їхній конфіг.
Результат на живому C320: 26 555 рядків.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Регресія від переносу списку в портал. Портал рве DOM-дерево, але не
дерево React: подія з нього спливає до компонента-батька, тобто до
модального вікна. Воно закривається за натисканням «повз панель» — і
клік по пункту, який фізично лежить у body, вважався кліком поза формою.
Зупиняємо спливання на самому списку. Клік по підкладці закриває форму,
як і раніше.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Два дефекти на одному екрані.
Модальні вікна ставали аркушем знизу під 768 px. Вікно браузера на
740 px — не телефон, а десктоп у півекрана, і аркуш там притискався до
низу й обрізався. Поріг знижено до 640 px.
Випадний список малювався absolute усередині вікна з власною
прокруткою, і та його обрізала: видно було стільки пунктів, скільки
лишалось місця до краю форми. Тепер портал у body з fixed-координатами
від поля, з розгортанням угору, коли знизу тісно. Портал, а не більший
max-height: обрізає будь-який предок із прокруткою.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Питання «чому всі профілі ssh» виявило справжню помилку. Список
правильний: ssh — розумний типовий вибір. Але це поле ВИРІШУВАЛО
транспорт, а профіль описує модель, не конкретну коробку.
Хост зі старою прошивкою, де є лише telnet, і з чесно заведеним
telnet-доступом усе одно набирався по SSH — і не збирався ніколи.
inv.credentials.proto вже ніс потрібну відповідь і доїжджав до зонда
невикористаним.
Тепер транспорт бере доступ; профільне поле лишилось підказкою й діє,
поки доступу немає. Логіка винесена в jobTransport() — щоб її можна
було перевірити без бази й без пристрою.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 вбудованих профілів досі жили лише в базі — виправити команди під
свою прошивку було ніяк. Тепер є сторінка з пошуком і формою.
Вбудований не правиться, а перекривається власною копією з тим самим
ключем: вони спільні для всіх кабінетів, і правка під одну прошивку не
має міняти їх усім. resolveProfile уже віддавав перевагу тенантському,
тож нічого дописувати не довелося.
Профіль — це три регулярні вирази й перелік команд, і порожня форма з
такими полями не підказує нічого. Тому три робочі заготовки, кнопка
«За зразок» на кожному вбудованому й приклад у кожному полі.
Вирази компілюються при збереженні: інакше про друкарську помилку
дізнаються з бекапу, який завис, чекаючи неіснуючого запрошення.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Чотири види правил над зібраними конфігами — має містити, не має
містити, збіг за виразом, немає збігу. jsonpath зі схеми свідомо не
реалізовано: він для конфігів у JSON, а писати його без жодного такого
пристрою під рукою означало б писати навмання.
Перевірка читає вже зібране й не створює сесій до заліза, тому прогін
синхронний і безкоштовний для мережі. Хост без конфігу пропускається,
а не рахується проваленим: «ще не збирали» і «не відповідає» — різні
речі, і плутати їх означає ховати справжні знахідки.
Знахідка показує рядок і його номер. Для правил «має бути» рядка немає,
і таблиця так і пише: нічого — саме це й проблема.
Вираз компілюється при збереженні, а не під час перевірки, інакше про
друкарську помилку дізнаються з правила, яке мовчки нічого не знаходить.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Телевізор нікуди не залогиниш — зранку на стіні висітиме форма входу
замість карти мережі. Тому /tv/<токен> розгалужується до перевірки
сесії, а доступ дає токен у посиланні.
Публічний зріз навмисно вузький: розкладка, алерти, хости й метрики
ЛИШЕ тих хостів, які показані на цьому дашборді. Ширший доступ був би
простішим у коді й перетворив би забутий лінк на ключ до кабінету.
Плитки ті самі, що в кабінеті: джерело даних підмінюється контекстом.
Дорогою знайдено дві помилки. Вбудовані icmp-тригери мали умову у
формі, якої движок не розуміє, і мовчали, засипаючи журнал помилками —
виправлено міграцією 0026 і в редакторі. База стенда виявилась у
SQL_ASCII: тепер міграція відмовляється накочуватись на не-UTF8, а
compose задає кодування явно.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Дванадцять пунктів у стовпчик означали читати всі дванадцять, щоб
знайти один. Тепер чотири групи за питанням, з яким відкривають меню:
огляд, мережа, налаштування збору, організація. Група без доступних
ролі пунктів зникає разом із заголовком; у згорнутому вигляді заголовок
замінює риска, а назва групи переїжджає в підказку.
Зсув значків мала не розмітка (0.4 px), а смуга прокрутки — вона
забирає ширину справа й тягне колонку вліво. Плюс значок сидів у
коробці w-4 з паддінгом px-3, тобто з нульовою шириною вмісту, і
емодзі різної ширини центрувались випадково. Тепер квадрат h-5 w-5 з
flex-центруванням, смуга прокрутки схована. Зсув рівно 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
«Правила» пішли з бічної панелі у вкладку поруч з «Алертами»: тригери
переїхали в шаблони, а окремий пункт меню й далі пропонував заводити
правила там, звідки їх прибрали.
Кнопки перестали бути виділюваним текстом — у них типово
user-select: auto, і в підпис можна було поставити блимаючий курсор.
Бічна панель згортається у значки, стан у localStorage.
Дорогою закрито дірку: вбудований шаблон нікого не сповіщав. Тригери
стають правилами лише при збереженні шаблону, а вбудований нікому не
належить і не зберігається — хост із icmp-basic збирав пінг і мовчав
про недоступність. Плюс індекс правил з 0024 не враховував тенанта,
через що другий кабінет із тим самим вбудованим шаблоном отримав би
помилку унікальності.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Правило «процесор вище 85% — це проблема» описує клас пристроїв, а не
окремий хост. Тепер воно живе поруч із перевірками, які дають йому
дані, а не окремою сторінкою, де його доводилось повторювати руками
для кожного комутатора.
Тригер розгортається в ОДНЕ правило alr.rules із селектором за
шаблоном, а не в правило на кожен хост: призначили шаблон новому
пристрою — він одразу під правилом, без перегенерації.
Форма шаблону розкладена на вкладки Загальне/Перевірки/Графіки/Тригери
з лічильниками; смуга вкладок винесена у спільний компонент. Умова
тригера редагується полями, JSON лишився запасним виходом.
Правила з шаблону в списку правил помічені й не редагуються там.
Клон копіює тригери вимкненими, щоб копія не подвоїла сповіщення.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
go-git, чистий Go без cgo. Голий репозиторій на тенанта, гілка на
пристрій за ідентифікатором (ім'я змінюють, історія не має від цього
розсипатись), файл за іменем. Однаковий вміст нового коміту не створює.
netpulse-gitsync переливає накопичену історію й відтворює втрачений
репозиторій із бази — тіла конфігів там і так лежать зашифрованими.
У вебі з'явився вибір версії, з якою порівнювати: сервер це вмів
(?from=), інтерфейс — ні.
Дорогою виправлено чотири тести, які CI запустив уперше з базою.
Серед них справжня помилка: машинний токен не міг читати мапи —
ACL мап отримував порожній рядок замість uuid і давав 500.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порти на вузлі отримали імена (l/r/t/b) з парою «джерело + ціль» під
спільним іменем — інакше збережене ребро не знаходить пари й лінія
не малюється.
Бік підключення тепер правиться: UPDATE ребра розрізняє три стани —
не чіпати, зняти прив'язку (auto), задати конкретний бік. Раніше
UPDATE боків не торкався, і криво причеплену лінію лишалось видаляти.
Лінія без прив'язки бере бік із взаємного розташування вузлів, а не
перший-ліпший порт.
Типовий вузол — крапка з підписом знизу, без значка й цифр. Підпис
бере ім'я хоста, якщо власного немає.
Полотно возиться лівою кнопкою в обох режимах; рамка виділення — на
Ctrl/Shift. fitView при відкритті та після масового додавання хостів.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найбільше вузьке місце до запуску: агент заводився INSERT-ом у базу, а
токен вписувався в командний рядок руками. Поставити зонд у клієнта було
неможливо.
core.agent_enrollments тримає sha256 одноразового токена; сам токен
повертається рівно один раз. Видача під FOR UPDATE в одній транзакції:
два агенти з однієї скопійованої команди інакше створили б два зонди з
одного запрошення. Відповідь на «немає», «згоріло» і «використано»
однакова — розрізняти їх означає підказувати тому, хто підбирає токени.
Токен зонда їде окремим полем agent_token, а не в certificate:
сертифікат відповідає на інше питання й живе за іншим циклом.
Агент зберігає посвідчення в /etc/netpulse/agent.json з правами 0600,
через тимчасовий файл і перейменування — обрив живлення посеред запису
інакше лишив би половину токена.
Знайдено живим прогоном: реєстрація не проходила автентифікацію, бо
інтерсептор стоїть на всьому сервері, а не на окремому сервісі — мій же
коментар стверджував протилежне. І запуск із самим посвідченням падав:
validate() вимагав -agent-id, не знаючи про файл.
Сторінка зондів: команда встановлення з токеном, відкликання
запрошень, керування модулями й лімітами, видалення.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Мапа: підпис ребра більше не друкує «? → ?» і «— з 10.0 Гбіт/с» —
невідоме краще не обіцяти. Вузол став карткою з градієнтом, кольоровою
смугою стану зліва й крапкою стану праворуч; смуга несе стан навіть
тоді, коли рамку перефарбували під майданчик. Пульсація лише за
обривом, порти — на наведення. Фон: радіальна підсвітка й дві сітки.
Вбудовані шаблони редагуються: правка створює власну копію тенанта з
тим самим ключем, вона замінює оригінал у списку, оригінал лишається
недоторканим для решти. Плюс окрема кнопка «Копія» з суфіксом у ключі.
Ключі елементів і графіків тепер виводяться на сервері — прямий виклик
API без key давав 500 від core.slug.
Вибір шаблонів у формі хоста став картками з описом і лічильником
метрик; обрані спливають угору.
Мобільна сітка: min-w-0 на картці. Елемент грід-сітки типово має
min-width:auto й не стискається нижче ширини вмісту — картка на 410 px
обрізалась у 375 px екрані.
Межа помилок навколо полотна: цикл рендеру в React Flow одного разу вже
поклав увесь застосунок, тепер падатиме лише мапа.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Форма хоста стала вкладками: Хост, Шаблони, Доступи, Збір конфігів,
Ручні перевірки. Інтервалів опитування тут більше немає — вони живуть
у шаблонах, і два джерела правди про одне число розійшлися б саме тоді,
коли треба швидко зрозуміти, чому хост опитується не так, як написано.
Новий хост отримує шаблон «Доступність (ICMP)», а не ручний чек.
Групи, шаблони й доступи обираються пошуком із чипами: список галочок
працює на пʼяти позиціях і перестає працювати на пʼятдесяти.
Графіки описуються в шаблоні (tpl.graphs): шість видів, жорсткі межі
осей, перелік ключів метрик замість посилань на елементи — графік має
право показувати й те, що прийшло з іншого шаблону. Графік без жодного
знайденого ряду не показується: порожня рамка з підписом — це обіцянка
даних, яких немає.
Знайдено живим прогоном і виправлено: обробник виділення на полотні
створював новий масив щорендеру й зациклював оновлення. Цикл зʼїдав
головний потік, і мертвими ставали всі сторінки, не лише мапа.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Редагування вмикається кнопкою. Полотно, яке рухається від випадкового
кліку, — найшвидший спосіб зіпсувати схему, на яку дивиться черговий.
Доступи за групами користувачів (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>
Дашборди: схема core.dashboards лежала з Етапу 1 без жодного рядка коду.
Сітка на 12 колонок, шість видів плиток (графік, число, шкала, список
алертів, сітка хостів, текст), автооновлення з інтервалом дашборда,
режим редагування. Права окремі від maps:* — дашборд збирає дані з
усього тенанта.
Esc і клік повз панель закривають будь-яке вікно. Слухач на document, бо
фокус може стояти де завгодно; закриття за mousedown, а не click, щоб
виділення тексту, доведене за межі вікна, не втрачало набране.
Канали сповіщень редагуються (раніше лише створювались і видалялись).
Протокол доступу змінюється — з вимогою ввести пароль заново, бо секрет
зашифрований під видом старого протоколу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
«Просить логін після кожного оновлення» — дві поломки в одному симптомі.
Перша: RotateSession читав email::text без COALESCE, а пошта стала
необов'язковою разом із входом за логіном. Користувач без адреси міг
увійти й не міг поновити сесію взагалі.
Друга: ротація не переживала подвійного обміну. React у режимі розробки
виконує ефекти двічі, дві вкладки в проді дають ту саму гонку: перший
запит відкликав токен, другий приносив уже відкликаний. Тепер
core.sessions.replaced_by тримає ланцюг, вікно 30 секунд — вистачає на
гонку від того самого клієнта й замало для реального повтору.
PageBody один на всіх сторінках: відступи й прокрутка більше не
залежать від того, хто писав сторінку. Заміряно — рівно 16 px скрізь.
Мапа: видалення (ендпоїнт існував із Етапу 4, кнопки не було) з
підтвердженням, і вигляд вузла — форма (картка/пігулка/крапка), товщина
рамки, заливка, приховування підпису й цифр. Аварія й попередження
світяться ореолом: рамка іншого кольору не помітна периферійним зором.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Свій SVG-графік замість recharts: із сотні кілобайт бібліотеки потрібна
одна ламана, а SVG ще й масштабується без переобчислення на ресайз.
Ряди групуються за одиницею виміру — відсотки й біти на секунду на
спільній осі дають пряму лінію біля нуля. Пропуск у даних розриває
лінію, а не з'єднує сусідів прямою.
Перевірено наживо: 6 годин читаються з сирих даних (крок 72 с),
тиждень — з роллапу 5m (крок 2016 с), підписи портів і одиниці на місці.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Метрики збиралися в 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>
Схема 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>
Збір конфігів перестав залежати від того, чи згадає людина натиснути
кнопку.
Свій парсер cron (internal/cronx) замість залежності: бітові маски
uint64 на поле, пошук наступного запуску покроково по хвилинах із
запобіжником у чотири роки. Правило dom/dow — об'єднання, як у справжнього
cron. Неможливий розклад (30 лютого) чесно відмовляє замість зациклення.
Планувальник тікає раз на хвилину під advisory-блокуванням, тож у
кластері розклад розкручує рівно один екземпляр. Спершу переноситься
next_backup_at, потім ставиться завдання: падіння між кроками коштує
одного пропущеного бекапу, зворотний порядок дав би нескінченну чергу.
Форма розкладу: чотири пресети плюс довільний cron. Виправлено помилку,
через яку пункт «свій розклад…» нічого не робив — обробник select
відсікав порожнє значення, тобто саме той випадок, заради якого пункт
існує.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Збір працював, але подивитись на зібране було ніде: жодної сторінки, а
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>
Форма створення хоста збирала назву, адресу, тип і групи — і не
збирала головного. Хост, доданий через 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>
Логін:
- 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>
React Flow поверх наявного API: вузли фарбуються статусом, ребра
кольором із порогів, точка на ребрі біжить зі швидкістю, оберненою до
завантаження каналу. WebSocket дає живі статуси й util, реконект
повертає підписку сам.
Робота з живим UI знайшла дві вади:
1. Ревізія мапи росла від самих кліків — React Flow віддає
onNodeDragStop на будь-яке натискання, і кожен клік писав порожню
ревізію, змушуючи інші відкриті полотна перечитуватись.
2. Після збереження хук оновлював лише номер ревізії, тож наступна
подія device.status відкидала щойно перетягнутий вузол назад.
Перевірено в браузері проти повного стека зі справжнім snmpd: два
зелені вузли з живим RTT, ребро eth0 з util і capacity 10 Гбіт/с,
індикатор "наживо", кнопка автопобудови з topo.links.
Драг через автоматизацію відтворити не вдалося — синтетичні події не
запускають d3-drag; цей шлях покритий тестами сервера.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>