From ec43591a992c1f3f12e2a1e659eb3da028c563bc Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 8 Sep 2026 01:36:48 +0700 Subject: [PATCH] =?UTF-8?q?feat(db):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=D1=89=D0=B5=20=D1=81=20SQLite=20=D0=BD=D0=B0=20PostgreSQ?= =?UTF-8?q?L?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump. Co-authored-by: Cursor --- .gitea/workflows/docker.yml | 43 +- README.md | 51 +- app/(main)/alerts/page.tsx | 2 +- app/(main)/data-collection/page.tsx | 6 +- app/(main)/settings/page.tsx | 16 +- backend/.env.example | 6 +- backend/Dockerfile | 10 +- backend/drizzle.config.ts | 4 +- backend/drizzle/0000_living_xorn.sql | 36 - backend/drizzle/0000_postgresql.sql | 626 ++++++++++ backend/drizzle/meta/_journal.json | 8 +- backend/package.json | 11 +- backend/scripts/inventory-sqlite.mjs | 39 + backend/src/config.ts | 6 + backend/src/db/bootstrap.ts | 33 + backend/src/db/index.ts | 1096 +---------------- backend/src/db/json.ts | 43 + backend/src/db/migrate.ts | 43 + backend/src/db/partitions.ts | 129 ++ backend/src/db/pg-schema.test.ts | 56 + backend/src/db/schema.ts | 712 ++++++----- backend/src/db/sql-bind.test.ts | 21 + backend/src/db/sql-bind.ts | 22 + backend/src/db/sqlite-import.ts | 479 +++++++ backend/src/index.ts | 8 +- .../events/repository/events-repository.ts | 27 +- .../modules/events/service/events-service.ts | 12 +- .../modules/servers/mapper/servers-mapper.ts | 39 +- .../servers/repository/servers-repository.ts | 34 +- .../servers/service/servers-service.ts | 33 +- backend/src/modules/users/bindings.test.ts | 138 +-- backend/src/modules/users/iface-type.ts | 8 +- .../users/repository/users-repository.ts | 62 +- .../modules/users/service/users-service.ts | 119 +- backend/src/plugins/auth.smoke.test.ts | 1 - backend/src/routes/alerts.ts | 24 +- backend/src/routes/backups.ts | 22 +- backend/src/routes/bgp.ts | 8 +- backend/src/routes/certificates.ts | 20 +- backend/src/routes/events.ts | 4 +- backend/src/routes/evobgp.ts | 36 +- backend/src/routes/exec.ts | 4 +- backend/src/routes/filters.ts | 74 +- backend/src/routes/firewall.ts | 22 +- backend/src/routes/internet-path.ts | 20 +- backend/src/routes/ospf.ts | 51 +- backend/src/routes/probes.ts | 12 +- backend/src/routes/recursive-routes.ts | 27 +- backend/src/routes/scheduler.ts | 10 +- backend/src/routes/servers-api-ping.ts | 8 +- backend/src/routes/servers.ts | 28 +- backend/src/routes/sidebar-counts.ts | 22 +- backend/src/routes/system-database.ts | 8 +- backend/src/routes/traffic-flow.ts | 32 +- backend/src/routes/traffic.ts | 53 +- backend/src/routes/uptime.ts | 66 +- backend/src/routes/users.ts | 14 +- backend/src/routes/wireguard.ts | 18 +- backend/src/scripts/migrate-sqlite-to-pg.ts | 20 + backend/src/services/acme-cloudflare.ts | 22 +- backend/src/services/alert-collector-hooks.ts | 6 +- .../alert-engine/confirm-stability.test.ts | 21 +- .../alert-engine/confirm-stability.ts | 34 +- .../services/alert-engine/decision-engine.ts | 8 +- backend/src/services/alert-engine/outbox.ts | 34 +- .../services/alert-engine/prev-live-store.ts | 33 +- backend/src/services/alert-engine/run-once.ts | 49 +- .../services/alert-engine/signal-ingestor.ts | 4 +- .../alert-engine/signals-from-collectors.ts | 46 +- backend/src/services/alert-engine/snapshot.ts | 4 +- .../services/alert-engine/source-watermark.ts | 24 +- backend/src/services/alerts-service.ts | 223 ++-- .../services/backup-scheduler-collector.ts | 16 +- backend/src/services/backup-service.ts | 81 +- backend/src/services/bgp-peers-live.ts | 2 +- .../src/services/certificate-issue-runner.ts | 28 +- .../services/certificate-renew-collector.ts | 16 +- backend/src/services/certificates-service.ts | 116 +- backend/src/services/firewall-live.ts | 2 +- .../services/gre-bgp-snapshot-collector.ts | 4 +- backend/src/services/gre-tunnels-live.ts | 2 +- .../src/services/internet-path-collector.ts | 77 +- backend/src/services/poller.ts | 24 +- backend/src/services/scheduler.ts | 77 +- .../services/servers-rest-ping-collector.ts | 96 +- backend/src/services/sqlite-write-opt.test.ts | 144 --- .../src/services/system-database-backup.ts | 104 +- backend/src/services/traffic-collector.ts | 66 +- .../services/traffic-flow-analytics.test.ts | 136 +- .../src/services/traffic-flow-analytics.ts | 52 +- backend/src/services/traffic-flow-classify.ts | 2 +- .../services/traffic-flow-collector-ipc.ts | 1 - .../services/traffic-flow-collector-worker.ts | 23 +- backend/src/services/traffic-flow-engine.ts | 331 +++-- .../services/traffic-flow-hardening.test.ts | 38 - .../src/services/traffic-flow-host-files.ts | 26 +- backend/src/services/traffic-flow-ifaces.ts | 2 +- .../src/services/traffic-flow-ingest.test.ts | 32 +- backend/src/services/traffic-flow-ingest.ts | 211 ++-- .../services/traffic-flow-map-hops.test.ts | 24 +- backend/src/services/traffic-flow-map-hops.ts | 24 +- backend/src/services/traffic-flow-overlay.ts | 27 +- .../src/services/traffic-flow-purge.test.ts | 115 +- backend/src/services/traffic-flow-ripe.ts | 70 +- backend/src/services/traffic-flow-settings.ts | 101 +- backend/src/services/traffic-flow-topology.ts | 41 +- backend/src/services/traffic-users.ts | 22 +- backend/src/services/uptime-collector.ts | 139 +-- backend/src/services/uptime-speed-service.ts | 40 +- backend/src/services/wireguard-live.ts | 12 +- backend/src/test/pg.ts | 22 + backend/src/types/scheduler-run-snapshot.ts | 2 +- components/traffic/netflow-purge-dialog.tsx | 11 +- components/traffic/netflow-settings-panel.tsx | 4 +- deploy/docker-compose.cdn-mm.yml | 54 +- deploy/docker-compose.postgres.yml | 50 + deploy/docker-compose.traefik-cdn.yml | 54 +- deploy/docker-compose.traefik.yml | 54 +- deploy/docker-compose.yml | 60 +- deploy/env.cdn-mm.example | 3 + deploy/env.postgres.example | 8 + deploy/env.postgres.snippet.example | 4 + deploy/env.traefik.example | 3 + deploy/run-beside-cdn-traefik.sh | 33 +- deploy/updater/Dockerfile | 2 +- deploy/updater/entrypoint.sh | 2 +- lib/scheduler-settings.ts | 6 +- package-lock.json | 157 ++- 128 files changed, 4304 insertions(+), 3639 deletions(-) delete mode 100644 backend/drizzle/0000_living_xorn.sql create mode 100644 backend/drizzle/0000_postgresql.sql create mode 100644 backend/scripts/inventory-sqlite.mjs create mode 100644 backend/src/db/bootstrap.ts create mode 100644 backend/src/db/json.ts create mode 100644 backend/src/db/migrate.ts create mode 100644 backend/src/db/partitions.ts create mode 100644 backend/src/db/pg-schema.test.ts create mode 100644 backend/src/db/sql-bind.test.ts create mode 100644 backend/src/db/sql-bind.ts create mode 100644 backend/src/db/sqlite-import.ts create mode 100644 backend/src/scripts/migrate-sqlite-to-pg.ts delete mode 100644 backend/src/services/sqlite-write-opt.test.ts delete mode 100644 backend/src/services/traffic-flow-hardening.test.ts create mode 100644 backend/src/test/pg.ts create mode 100644 deploy/docker-compose.postgres.yml create mode 100644 deploy/env.postgres.example create mode 100644 deploy/env.postgres.snippet.example diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index 136728d..65abb4b 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -38,8 +38,49 @@ jobs: node .ci/scripts/compute-release.mjs test -f .ci/release/release-manifest.json + backend-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Start PostgreSQL 18 + shell: bash + run: | + set -euo pipefail + docker run -d --name mm-pg \ + -e POSTGRES_USER=mmapp \ + -e POSTGRES_PASSWORD=mmapp \ + -e POSTGRES_DB=mmapp \ + -p 5432:5432 \ + postgres:18-alpine + for i in $(seq 1 40); do + if docker exec mm-pg pg_isready -U mmapp -d mmapp; then + exit 0 + fi + sleep 1 + done + echo "PostgreSQL не поднялся" + exit 1 + + - name: Install and test backend + shell: bash + env: + DATABASE_URL: postgres://mmapp:mmapp@127.0.0.1:5432/mmapp + run: | + set -euo pipefail + npm ci + npm run test --prefix backend + backend-image: - needs: prepare-release + needs: + - prepare-release + - backend-test runs-on: ubuntu-latest steps: - name: Checkout diff --git a/README.md b/README.md index b0a340b..50f126c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ | Компонент | Путь | Стек | Порт (runtime) | Docker-образ | |-----------|------|------|----------------|--------------| | Frontend | корень (`app/`, `components/`, `lib/`, …) | Next.js 16.2.4 (App Router), React 19 | 3000 | `…-frontend` | -| Backend | `backend/` | Fastify 5, TypeScript ESM, SQLite, Drizzle | 8000 | `…-backend` | +| Backend | `backend/` | Fastify 5, TypeScript ESM, PostgreSQL 18, Drizzle | 8000 | `…-backend` | | Контракты | `packages/contracts/` | Zod 4, `@mmapp/contracts` | — | встраиваются в frontend/backend | | Updater | `deploy/updater/` | bash, `docker:27-cli`, curl, jq | — | `…-updater` | | Деплой | `deploy/docker-compose.yml` | Docker Compose | — | — | @@ -43,11 +43,11 @@ flowchart TB Browser["Браузер"] NextDev["Next.js :3000"] FastifyDev["Fastify :8000"] - SqliteDev["SQLite файл"] + PostgresDev["PostgreSQL 18"] Contracts["@mmapp/contracts"] Browser --> NextDev Browser -->|"fetch /api, CORS"| FastifyDev - FastifyDev --> SqliteDev + FastifyDev --> PostgresDev Contracts --> NextDev Contracts --> FastifyDev end @@ -60,7 +60,8 @@ flowchart TB subgraph prod [Прод-сервер] FE["mmapp-frontend :3000"] BE["mmapp-backend :8000"] - DBVol["volume backend-data /app/data"] + PG["mmapp-postgres"] + DBVol["volume sqlite ETL /app/data"] UPD["mmapp-updater"] Sock["/var/run/docker.sock"] Reg --> FE @@ -69,6 +70,7 @@ flowchart TB UPD --> Sock UPD --> FE UPD --> BE + BE --> PG BE --> DBVol BrowserProd["Клиент"] --> FE BrowserProd -->|"live + backendUrl"| BE @@ -117,7 +119,7 @@ sequenceDiagram ``` - Триггер workflow: `push` в ветку `main`, ручной `workflow_dispatch`. -- Параллельные jobs: `backend-image`, `frontend-image`, `updater-image`; опционально `notify-webhook` после backend и frontend, если задан секрет `DEPLOY_WEBHOOK_URL`. +- Параллельные jobs: `backend-test` (PostgreSQL 18), `backend-image`, `frontend-image`, `updater-image`; опционально `notify-webhook` после backend и frontend, если задан секрет `DEPLOY_WEBHOOK_URL`. - Теги на каждый успешный push: **`:latest`** и **`:`**; платформа **linux/amd64**. - На сервере образы подтягиваются вручную (`docker compose pull`) и/или через **updater** (сравнение digest у тега из `targets.json`). Webhook CI **не заменяет** updater. @@ -125,7 +127,8 @@ sequenceDiagram | Зависимость | Реализация | |-------------|------------| -| SQLite | Не отдельный контейнер. Файл `mikrotik.db` в томе `backend-data` → `/app/data` (`DATABASE_PATH=/app/data/mikrotik.db` в образе backend). | +| PostgreSQL | Контейнер `mmapp-postgres` (`postgres:18-alpine`). `DATABASE_URL=postgres://mmapp:…@postgres:5432/mmapp`. | +| SQLite (ETL) | Файл `mikrotik.db` на томе `/app/data` (`DATABASE_PATH=/app/data/mikrotik.db`). При первом старте, если PG пустой, backend сам импортирует данные и ставит маркер. Повторный старт не копирует заново. | | Docker socket | Только у контейнера updater: `/var/run/docker.sock` — доступ к Docker API хоста (управление контейнерами, pull). | ## Локальная разработка @@ -134,7 +137,8 @@ sequenceDiagram - **Node.js 22** (как в `Dockerfile.frontend` и `backend/Dockerfile`). - **npm** с workspaces; установка из корня: `npm ci` или `npm install`. -- Для нативной сборки `better-sqlite3` на Linux может понадобиться toolchain (`python3`, `make`, `g++`); в Docker-образе backend они уже ставятся. +- Для нативной сборки `better-sqlite3` на Linux может понадобиться toolchain (`python3`, `make`, `g++`); в Docker-образе backend они уже ставятся (нужен только для одноразового boot-ETL из `mikrotik.db`). +- Локально нужен PostgreSQL 18 (`deploy/docker-compose.postgres.yml` или `DATABASE_URL`). - Backend Docker-образ ставит только workspaces `backend` + `contracts` (без корневых Next/React deps); в production логи — JSON без `pino-pretty`. ### Запуск @@ -161,7 +165,8 @@ Backend — скопировать `backend/.env.example` в `backend/.env`: | Переменная | По умолчанию | Назначение | |------------|--------------|------------| -| `DATABASE_PATH` | `./mikrotik.db` | путь к файлу SQLite | +| `DATABASE_URL` | `postgres://mmapp:mmapp@127.0.0.1:5432/mmapp` | подключение к PostgreSQL | +| `DATABASE_PATH` | `./mikrotik.db` | SQLite только для boot-ETL, если PG пустой | | `PORT` | `8000` | порт Fastify | | `CORS_ORIGIN` | `http://localhost:3000` | origin фронтенда для CORS | @@ -179,12 +184,10 @@ Frontend — в репозитории нет корневого `.env.example`. npm run build -w @mmapp/contracts ``` -После изменения схем Drizzle: +Схема PostgreSQL накатывается при старте backend из [`backend/drizzle/0000_postgresql.sql`](backend/drizzle/0000_postgresql.sql). Опциональный ETL: ```bash -npm --prefix backend run db:generate -npm --prefix backend run db:migrate -npm --prefix backend run db:studio +npm --prefix backend run db:migrate-from-sqlite ``` ## CI/CD (Gitea Actions) @@ -281,7 +284,8 @@ SSO auth-portal: [`docs/integrate-auth-portal.md`](docs/integrate-auth-portal.md | Сервис | `container_name` | Образ (пример) | Порты host:container | Тома | `restart` | |--------|------------------|----------------|----------------------|------|-----------| -| backend | `mmapp-backend` | `git.shx.one/denozord/mikrotikmanager-backend:latest` | `8000:8000` | `backend-data` → `/app/data` | `unless-stopped` | +| postgres | `mmapp-postgres` | `postgres:18-alpine` | нет (внутренняя сеть) | `mmapp-pgdata` | `unless-stopped` | +| backend | `mmapp-backend` | `git.shx.one/denozord/mikrotikmanager-backend:latest` | `8000:8000` | sqlite ETL → `/app/data` | `unless-stopped` | | frontend | `mmapp-frontend` | `git.shx.one/denozord/mikrotikmanager-frontend:latest` | `3000:3000` | — | `unless-stopped` | | updater | `mmapp-updater` | `git.shx.one/denozord/mikrotikmanager-updater:latest` | не публикуются | docker.sock, `updater-state` → `/state`, `targets.json` → `/etc/updater/targets.json:ro` | `unless-stopped` | @@ -297,11 +301,14 @@ SSO auth-portal: [`docs/integrate-auth-portal.md`](docs/integrate-auth-portal.md ### Переменные окружения (прод) -**Backend** (в образе заданы `NODE_ENV=production`, `PORT=8000`, `DATABASE_PATH=/app/data/mikrotik.db`; в compose обычно переопределяют только CORS): +**Backend** (в образе заданы `NODE_ENV=production`, `PORT=8000`; в compose задают `DATABASE_URL`, `DATABASE_PATH` для ETL и CORS): | Переменная | Источник в compose | Назначение | |------------|-------------------|------------| | `CORS_ORIGIN` | `${CORS_ORIGIN:-http://localhost:3000}` | origin UI, с которого браузер вызывает API | +| `DATABASE_URL` | `postgres://mmapp:${POSTGRES_PASSWORD}@postgres:5432/mmapp` | PostgreSQL | +| `DATABASE_PATH` | `/app/data/mikrotik.db` | SQLite на томе для одноразового ETL | +| `POSTGRES_PASSWORD` | обязателен | пароль пользователя `mmapp` | **Frontend** (в образе: `PORT=3000`, `HOSTNAME=0.0.0.0`). URL API в образ **не** зашит — задаётся в браузере (режим live + URL бэкенда) вместе с `CORS_ORIGIN` на backend. @@ -312,7 +319,7 @@ SSO auth-portal: [`docs/integrate-auth-portal.md`](docs/integrate-auth-portal.md | `TARGETS_FILE` | `/etc/updater/targets.json` | список целей | | `STATE_FILE` | `/state/updater-state.json` | digest и ошибки | | `POLL_INTERVAL_SECONDS` | `300` | пауза между циклами опроса | -| `HEALTH_TIMEOUT_SECONDS` | `120` | ожидание HTTP health | +| `HEALTH_TIMEOUT_SECONDS` | `600` | ожидание HTTP health (первый boot с ETL может занять минуты) | | `STOP_TIMEOUT_SECONDS` | `30` | `docker stop -t` | | `REGISTRY` | `git.shx.one` | registry для `docker login` | | `REGISTRY_USERNAME` | пусто | логин (если заданы оба с паролем) | @@ -516,18 +523,20 @@ docker compose up -d --force-recreate ### Откат - **Автоматически:** updater при failed health после обновления — контейнер на `previous_digest`. -- **Вручную:** остановить контейнер, запустить образ с нужным тегом или digest из registry, сохранив те же volume и labels. Откат **схемы SQLite** updater не выполняет — нужен отдельный backup тома `backend-data` / файла `mikrotik.db`. +- **Вручную:** остановить контейнер, запустить образ с нужным тегом или digest из registry, сохранив те же volume и labels. Откат **схемы PostgreSQL** updater не выполняет — нужен `pg_dump` / restore или том `mmapp-pgdata`. -### Резервное копирование SQLite +### Резервное копирование PostgreSQL -Том `backend-data` (или `mmapp-backend-data` при явном `docker volume create`). Пример остановки backend для консистентной копии: +Том `mmapp-pgdata` и (до cleanup) sqlite `mikrotik.db` на `/app/data`. Из UI: Настройки → «Скачать бэкап» (`pg_dump -Fc`). С хоста: ```bash -docker stop mmapp-backend -docker run --rm -v backend-data:/data -v $(pwd):/backup alpine tar czf /backup/mikrotik-db-backup.tar.gz -C /data . -docker start mmapp-backend +docker exec mmapp-postgres pg_dump -Fc -U mmapp mmapp > mikrotik-manager.dump ``` +Восстановление: UI «Восстановить из файла» или `pg_restore --clean --if-exists -d …`. + +Том sqlite после успешного ETL и smoke **не удаляйте сразу** — оставьте как fallback, затем уберите `mikrotik.db`, когда маркер `data_migration.sqlite_imported_at` подтверждён. + ### Отладка | Действие | Команда | diff --git a/app/(main)/alerts/page.tsx b/app/(main)/alerts/page.tsx index 24cb09b..2b03a87 100644 --- a/app/(main)/alerts/page.tsx +++ b/app/(main)/alerts/page.tsx @@ -2846,7 +2846,7 @@ export default function AlertsPage() {

