refactor(vps): update vCPU, RAM, and disk fields to allow decimal values
Docker / build (push) Failing after 21s

Changed the step attribute for NumberField components in the VpsEditSheet to "any" for vCPU, RAM, and disk inputs. Updated the vpsSchema to accept real numbers for vCPU, diskGb, and bandwidthTb. Adjusted database schema to store vCPU, diskGb, and bandwidthTb as REAL instead of INTEGER for better precision.
This commit is contained in:
Denozordec
2026-07-31 16:46:26 +07:00
parent 72f9597ede
commit 53d5ed5d33
9 changed files with 572 additions and 14 deletions
@@ -0,0 +1,103 @@
---
name: Access form-8 polish
overview: "Доработать `/access` по DNA ReUI form-8: удобное копирование токена (полный — только после create/rotate; в списке — prefix), больше метаданных в таблице и лёгкий Frame/Alert polish без пересборки всей страницы."
todos:
- id: install-form-8
content: "CLI: pnpm dlx shadcn@latest add @reui/form-8 --yes → blocks/form-8, fix imports"
status: pending
- id: token-dialog
content: "ApiKeyTokenDialog: Alert + InputGroup reveal/copy + Bearer snippet"
status: pending
- id: grid-copy-meta
content: "Grid: copyable prefix/id, created_at, richer name cell"
status: pending
- id: frame-shell-session
content: Keys card → Frame+Alert; session copy tenant_id/user_id
status: pending
- id: web-verify
content: typecheck + lint + build @evobgp/web
status: pending
isProject: false
---
# Доработка страницы Доступов (form-8)
## Контекст
- Preview: [form-8](https://reui.io/preview/base/form-8) · Docs: [blocks](https://reui.io/blocks)
- Сейчас: [`apps/web/src/routes/_auth/access.tsx`](apps/web/src/routes/_auth/access.tsx) + [`access-api-keys-card.tsx`](apps/web/src/components/access/access-api-keys-card.tsx) / [`access-api-keys-grid.tsx`](apps/web/src/components/access/access-api-keys-grid.tsx) / [`api-key-token-dialog.tsx`](apps/web/src/components/access/api-key-token-dialog.tsx)
- Полный секрет токена API отдаёт **только** в `201` create / `200` rotate; в списке есть лишь `prefix` (8 символов). Копирование «ключа» в таблице = **prefix**; полный токен — в диалоге после создания/ротации.
- Уже есть хук [`useCopyToClipboard`](apps/web/src/hooks/use-copy-to-clipboard.ts) и DNA копирования в [`settings-14/.../columns.tsx`](apps/web/src/components/blocks/settings-14/components/columns.tsx) (`ApiKeyCell`).
- `InputGroup` уже в [`packages/ui/src/components/input-group.tsx`](packages/ui/src/components/input-group.tsx).
```mermaid
flowchart LR
createOrRotate["Create / Rotate"] --> fullToken["token once"]
fullToken --> tokenDialog["Token dialog InputGroup copy"]
listRow["List row"] --> prefixOnly["prefix only"]
prefixOnly --> copyPrefix["Copy prefix"]
```
## Подход
Сохранить KPI + сессию + DataGrid-список (раздел в целом ок). Оформить блок ключей и диалог токена по form-8 (Frame/Alert/InputGroup copy), не переписывать страницу целиком.
## Шаги
### 1. Установить референс form-8
Из `apps/web`:
```bash
pnpm dlx shadcn@latest add @reui/form-8 --yes
```
Сложить в `apps/web/src/components/blocks/form-8/` (reference). Post-add: импорты shadcn → `@evobgp/ui/components/*`. Не копипастить block в route — adapt в domain-компоненты.
### 2. Диалог полного токена (главное копирование)
Переработать [`api-key-token-dialog.tsx`](apps/web/src/components/access/api-key-token-dialog.tsx) по form-8:
- ReUI `Alert` (`warning`): «токен больше не покажем»
- `InputGroup` + read-only input (masked toggle reveal/hide) + кнопка Copy (`useCopyToClipboard` + toast)
- Доп. кнопка/строка: копировать `Authorization: Bearer <token>` (удобно для curl/UI settings)
- Автофокус / явный CTA «Копировать»
### 3. Таблица ключей — copy + больше info
В [`access-api-keys-grid.tsx`](apps/web/src/components/access/access-api-keys-grid.tsx):
- Ячейка **prefix**: mono + Copy (как `ApiKeyCell` в settings-14); tooltip «полный токен недоступен — только prefix»
- Добавить колонки: **создан** (`created_at`), **id** (mono, copy)
- Name-cell: hybrid tile DNA (icon `KeyRound` на `bg-muted`) + subtitle с role badge
- Actions: оставить rotate/revoke; при необходимости ButtonGroup как в form-8
Маленький shared helper при необходимости: `apps/web/src/components/access/copyable-mono.tsx` (reuse в grid + session).
### 4. Оболочка блока ключей
В [`access-api-keys-card.tsx`](apps/web/src/components/access/access-api-keys-card.tsx):
- Заменить `DataGridCard` на ReUI `Frame` / `FramePanel` (+ header) — surface lock form-8 / contract
- Короткий `Alert` info над гридом: полный токен только при создании/ротации; в списке копируется prefix
### 5. Сессия — доп. info + copy
В [`access.tsx`](apps/web/src/routes/_auth/access.tsx):
- Copy для `tenant_id``user_id`, если есть)
- Для JWT: показать `is_admin` badge и число permissions (без смены layout)
### 6. Проверка
```powershell
pnpm --filter @evobgp/web run typecheck
pnpm --filter @evobgp/web run lint
pnpm --filter @evobgp/web run build
```
## Вне скоупа
- Смена backend (reveal полного токена из БД невозможен — SHA-256)
- Полный redesign KPI (`SectionCards``KpiStatGrid`) — отдельная задача
- Перенос списка с DataGrid на row-list form-8
@@ -0,0 +1,101 @@
---
name: EvoBGP без NATS
overview: "Код EvoBGP уже работает без NATS и прочих брокеров: очередь задач in-process в `evobgp-all`. Оптимизировать Go под ваш compose не требуется — достаточно подчистить сам compose (убрать мёртвый сервис nats) и по желанию ужесточить prod-флаги."
todos:
- id: compose-drop-nats
content: Удалить nats + depends_on + runtime-logs entry из production.example (и sync microvps-full при необходимости)
status: pending
- id: docs-align
content: "Синхронизировать quickstart/architecture: NATS optional, minimal stack без брокера"
status: pending
- id: server-apply
content: На сервере применить тот же compose diff (без правок Go)
status: pending
isProject: false
---
# Анализ compose без NATS: нужен ли рефакторинг EvoBGP?
## Вердикт
**Нет — оптимизировать текущий Go-код под этот compose не нужно.** Приложение уже спроектировано как monolith (`evobgp-all`) с in-process очередью. NATS в репозитории — задел на будущее, не рабочая зависимость.
## Как устроен runtime без брокера
```mermaid
flowchart LR
subgraph host [Хост /opt/evobgp]
Web[evobgp-web nginx]
All[evobgp-all]
PG[(postgres)]
Bird[bird2]
Agent[evobgp-agent]
Edge[evobgp-edge Traefik]
end
Edge --> Web
Web -->|"/v1 proxy"| All
All --> PG
All --> Bird
Agent --> Bird
All -->|"HTTP wake NODE_DISPATCH"| Agent
```
| Компонент | Роль для кода | Нужен ли NATS? |
|-----------|---------------|----------------|
| `evobgp-all` | API + jobs + scheduler/ingest/render/deploy in-process | Нет |
| `postgres` | Store + `job_audit` reclaim (ADR-001) | Нет |
| `bird2` + `evobgp-agent` | Локальный speaker / birdc | Нет |
| `EVOBGP_NODE_DISPATCH_ENABLED` | HTTP `POST` на remote agent (`internal/nodedispatch`) | Нет |
| `EVOBGP_BROKER_URL` | Только [`internal/broker/connect.go`](c:/Users/shats/Dev/EvoBGP/internal/broker/connect.go) — лог, если URL задан | Нет |
| NATS-контейнер | Не читается бинарником | Можно удалить |
Источник правды: [`docs/architecture.md`](c:/Users/shats/Dev/EvoBGP/docs/architecture.md), [`docs/adr/001-durable-job-queue.md`](c:/Users/shats/Dev/EvoBGP/docs/adr/001-durable-job-queue.md) — «NATS JetStream remains optional (`EVOBGP_BROKER_URL` logged only)».
Ваш закомментированный `# EVOBGP_BROKER_URL` — корректно. Даже если URL оставить, поведение jobs не изменится.
## Что в вашем YAML лишнее / рискованное (ops, не код)
**Можно убрать без правок Go:**
1. Сервис `nats` целиком.
2. `depends_on: nats` у `evobgp-all`.
3. `nats` из списка `SERVICES` в `stack-runtime-logs`.
4. Проброс `4222:4222` (если не нужен снаружи).
**Prometheus** — тоже опционален для функционала: скрейпит `/metrics` с `evobgp-all`. Удаление не ломает BGP/UI; ломает только локальный monitoring. Код менять не нужно.
**Не трогать без замены:** `postgres`, `bird2`, `evobgp-agent`, `evobgp-all`, `evobgp-web`, `evobgp-edge`, volumes `bird_*` / `pgdata` / ACME.
## Prod-флаги в compose (важно, но это конфиг, не рефакторинг)
Сейчас example и ваш файл совпадают с lab-режимом:
- `EVOBGP_SEED_DEMO: "1"` → доступен `Bearer dev` (см. [`docs/access.md`](c:/Users/shats/Dev/EvoBGP/docs/access.md), [`docs/production-checklist.md`](c:/Users/shats/Dev/EvoBGP/docs/production-checklist.md))
- `EVOBGP_DEV_INSECURE: "1"` — dead/lab флаг; при `EVOBGP_PRODUCTION=1` процесс **не стартует** ([`internal/config/production.go`](c:/Users/shats/Dev/EvoBGP/internal/config/production.go))
- `POSTGRES_PASSWORD: evobgp` + `sslmode=disable` — ок только в private Docker network
- `EVOBGP_BUNDLE_SEED_HEX` захардкожен в yaml — ок для lab; в бою лучше свой секрет в `.env`
Для «настоящего» prod достаточно env, не переписывания архитектуры.
## Нужны ли изменения в коде проекта?
| Вопрос | Ответ |
|--------|--------|
| Убрать/упростить `internal/broker`? | Не обязательно: пакет уже no-op без URL |
| Переписать jobs на PG-only? | Уже есть durable reclaim через `job_audit`; брокер не требуется |
| Вырезать NATS из бинарника/зависимостей? | В Go NATS-клиент как runtime consumer не подключён — нечего «оптимизировать» |
| Менять `evobgp-all` под monolith? | Уже monolith; reference-воркеры (`scheduler`/`ingest`/…) вам не нужны |
**Итог:** отказ от NATS — правильное решение для текущего этапа; код уже соответствует этой модели. Оптимизация = правка compose/docs, не приложения.
## Рекомендуемые действия (если согласуете выполнение)
Только инфраструктура/документация в репо:
1. Обновить [`deploy/compose/docker-compose.production.example.yaml`](c:/Users/shats/Dev/EvoBGP/deploy/compose/docker-compose.production.example.yaml) (и при необходимости `stack.microvps-full.yaml` / `docker-compose.microvps-full.yaml`): удалить `nats`, `depends_on`, порт 4222, упоминание в runtime-logs.
2. Подправить [`docs/quickstart.md`](c:/Users/shats/Dev/EvoBGP/docs/quickstart.md) / architecture: NATS — optional future, не часть minimal production stack.
3. На сервере: тот же diff в `/opt/evobgp/docker-compose.yaml``docker compose up -d` (nats исчезнет).
Go/CI/образы — **не трогать** ради этого решения.
Опциональный hardening env на сервере (отдельно от «оптимизации кода»): `EVOBGP_SEED_DEMO=0`, убрать `EVOBGP_DEV_INSECURE`, при желании `EVOBGP_PRODUCTION=1` + checklist.
@@ -0,0 +1,138 @@
---
name: EvoFirewall full hardening
overview: "Полная поэтапная доработка EvoFirewall: сначала frontend (удаление dead blocks, wiring kit, UX ReUI), затем backend (split monolith, баги, N+1, RBAC/Zod), затем OpenAPI и тестовый пояс — всё в рамках project rules и ReUI PRO Frame."
todos:
- id: p1-delete-blocks
content: "Phase 1: удалить components/blocks/, почистить ссылки; встроить или удалить agents-fleet-chart"
status: completed
- id: p1-kit-wiring
content: "Phase 1: ui-surface.ts; LoadingButton/QueryState/FormSheet wiring; auth.callback Frame"
status: completed
- id: p1-agents-toolbar
content: "Phase 1: общий toolbar cards/table на /agents без hand-roll дубля"
status: completed
- id: p1-settings-qa
content: "Phase 1: SettingsShell hygiene + show_quick_actions UI; sync frontend rules docs"
status: completed
- id: p2-bugfixes
content: "Phase 2: bumpAgentsForList no-op; Zod settings whitelist; RBAC install-links/integrations"
status: completed
- id: p2-perf-split
content: "Phase 2: N+1 batch; split control.ts + repos; row-mappers"
status: completed
- id: p3-openapi-tests
content: "Phase 3: OpenAPI parity + error envelope + inject tests critical paths"
status: completed
isProject: false
---
# EvoFirewall — полный hardening (UI → API → OpenAPI/тесты)
Выбор: **D** (полный поэтапный) + **удалить** [`apps/web/src/components/blocks/`](apps/web/src/components/blocks/).
Эталоны ReUI (обязательные ссылки по зонам):
- KPI / Quick Actions: [stats-12](https://reui.io/preview/base/stats-12) · [card-12](https://reui.io/preview/base/card-12)
- Lists: [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2)
- Agents: [solution-agents-1](https://reui.io/preview/base/solution-agents-1)
- Settings: [settings-16](https://reui.io/preview/base/settings-16) · [settings-3](https://reui.io/preview/base/settings-3)
- Shell: [app-shell-12](https://reui.io/preview/base/app-shell-12)
- Empty / Auth callback: [empty-state-12](https://reui.io/preview/base/empty-state-12) · [auth-13](https://reui.io/preview/base/auth-13)
- Docs: [llms.txt](https://reui.io/llms.txt) · [MCP](https://reui.io/docs/mcp)
Перед UI-правками: MCP `user-reui` (`surface: frame`) + `validate_usage` / `get_audit_checklist`. После web: `pnpm --filter @evofw/web build`. После API: `pnpm --filter @evofw/api test`.
```mermaid
flowchart LR
P1[Phase1_UI] --> P2[Phase2_API]
P2 --> P3[Phase3_ContractTests]
```
---
## Phase 1 — Frontend / ReUI
### 1.1 Dead code
- Удалить весь [`apps/web/src/components/blocks/`](apps/web/src/components/blocks/) (card-3, solution-agents-2/3, solution-crm-4, solution-inventory-9).
- Почистить комментарии-ссылки на `blocks/...` в domain (`agent-card.tsx` и др.) — оставить только `previewUrl` reui.io.
- Решить судьбу [`agents-fleet-chart.tsx`](apps/web/src/components/agents/agents-fleet-chart.tsx): **встроить** в dashboard (`/_auth/index.tsx`) как chart-секцию после Quick Actions (контракт KPI → QA → charts), иначе удалить. Выбор по умолчанию: **встроить** на dashboard (данные уже есть через `agents` / `recentStats`).
### 1.2 Project lock + kit wiring
- Добавить [`apps/web/src/lib/ui-surface.ts`](apps/web/src/lib/ui-surface.ts): `export const UI_SURFACE = 'frame' as const`.
- Подключить shared kit вместо ad-hoc:
- [`LoadingButton`](apps/web/src/components/loading-button.tsx) — settings save, approve/delete mutations.
- [`QueryState`](apps/web/src/components/query-state.tsx) — error/retry на lists/rules/agents где сейчас raw Alert+Button.
- [`FormSheet`](apps/web/src/components/form-sheet.tsx) — миграция create/edit sheets (`add-agent-sheet`, list/rule create) **только если** API FormSheet совместим с текущими формами; иначе удалить FormSheet как мёртвый код (не держать orphan). Default: **мигрировать** `AddAgentSheet` + один list create; остальные — если fit без redesign.
### 1.3 `/agents` — убрать дубль toolbar
- Файл: [`routes/_auth/agents/index.tsx`](apps/web/src/routes/_auth/agents/index.tsx) (~19KB).
- Cards view сейчас hand-roll Tabs+Filters+Search (строки ~504550); table уже через [`AgentFleetDataGrid`](apps/web/src/components/agents/agent-fleet-data-grid.tsx) → `ResourcePage`.
- Вынести общий toolbar (CountedLineTabs + Filters + search + viewToggle + clear) в shared helper/component (например `agents-fleet-toolbar.tsx`) **или** расширить `ResourcePage` slot `toolbarExtra` / `headerExtra` так, чтобы cards mode использовал тот же chrome, что table (без копипаста FramePanel).
- Preview: [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) · [solution-agents-1](https://reui.io/preview/base/solution-agents-1).
### 1.4 Settings
- [`SettingsShell`](apps/web/src/components/reui-kit/settings-shell.tsx) сейчас ведёт на несуществующие `/settings/appearance|integrations`.
- **Default:** один экран settings остаётся ([`routes/_auth/settings.tsx`](apps/web/src/routes/_auth/settings.tsx) — Frame + `SettingRow` по [settings-16](https://reui.io/preview/base/settings-16)); починить `SettingsShell` defaults (убрать фантомные табы / сделать configurable tabs без мёртвых href) **или** не экспортировать мёртвый shell до появления 2+ секций.
- Добавить gating Quick Actions: ключ settings `show_quick_actions` (default `true`) + UI SettingRow (Switch) + чтение на `/` и `/agents`. Backend whitelist ключа — в Phase 2 вместе с Zod settings.
### 1.5 Auth callback + docs sync
- [`auth.callback.tsx`](apps/web/src/routes/auth.callback.tsx) — Frame + Empty/loading по DNA [auth-13](https://reui.io/preview/base/auth-13) / [empty-state-12](https://reui.io/preview/base/empty-state-12) (без локального login — portal SSO).
- Поправить `.cursor/rules/frontend-*.mdc`: убрать/пометить отсутствующие `login.tsx`, `KanbanBoard`, `settings/integrations.tsx`.
### 1.6 Verify Phase 1
- `pnpm --filter @evofw/web build` (+ typecheck/lint если есть в package).
---
## Phase 2 — Backend correctness + structure
### 2.1 Critical bugfixes (сначала)
- **`bumpAgentsForList`**: при `setIds.length === 0`**no-op**, не `bumpAllApprovedAgents` ([`packages/db/src/repositories/index.ts`](packages/db/src/repositories/index.ts)).
- **Zod `PUT /settings`**: whitelist ключей (`enroll_seed`, `evobgp_api_url`, `evobgp_api_token`, `agent_sync_interval_sec`, `show_quick_actions`, …) в `@evofw/shared`.
- **RBAC**: дополнить [`permissions.ts`](packages/shared/src/permissions.ts) — `install-links``fw:agents:*`, `integrations/*``fw:settings:read` / `fw:lists:read`.
- Тест на bump no-op + settings reject unknown key + RBAC 403.
### 2.2 N+1 / perf
- `GET /agents`: join/batch install links вместо per-row `getInstallLinkByAgentId`.
- `GET /lists` / policy-sets: batch counts (GROUP BY), не `listEntries().length` / dual count per row.
- `replaceIpListEntries` / `replaceResolvedForRule`: batch `values([...])`.
### 2.3 Split monoliths (как vps-tracker)
- Разрезать [`control.ts`](apps/api/src/routes/control.ts) (~1020 строк) → `routes/{agents,lists,policy-sets,rules,install-links,settings,integrations-evobgp,stats,dashboard}.ts` + thin register в `app.ts`.
- Разрезать [`repositories/index.ts`](packages/db/src/repositories/index.ts) → `repositories/{agents,lists,policy,stats,install-links,settings}.ts` + barrel.
- Вынести `mapAgent` / `mapPolicySet` / `mapPolicyRule``apps/api/src/services/row-mappers.ts`.
- Общий `uniq()` → один util.
### 2.4 Legacy hygiene (без breaking agent binary)
- Документировать `policy_mode` как deprecated read-only; не расширять usage.
- `domains` list type: UI/API уже нормализуют в `static` — убрать из новых create paths / labels где ещё светится.
- Не дропать колонки в этой фазе (отдельная migration later).
### 2.5 Verify Phase 2
- `pnpm --filter @evofw/api test` + smoke inject на approve/list/settings.
---
## Phase 3 — Contract + tests
### 3.1 OpenAPI
- Дописать [`docs/openapi.yaml`](docs/openapi.yaml) до паритета с реальными routes (agents, lists, policy-sets, rules, overrides, install-links, preview, stats, settings, integrations, agent enroll/policy/apply).
- Пометить в [`docs/README.md`](docs/README.md): OpenAPI = SoT для HTTP; живой runtime validation = Zod shared.
- CI: redocly lint (по паттерну EvoBGP) — добавить job/script если CI уже есть.
### 3.2 Error envelope
- Унифицировать SPA 404 и API errors на один формат `{ error: { code, message } }` (текущий API) **или** problem+json везде. Default: **оставить API `{error}`**, поправить SPA fallback в [`app.ts`](apps/api/src/app.ts) под тот же envelope.
### 3.3 Test belt
- Inject tests: agents CRUD approve/revoke, policy rule create+reorder, list refresh bump, settings whitelist, RBAC install-links/integrations, `bumpAgentsForList` unused list.
- Не гнаться за 100% coverage — critical paths only.
### 3.4 Verify Phase 3
- API tests green; OpenAPI lint; web build still green.
---
## Out of scope (явно)
- Локальный login page (SSO portal).
- KanbanBoard (нет домена board в EvoFirewall).
- Drop DB columns `policy_mode` / migrate all `domains` rows (отдельный breaking change).
- Prefix-overlap conflict resolution в evaluate (менять семантику только отдельным RFC).
@@ -0,0 +1,216 @@
---
name: EvoFW classic policy
overview: Классическая цепочка deny→allow→default_action (без white/blacklist). Страница агента по layout DNA solution-agents-3. Multi-set preview. EvoBGP community autocomplete/refresh.
todos:
- id: policy-semantics
content: Убрать white/blacklist; default_action accept|drop; единый apply deny→allow→default; deny-wins; apply_version; тесты
status: completed
- id: preview-api
content: "GET /agents/:id/preview: summary, chain, CIDRs, conflicts; поле default_action"
status: completed
- id: agent-page-sa3
content: Layout DNA solution-agents-3; Facts = default_action + identity (без mode toggle BL/WL)
status: completed
- id: agent-rules-ui
content: Sortable sets; set detail без policy_mode; CIDR tabs Блок/Accept
status: completed
- id: evobgp-prefixes
content: EvoBGP prefixes-by-community + FW refresh + docs
status: completed
- id: community-autocomplete
content: FW proxy GET /v1/communities + ReUI Autocomplete
status: completed
isProject: false
---
# Классическая политика EvoFirewall (без white/blacklist)
## Решение по white/blacklist
**White/blacklist как режимы не нужны.** Правила уже несут `deny`/`allow`; exclusive mode, который применял только один мешок CIDR, — ошибка модели.
Остаётся одна вещь, которую режимы маскировали: **что делать с пакетом, не попавшим ни в deny, ни в allow.**
| Было | Стало |
|------|--------|
| `policy_mode: blacklist` → только deny-set, default accept | `default_action: accept` |
| `policy_mode: whitelist` → только allow-set, default drop | `default_action: drop` |
| Набор со своим `policy_mode` + same-mode lock | У набора **нет** mode; только правила |
| Две разные nft/MikroTik цепочки | **Одна** цепочка всегда |
```text
1. established / lo → accept (Linux)
2. @deny → drop
3. @allow → accept
4. default_action → accept | drop
```
Миграция значений: `blacklist``accept`, `whitelist``drop`.
---
## Вердикт SA3 для страницы агента
**Да как layout DNA** (Header + Trace 2/3 + Facts 1/3), **нет** как AI-run шаблон.
| Элемент SA3 | Решение |
|-------------|---------|
| Header + actions | Approve / Revoke / Install / More |
| Trace Timeline | Policy chain (правила Блок/Accept) |
| Facts EditableDetailRow | **`default_action`** + identity/timestamps |
| Retry / Pause / Owners / tool calls | Не копировать |
| QuickActionGrid на detail | Убрать (действия в header) |
Preview: [solution-agents-3](https://reui.io/preview/base/solution-agents-3) · [Solutions/Agents](https://reui.io/blocks/solutions/agents)
Страница: [`agents/$id.tsx`](c:\Users\shats\Dev\EvoFirewall\apps\web\src\routes\_auth\agents\$id.tsx)
---
## Исходное состояние (проблемы)
- Same-mode lock на assign sets ([`setAgentPolicySets`](c:\Users\shats\Dev\EvoFirewall\packages\db\src\repositories\index.ts))
- Evaluate берёт mode с первого set; whitelist **игнорирует deny** в nft; blacklist **игнорирует allow**
- MikroTik RSC **переключает** bl/wl, а не держит обе list-правила
- Preview UI нет; EvoBGP refresh path мёртвый
---
## 1. Backend
### Модель
- **Правило:** `action: deny | allow` + источник (list / cidr / hostname)
- **Агент:** `default_action: accept | drop` (колонка: migrate `agents.policy_mode``default_action` **или** переиспользовать колонку с новыми значениями + backfill)
- **Набор:** без mode; убрать UI/валидацию `policy_mode`; колонка `policy_sets.policy_mode` — legacy ignore / drop в след. релизе
- Убрать same-mode check; **не** писать default агента из sets
### Evaluate
[`evaluate.ts`](c:\Users\shats\Dev\EvoFirewall\apps\api\src\services\policy\evaluate.ts):
- Сбор bags по цепочке set.sort → rule.priority → overrides
- `allowCidrs = allow \ deny` (exact) → `conflicts_dropped`
- `defaultAction` с агента
- Hash: `{ apply_version: 2, defaultAction, denyCidrs, allowCidrs, generation }`
### Agent protocol / apply
Единая цепочка (nft, ipset/iptables, MikroTik install + [`mikrotik-rsc.ts`](c:\Users\shats\Dev\EvoFirewall\apps\api\src\services\policy\mikrotik-rsc.ts)):
1. established/lo accept
2. deny → drop
3. allow → accept
4. default = `default_action`
JSON агенту (apply_version ≥ 2):
```json
{
"apply_version": 2,
"default_action": "accept",
"deny_cidrs": [],
"allow_cidrs": [],
"hash": "...",
"generation": 1
}
```
Compat на один релиз (опционально): зеркало `policy_mode: accept→blacklist, drop→whitelist` **только** для старых бинарников; новые скрипты читают `default_action` + всегда единую цепочку. После обновления one-liner — убрать зеркало.
### API / contracts
[`packages/shared/src/contracts.ts`](c:\Users\shats\Dev\EvoFirewall\packages\shared\src\contracts.ts):
- `defaultActionSchema = z.enum(['accept', 'drop'])`
- Agent PATCH/response: `default_action` (accept legacy `policy_mode` blacklist/whitelist → map)
- Policy set create/patch: **не** требовать / не экспозить mode в UI; API может игнорировать поле
- Preview:
```ts
{
default_action, hash, generation, sync_interval_sec, apply_version: 2,
summary: { sets, rules_deny, rules_allow, cidrs_deny, cidrs_allow, overrides, conflicts_dropped },
chain: [{ set_id, set_name, rule_id, action, source_kind, source_label, cidr_count }],
deny_cidrs, allow_cidrs, deny_cidrs_total, allow_cidrs_total
}
```
Только snake_case. Удалить `PolicyModeToggle` / mode с set detail.
Тесты: mixed deny+allow; exact conflict; unified apply оба default; assign разных бывших «режимов» sets без 400; hash apply_version.
---
## 2. UI: агент (SA3 layout DNA)
```bash
cd apps/web && pnpm dlx shadcn@latest add @reui/solution-agents-3 --yes
```
```
PageShell
├── AgentHeader
├── KpiStatGrid ← summary [stats-12]
├── grid @4xl: 2/3 + 1/3
│ ├── AgentPolicyTrace ← chain Timeline + Collapsible + Alert
│ └── AgentFactsPanel ← default_action Select + identity/dates
├── AgentPolicySetsSortable ← без фильтра mode
└── Effective CIDR Tabs ← Блок / Accept
```
Facts editable: «Если не совпало» → Accept / Drop (`default_action`).
Не показывать слова blacklist/whitelist.
### Preview-референсы
| Зона | Preview | Docs |
|------|---------|------|
| Каркас | [solution-agents-3](https://reui.io/preview/base/solution-agents-3) | [agents blocks](https://reui.io/blocks/solutions/agents) |
| Frame | [frame](https://reui.io/components/frame) | [docs](https://reui.io/docs/components/base/frame) |
| Timeline | [c-timeline-6](https://reui.io/preview/base/components/c-timeline-6) | [timeline](https://reui.io/docs/components/base/timeline) |
| Facts rows | SA3 RunFacts · [settings-3](https://reui.io/preview/base/settings-3) | [settings](https://reui.io/blocks/application/settings) |
| KPI | [stats-12](https://reui.io/preview/base/stats-12) | `KpiStatGrid` |
| Sortable | [c-sortable-5](https://reui.io/preview/base/components/c-sortable-5) | [sortable](https://reui.io/components/sortable) |
| CIDR tabs | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) · [c-tabs-2](https://reui.io/preview/base/components/c-tabs-2) | [data-grid](https://reui.io/docs/components/base/data-grid) |
| Alert | [c-alert-7](https://reui.io/preview/base/components/c-alert-7) | [alert](https://reui.io/docs/components/base/alert) |
| Empty | [empty-state-12](https://reui.io/preview/base/empty-state-12) | — |
| Sheets | [c-sheet-1](https://reui.io/preview/base/components/c-sheet-1) · [form-7](https://reui.io/preview/base/form-7) | [sheet](https://reui.io/components/sheet) |
| Autocomplete | [c-autocomplete-8](https://reui.io/preview/base/components/c-autocomplete-8) · [c-autocomplete-9](https://reui.io/preview/base/components/c-autocomplete-9) | [autocomplete](https://reui.io/docs/components/base/autocomplete) |
| Badge | [badge](https://reui.io/components/badge) | [docs](https://reui.io/docs/components/base/badge) |
Set detail: только правила deny/allow + sortable; без mode.
Lists: community Autocomplete.
---
## 3. EvoBGP
- Autocomplete: proxy → `GET /v1/communities`
- Refresh: новый `GET /v1/communities/{id}/prefixes` в EvoBGP + fix [`refresh.ts`](c:\Users\shats\Dev\EvoFirewall\apps\api\src\services\lists\refresh.ts)
- Docs [`integrate-evobgp.md`](c:\Users\shats\Dev\EvoFirewall\docs\integrate-evobgp.md)
---
## 4. Docs
- architecture / agents: нет BL/WL; `default_action`; единый apply
- OpenAPI: `default_action`, preview, communities proxy
---
## Порядок работ
1. Schema/contracts: `default_action`; deprecate set/agent `policy_mode` semantics
2. Evaluate + unified apply (nft/ipset/MikroTik) + tests + apply_version
3. Preview API
4. SA3 agent page + Facts `default_action`
5. Sets sortable / set detail cleanup
6. EvoBGP prefixes + refresh
7. Communities autocomplete
8. Docs + build
## Вне скоупа
- Full first-match без sets
- IPv6
- AI-run UX из SA3 (Retry/Pause/Owners)
@@ -205,7 +205,7 @@ export function VpsEditSheet({
<NumberField
id="vps-vcpu"
min={0}
step={1}
step="any"
value={Number(field.value ?? 0)}
onValueChange={(val) => field.onChange(val ?? 0)}
>
@@ -226,7 +226,7 @@ export function VpsEditSheet({
<NumberField
id="vps-ram"
min={0}
step={1}
step="any"
value={Number(field.value ?? 0)}
onValueChange={(val) => field.onChange(val ?? 0)}
>
@@ -247,7 +247,7 @@ export function VpsEditSheet({
<NumberField
id="vps-disk"
min={0}
step={1}
step="any"
value={Number(field.value ?? 0)}
onValueChange={(val) => field.onChange(val ?? 0)}
>
+1 -1
View File
@@ -50,7 +50,7 @@ export const vpsSchema = z.object({
country: z.string().optional().default(''),
city: z.string().optional().default(''),
datacenter: z.string().optional().default(''),
vcpu: z.coerce.number().int().min(0).default(1),
vcpu: z.coerce.number().min(0).default(1),
ramGb: z.coerce.number().min(0).default(1),
diskGb: z.coerce.number().min(0).default(10),
status: vpsStatusSchema.default('active'),
+3 -3
View File
@@ -77,12 +77,12 @@ const CORE_TABLE_MIGRATIONS: string[] = [
city TEXT,
datacenter TEXT,
os TEXT,
vcpu INTEGER,
vcpu REAL,
ramGb REAL,
diskGb INTEGER,
diskGb REAL,
diskType TEXT,
virtualization TEXT,
bandwidthTb INTEGER,
bandwidthTb REAL,
sshPort INTEGER,
rootUser TEXT,
purpose TEXT,
+4 -4
View File
@@ -119,12 +119,12 @@ export const vps = sqliteTable('vps', {
city: text('city'),
datacenter: text('datacenter'),
os: text('os'),
vcpu: integer('vcpu'),
ramGb: integer('ramGb'),
diskGb: integer('diskGb'),
vcpu: real('vcpu'),
ramGb: real('ramGb'),
diskGb: real('diskGb'),
diskType: text('diskType'),
virtualization: text('virtualization'),
bandwidthTb: integer('bandwidthTb'),
bandwidthTb: real('bandwidthTb'),
sshPort: integer('sshPort'),
rootUser: text('rootUser'),
purpose: text('purpose'),
+3 -3
View File
@@ -67,12 +67,12 @@ CREATE TABLE IF NOT EXISTS vps (
city TEXT,
datacenter TEXT,
os TEXT,
vcpu INTEGER,
vcpu REAL,
ramGb REAL,
diskGb INTEGER,
diskGb REAL,
diskType TEXT,
virtualization TEXT,
bandwidthTb INTEGER,
bandwidthTb REAL,
sshPort INTEGER,
rootUser TEXT,
purpose TEXT,