Netpulse_SasS/ROADMAP.md
byrsapty 6d1786e111
All checks were successful
CI / hygiene (push) Successful in 9s
CI / web (push) Successful in 1m7s
CI / server (push) Successful in 1m27s
CI / agent (push) Successful in 59s
ROADMAP: свіжий перелік відкритого замість застарілого від 25 серпня
Старий розділ лишено як опис причин, але половина його вже неправда:
CI-раннер стоїть, build.py --check у конвеєрі, тести вебу виросли до
340, приймач кнопок Telegram працює. Новий розділ — те, що відкрите
сьогодні, у порядку ціни помилки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 23:23:06 +03:00

638 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NetPulse — план робіт
Документ доповнює [HISTORY.md](HISTORY.md): той описує зроблене, цей — що лишилось
і чому саме в такому порядку.
## Де ми зараз
Працює наскрізний ланцюг **кабель → браузер**: зонд знаходить сусідів по
LLDP/CDP/ARP, сервер зводить їх у `topo.links`, сам заводить чеки на трафік,
API віддає готове полотно з живими статусами, редактор пише назад із контролем
конфліктів і відкатом.
Перевірено на живому стенді проти справжнього `snmpd`. Деталі — в README кожного
компонента.
## Чого немає: чесний зріз
Схема БД з Етапу 1 покриває майже все з технічного завдання, але **схема ≠
реалізація**. Нижче — розрив між ними великими мазками; поіменний перелік
із причинами — у розділі «Чого немає: повний перелік» наприкінці.
| Підсистема | Схема | Реалізація |
|------------|-------|------------|
| Мапа, топологія, телеметрія | ✅ | ✅ |
| Автовиявлення LLDP/CDP/ARP/FDB | ✅ | ✅ |
| **Користувачі, ролі, вхід** | ✅ | ✅ |
| **Шаблони опитування** | ✅ | ✅ з тригерами, автопризначенням, `snmp.walk` і прототипами (0059) |
| **NCM (збір конфігів)** | ✅ | ✅ збір, розклад, Git, syslog, compliance і відкат (0060) |
| **Керування зондом із UI** | ✅ | ✅ |
| **Алерти й сповіщення** | ✅ | ✅ |
| **Мобільна адаптивність, PWA** | — | ⚠️ адаптив є, PWA немає |
| Веб-інтерфейс (навігація, сторінки) | — | ✅ |
| Дашборди, NOC TV | ✅ | ✅ |
| Білінг, ліцензії | ✅ | ❌ |
| Пакування, розгортання | — | ✅ зібрано й розгорнуто на живому сервері |
---
## Етап 5. Веб із користувачами і правами — ✅ зроблено 2026-08-15
> Реалізовано: вхід, JWT + ротація refresh-сесій, RBAC на всіх ендпоїнтах,
> `netpulse-user` для першого власника, сторінка входу, приховування дій без
> права, мобільний адаптив. Подробиці — [HISTORY.md](HISTORY.md),
> контракт — [server/API.md](server/API.md).
>
> **Доповнено 2026-08-15:** повноцінний веб — навігація й вісім сторінок
> (мапа, пристрої, алерти, правила, канали, зонди, команда, профіль),
> примітиви UI з мобільними картками замість таблиць, захист маршрутів
> правами.
>
> **Відкладено з цього етапу:** TOTP (`totp_secret_enc` є в схемі, коду немає),
> запрошення поштою (`core.invitations` порожня — користувача заводять із
> паролем одразу), кастомні ролі (`POST /api/v1/roles`), фільтр за
> `memberships.scope_group_ids`, редагування умови правила (лише створення
> й вимкнення), маршрути сповіщень і вікна обслуговування в UI.
**Навіщо перше.** Зараз API автентифікує лише машинні токени
(`core.api_tokens`). Людина увійти не може, а `core.roles` / `permissions` /
`memberships` лежать порожні. Без цього не можна ані впустити клієнта, ані
розмежувати доступ між інженером і глядачем — тобто продукт неможливо продати
навіть одній команді.
### Сервер
```
POST /api/v1/auth/login email + пароль → access (JWT, 15 хв) + refresh
POST /api/v1/auth/refresh обмін refresh-токена
POST /api/v1/auth/logout відкликання сесії
GET /api/v1/me профіль + ефективні права
POST /api/v1/auth/totp/enroll двофакторка (поле totp_secret_enc уже є)
GET /api/v1/users керування командою
POST /api/v1/users/invite запрошення (core.invitations уже є)
PATCH /api/v1/users/{id} зміна ролі, скоупу груп
GET /api/v1/roles системні + кастомні (Enterprise)
POST /api/v1/roles кастомна роль із набором прав
```
**Паролі — argon2id**, не bcrypt: він стійкіший до GPU-перебору, а `password_hash`
у схемі вже text і формат не диктує.
**Refresh-токен зберігається як `sha256` у `core.sessions`** — так само, як
агентські. Витік дампа БД не дає жодної живої сесії.
**RBAC замінює перевірку scopes.** Зараз обробники питають `tok.Can("maps:write")`.
Стане: посередник резолвить `memberships → roles → role_permissions` у набір прав
і кладе в контекст; машинні токени лишаються, але їхні scopes перетинаються з
правами власника. Скоуп груп (`memberships.scope_group_ids`) фільтрує вибірки
пристроїв — це вже не middleware, а предикат у запитах `store`.
### Фронтенд
Сторінка входу, зберігання refresh у httpOnly-cookie, сторінки «Команда» й «Ролі»,
приховування дій без права (кнопка, яка завжди дає 403, гірша за її відсутність).
**Обсяг:** ~23 дні. Ризиків мало: схема готова, візерунок автентифікації в
проєкті вже відпрацьований на агентських токенах.
---
## Етап 6. Шаблони опитування (Zabbix-подібні) — ⚠️ наполовину, 2026-08-24
> **Зроблено:** схема `tpl.*`, реконсиляція шаблонів у `core.checks`,
> звірка планів зонда, REST і редактор у вебі, чотири вбудовані шаблони.
> Перевірено наскрізно на живому net-snmp.
>
> **Додано 2026-08-24:** шаблон описує перевірки будь-якого типу (не лише
> OID), імпорт/експорт глобальний і поштучний, ручні інтервали.
>
> **Додано 2026-08-24:** графіки описуються в шаблоні (шість видів,
> жорсткі межі осей), картка хоста на вкладках без інтервалів.
>
> **Додано 2026-08-25:** тригери описуються в шаблоні й розгортаються в
> правила сповіщень; автопризначення за `sysObjectID` — пристрій сам
> каже, що він таке, і шаблон чіпляється без жодного натискання.
> За тим самим `sysObjectID` тепер підбирається й профіль збору конфігів
> (`ncm.profile_auto_assign`, уточнення за `sysDescr` для випадків, коли
> один OID покриває різні типи заліза). Щоб було з чого підбирати, хост
> зі SNMP-доступом сам отримує чек розпізнавання: полегшений
> `topology.discover` — три OID, без сусідів і без обходу `ifTable`.
>
> **Лишилось:** прототипи шаблонів і `snmp.walk` як тип елемента
> (таблиці з динамічним індексом).
## Етап 6 (початковий план)
**Найбільша нова підсистема.** Зараз `snmp.if`-чеки народжує захардкоджений
`autochecks.go`. Це працює рівно для одного випадку — інтерфейсів. Щойно
знадобиться CPU Cisco, температура MikroTik чи ємність UPS, доведеться дописувати
Go-код під кожен вендор. Шаблони роблять це даними.
### Нова схема: `tpl`
```sql
tpl.templates -- key, name, vendor, is_builtin, tenant_id NULL = вбудований
tpl.template_links -- успадкування шаблон → шаблон
tpl.macros -- {$SNMP_COMMUNITY}, три рівні: глобальний → шаблон → пристрій
tpl.items -- один OID → одна метрика: key, check_type, params, interval,
-- units, value_type, preprocessing, metric_key
tpl.discovery_rules -- LLD: walk по таблиці (ifTable, entPhysicalTable, dot1dBase)
tpl.item_prototypes -- прототипи з {#IFNAME}, {#SNMPINDEX}
tpl.trigger_prototypes -- пороги, що народжують alr.rules
inv.device_templates -- прив'язка шаблон → пристрій
inv.group_templates -- прив'язка шаблон → динамічна група
```
### Рішення, які треба зафіксувати одразу
**Шаблон — декларація, `core.checks` — матеріалізація.** Агент не має знати про
шаблони взагалі: він і далі отримує плаский план задач. Сервер реконсилює
`шаблони × пристрої × LLD → core.checks` при кожній зміні шаблону, складу групи
або результату виявлення. Це зберігає контракт агента незмінним і дозволяє
міняти шаблони без оновлення зондів у полі.
**Low-level discovery — це той самий `snmp.walk`,** результат якого йде не в
метрики, а в реконсиляцію. Тобто `autochecks.go` узагальнюється: замість
«знайшов інтерфейси → створив snmp.if» стане «правило виявлення повернуло
рядки → застосував прототипи → створив items».
**Препроцесинг ділиться між агентом і сервером.** Дельта лічильника (`change per
second`) лишається на агенті: **лише він знає фактичний інтервал** між двома
опитуваннями — це вже реалізовано й перевірено. Решта (множник, регулярка,
JSONPath, `discard unchanged`) — на сервері при записі: інакше кожна зміна
правила вимагала б оновлення агентів.
**Макроси розшифровуються на сервері** й доїжджають до агента вже підставленими,
разом із креденшелами. Секретні макроси (`{$SNMP_COMMUNITY}`) лягають у
`core.secrets` тим самим механізмом, що й паролі.
### Готова база шаблонів
Формат `tpl.*` варто спроєктувати так, щоб він приймав **Zabbix-експорт**
(YAML/JSON з items, discovery rules, prototypes, macros): його структура майже
один-до-одного лягає на запропоновану схему, а публічних шаблонів там сотні.
Це знімає потребу набивати базу вручну.
Команди збору конфігу вже є власним каталогом — `db/profiles/catalog.json`,
147 платформ. Шаблони опитування SNMP — окрема задача, і базу для них варто
починати саме з імпортера, а не з ручного наповнення.
**Обсяг:** ~57 днів на схему + реконсиляцію + імпортер + UI редактора
шаблонів.
---
## Етап 7. NCM — збір конфігів до кінця — ✅ зроблено 2026-08-25
> **Зроблено:** агентський модуль SSH/Telnet, черга завдань, диспетчер,
> вивантаження зі стисненням і дедуплікацією, візуальний diff у вебі,
> планувальник за cron, Git-двигун на go-git, порівняння з довільною
> версією в UI, переливання історії командою `netpulse-gitsync`.
> Перевірено наскрізно.
>
> **Додано 2026-08-25:** приймач syslog на зонді (RFC3164/5424, черга,
> ліміт частоти на джерело), транспорт `StreamLogs`, зіставлення хоста
> за адресою джерела й тригер позачергового бекапу за зразком у
> `ncm.device_policies.syslog_match`.
## Етап 7 (початковий план)
Половина шляху вже є: `ncm.*` у схемі, `ConfigJob`/`ConfigUpload` у контракті,
сервер приймає чанки, звіряє sha256, дедуплікує за `content_hash` і шифрує тіло.
Бракує трьох частин.
**Модуль `ncm` на агенті.** SSH через `golang.org/x/crypto/ssh`, Telnet своїм
кодом (протокол тривіальний). Виконує команди з профілю, ловить prompt за
регуляркою, віддає сирий текст чанками. Для `bdcom-olt` і `mikrotik-routeros`
профілі вже в сіді — вони й стануть першими підопічними.
**Планувальник на сервері.** ✅ Cron із `ncm.device_policies.cron` — власний
парсер `internal/cronx`, тік раз на хвилину під advisory-блокуванням. Лишився
тригер за Syslog-подією (`on_syslog`, `%SYS-5-CONFIG_I`) — поле в схемі є,
читати його нікому.
**Git-двигун.**`go-git`, чистий Go без cgo. Голий репозиторій на тенанта,
гілка `refs/heads/device/<device_id>` на пристрій, файл `<пристрій>/<тип>.cfg`.
Однаковий вміст нового коміту не створює. `netpulse-gitsync` переливає вже
накопичену історію й відтворює втрачений репозиторій із бази.
**UI:** ✅ список версій і порівняння з довільною попередньою. Кнопка відкату
лишилась (сутність `ncm.rollbacks` із двоетапним погодженням уже є).
**Обсяг:** ~45 днів.
---
## Етап 8. Керування зондом із UI — ✅ зроблено 2026-08-25
> **Зроблено:** `EnrollmentService` із одноразовими запрошеннями, файл
> посвідчення на агенті, сторінка зондів із командою встановлення,
> керування модулями й лімітами, видалення зонда. Перевірено наскрізно.
>
> **Лишилось:** справжній CA з підписом CSR — контракт це передбачає,
> але власний центр сертифікації з ротацією є окремою системою.
## Етап 8 (початковий план)
Транспорт готовий повністю: сервер уже вміє штовхати `ModuleControl` (які модулі
вмикати), `TaskDelta` (що опитувати), ліміти в `Welcome` (паралельність, розмір
батчу, темп ICMP) і `Directive` (пауза, перезавантаження конфігу, оновлення).
Бракує того, що це все вмикає.
```
POST /api/v1/agents створити зонд + одноразовий enrollment-токен
PATCH /api/v1/agents/{id} ліміти, дозволені модулі, сайт
POST /api/v1/agents/{id}/discover запустити автовиявлення зараз (TriggerNow уже є)
DELETE /api/v1/agents/{id}
```
**`EnrollmentService`** — реалізувати серверну сторону: зонд приходить із
одноразовим токеном і CSR, іде з підписаним сертифікатом. Контракт написано ще на
Етапі 2, реалізації немає. Без цього зонди заводяться `INSERT`ом, що прийнятно на
стенді й неприйнятно у клієнта.
**UI:** сторінка зонда з самометриками (`ts.agent_health` уже наповнюється),
повзунки лімітів, перемикачі модулів, кнопка «запустити виявлення», інструкція
встановлення з готовою командою й токеном.
**Обсяг:** ~23 дні.
---
## Етап 9. Алерти й сповіщення — ✅ зроблено 2026-08-15
> Реалізовано: движок правил (icmp/interface/metric/no_data), антифлап
> вікном, кореляція за топологією, вікна обслуговування й ручне
> заглушення, маршрути з тихими годинами, доставка в Telegram/webhook/
> SMTP, панель алертів у UI. Подробиці — [HISTORY.md](HISTORY.md),
> контракт — [server/API.md](server/API.md).
>
> **Відкладено з цього етапу:** ескалації й повторні сповіщення,
> приймач кнопок Telegram (`callback_data`), Web Push (іде з Етапом 10),
> правила з джерел `syslog`/`trap`/`ncm`/`compliance`.
**Те, без чого це не моніторинг.** Система малює мапу, але мовчить, коли щось
падає. Схема готова з Етапу 1 (`alr.rules`, `alerts`, `channels`, `routes`,
`escalation_policies`, `maintenance_windows`, `mutes`) і повністю порожня.
Дані вже течуть: зміни статусу йдуть через `core.event_outbox`, метрики лежать у
TSDB, пороги описані в `map_edges.thresholds`.
**Движок правил** читає `ts.icmp_samples`, `ts.if_counters` і `ts.samples_5m`,
застосовує `for_seconds` (антифлап) і пише в `alr.alerts`. Дедуплікацію вже
гарантує унікальний індекс `alerts_active_dedup_uniq`.
**Кореляція за топологією** — та, заради якої будувалась `topo.links`: коли падає
маршрутизатор, 40 пристроїв за ним не мають дати 40 сповіщень. Поля
`root_alert_id` і `depends_on_topology` для цього вже є.
**Канали:** Telegram (бот + Mini App із кнопками *Ack*/*Mute*), Web Push, email,
webhook. `alr.push_subscriptions` у схемі готова.
**Обсяг:** ~45 днів.
---
## Етап 10. Мобільна адаптивність і PWA
Зараз UI розрахований лише на десктоп: фіксований сайдбар 240 px, шапка з десятком
елементів в один рядок, **жодного брейкпойнта**, полотно без тач-жестів.
**Адаптив:** сайдбар у висувну панель під `md`, шапка в дві смуги, цілі дотику не
менше 44 px, інспектор вузла — нижнім аркушем замість бічної колонки.
**Полотно на дотик:** React Flow вміє pinch-zoom і pan, але перетягування вузла
пальцем конфліктує з панорамуванням — потрібен явний режим «редагування», інакше
кожна спроба посунути карту рухатиме вузол.
**PWA:** manifest, service worker (кеш оболонки, не даних — застаріла мапа гірша
за її відсутність), Web Push через `alr.push_subscriptions`.
**Telegram Mini App:** перегляд мапи й алертів, кнопки *Ack* / *Mute* / *View
Diff* — усе з ТЗ.
**Обсяг:** ~34 дні на адаптив + PWA, Mini App окремо.
---
## Етап 11. Сім задач одним заходом — 2026-08-27
> Міграції 00580064, зроблено паралельно. Розбір спільного знаменника —
> в [HISTORY.md](HISTORY.md), розділ «Сім задач одним заходом».
>
> - **0058 подієві алерти** — `syslog`, `ncm`, `compliance` спрацьовують
> у мить надходження події; правило з нереалізованим джерелом більше
> не зберігається мовчки.
> - **0059 `snmp.walk` і прототипи** — таблиці з динамічним індексом
> описуються шаблоном, а не Go.
> - **0060 відкат конфігу** — план як різниця, маскування паролів із
> підписом плану, обов'язковий контрольний збір, `verifying` при
> обриві. MikroTik і Juniper відмовлені з поясненням.
> - **0061 кнопки Telegram** — довге опитування (домену немає й не
> передбачається), авторизація не з `callback_data`, прив'язка
> акаунта одноразовим кодом.
> - **0062 аудит і архів хостів** — вісім відсутніх назв дій; тест на
> AST, що падає на ключі без назви; «відновити» повертає хост робочим,
> а не мовчазним.
> - **0063 RLS** — три ролі, окремий пул для фонових тактів. Інертна до
> перемикання DSN.
> - **0064 строки зберігання** — три гіпертаблиці й дві звичайні
> таблиці, що росли назавжди; сторінка сховища з прогнозом.
### Лишилось із цього етапу
- ~~**Тест ізоляції RLS не прогнано.**~~ ✅ 2026-08-27: прогнано на
бойовій базі, перемикання зроблено. Ізоляція діє, вхідні шляхи
переведено на воркерний пул. Подробиці — [HISTORY.md](HISTORY.md),
розділ «Перехід на роль без BYPASSRLS». **Лишилось:** телеметрія,
аудит та історія алертів під RLS не підпадають і не підпадуть —
TimescaleDB не поєднує стиснення з row level security. Їхню ізоляцію
далі тримає предикат у запиті.
- **0064 не прогнано на живій БД.** Перевірити першими:
`chunks_detailed_size` над матеріалізованою гіпертаблицею,
`hypertable_compression_stats` на нестиснутій, `add_retention_policy`
всередині транзакції під `SECURITY DEFINER`.
- **Алерт про вичерпання диска** — найдешевший шлях без правок движка:
писати `db.size.bytes` і `db.days_left` звичайними метриками на хості
машини зонда, тоді наявне метричне правило працює як є.
- **`apply_*` у генераторі профілів.** Поля заливки задані міграцією
через `UPDATE`; `db/profiles/catalog.json` про них не знає.
- **`plural()` повертає рядок разом із числом**, а частина місць виклику
додає число ще раз — на екрані «5 5 хостів». Стара вада, не з цього
етапу.
## Етап 12. Друга сімка — 2026-08-27
> Міграції 00650068 плюс роботи без міграцій. Розбір спільного — у
> [HISTORY.md](HISTORY.md), розділ «Друга сімка».
>
> - **0065 трапи** — приймач 162/udp на зонді, словник із шести
> протокольних трапів плюс словник кабінету, джерело алертів `trap`.
> - **0066 ескалації** — драбина сходинок, стан у базі, зупинка при
> підтвердженні, заглушення відкладає сходинку, а не витрачає.
> - **0067 алерт про диск** — пороги за часом (21 доба / 4 доби), а не
> за відсотками; вільне місце міряється `statfs` по `Bavail`.
> - **0068 каталог профілів** — поля заливки переїхали з разової
> міграції в `catalog.json`.
> - **`plural`** — 69 місць виклику, 13 друкували число двічі; підпис
> змінено так, щоб помилка стала неможливою.
> - **Тести вебу** — з нуля до 137; знайшли 11 справжніх вад,
> усі виправлені.
### Увімкнути трапи — рішення власника
Код розгорнуто, модуль **не увімкнено**. Щоб запрацював, потрібні три
речі, і третя виходить за межі технічної:
1. `traps` у `-modules` зонда;
2. `NET_BIND_SERVICE` — процес не root, а 162 привілейований;
3. **публікація 162/udp на хост** — порт без автентифікації приймає
будь-кого, хто знає адресу.
Обмеження в модулі є (20 трапів/с з адреси, стеля черги, окремий облік
невідомих джерел), але вони зменшують шкоду, а не прибирають рішення.
### Лишилось із цього етапу
- **`db/profiles/build.py --check` не в CI.** Один рядок у кроці «Схема»
ловив би розходження каталогу зі згенерованим — саме те, що цього разу
знайшлось випадково.
- **CI без раннера.** `scripts/check.sh` робить те саме однією командою
вже сьогодні; сам workflow чекає на раннера.
- **Перетягування вузлів на мапі не покрите й не буде** — d3-drag не
запускається синтетичними подіями. Наслідок: вузол візуально стає на
місце, запит не йде, розкладка «сама відкочується» після
перезавантаження, і всі тести при цьому зелені.
- **Тригери шаблонів не можуть отримати драбину ескалації** — поля в
тригері шаблону немає, а драбина ще й тенант-специфічна.
- **SNMPv3-трапи не перевіряються** — розбираються й зберігаються, підпис
і шифрування не звіряються.
## Етап 13. Випуск: реєстр образів, релізи, пакети — НЕ ЗРОБЛЕНО
Поставлена задача, дослівно: «ми ж зможемо його встановлювати з пакетів?
А то зараз не дуже».
Зауваження справедливе. Сьогоднішній шлях клієнта — клонувати
репозиторій, зібрати образи в себе (кілька хвилин і гігабайти) і
запустити установник. Це шлях розробника: клієнт бачить вихідний код,
якого не мав би бачити, і платить за збірку часом свого сервера.
### Чому «пакет» неможливий просто зараз
Не тому, що ліньки написати `.deb`. **Немає що в нього класти.** Образи
збираються на машині клієнта з джерел, тобто продукт як артефакт не
існує — існує лише рецепт. Пакет у такому стані ставив би те саме
«зберіть самі», лише з гарнішою обгорткою.
Тому порядок жорсткий: спершу реєстр, потім релізи, і лише потім пакети.
### 13.1 Реєстр образів
**Інфраструктура вже є:** Forgejo має вбудований реєстр контейнерів, а
CI-раннер ми запустили 2026-08-27. Бракує лише того, щоб почати ним
користуватись.
Образи `netpulse/server` і `netpulse/agent` мають публікуватися туди з
тегом версії. Після цього `docker compose` на машині клієнта тягне
готове, а не збирає.
### 13.2 Версіонування, якого немає
`NETPULSE_VERSION=v0.1.0` — зараз просто рядок у `.env`. Тега в git
немає, образу з таким тегом ніде, крім бойового стенду, немає теж.
Зв'язку «версія → коміт → образ» не існує, тобто на питання «що саме у
вас працює» відповіді немає.
Потрібен тег у git як єдина точка істини, і збірка, що бере версію
звідти, а не з рядка в конфігурації.
### 13.3 Випускальний конвеєр
git tag v0.2.0 → CI збирає → штовхає образи в реєстр
→ публікує netpulse-v0.2.0.tar.gz
В архіві — `netpulse`, compose-файли, `netpulse.conf.example`. **Без
джерел.** Плюс `install.sh`, який цей архів завантажує:
curl -fsSL https://git.zotac.keenetic.link/.../install.sh | sh
І `netpulse upgrade` починає ходити в реєстр замість перезбірки.
### 13.4 Пакети .deb / .rpm — кроком пізніше
`netpulse` у `/usr/bin`, compose у `/opt/netpulse`, `netpulse.conf` у
`/etc/netpulse`, systemd-юніт. Тоді працює те, чого й очікують:
apt install netpulse && netpulse install
Робити це до 13.1 безглуздо з причини, названої вище.
### Чого треба досягти ДО першого тегу
**Оновлення з версії на версію ніхто не перевіряв.** Пісочниця
установника (Етап 12) перевіряє установку з нуля — і саме вона знайшла
дві вади, які чекали на першого клієнта. Але шляху «стояла 0.1, стала
0.2» не перевіряє ніщо, а ламається найчастіше саме він: міграції
поверх наявних даних, зміна конфігурації, несумісність зонда з новим
колектором.
Потрібен другий режим пісочниці: підняти попередню версію, налити в неї
даних, оновити до нової, і прогнати ту саму самоперевірку. Без цього
перший же реліз перевіряє себе на клієнтові.
**Сумісність зонда й сервера.** Зонди стоять у мережах клієнтів і
оновлюються не одночасно з сервером. Правило «який зонд працює з яким
сервером» ніде не записане й не перевіряється.
## Порядок і чому саме такий
1. ~~**Етап 5 (користувачі)** — без входу продукт не можна віддати нікому.~~
2. ~~**Етап 9 (алерти)** — без сповіщень це не моніторинг.~~
3. **Етап 6 (шаблони)** — знімає потребу дописувати Go під кожен вендор; що
раніше, то менше захардкодженого коду доведеться викидати.
4. ~~**Етап 8 (керування зондом)** — дешевий і робить онбординг можливим.~~
5. **Етап 7 (NCM)** — головний аргумент Enterprise-тарифу.
6. **Етап 10 (мобільний)** — після того, як є що показувати.
Дашборди з NOC TV-режимом зроблено 2026-08-25. Що лишається — нижче,
окремим переліком із причинами.
## Що лишається — стан на 2026-08-28
Перелік нижче (розділ «Чого немає») складено 2026-08-25 і частина його
вже неправдива. Тут — те, що справді відкрите СЬОГОДНІ, у порядку ціни
помилки. Нижчий розділ лишено як довший опис причин.
### Перше: до першого тегу (Етап 13)
Це єдина річ, яка блокує продаж, і вона не про код.
1. **Оновлення з версії на версію не перевіряв ніхто.** Пісочниця
перевіряє установку з нуля. Шлях «стояла 0.1 — стала 0.2» ламається
частіше, а перевіряє його зараз перший клієнт.
2. **Немає версіонування.** Образ завжди `v0.1.0`, реєстру немає, тега
немає. Поки цього немає, «оновитись» означає «залити дерево й
зібрати», тобто те, що роблю я вручну.
3. **Сумісність зонда й сервера ніде не записана.** Зонди стоять у
чужих мережах і оновлюються не разом із сервером.
### Друге: перевірки, які є, але не бігають самі
4. **`scripts/dbtest.sh` не в CI.** Раннер уже стоїть (контейнер
`netpulse-ci-runner`), `check.sh` у конвеєрі є, `build.py --check`
теж. Бракує саме тестів проти бази — тобто тих, що 28 серпня знайшли
чотири справжні вади за один прогін. Зараз їх запускаю руками я.
5. **Токен у `origin` замість ключа розгортання.**
### Третє: діри, названі рецензіями й не закриті
6. **`LoadChannels` падає цілком через один нерозшифровний секрет** —
кабінет лишається без ескалацій до стелі життя драбини.
7. **Перетягування вузла на мапі не покрите тестом і не буде** (d3-drag
не запускається синтетичними подіями). Наслідок відомий: вузол стає
на місце, запит не йде, тести зелені.
8. **SNMPv3-трапи не перевіряються** — розбираються й зберігаються,
підпис і шифрування не звіряються.
9. **Тригери шаблонів не можуть отримати драбину ескалації.**
10. **`plural()` повертає число разом із рядком**, а частина викликів
додає його ще раз: «5 5 хостів».
### Четверте: ніколи не працювало на живому залізі
11. **Відкат конфігурації** — код є, на справжньому обладнанні не
виконувався жодного разу.
12. **Джерело тригера `trap`** — свідомо не зроблено без словника MIB.
13. **Прототипи шаблонів** (0059) увімкнені, але `snmp.if` через них не
виражається.
### П'яте: велике й відоме
14. **Білінг** — схема з 0009 і 0069 є, продавати без нього не можна.
15. **Web Push**, **збережені подання**, **прокрутка дашбордів на TV**,
**підкладки-плани приміщень**, **мобільний режим мапи**.
16. **Справжній TLS** — потрібен домен; на IP Let's Encrypt не видає.
---
## Чого немає: повний перелік
Стан на 2026-08-25, після розгортання на бойовому сервері. Перелік
складений перевіркою коду, а не з пам'яті: для кожного пункту звірено,
чи є під нього щось у `server/internal`, `agent/internal` і `web/src`.
Порядок — за тим, наскільки відсутність помітна тому, хто користується
системою.
### Схема є, коду немає зовсім
Ці таблиці створені міграціями й порожні. Небезпека тут не в самій
відсутності, а в тому, що схема виглядає як обіцянка.
| Що | Де схема | Чого бракує |
|---|---|---|
| **Білінг і ліцензії** | `0009_billing_licensing.sql` | усього: тарифи, ліміти, Stripe, ключі. Для Micro-SaaS це те, через що продають |
| **Web Push** | `alr.push_subscriptions` | підписки й доставка. Потрібне для PWA |
| **Звіти SLA** | `core.sla_targets`, `core.sla_periods` | розрахунок доступності за період і вивантаження |
| **Збережені подання** | `core.saved_views` | фільтри інвентарю, які можна назвати й повернутись |
| **Прокрутка дашбордів на TV** | `dashboards.tv_options.rotate_sec` | сам режим NOC TV працює, але сторінки не гортаються по колу |
### Оголошено в переліку можливостей, не написано
| Що | Стан | Чому не зроблено |
|---|---|---|
| **Modbus-TCP** | плагін у сіді, `is_core = false` | немає жодного інвертора чи UPS під рукою. Неперевірений промисловий протокол у мережі з живим обладнанням — гірше, ніж його відсутність |
| **NetFlow / sFlow** | плагін у сіді, `is_core = false` | окремий приймач потоків, за обсягом — власний етап |
Обидва плагіни позначені `is_core = false`, тож у переліку перевірок
система показує їх недоступними — обіцянки користувачу немає.
### Зроблено наполовину
**Сповіщення за подіями — лишився `trap`.** `syslog`, `ncm` і
`compliance` зроблено подієво (0058): правило перевіряється в мить
надходження події. `trap` свідомо не реалізовано — без словника MIB
умова звелась би до порівняння сирих OID, тобто до другої мовчазної
обіцянки замість першої. Джерела `link` і `agent` не подієві за
природою. Правило з нереалізованим джерелом тепер не зберігається, а не
мовчить.
**Підкладки-плани приміщень.** `topo.map_backgrounds` віддається в
`GET /maps/{id}`, полотно їх не малює. Потрібен прийом і роздача файлів
— іконки мап уже зберігаються в базі, тож можна тим самим шляхом, без
S3. Приблизно пів дня.
**Мобільний режим мапи.** Адаптив сторінок є, але на полотні
перетягування вузла пальцем не розведене з панорамуванням: потрібен
явний режим редагування, інакше кожна спроба посунути карту рухатиме
вузол.
**Інтерфейси досі захардкоджені.** Прототипи шаблонів зроблено (0059),
але `snmp.if` через них не виражається: зонд тримає попередній замір,
рахує швидкість за фактичним інтервалом і ловить перевертання
лічильника, а лічильники лягають у `ts.if_counters` за `interface_id`, а
не в `ts.samples` за міткою. На цьому `interface_id` тримаються анімація
трафіку на мапі, інспектор лінка й тригери з джерелом `interface`.
Виграш — мінус ~200 рядків Go; ризик — обірвані графіки на живих хостах.
Свідомо відкладено.
### Перевірки, яких немає
**Тести вебу з'явились 2026-08-27** — vitest, 137 перевірок, і
`scripts/check.sh` проганяє обидва світи однією командою. Що покрито і,
головне, що НІу `web/TESTING.md`. Найбільша діра лишається та сама:
перетягування вузла на мапі не покрите, бо d3-drag не запускається
синтетичними подіями; серверний бік цієї дії тестами покритий.
**CI жодного разу не запускався.** `.forgejo/workflows/ci.yml` написано,
але раннера немає. Прогони робились руками; те, що CI мав би ловити
(`gofmt`, `go vet`, тести з базою, крос-збірка зонда), проганялось
окремо перед кожним комітом.
### Чого не буде без зовнішніх умов
- **Справжній TLS** на бойовому сервері — потрібен домен. Зараз
самопідписаний: Let's Encrypt не видає сертифікатів на IP-адреси.
- **Telegram Mini App** — окрема робота, і почати варто з приймача
кнопок бота: він дає 90% користі за 10% зусиль.