За другою рецензією:
* scripts/dbtest.sh писав у шапці «не напрямляйте на робочу базу» й
нічого для цього не робив — перевірено, пішов котити міграції на базу
з бойовим іменем. Тепер вимагає probe/test в імені.
* sendText ковтав помилку, тож журнал ескалацій писав «надіслано» на
сходинці, жодне повідомлення якої не дійшло. Три результати замість
двох: no_channels, failed, sent.
* stopped_at IS NULL рятував лише від ack; гасіння правилом і
ResolveMissing рядка драбини не чіпають, і сходинка дзвонила за
погашеним алертом. Додано перевірку стану алерту в тому ж UPDATE.
* escalate() блокував весь тік движка — мертвий вебхук одного кабінету
зупиняв обчислення правил усім. Винесено в RunEscalations.
* алерт, народжений під заглушенням, не сповіщався ніколи: ні при
народженні, ні коли вікно скінчилось. Тепер перехід suppressed→firing
сповіщається, а драбина рахує час від першого сповіщення.
* alr.rules.channel_ids приймав чужі канали, глушачи і сповіщення, і
драбину. Перевірка як для сходинок; DeleteChannel чистить посилання.
І перше, що зловив прогін проти справжньої бази: nil-зріз каналів їде
явним NULL повз DEFAULT '{}' — правило без каналів давало 500.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три вади, знайдені рецензією, яких щасливий шлях показати не міг:
* outcome='sent' писався до доставки; помилка читання каналів клала в
кеш порожню мапу й з'їдала всі сходинки кабінету за тік — усі зі
слідом «надіслано». Канали тепер читаються до просування стану,
журнал пишеться після доставки, з правдою.
* UPDATE не мав stopped_at IS NULL — підтвердження алерту посеред
партії не рятувало людину від дзвінка.
* час брався раз на партію.
Плюс суміжне: UpdateRule не гасив алертів вимкненого правила, сервер
домислював enabled на оновленні, channel_ids сходинок не звірялись із
каналами кабінету (зокрема чужого).
І scripts/dbtest.sh — тести проти бази перестали мовчки пропускатись.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Маршрут PUT /api/v1/channels/{id} стояв на handleCreateChannel з
першого дня. Обробник читав тіло й ЗАВЖДИ кликав CreateChannel: він
жодного разу не читав id зі шляху.
При цьому UpdateChannel у store написана повністю й ретельно
прокоментована — включно з рішенням «порожній секрет означає лишити
токен як є, бо розшифрувати збережений заради показу означало б
віддати його туди, звідки він не повернеться». У всьому дереві її не
кликав НІХТО.
Ззовні це виглядало так: правка каналу або заводила дубль, або — якщо
назву лишили — падала з «канал із такою назвою вже є». Тобто
відредагувати канал було неможливо взагалі, а повідомлення про помилку
вказувало не на ту причину.
Візерунок узято з правил у тому самому файлі: там гілка оновлення за
PathValue уже стояла й працювала.
Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (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 серпня.
Метрики збиралися в 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>
Логін:
- 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>
Система вміла малювати мапу, але мовчала, коли щось падало. Схема 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>