diff --git a/.cursor/plans/access_form-8_polish_9980a07f.plan.md b/.cursor/plans/access_form-8_polish_9980a07f.plan.md new file mode 100644 index 0000000..cfe67b8 --- /dev/null +++ b/.cursor/plans/access_form-8_polish_9980a07f.plan.md @@ -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 ` (удобно для 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 diff --git a/.cursor/plans/evobgp_без_nats_5c5f192d.plan.md b/.cursor/plans/evobgp_без_nats_5c5f192d.plan.md new file mode 100644 index 0000000..37aa8ab --- /dev/null +++ b/.cursor/plans/evobgp_без_nats_5c5f192d.plan.md @@ -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. \ No newline at end of file diff --git a/.cursor/plans/evofirewall_full_hardening_c6b872a5.plan.md b/.cursor/plans/evofirewall_full_hardening_c6b872a5.plan.md new file mode 100644 index 0000000..41479d6 --- /dev/null +++ b/.cursor/plans/evofirewall_full_hardening_c6b872a5.plan.md @@ -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 (строки ~504–550); 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). diff --git a/.cursor/plans/evofw_classic_policy_3641e7fb.plan.md b/.cursor/plans/evofw_classic_policy_3641e7fb.plan.md new file mode 100644 index 0000000..fe82dc7 --- /dev/null +++ b/.cursor/plans/evofw_classic_policy_3641e7fb.plan.md @@ -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) diff --git a/apps/web/src/components/domain/vps-edit-sheet.tsx b/apps/web/src/components/domain/vps-edit-sheet.tsx index 78311e8..ae04155 100644 --- a/apps/web/src/components/domain/vps-edit-sheet.tsx +++ b/apps/web/src/components/domain/vps-edit-sheet.tsx @@ -205,7 +205,7 @@ export function VpsEditSheet({ field.onChange(val ?? 0)} > @@ -226,7 +226,7 @@ export function VpsEditSheet({ field.onChange(val ?? 0)} > @@ -247,7 +247,7 @@ export function VpsEditSheet({ field.onChange(val ?? 0)} > diff --git a/apps/web/src/lib/schemas.ts b/apps/web/src/lib/schemas.ts index 0695dc2..a9ed1ad 100644 --- a/apps/web/src/lib/schemas.ts +++ b/apps/web/src/lib/schemas.ts @@ -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'), diff --git a/packages/db/src/runtime-migrate.ts b/packages/db/src/runtime-migrate.ts index c711ee1..d270852 100644 --- a/packages/db/src/runtime-migrate.ts +++ b/packages/db/src/runtime-migrate.ts @@ -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, diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index cd63468..68617aa 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -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'), diff --git a/packages/db/src/test-setup.ts b/packages/db/src/test-setup.ts index fc1373f..d6f83fb 100644 --- a/packages/db/src/test-setup.ts +++ b/packages/db/src/test-setup.ts @@ -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,