Files
vidconf/docs/architecture/README.md
Max Ronzhin 693db6e774 install.sh, docs: pass --env-file explicitly to docker compose
docker compose определяет .env для подстановки ${VAR} по каталогу
compose-файла (deploy/), а не по текущей директории — repo-root .env,
который использует install.sh и вся документация, молча не подхватывался.
Это и была причина "WARN: LIVEKIT_API_KEY not set" на боевом сервере:
секреты были в .env, но compose их не видел и подставлял небезопасные
дефолты (см. таблицу разведки сервера).

Теперь ставшие обязательными ${VAR:?} в docker-compose.yml (см. предыдущий
коммит) без этого немедленно проваливали бы конфиг на любой из
документированных команд. Добавлен `--env-file .env`/"$ENV_FILE" ко всем
вызовам docker compose в install.sh и в командах из README/docs.

install.sh дополнительно: ensure_default (аналог ensure_secret без генерации
секрета) для новых не-секретных параметров nginx/coturn/livekit
(NGINX_SERVER_NAMES, NGINX_CERT_NAME, LIVEKIT_USE_EXTERNAL_IP,
LIVEKIT_NODE_IP, TURN_EXTERNAL_IP) — дефолты только для локальной
разработки, не перезаписывают значения, заданные вручную на боевом
сервере. Плюс вызов deploy/render-templates.sh перед сборкой/подъёмом
стека.
2026-07-25 21:58:03 +03:00