- Движок читает последние сэмплы из SQLite (uptime ресурсы, REST-пинг, пробы, GRE/BGP). Условие + Движок читает последние сэмплы из PostgreSQL (uptime ресурсы, REST-пинг, пробы, GRE/BGP). Условие срабатывает только если цепочка состояний совпадает с текстом правила.

    diff --git a/app/(main)/data-collection/page.tsx b/app/(main)/data-collection/page.tsx index 91d5a3f..ed48669 100644 --- a/app/(main)/data-collection/page.tsx +++ b/app/(main)/data-collection/page.tsx @@ -220,7 +220,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { ) : null}

    - Запись в SQLite на{" "} + Запись в PostgreSQL на{" "} {new Date(g.sampledAt).toLocaleString("ru-RU")} {" "}— строки GRE и BGP для движка оповещений.

    @@ -347,7 +347,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {

    Снимок на{" "} {new Date(a.sampledAt).toLocaleString("ru-RU")} - . Данные для правил берутся из SQLite после джоб сбора (трафик, uptime, REST, GRE+BGP и т.д.). + . Данные для правил берутся из PostgreSQL после джоб сбора (трафик, uptime, REST, GRE+BGP и т.д.).

    @@ -1215,7 +1215,7 @@ export default function DataCollectionPage() {