# MikrotikManager-3

Монорепозиторий веб-приложения для управления MikroTik: UI на Next.js, API на Fastify, общие Zod-контракты, сборка Docker-образов через Gitea Actions и автоматическое обновление контейнеров на сервере через sidecar updater.

## Содержание

1. [Состав монорепозитория](#состав-монорепозитория)
2. [Архитектура](#архитектура)
3. [Локальная разработка](#локальная-разработка)
4. [CI/CD (Gitea Actions)](#cicd-gitea-actions)
5. [Прод-развёртывание Docker](#прод-развёртывание-docker)
6. [Сервис updater](#сервис-updater)
7. [Первичная настройка сервера](#первичная-настройка-сервера)
8. [Эксплуатация и сопровождение](#эксплуатация-и-сопровождение)
9. [Чеклист деплоя](#чеклист-деплоя)

## Состав монорепозитория

| Компонент | Путь | Стек | Порт (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` |
| Контракты | `packages/contracts/` | Zod 4, `@mmapp/contracts` | — | встраиваются в frontend/backend |
| Updater | `deploy/updater/` | bash, `docker:27-cli`, curl, jq | — | `…-updater` |
| Деплой | `deploy/docker-compose.yml` | Docker Compose | — | — |
| CI | `.gitea/workflows/docker.yml` | Gitea Actions, Buildx | — | push в `git.shx.one` |

Имена образов в registry для репозитория `denozord/MikrotikManager-3`:

- `git.shx.one/denozord/mikrotikmanager-backend`
- `git.shx.one/denozord/mikrotikmanager-frontend`
- `git.shx.one/denozord/mikrotikmanager-updater`

Для другого owner/repo подставьте имя по правилу CI (см. [CI/CD](#cicd-gitea-actions)).

## Архитектура

### Общая схема

```mermaid
flowchart TB
  subgraph dev [Локальная разработка]
    Browser["Браузер"]
    NextDev["Next.js :3000"]
    FastifyDev["Fastify :8000"]
    SqliteDev["SQLite файл"]
    Contracts["@mmapp/contracts"]
    Browser --> NextDev
    Browser -->|"fetch /api, CORS"| FastifyDev
    FastifyDev --> SqliteDev
    Contracts --> NextDev
    Contracts --> FastifyDev
  end

  subgraph ci [Gitea Actions]
    WF[".gitea/workflows/docker.yml"]
    WF --> Reg["git.shx.one registry"]
  end

  subgraph prod [Прод-сервер]
    FE["mmapp-frontend :3000"]
    BE["mmapp-backend :8000"]
    DBVol["volume backend-data /app/data"]
    UPD["mmapp-updater"]
    Sock["/var/run/docker.sock"]
    Reg --> FE
    Reg --> BE
    Reg --> UPD
    UPD --> Sock
    UPD --> FE
    UPD --> BE
    BE --> DBVol
    BrowserProd["Клиент"] --> FE
    BrowserProd -->|"live + backendUrl"| BE
  end
```

### Frontend и backend

- Браузер загружает UI с порта **3000**.
- Запросы к API выполняются **напрямую с клиента** (`fetch`) на URL бэкенда. Next.js rewrites/proxy **не используются** (`next.config.ts` — только `output: "standalone"`).
- URL бэкенда и режим данных (`mock` / `live`) хранятся в `localStorage` (`routerlists:data-source`, `routerlists:backend-url`); по умолчанию `http://localhost:8000`. Настройка — страница **Настройки** в UI (`app/(main)/settings/page.tsx`, провайдер `lib/data-source.tsx`).
- Backend слушает **8000**, отдаёт `GET /health` и маршруты под `/api/…`. CORS разрешён для **одного** origin — переменная `CORS_ORIGIN` (`backend/src/config.ts`, `backend/src/index.ts`).
- Отдельно от UI-режима: `NEXT_PUBLIC_API_MODE` (`mock` по умолчанию, `live` для RouterOS REST) — `lib/api-mock.ts`; для работы с API приложения через бэкенд в проде достаточно режима **live** в UI и корректного `backendUrl`.

### Общие контракты (`packages/contracts`)

- npm workspaces: корень + `packages/*` + `backend` (`package.json`).
- Пакет `@mmapp/contracts`: сборка `tsc` → `dist/`, exports `./servers`, `./events`, `./alerts`.
- При `npm install` выполняется `postinstall` → `npm run build -w @mmapp/contracts`.
- Backend: валидация и типы в маршрутах; frontend: типы и разбор ответов в `shared/api/`.

### Поток образов: CI → registry → сервер

```mermaid
sequenceDiagram
  participant Gitea
  participant Actions
  participant Registry
  participant Updater
  participant Backend
  participant Frontend

  Gitea->>Actions: push main / workflow_dispatch
  Actions->>Registry: push backend/frontend/updater :latest и :sha
  Actions-->>Gitea: optional DEPLOY_WEBHOOK_URL POST
  loop каждые POLL_INTERVAL_SECONDS
    Updater->>Registry: imagetools inspect digest
    alt digest изменился
      Updater->>Registry: docker pull
      Updater->>Backend: stop/rm/run из snapshot
      Updater->>Frontend: stop/rm/run из snapshot
      Updater->>Backend: HTTP health
      Updater->>Frontend: HTTP health
    end
  end
```

- Триггер workflow: `push` в ветку `main`, ручной `workflow_dispatch`.
- Параллельные jobs: `backend-image`, `frontend-image`, `updater-image`; опционально `notify-webhook` после backend и frontend, если задан секрет `DEPLOY_WEBHOOK_URL`.
- Теги на каждый успешный push: **`:latest`** и **`:<commit-sha>`**; платформа **linux/amd64**.
- На сервере образы подтягиваются вручную (`docker compose pull`) и/или через **updater** (сравнение digest у тега из `targets.json`). Webhook CI **не заменяет** updater.

### Зависимости в проде

| Зависимость | Реализация |
|-------------|------------|
| SQLite | Не отдельный контейнер. Файл `mikrotik.db` в томе `backend-data` → `/app/data` (`DATABASE_PATH=/app/data/mikrotik.db` в образе backend). |
| Docker socket | Только у контейнера updater: `/var/run/docker.sock` — доступ к Docker API хоста (управление контейнерами, pull). |

## Локальная разработка

### Требования

- **Node.js 22** (как в `Dockerfile.frontend` и `backend/Dockerfile`).
- **npm** с workspaces; установка из корня: `npm ci` или `npm install`.
- Для нативной сборки `better-sqlite3` на Linux может понадобиться toolchain (`python3`, `make`, `g++`); в Docker-образе backend они уже ставятся.
- Backend Docker-образ ставит только workspaces `backend` + `contracts` (без корневых Next/React deps); в production логи — JSON без `pino-pretty`.

### Запуск

Два процесса (frontend и backend):

```powershell
npm install
npm run dev
```

```powershell
npm --prefix backend run dev
```

| Сервис | URL | Проверка |
|--------|-----|----------|
| Frontend | http://localhost:3000 | открыть в браузере |
| Backend | http://localhost:8000 | `curl -fsS http://localhost:8000/health` |

### Переменные окружения (разработка)

Backend — скопировать `backend/.env.example` в `backend/.env`:

| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `DATABASE_PATH` | `./mikrotik.db` | путь к файлу SQLite |
| `PORT` | `8000` | порт Fastify |
| `CORS_ORIGIN` | `http://localhost:3000` | origin фронтенда для CORS |

Frontend — в репозитории нет корневого `.env.example`. Опционально `.env.local`:

| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `NEXT_PUBLIC_API_MODE` | `mock` | `live` — вызовы RouterOS REST через `lib/api-mock.ts` |

Режим **live** для API приложения и URL бэкенда задаются в UI (не через build-time env).

### Контракты и БД в dev

```bash
npm run build -w @mmapp/contracts
```

После изменения схем Drizzle:

```bash
npm --prefix backend run db:generate
npm --prefix backend run db:migrate
npm --prefix backend run db:studio
```

## CI/CD (Gitea Actions)

Файл: `.gitea/workflows/docker.yml` (имя workflow: **Docker images**).

| Job | Build context | Dockerfile | Имя образа |
|-----|---------------|------------|------------|
| `backend-image` | `.ci/docker/backend` (staging в CI) | `backend/Dockerfile` | `git.shx.one/<owner>/<stem>-backend` |
| `frontend-image` | `.ci/docker/frontend` | `Dockerfile.frontend` | `git.shx.one/<owner>/<stem>-frontend` |
| `updater-image` | `deploy/updater` | `deploy/updater/Dockerfile` | `git.shx.one/<owner>/<stem>-updater` |

- `<owner>` — первая часть `gitea.repository`, lower case.
- `<stem>` — имя репозитория lower case без суффикса `-<цифры>` в конце (например `MikrotikManager-3` → `mikrotikmanager`).

Логин в registry в CI: `gitea.actor` + секрет **`ACTIONS_PAT`**.

Кэш Buildx: `type=gha`, отдельные scope `backend`, `frontend`, `updater`.

Опциональный job **`notify-webhook`**: POST JSON `{"repository","sha","ref","version","tag","releaseUrl"}` на URL из секрета **`DEPLOY_WEBHOOK_URL`** после успешной сборки backend и frontend и публикации релиза (updater в `needs` не входит).

### Версионирование и релизы

- Базовая версия: **`v1.0.0`**. Линия semver: `1.x.y` (major `2.x` вне scope).
- Job **`prepare-release`** запускает [`.ci/scripts/compute-release.mjs`](.ci/scripts/compute-release.mjs): коммиты с последнего тега `v*.*.*`, bump по **Conventional Commits**.
- **`feat` / `feat!` / `BREAKING CHANGE`** → minor (`1.x.0`); **`fix`**, `chore`, `docs`, `refactor`, `style`, `test`, `build`, `ci` → patch (`1.0.x`).
- Если после последнего тега нет новых коммитов, **релиз** пропускается; Docker-образы при этом всё равно собираются и публикуются с `:latest` и `:<sha>`.
- Первый релиз без тега `v1.0.0` возможен автоматически: CI берёт историю `HEAD`, считает bump от `1.0.0` и создаёт тег (например `v1.1.0` при `feat:`).
- Job **`publish-release`**: annotated tag `v1.2.3`, Gitea Release на `https://git.shx.one` (markdown notes), образы с тегами `:latest`, `:<sha>`, `:<semver>`.
- UI: версия в sidebar и страница **`/releases`**; manifest `public/release-manifest.json` (в CI подставляется из артефакта).
- Bootstrap: один раз выставить `1.0.0` в workspace `package.json` и создать тег **`v1.0.0`** на `main` перед первым автоматическим bump.
- Сообщения коммитов: см. [`.cursor/rules/release-versioning.mdc`](.cursor/rules/release-versioning.mdc); subject и body — **на русском**, префикс Conventional Commits — на английском.

## Прод-развёртывание Docker

Эталон без reverse-proxy: `deploy/docker-compose.yml` (порты `3000` / `8000` на хост).

Стек с Traefik + HTTPS (Let's Encrypt DNS-01 / Cloudflare), по аналогии с CDNManager: [`deploy/docker-compose.traefik.yml`](deploy/docker-compose.traefik.yml) + [`deploy/env.traefik.example`](deploy/env.traefik.example). На сервере публикуются только `:80`/`:443`; frontend получает HTTPS, `/api` и `/health` проксируются на backend внутри сети `mmapp`. Домен по умолчанию: `mm.shnt.top`.

### CDN Manager + MikrotikManager (один Traefik)

Полный стек: Traefik + `cdn.shnt.top` + `mm.shnt.top` в одном Compose.

| Файл | Назначение |
|------|------------|
| [`deploy/docker-compose.cdn-mm.yml`](deploy/docker-compose.cdn-mm.yml) | Traefik + CDN Manager + MM backend/frontend/updater |
| [`deploy/env.cdn-mm.example`](deploy/env.cdn-mm.example) | общий `.env` |

```bash
mkdir -p /opt/cdn-mm/{data/cdn,data/mm,state,updater}
cp deploy/docker-compose.cdn-mm.yml /opt/cdn-mm/docker-compose.yml
cp deploy/env.cdn-mm.example /opt/cdn-mm/.env
cp deploy/updater/targets.json.example /opt/cdn-mm/updater/targets.json
# заполнить CF_DNS_API_TOKEN, CLOUDFLARE_API_TOKEN, AUTH_JWT_SECRET, CORS_ORIGIN, …
docker login git.shx.one
cd /opt/cdn-mm && docker compose pull && docker compose up -d
curl -fsS https://cdn.shnt.top/health
curl -fsS https://mm.shnt.top/health
```

Данные: `./data/cdn` (CDN), `./data/mm` (MM). Сеть Traefik: `edge`. Не запускайте параллельно standalone `docker-compose.traefik.yml` CDNManager или MM на тех же 80/443.

**Если на сервере уже крутится CDNManager Traefik** (`cdnmanager-traefik`, сеть `cdnmanager`) — **не** поднимайте второй Traefik. Варианты:

| Способ | Файл |
|--------|------|
| Compose без своего Traefik | [`deploy/docker-compose.traefik-cdn.yml`](deploy/docker-compose.traefik-cdn.yml) |
| Plain `docker` CLI (скрипт) | [`deploy/run-beside-cdn-traefik.sh`](deploy/run-beside-cdn-traefik.sh) |

Frontend вешается в сеть `cdnmanager` с Traefik-labels; backend/updater остаются в `mmapp`. Сертификат для `MM_DOMAIN` выпускает уже работающий Traefik CDNManager (тот же `letsencrypt` / Cloudflare DNS-01).

```bash
# Compose (рекомендуется)
mkdir -p /opt/mmapp/{data,state,updater}
cp deploy/docker-compose.traefik-cdn.yml /opt/mmapp/docker-compose.yml
cp deploy/env.traefik.example /opt/mmapp/.env          # MM_DOMAIN + CORS_ORIGIN
cp deploy/updater/targets.json.example /opt/mmapp/updater/targets.json
cd /opt/mmapp && docker compose pull && docker compose up -d

# Или одной CLI-командой (скрипт сам сделает network/pull/run/connect):
curl -fsSL -o /tmp/run-beside-cdn-traefik.sh \
  https://git.shx.one/denozord/MikrotikManager/raw/branch/main/deploy/run-beside-cdn-traefik.sh
chmod +x /tmp/run-beside-cdn-traefik.sh
sudo MM_DOMAIN=mm.shnt.top CORS_ORIGIN=https://mm.shnt.top /tmp/run-beside-cdn-traefik.sh
```

DNS: `A`/`AAAA` для `mm.shnt.top` → IP VPS, Cloudflare **DNS only**. Проверка: `curl -fsS https://mm.shnt.top/health`.

SSO auth-portal: [`docs/integrate-auth-portal.md`](docs/integrate-auth-portal.md) (app id `mm`).

Рабочий каталог для команд compose — `deploy/` (или `-f deploy/docker-compose.yml` / `-f deploy/docker-compose.traefik.yml` / `-f deploy/docker-compose.traefik-cdn.yml` / `-f deploy/docker-compose.cdn-mm.yml` из корня).

### Прод-контейнеры

| Сервис | `container_name` | Образ (пример) | Порты host:container | Тома | `restart` |
|--------|------------------|----------------|----------------------|------|-----------|
| backend | `mmapp-backend` | `git.shx.one/denozord/mikrotikmanager-backend:latest` | `8000:8000` | `backend-data` → `/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` |

Метки для updater на backend и frontend:

| Метка | Пример значения |
|-------|-----------------|
| `mmapp.updater.managed` | `true` |
| `mmapp.updater.target` | `backend` / `frontend` |
| `mmapp.updater.image` | полное имя образа с тегом |

Явной пользовательской сети в compose нет — используется сеть проекта Compose по умолчанию.

### Переменные окружения (прод)

**Backend** (в образе заданы `NODE_ENV=production`, `PORT=8000`, `DATABASE_PATH=/app/data/mikrotik.db`; в compose обычно переопределяют только CORS):

| Переменная | Источник в compose | Назначение |
|------------|-------------------|------------|
| `CORS_ORIGIN` | `${CORS_ORIGIN:-http://localhost:3000}` | origin UI, с которого браузер вызывает API |

**Frontend** (в образе: `PORT=3000`, `HOSTNAME=0.0.0.0`). URL API в образ **не** зашит — задаётся в браузере (режим live + URL бэкенда) вместе с `CORS_ORIGIN` на backend.

**Updater**:

| Переменная | По умолчанию | Назначение |
|------------|--------------|------------|
| `TARGETS_FILE` | `/etc/updater/targets.json` | список целей |
| `STATE_FILE` | `/state/updater-state.json` | digest и ошибки |
| `POLL_INTERVAL_SECONDS` | `300` | пауза между циклами опроса |
| `HEALTH_TIMEOUT_SECONDS` | `120` | ожидание HTTP health |
| `STOP_TIMEOUT_SECONDS` | `30` | `docker stop -t` |
| `REGISTRY` | `git.shx.one` | registry для `docker login` |
| `REGISTRY_USERNAME` | пусто | логин (если заданы оба с паролем) |
| `REGISTRY_PASSWORD` | пусто | пароль registry |

### Docker Compose

```bash
docker login git.shx.one
export CORS_ORIGIN=http://<хост>:3000
export REGISTRY_USERNAME=<user>
export REGISTRY_PASSWORD=<token>
```

Скопировать и отредактировать конфиг updater (health URL должны быть достижимы **из контейнера updater**):

```bash
cp deploy/updater/targets.json.example deploy/updater/targets.json
```

В `deploy/docker-compose.yml` для updater заменить bind-mount на рабочий файл:

```yaml
- ./updater/targets.json:/etc/updater/targets.json:ro
```

Запуск:

```bash
cd deploy
docker compose pull
docker compose up -d
```

### Эквивалентные `docker run`

Имена томов можно согласовать с compose (`backend-data`, `updater-state`) или задать явно.

**Backend:**

```bash
docker volume create backend-data
docker run -d \
  --name mmapp-backend \
  --restart unless-stopped \
  -p 8000:8000 \
  -e CORS_ORIGIN=http://localhost:3000 \
  -v backend-data:/app/data \
  --label mmapp.updater.managed=true \
  --label mmapp.updater.target=backend \
  --label mmapp.updater.image=git.shx.one/denozord/mikrotikmanager-backend:latest \
  git.shx.one/denozord/mikrotikmanager-backend:latest
```

**Frontend:**

```bash
docker run -d \
  --name mmapp-frontend \
  --restart unless-stopped \
  -p 3000:3000 \
  --label mmapp.updater.managed=true \
  --label mmapp.updater.target=frontend \
  --label mmapp.updater.image=git.shx.one/denozord/mikrotikmanager-frontend:latest \
  git.shx.one/denozord/mikrotikmanager-frontend:latest
```

**Updater:**

```bash
docker volume create updater-state
docker run -d \
  --name mmapp-updater \
  --restart unless-stopped \
  -e REGISTRY=git.shx.one \
  -e REGISTRY_USERNAME=<user> \
  -e REGISTRY_PASSWORD=<token> \
  -e POLL_INTERVAL_SECONDS=300 \
  -e HEALTH_TIMEOUT_SECONDS=120 \
  -e STOP_TIMEOUT_SECONDS=30 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v updater-state:/state \
  -v /path/to/targets.json:/etc/updater/targets.json:ro \
  git.shx.one/denozord/mikrotikmanager-updater:latest
```

### Проверка после деплоя

| Проверка | Команда / ожидание |
|----------|-------------------|
| Backend health | `curl -fsS http://127.0.0.1:8000/health` → JSON со `status` |
| Frontend | `curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/` → `200` |
| Контейнеры | `docker ps --filter name=mmapp-` |
| UI | режим **live**, URL бэкенда совпадает с доступом клиента и с `CORS_ORIGIN` |

## Сервис updater

Исходники: `deploy/updater/entrypoint.sh`, образ `deploy/updater/Dockerfile`.

### Назначение

Sidecar на хосте с Docker: периодически сравнивает **digest манифеста** образа в registry с последним применённым digest, при изменении выполняет `docker pull`, пересоздаёт целевой контейнер и проверяет HTTP health. Состояние — файл `STATE_FILE` (том `updater-state`).

### Обнаружение новых образов

- `docker buildx imagetools inspect <image>` → digest манифеста (тег в `targets.json`, обычно `:latest`).
- Сравнение с `last_applied_digest` в state; при совпадении — noop.
- При первом запуске — baseline из digest работающего контейнера или remote, чтобы не пересоздавать без смены digest.
- Если remote digest недоступен — running контейнер **не** останавливается, в state пишется ошибка.

### Перезапуск backend/frontend

Только контейнеры с метками `mmapp.updater.managed=true`, совпадающими `mmapp.updater.target` и `mmapp.updater.image` с записью в `targets.json`.

Последовательность: `docker inspect` (snapshot) → `docker pull` → `docker stop` → `docker rm` → `docker run -d` с восстановлением env, mounts, `-p`, `--network`, labels, restart policy из snapshot.

При неуспешном health после обновления — откат на `previous_digest` (`image@digest`). Если предыдущего digest нет — откат пропускается (лог `rollback_skipped`).

### Конфигурация целей

Пример: `deploy/updater/targets.json.example`. Поля цели: `id`, `container_name`, `image`, `health.type`, `health.url`, `health.expect_status`.

**Важно:** в примере health URL — `http://127.0.0.1:8000/health` и `http://127.0.0.1:3000/`. В default bridge-сети контейнера updater `127.0.0.1` указывает на **сам updater**, а не на backend/frontend. Перед продом задайте URL, **достижимые из контейнера updater** (например опубликованные порты хоста при `network_mode: host` у updater, `host.docker.internal` с `extra_hosts`, или иной согласованный с вашей сетью вариант). Скопируйте example в `deploy/updater/targets.json` и отредактируйте.

### Docker socket и безопасность

- Монтирование `/var/run/docker.sock` даёт updater права на Docker API хоста: остановка, удаление и создание контейнеров, pull от имени хоста.
- Ограничение по меткам снижает риск случайного пересоздания чужих контейнеров, но **не** изолирует updater от остального Docker на хосте.
- Учётные данные registry передавайте через env, не коммитьте в git.
- Блокировка цели: `/state/locks/<target_id>`; при занятом lock цикл для цели пропускается.
- При ошибке цикла пауза удваивается (`2 × POLL_INTERVAL_SECONDS`) с джиттером до +10%.

### Валидация и staging

```bash
bash deploy/updater/validate.sh
```

Чеклист сценариев на staging: `deploy/updater/test-staging.sh`.

## Первичная настройка сервера

Пошагово на чистом Linux-хосте с доступом в интернет и к `git.shx.one`.

1. Установить Docker Engine и плагин Compose (официальная документация Docker для вашего дистрибутива).
2. Проверить:

```bash
docker version
docker compose version
```

3. Открыть входящие порты **3000** (frontend) и **8000** (backend) в firewall или настроить внешний доступ согласно вашей схеме (reverse proxy в репозитории не описан).
4. Войти в registry:

```bash
docker login git.shx.one
```

5. Получить файлы деплоя: клонировать репозиторий или скопировать каталог `deploy/` и подготовить `deploy/updater/targets.json`.
6. Задать переменные окружения для compose (`CORS_ORIGIN`, `REGISTRY_USERNAME`, `REGISTRY_PASSWORD` при приватных образах).
7. Подтянуть образы и запустить стек:

```bash
cd deploy
docker compose pull
docker compose up -d
```

8. Проверить health (см. таблицу выше) и логи updater:

```bash
docker logs mmapp-updater --tail 50
```

9. В UI включить режим **live** и указать URL бэкенда, доступный браузеру пользователя; убедиться, что значение совпадает с `CORS_ORIGIN` на backend.

## Эксплуатация и сопровождение

### Автообновление

Updater опрашивает registry с интервалом `POLL_INTERVAL_SECONDS` (по умолчанию 300 с). Обновление привязано к смене digest у образа из `targets.json` (часто тег `:latest` после push в `main`).

### Ручное обновление

```bash
docker pull git.shx.one/denozord/mikrotikmanager-backend:latest
docker pull git.shx.one/denozord/mikrotikmanager-frontend:latest
```

Через compose (пересоздание при смене образа):

```bash
cd deploy
docker compose pull
docker compose up -d --force-recreate
```

Пинning версии: тег `:<commit-sha>` из CI вместо `:latest` в `image`, метке `mmapp.updater.image` и в `targets.json`.

### Откат

- **Автоматически:** updater при failed health после обновления — контейнер на `previous_digest`.
- **Вручную:** остановить контейнер, запустить образ с нужным тегом или digest из registry, сохранив те же volume и labels. Откат **схемы SQLite** updater не выполняет — нужен отдельный backup тома `backend-data` / файла `mikrotik.db`.

### Резервное копирование SQLite

Том `backend-data` (или `mmapp-backend-data` при явном `docker volume create`). Пример остановки backend для консистентной копии:

```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 logs mmapp-backend`, `docker logs mmapp-frontend`, `docker logs mmapp-updater` |
| Следить в реальном времени | `docker logs -f mmapp-updater` |
| Конфигурация контейнера | `docker inspect mmapp-backend` |
| Состояние updater | том `updater-state`, файл `/state/updater-state.json` внутри контейнера |

## Чеклист деплоя

1. Установить Docker Engine и Compose plugin; проверить `docker version` и `docker compose version`.
2. Открыть порты 3000 и 8000 (или обеспечить доступ клиентов к UI и API).
3. Выполнить `docker login git.shx.one`.
4. Склонировать репозиторий или скопировать `deploy/`.
5. Задать `CORS_ORIGIN` (origin фронтенда для браузера).
6. Задать `REGISTRY_USERNAME` и `REGISTRY_PASSWORD` для updater при приватном registry.
7. Создать `deploy/updater/targets.json` из example; настроить health URL для сети хоста.
8. Обновить bind-mount targets в `deploy/docker-compose.yml` на `./updater/targets.json`.
9. Выполнить `docker compose pull` и `docker compose up -d` в каталоге `deploy`.
10. Проверить `curl` на `:8000/health` и `:3000/`.
11. В UI: режим live и URL бэкенда; сверить с `CORS_ORIGIN`.
12. Просмотреть `docker logs mmapp-updater`; настроить регулярный backup тома `backend-data`.