536 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура
Высокоуровневый обзор дизайна системы VidConf.
## Быстрые ссылки
- **[frontend-themes.md](./frontend-themes.md)** — архитектура темы оболочки: механизм `data-theme`, localStorage, React-hook, анти-FOUC, инвариант комнаты.
## Архитектура системы
```
┌─────────────────────────────────────────────────────────────┐
│ Web Browsers │
│ (React SPA + LiveKit JS SDK) │
└──────────────────────────┬──────────────────────────────────┘
│ HTTPS
┌──────▼───────┐
│ Nginx │ (TLS termination)
│ Reverse │
│ Proxy │
└──────┬───────┘
┌──────────────────┼──────────────────┐
│ │ │
┌────▼────┐ ┌──────▼──────┐ ┌─────▼─────┐
│ FastAPI │ │ LiveKit │ │ Coturn │
│ Backend │ │ SFU │ │ TURN │
└────┬────┘ └──────┬──────┘ └───────────┘
│ │
│ (TCP/Postgres) │ (RTP/SRTP WebRTC)
┌────▼──────────────────┘
├──► PostgreSQL 16
│ - 14 таблиц: users, teams, conferences, conference_sessions и т.д.
├──► Redis
│ - Celery broker
│ - Rate limiting, refresh-токены, pub/sub чата
└──► Celery воркеры (раздельные очереди)
├─► transcription (faster-whisper small/medium/large-v3 → фразы)
├─► summarize (Qwen3.5 4B/9B/35B-A3B → резюме, 3 уровня)
└─► notify (email + .ics → SMTP)
```
## Основные компоненты
### 1. Frontend (React SPA)
**Location:** `frontend/`
**Tech:** React 18 + TypeScript + Vite, Tailwind CSS 4 + @tailwindcss/vite, shadcn/ui, `@livekit/components-react`, FullCalendar
**Функциональность:**
- Аутентификация (вход, регистрация, верификация email), JWT (access — заголовок Authorization, refresh — httpOnly cookie)
- Лобби-хаб, календарь (создание/редактирование/отмена конференций, повторение), «мои конференции»
- Вход в конференцию по номеру/ссылке, гостевой вход
- Комната конференции: видео/аудио через LiveKit JS SDK, чат в реальном времени, настройки устройств, fullscreen, Picture-in-Picture, демонстрация экрана
- Профиль пользователя (ФИО, команда, аватар, смена пароля)
- Админ-панель (конференции, пользователи, команды, настройки инстанса)
- Светлая/тёмная тема оболочки (см. [frontend-themes.md](./frontend-themes.md))
**Коммуникация:**
- HTTP + WebSocket → FastAPI backend (`/api/v1/*`)
- WebRTC → LiveKit SFU
Подробнее: [frontend/README.md](../../frontend/README.md).
---
### 2. Backend (FastAPI)
**Location:** `backend/`
**Tech:** Python 3.12, FastAPI, SQLAlchemy 2.0 async, Alembic
**Реализовано:**
- Схема БД + миграции (14 таблиц + расширение `btree_gist`, установлено, но не используется текущими constraint'ами)
- Контракты плагинов (Transcriber, Summarizer) + factory с реестром (`@register_transcriber`, `@register_summarizer`)
- `NullTranscriber`, `NullSummarizer` (no-op), `FasterWhisperCPU`/`FasterWhisperGPU`, `QwenLocal`
- JWT аутентификация (вход, регистрация, верификация email, refresh, logout), OAuth2 password flow
- Генерация LiveKit токенов, приём и обработка webhook-событий LiveKit (room_started, participant_joined/left, room_finished)
- Динамические конференции: создание (мгновенное/плановое), календарь, вход по номеру/ссылке/паролю, гостевой вход, повторение
- Приглашения на конференцию (пользователь или внешний email), рассылка .ics
- Чат конференции (WebSocket, история, broadcast через Redis pub/sub)
- Администрирование: конференции, пользователи, команды, настройки инстанса
- Endpoint `GET /api/health` (проверка БД/Redis), `GET /metrics` (Prometheus)
**API структура:**
```
/api/v1/
├── auth/ register, verify-email, token, refresh, logout, registration-options
├── users/ me (GET/PATCH), me/avatar (POST/DELETE), me/password, поиск (GET ?q=)
├── teams/ справочник команд (GET)
├── conferences/ POST create, GET /my, GET /calendar, GET /resolve,
│ POST /{id}/join, POST /{id}/guest-join, GET/PATCH/DELETE /{id}
├── conferences/{id}/chat WebSocket-чат
├── admin/ conferences, users, teams, settings
└── livekit/ webhook приёмник
/api/health статус системы (БД, Redis)
/metrics метрики Prometheus
```
Полная спецификация: [docs/api/README.md](../api/README.md).
---
### 3. Database (PostgreSQL 16)
**Location:** миграции в `backend/alembic/versions/` (9 миграций)
**Основные таблицы:**
| Таблица | Назначение |
|---------|---------|
| `users` | Зарегистрированные пользователи (email, password_hash, role, is_blocked, team_id, avatar_path) |
| `teams` | Справочник команд |
| `email_verification_tokens` | Одноразовые токены верификации email |
| `conferences` | Постоянные сущности конференций (номер, slug, владелец, статус, recurrence, summary_recipients, ics_sequence) |
| `conference_invitees` | Приглашённые на конференцию (user_id ИЛИ email) |
| `guest_access` | Гости, представившиеся при входе (display_name, email) |
| `conference_sessions` | Один запуск конференции — единица AI-пайплайна (t_start, t_end, pipeline_status, summary_data) |
| `conference_participants` | Отслеживание участия в сеансе (user_id ИЛИ guest_id, session_id, joined_at, left_at) |
| `session_audio_tracks` | Аудиотреки сеанса (per-track запись, статус транскрибации, segments JSONB) |
| `phrases` | Текстовые сегменты транскрибации (participant_id, session_id, t_start, t_end) |
| `chat_messages` | Сообщения в сеансе (session_id, автор — user_id или guest_access_id, author_name) |
| `email_deliveries` | Журнал отправленных писем (саммари, приглашения) — идемпотентность рассылки |
| `instance_settings` | Настройки инстанса (key-value JSONB): AI-уровень, чат, таймзона, режим рассылки, опции регистрации |
| `livekit_webhook_events` | Журнал webhook-событий для идемпотентности (event_id, event_type, received_at) |
**Ключевые особенности:**
- **UUID первичные ключи** (кроме `phrases`/`chat_messages` → BIGINT Identity)
- **Все времена в UTC** (`timestamptz`)
- **Номер (9 цифр) и slug (base64url)** конференции — публичные идентификаторы доступа
- **Recurrence как JSONB** (не RRULE; 4 типа: weekly/biweekly/monthly/every_n_days)
- **Гости как полноценные участники** (`guest_id` в `conference_participants`)
См. [docs/db/schema.md](../db/schema.md) — полная ER-диаграмма и обоснования дизайна.
---
### 4. LiveKit SFU
**Profile compose:** `media`
**Tech:** LiveKit (Go-based SFU)
**Роль:**
- SFU (Selective Forwarding Unit) для WebRTC
- Per-track аудио (один трек = один спикер, исключает необходимость диаризации)
- Запись треков через Egress (`.ogg`, opus) для последующей транскрибации
- FastAPI генерирует LiveKit-токены (JWT, TTL 6 часов, per-room), браузер подключается через LiveKit JS SDK
---
### 5. Celery Workers
**Location:** `workers/`
**Tech:** Celery + Redis (message broker), раздельные очереди (`transcription`/`summarize`/`notify`)
**Задачи:**
| Задача | Вход | Процесс | Выход |
|--------|-------|---------|--------|
| `run_pipeline()` | session_id | Диспетчер + транскрибация (faster-whisper) + реконструкция фраз | `session_audio_tracks.segments`, `phrases` |
| `summarize_session()` | session_id | Map-reduce Qwen3.5 (уровень из instance_settings) | `conference_sessions.summary_data` |
| `notify_session()` | session_id | Генерация email + .ics, рассылка по режиму (all/owner) | `email_deliveries` |
| `send_invitations()` | conference_id, emails | Генерация .ics, рассылка приглашений | `email_deliveries` |
| `cleanup_conferences()` (beat) | — | Закрытие зависших сеансов, завершение просроченных конференций | — |
| `recover_stuck_summaries()` / `recover_stuck_notifications()` (beat) | — | Переставить зависшую `summarize_session`/`notify_session` в очередь | завершение пайплайна |
**State machine pipeline_status:**
```
recording (создана при t_start сеанса, LiveKit Egress записывает)
↓ (webhook room_finished → enqueue run_pipeline в очередь transcription)
transcribing (транскрибация faster-whisper + реконструкция фраз)
summarizing (map-reduce Qwen3.5 → очередь summarize; summary_data записан, статус не меняется)
↓ (notify_session отправляет уведомления → очередь notify)
notified
↓ Успех: история сохранена в БД
↘ (при ошибке на любом шаге)
↘ failed
↘ Лог; beat-задачи recover_stuck_* могут переставить зависший шаг
```
**Переход в notified:**
- Собрать получателей по эффективному режиму рассылки (`conference.summary_recipients or cfg.summary_recipients`)
- Режим `'all'` → участники сеанса (users) + гости с email
- Режим `'owner'` → email владельца конференции
- Отправить письмо для каждого нового адреса (таблица `email_deliveries` обеспечивает идемпотентность)
- Переход в `notified` независимо от того, были ли получатели
**Идемпотентность:** каждый шаг проверяет `pipeline_status` перед началом и пропускает уже пройденные шаги.
Подробнее: [workers/README.md](../../workers/README.md).
---
### 6. AI Plugins (Strategy Pattern)
**Location:** `backend/core/plugins/`
**Interfaces:**
- `Transcriber.transcribe(audio_path, language) → list[Segment]`
- `Summarizer.summarize(transcript) → str`
**Factory:**
- Registry: `@register_transcriber`, `@register_summarizer`
- Configuration: `config/plugins.yaml`
- No-op defaults: `NullTranscriber`, `NullSummarizer`
**Расширяемость:**
- Добавить нового провайдера = новый класс + строка в конфиге
- Никаких изменений в основном коде
См. [docs/plugins/contracts.md](../plugins/contracts.md).
---
### 7. Nginx Reverse Proxy
**Конфиг:** `deploy/nginx.conf`
**Задачи:**
- Завершение TLS
- Обслуживание статических файлов (frontend SPA)
- Маршрутизация запросов API → FastAPI
- Проксирование WebSocket (чат конференции)
- Сжатие Gzip
---
## Поток данных
### 1. Пользователь входит в конференцию
```
Browser
│ 1. POST /api/v1/conferences/{id}/join (пароль опционально)
├─────────────────────────────────► FastAPI
│ │
│ 2. Проверка доступа, генерация LiveKit-токена
│ │
│ 3. {"token": "...", "url": "...", "chat_enabled": true}
◄─────────────────────────────────────┤
4. Подключение через LiveKit JS SDK
├────────────────► LiveKit SFU
│ │
│ 5. Запись аудио per-track через Egress (при завершении сеанса)
└ (video/audio stream) ──────────►
```
БД: INSERT `conference_participants` (user_id ИЛИ guest_id, session_id, joined_at).
---
### 2. Конференция заканчивается → пост-обработка
```
room_finished webhook
1. Backend помечает завершение записи, ожидает завершения egress
2. enqueue_pipeline(session_id) → Redis, очередь `transcription`
3. run_pipeline(session_id) — диспетчер пайплайна
├─► Ожидание завершения egress (retry с backoff)
├─► Для каждого трека:
│ ├─► Загрузить плагин transcriber (FasterWhisperCPU/GPU или null)
│ ├─► transcriber.transcribe(file_path, language='ru')
│ └─► Сохранить segments в session_audio_tracks.segments (JSONB)
├─► Собрать сегменты по участникам (с учётом смещений)
├─► build_phrases(segments_by_participant, track_offsets)
├─► DELETE phrases + INSERT новые → таблица phrases
├─► UPDATE pipeline_status = 'summarizing'
4. send_task_with_retry ставит summarize_session → очередь `summarize`
5. summarize_session
├─► Получить фразы сеанса, собрать транскрипт
├─► Разбить на чанки (20 мин по умолчанию, зависит от уровня AI)
├─► Qwen3.5 map-reduce (промпты в workers/summarizer/prompts/)
├─► UPDATE conference_sessions.summary_data = '{...}' (pipeline_status остаётся 'summarizing')
6. send_task_with_retry ставит notify_session → очередь `notify`
7. notify_session
├─► Собрать получателей (режим: all=участники+гости / owner=владелец)
├─► Для каждого получателя: проверить email_deliveries, отправить письмо
├─► Сгенерировать HTML + plaintext письмо (backend/services/email_templates.py)
├─► Отправить через email-бэкенд (console|smtp, backend/services/email.py)
├─► UPDATE email_deliveries (идемпотентность повторной отправки)
├─► UPDATE pipeline_status = 'notified' (когда все получатели обработаны)
8. Beat-задачи recover_stuck_summaries/recover_stuck_notifications (интервал 300 сек)
├─► Найти сеансы, зависшие в 'summarizing' (без summary_data или без уведомления)
├─► Переставить соответствующую задачу в очередь для восстановления после сбоя брокера
```
**Ключевые особенности:**
- **Диспетчер единый:** `run_pipeline()` управляет всеми шагами транскрибации
- **Per-track идемпотентность:** каждый трек коммитится отдельно (точка возобновления)
- **Реконструкция фраз:** группировка по спикеру, короткие вставки, перекрытия речи (`workers/transcription/phrases.py`)
- **Атрибуция гостям:** фразы и треки связаны с `conference_participants`, не `users` (ADR-002)
- **Проверка pipeline_status:** guard идемпотентности (повторный запуск пропустит готовые шаги)
---
## Обработка времени
**Инвариант:** все времена в БД в UTC (`timestamptz`).
### Server-side (PostgreSQL)
```sql
INSERT INTO conferences (scheduled_at, duration_minutes)
VALUES ('2026-07-15T14:30:00+00:00', 60);
```
### Client-side (JavaScript)
```typescript
const startUtc = new Date('2026-07-15T14:30:00Z');
// Браузер форматирует в локальный часовой пояс пользователя
```
### .ics export
```ics
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//VidConf//EN
BEGIN:VTIMEZONE
TZID:Europe/Moscow
...
END:VTIMEZONE
BEGIN:VEVENT
UID:...
DTSTART;TZID=Europe/Moscow:20260715T143000
DTEND;TZID=Europe/Moscow:20260715T153000
...
END:VEVENT
END:VCALENDAR
```
---
## Ключевые архитектурные решения
1. **Динамические конференции вместо бронирований** (ADR-001) — конференция как пользовательская сущность
- Номер (9 цифр) + slug (base64url) — постоянные идентификаторы доступа
- Статусы: scheduled/active/ended
- Повторение: RecurrenceRule (JSON, 4 типа)
- Гости — полноценные участники (`guest_access`)
2. **Номер и slug как идентификаторы доступа** (ADR-001, п.4) — публичные эндпоинты без auth
- **Номер:** 9 цифр, первая 1..9 (энтропия ≈2^29.75); перебор при rate limit ≈60+ дней
- **Slug:** base64url 8 байт → 11 символов (64 бита энтропии); имя LiveKit-комнаты
- **Резолв:** единообразный 404 (живая/мёртвая неразличимы для безопасности)
- **Rate limit:** 10 запросов в минуту на IP для `/resolve` и `/guest-join`
3. **Гости как полноценные участники** (ADR-001, п.6) — представление при входе
- `guest_access`: display_name (обязателен), email (факультативен)
- LiveKit identity: `guest:{guest_id}` (email не передаётся)
- Участвуют в пайплайне саммари (email в рассылке, если заполнен)
4. **Recurrence как собственная модель, не RRULE** (ADR-001, п.3) — 4 типа, привязаны к форме UI
- `type`: weekly | biweekly | monthly | every_n_days
- Хранение в JSONB (`conferences.recurrence`), развёртка occurrences в памяти
- Правило содержит локальное время + IANA-таймзону; развёртка — в UTC
5. **Паттерн Strategy для плагинов** — подключаемые AI-реализации без изменений ядра
- Интерфейсы Transcriber/Summarizer в `backend/core/plugins/`
- Паттерн Factory с реестром
- Конфиг через YAML
6. **Идемпотентный pipeline пост-обработки** — безопасно повторять любой шаг
- Каждый шаг проверяет `pipeline_status` перед продолжением
- Per-track коммит сегментов (точка возобновления в БД)
- Неудачные задачи можно повторить без дублирования (DELETE+INSERT фраз в одной транзакции)
7. **Атрибуция фраз и треков к участнику сеанса** (ADR-002) — поддержка гостей без user_id
- `phrases.participant_id` → FK `conference_participants.id` (вместо user_id)
- `session_audio_tracks.participant_id` → FK `conference_participants.id`
- Гость без `user_id` проходит пайплайн наравне с пользователем
8. **Per-track аудио в SFU** — исключает необходимость диаризации спикеров
- LiveKit предоставляет per-track recording (один трек = один микрофон = один спикер)
- Track SID прямо соответствует identity участника
- Запись через Track Egress в `.ogg` (opus)
9. **UUID первичные ключи** — поддержка распределённых систем и репликации
- Сгенерировано через `gen_random_uuid()`
- Исключения: таблицы высокой частоты (`phrases`, `chat_messages`) используют BIGINT Identity
10. **Настройки инстанса в БД** — бутстрап из `plugins.yaml`
- Таблица `instance_settings` (key-value JSONB) импортирует дефолты `config/plugins.yaml` при старте backend
- Однократно и идемпотентно (`INSERT ... ON CONFLICT DO NOTHING`)
- Воркеры читают эффективную конфигурацию на старте каждой задачи
- Административный интерфейс может менять настройки без рестарта
11. **Рассылка саммари с переопределением на уровне конференции** — гибкая конфигурация
- `instance_settings.summary_recipients` — дефолт инстанса (`'all'` или `'owner'`)
- `conferences.summary_recipients` — переопределение для конкретной конференции (nullable)
- Эффективный режим = `conference.summary_recipients or cfg.summary_recipients`
12. **Идемпотентная рассылка саммари** — без дублирования писем
- Таблица `email_deliveries` с уникальным частичным индексом `(session_id, recipient_email)` WHERE `kind='summary'`
- Повторный запуск уведомления отправляет письмо только адресатам, которых ещё нет в таблице
- Семантика at-least-once: редкий дубль письма возможен, потеря — нет
- Per-получательный commit обеспечивает точку возобновления при обрыве
13. **Email-транспорт только через `.env`** — секреты никогда не попадают в БД или API
- Переключатель бэкенда (`EMAIL_BACKEND=console|smtp`) — переменная окружения
- Все SMTP-реквизиты (хост, порт, пароль, from) — только в `.env`
- Секреты не логируются и не попадают в `SettingsOut` API
- `aiosmtplib` с обработкой ошибок (retryable vs. skip)
14. **Управление командами и опциями регистрации** — справочник команд и гибкий вход
- Таблица `teams` — справочник команд (id, name UNIQUE, created_at); админ-API CRUD
- `users.team_id` (nullable) → FK `teams.id` с `ON DELETE SET NULL`
- Настройка инстанса `registration_team_choice` (bool) — показ выбора команды при регистрации; `GET /auth/registration-options` возвращает список доступных команд
- Настройка инстанса `registration_email_domain` (bool, domain: str|null) — обязательное совпадение домена email при регистрации (иначе 400 `invalid_email_domain`)
---
## Безопасность
- **Пароли:** хэширование Argon2 (никогда не логируется и не раскрывается)
- **JWT access-токен:** HS256, TTL по умолчанию 15 минут (`ACCESS_TOKEN_TTL_MINUTES`), передаётся в заголовке `Authorization: Bearer`
- **Refresh-токен:** httpOnly SameSite=Strict cookie, TTL по умолчанию 14 дней; отзывается при logout
- **LiveKit токены:** ограничены по времени (TTL 6 часов), выдаются на конкретную комнату
- **HTTPS/TLS:** терминируется на Nginx; dev-окружение использует HTTP
- **CSRF:** митигируется `SameSite=Strict` на cookie с refresh-токеном (не отправляется с cross-site запросов); access-токен передаётся в заголовке, а не в cookie, и потому не подвержен CSRF
- **SQL injection:** SQLAlchemy ORM с параметризованными запросами
- **Rate limiting:** Redis-based, публичные эндпоинты (`/resolve`, `/guest-join`) — 10 запросов/мин на IP
---
## ADR (Записи архитектурных решений)
Подробные обоснования см. в папке [adr/](./adr/):
- [001-dynamic-conferences-pivot.md](./adr/001-dynamic-conferences-pivot.md) — динамические конференции вместо бронирований
- [002-phrase-attribution-session-participant.md](./adr/002-phrase-attribution-session-participant.md) — атрибуция фраз и треков к участнику сеанса
- [003-conference-invitees.md](./adr/003-conference-invitees.md) — приглашённые на конференцию
- [004-ai-tier-matrix.md](./adr/004-ai-tier-matrix.md) — матрица уровней AI
- [005-password-reset-deferred.md](./adr/005-password-reset-deferred.md) — сброс пароля по email отложен
- [000-template.md](./adr/000-template.md) — шаблон ADR
---
## Производительность и масштабируемость
### Уровни AI качества (ADR-004)
**Пресет 12 (без AI):**
- 1020 одновременных конференций
- 4 vCPU, 8 GB RAM, 40 GB диск
**Пресет 3: AI min** (faster-whisper small + Qwen3.5-4B)
- 1020 одновременных конференций
- 8 vCPU, 16 GB RAM, 100 GB диск
- Транскрибация: ~2x realtime (10мин → 5мин на 8-ядерном CPU)
**Пресет 4: AI medium** (faster-whisper medium + Qwen3.5-9B; GPU опционально)
- 1530 одновременных конференций (CPU) или 3050 (GPU)
- 1216 vCPU, 32 GB RAM, 150 GB диск
- CPU: ~3x realtime; GPU (≥8 GB): ~1.5x realtime
**Пресет 5: AI max** (faster-whisper large-v3 + Qwen3.5-35B-A3B; GPU обязателен)
- 50100+ одновременных конференций
- 16+ vCPU, 64 GB RAM, 250 GB диск, GPU ≥16 GB VRAM
- Транскрибация: ~0.5x realtime (10мин → 20сек на V100)
См. [docs/deploy/hardware-profiles.md](../deploy/hardware-profiles.md) и [ADR-004](./adr/004-ai-tier-matrix.md).
---
## Развёртывание
### Установка
```bash
./install.sh # интерактивный опросник (автодетект железа)
./install.sh --preset 3 # неинтерактивно (пресет 3 = AI min)
```
Инсталлятор автоматически:
1. Детектирует CPU/RAM/GPU
2. Рекомендует пресет
3. Генерирует `.env` с секретами
4. Запускает `docker compose` с нужными профилями
5. Выполняет миграции и seed
### Локальная разработка (вручную)
```bash
docker compose -f deploy/docker-compose.yml --env-file .env up -d # пресет 1 (без AI)
docker compose -f deploy/docker-compose.yml --env-file .env \
--profile media --profile transcribe --profile llm up -d # пресет 3 (с AI min)
```
### Продакшен
- Docker Compose на одном хосте (текущий целевой сценарий)
- Nginx для TLS + статические файлы
- Переменные окружения для секретов (`.env`)
- Резервные копии БД (PostgreSQL dump)
- Мониторинг (Prometheus + Grafana, `--profile monitoring`)
См. [docs/deploy/dev-setup.md](../deploy/dev-setup.md).
---
## Мониторинг и наблюдаемость
- **Логи:** Docker-логи + логирование приложений
- **Метрики:** `GET /metrics``vidconf_http_request_duration_seconds`, `vidconf_pipeline_sessions`, `vidconf_celery_queue_depth` (см. [workers/README.md](../../workers/README.md#метрики))
- **Визуализация:** Grafana (compose-профиль `monitoring`)
- **БД:** журнал медленных запросов PostgreSQL
См. [docs/deploy/monitoring.md](../deploy/monitoring.md).
---
## Стратегия тестирования
- **Backend:** pytest (unit + интеграционные тесты)
- **Frontend:** ESLint + TypeScript (`tsc -b`); автоматических unit/E2E тестов пока нет
- **БД:** проверка версии Alembic (`alembic current`), автогенерация миграций
---
## Ссылки
- [Корневой README](../../README.md)
- [Схема БД](../db/schema.md)
- [Справка API](../api/README.md)
- [Архитектура плагинов](../plugins/contracts.md)
- [Профили развёртывания](../deploy/hardware-profiles.md)
- [Backend README](../../backend/README.md)
- [Frontend README](../../frontend/README.md)
- [Workers README](../../workers/README.md)