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). Условие срабатывает только если цепочка состояний совпадает с текстом правила.