41 KiB
Схема БД
ER-диаграмма
erDiagram
USERS ||--o{ CONFERENCES : owns
USERS ||--o{ CONFERENCE_PARTICIPANTS : joins
USERS ||--o{ CONFERENCE_INVITEES : invites
USERS ||--o{ PHRASES : speaks
USERS ||--o{ CHAT_MESSAGES : "sends (author)"
USERS ||--o{ EMAIL_VERIFICATION_TOKENS : receives
CONFERENCES ||--o{ CONFERENCE_INVITEES : "has invitees"
CONFERENCES ||--o{ CONFERENCE_SESSIONS : "has sessions"
CONFERENCES ||--o{ GUEST_ACCESS : "may allow"
CONFERENCES ||--o{ EMAIL_DELIVERIES : "invitations"
CONFERENCE_SESSIONS ||--o{ CONFERENCE_PARTICIPANTS : tracks
CONFERENCE_SESSIONS ||--o{ PHRASES : contains
CONFERENCE_SESSIONS ||--o{ CHAT_MESSAGES : contains
CONFERENCE_SESSIONS ||--o{ EMAIL_DELIVERIES : "summaries"
GUEST_ACCESS ||--o{ CONFERENCE_PARTICIPANTS : "may join"
GUEST_ACCESS ||--o{ CHAT_MESSAGES : "sends (author)"
LIVEKIT_WEBHOOK_EVENTS ||--o{ CONFERENCE_SESSIONS : "triggers"
INSTANCE_SETTINGS ||--|| CONFERENCES : "configure"
TEAMS ||--o{ USERS : "groups"
USERS {
uuid id PK
string email UK
string name_user
text password_hash
string role "admin|user"
boolean email_verified
boolean is_blocked
uuid team_id FK "nullable"
string avatar_path "nullable"
datetime created_at
}
EMAIL_VERIFICATION_TOKENS {
uuid id PK
uuid user_id FK
string token_hash UK
datetime expires_at
datetime used_at "nullable"
datetime created_at
}
CONFERENCES {
uuid id PK
string number UK "9 digits"
string slug UK "base64url"
string title "nullable"
uuid owner_id FK "nullable"
string status "enum: scheduled|active|ended"
boolean is_pinned
boolean is_closed
text password_hash "nullable"
datetime scheduled_at "nullable"
integer duration_minutes "nullable"
jsonb recurrence "nullable"
datetime ended_at "nullable"
datetime created_at
}
CONFERENCE_INVITEES {
uuid id PK
uuid conference_id FK
uuid user_id FK "nullable, для зарег. пользователей"
string email "nullable, для внешних"
datetime created_at
"CHECK: ровно один из user_id/email"
"UNIQUE: (conference_id, user_id)"
"UNIQUE: (conference_id, lower(email))"
}
CONFERENCE_SESSIONS {
uuid id PK
uuid conference_id FK
string title "nullable"
datetime t_start
datetime t_end "nullable"
text summary_data "nullable"
string pipeline_status "enum"
datetime created_at
}
GUEST_ACCESS {
uuid id PK
uuid conference_id FK
string display_name
string email "nullable"
datetime created_at
}
CONFERENCE_PARTICIPANTS {
uuid id PK
uuid session_id FK
uuid user_id FK "nullable"
uuid guest_id FK "nullable"
datetime joined_at
datetime left_at "nullable"
}
SESSION_AUDIO_TRACKS {
uuid id PK
uuid session_id FK
uuid participant_id FK
string track_sid
string egress_id "nullable"
text file_path "nullable"
string status "enum"
datetime started_at
datetime ended_at "nullable"
jsonb segments "nullable"
}
PHRASES {
bigint id PK "Identity"
uuid participant_id FK
uuid session_id FK
text data
datetime t_start
datetime t_end
}
CHAT_MESSAGES {
bigint id PK "Identity"
uuid session_id FK
uuid user_id FK "nullable"
uuid guest_access_id FK "nullable"
string author_name "255 chars"
text text
datetime created_at
}
LIVEKIT_WEBHOOK_EVENTS {
string event_id PK
string event_type
datetime received_at
}
INSTANCE_SETTINGS {
string key PK
jsonb value
datetime updated_at
}
EMAIL_DELIVERIES {
uuid id PK
uuid session_id FK "nullable"
uuid conference_id FK "nullable"
string recipient_email
string kind "summary|invitation"
datetime sent_at
}
Таблицы
users
Зарегистрированные пользователи приложения.
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key, генерируется как gen_random_uuid() |
email |
VARCHAR(255) | Уникальный адрес электронной почты |
name_user |
VARCHAR(255) | Имя пользователя |
password_hash |
TEXT | Argon2-хэш пароля |
role |
VARCHAR(16) | 'admin' или 'user'; проверка CHECK |
email_verified |
BOOLEAN | Флаг подтверждения почты |
is_blocked |
BOOLEAN | Флаг блокировки администратором; заблокированный пользователь получает 401 немедленно, без ожидания истечения access-токена |
team_id |
UUID | FK → teams, nullable; ON DELETE SET NULL — удаление команды не удаляет пользователей |
avatar_path |
VARCHAR(512) | Путь к загруженному аватару (относительно MEDIA_ROOT); NULL — заглушка с инициалами на фронте; пример: avatars/550e8400-e29b-41d4-a716-446655440000.jpg |
created_at |
TIMESTAMPTZ | UTC-время создания |
teams
Справочник команд для группировки пользователей.
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key, генерируется как gen_random_uuid() |
name |
VARCHAR(255) | Название команды; уникально |
created_at |
TIMESTAMPTZ | UTC-время создания |
Логика: удаление команды не каскадирует на пользователей — users.team_id просто обнуляется (ON DELETE SET NULL); привязка необязательна.
email_verification_tokens
Одноразовые токены подтверждения email при регистрации.
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key, генерируется как gen_random_uuid() |
user_id |
UUID | FK → users; CASCADE удаление |
token_hash |
TEXT | SHA256-хэш одноразового токена (hex); уникален |
expires_at |
TIMESTAMPTZ | Срок действия (UTC); по умолчанию 24 часа от создания |
used_at |
TIMESTAMPTZ | Время использования; NULL если не подтверждён |
created_at |
TIMESTAMPTZ | UTC-время создания |
Ограничения:
UNIQUE(token_hash)— уникальный индекс на хэш токенаINDEX(user_id)— для быстрого поиска токенов пользователя
Логика:
- При регистрации создаётся новый токен; клиент получает открытый токен в письме
- Сервер хранит только SHA256-хэш (токен не может быть восстановлен из БД)
- Первое использование (POST /verify-email) заполняет
used_at, устанавливаетuser.email_verified=true - Повторное использование того же токена → 400 (used_at IS NOT NULL)
conferences
Конференция — постоянная сущность с номером и постоянной ссылкой (slug). Может быть мгновенной или плановой с повторением (ADR-001).
Миграция: Таблица conferences переименована в conference_sessions (см. ниже); новая таблица conferences создана миграцией f418dd65e7b1.
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key, генерируется как gen_random_uuid() |
number |
VARCHAR(9) | 9 десятичных цифр, первая 1..9, уникален; генерируется с retry при коллизии |
slug |
VARCHAR(22) | secrets.token_urlsafe(8) — 11 base64url-символов; используется как имя комнаты в LiveKit и часть URL /j/{slug} |
title |
VARCHAR(255) | Опциональное название конференции |
owner_id |
UUID | FK → users; SET NULL при удалении пользователя; может быть NULL для исторических данных |
status |
ENUM | scheduled (плановая, время ещё не наступило), active (идёт сейчас), ended (завершена; история остаётся) |
is_pinned |
BOOLEAN | Закреплена ли (видна в «Моих конференциях», может повторяться) |
is_closed |
BOOLEAN | Требуется ли пароль для входа |
password_hash |
TEXT | Argon2-хэш пароля (только если is_closed=true); CHECK: is_closed=false OR password_hash IS NOT NULL |
scheduled_at |
TIMESTAMPTZ | Время запуска (UTC); NULL для мгновенных |
duration_minutes |
INT | Ожидаемая длительность в минутах; опционально, для информации |
recurrence |
JSONB | RecurrenceRule (JSON с type/weekdays/day_of_month/interval_days/anchor_date/time_local/timezone/duration_minutes); только если is_pinned=true; CHECK: recurrence IS NULL OR is_pinned = true |
ended_at |
TIMESTAMPTZ | Время завершения (UTC); заполняется при статусе → ended |
summary_recipients |
VARCHAR(16) | Переопределение рассылки саммари для конференции: 'all' (всем участникам и гостям с email), 'owner' (только организатору), NULL (используется дефолт инстанса) |
ics_sequence |
INT | Счётчик изменений расписания: растёт при правке scheduled_at, duration_minutes, recurrence, title; пишется в VEVENT SEQUENCE для обновления приглашений календарными клиентами |
created_at |
TIMESTAMPTZ | UTC-время создания |
Ограничения:
UNIQUE(number)— уникальность номераUNIQUE(slug)— уникальность постоянной ссылки- CHECK:
is_closed = false OR password_hash IS NOT NULL - CHECK:
recurrence IS NULL OR is_pinned = true
conference_sessions
Один запуск конференции, единица AI-пайплайна пост-обработки.
Миграция: До миграции f418dd65e7b1 это была таблица conferences; переименована в conference_sessions, связь conference_id добавлена.
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key |
conference_id |
UUID | FK → conferences; CASCADE удаление |
title |
VARCHAR(255) | Снапшот названия конференции на момент запуска (опционально) |
t_start |
TIMESTAMPTZ | Время начала сеанса (UTC) |
t_end |
TIMESTAMPTZ | Время окончания сеанса (UTC); опционально, заполняется при завершении |
summary_data |
TEXT | JSON со сводкой (заполняется после summarize-шага); опционально |
pipeline_status |
ENUM | Статус пайплайна: recording → transcribing → summarizing → notified / failed |
created_at |
TIMESTAMPTZ | UTC-время создания |
Индексы:
(conference_id, t_start)для поиска сеансов конференции(pipeline_status)для поиска активных обработок
Статус-машина пост-обработки живёт в этой таблице (см. ADR-001).
guest_access
Гость, представившийся при входе в конференцию без аутентификации (ADR-001, п.6).
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key |
conference_id |
UUID | FK → conferences; CASCADE удаление |
display_name |
VARCHAR(255) | Отображаемое имя гостя (обязательно) |
email |
VARCHAR(320) | Адрес электронной почты (опционально; для рассылки саммари) |
created_at |
TIMESTAMPTZ | UTC-время создания записи (при первом входе гостем) |
Назначение:
- Гость входит в конференцию без аккаунта (публичный эндпоинт
/guest-join) - LiveKit identity:
guest:{guest_access.id} - Email не передаётся в LiveKit (PII — только в БД)
conference_participants
Отслеживает присутствие участника (пользователя ИЛИ гостя) в сеансе конференции (ADR-001, п.6).
Миграция: Колонка conference_id переименована в session_id (f418dd65e7b1); user_id стал nullable; добавлены guest_id и CHECK для ровно одного identity.
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key |
session_id |
UUID | FK → conference_sessions; CASCADE удаление |
user_id |
UUID | FK → users; NULL для гостей; nullable |
guest_id |
UUID | FK → guest_access; NULL для зарегистрированных; nullable |
joined_at |
TIMESTAMPTZ | Время входа (UTC) |
left_at |
TIMESTAMPTZ | Время выхода (UTC); опционально |
Ограничения:
- CHECK:
(user_id IS NOT NULL)::int + (guest_id IS NOT NULL)::int = 1— ровно одно из user_id/guest_id заполнено
Индексы:
(session_id)для поиска участников сеанса
conference_invitees
Приглашённые на конференцию участники (зарегистрированные пользователи или внешние email'ы) — ADR-003.
Миграция: d87681e12784
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key, генерируется как gen_random_uuid() |
conference_id |
UUID | FK → conferences; CASCADE удаление |
user_id |
UUID | FK → users; nullable; CASCADE удаление; заполнено для зарегистрированных пользователей |
email |
VARCHAR(255) | Email для внешних приглашённых; nullable; нормализуется в lower-case |
created_at |
TIMESTAMPTZ | UTC-время создания приглашения |
Ограничения:
- CHECK:
(user_id IS NOT NULL)::int + (email IS NOT NULL)::int = 1— ровно один из user_id/email должен быть заполнен (единственная identity приглашённого) - UNIQUE INDEX
uq_conference_invitees_userна(conference_id, user_id)WHEREuser_id IS NOT NULL— один приглашённый пользователь на конференцию - UNIQUE INDEX
uq_conference_invitees_emailна(conference_id, lower(email))WHEREemail IS NOT NULL— один внешний email на конференцию (регистронезависимо)
Назначение:
- Хранение состава приглашённых участников, заданного организатором при создании/редактировании конференции
- Организатор НЕ хранится в этой таблице — он выводится из
conferences.owner_idи всегда в составе (инвариант обеспечен конструктивно, без триггеров) - Не путать с
conference_participants— та таблица фиксирует ФАКТИЧЕСКОЕ присутствие в сеансе; эта — планируемый состав
Семантика:
- Дедупликация при POST/PATCH конференции: если организатор передан в массиве participants, он автоматически вычищается (не хранится дважды)
- Внешние приглашённые (email) при входе по ссылке создают запись в
guest_access, которая затем связывается сconference_participantsсеанса
session_audio_tracks
Аудиодорожка сеанса, записанная LiveKit Track Egress. Каждая строка соответствует одному аудиотреку (микрофону) одного участника.
Таблица используется для:
- Маппинга файлов записи к участникам сеанса (включая гостей без user_id)
- Отслеживания статуса транскрибации каждого трека (recording → recorded → transcribed / failed)
- Хранения сегментов Whisper в JSONB (
segments) как промежуточного артефакта пайплайна
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key, генерируется как gen_random_uuid() |
session_id |
UUID | FK → conference_sessions; CASCADE удаление |
participant_id |
UUID | FK → conference_participants; CASCADE удаление; NOT NULL (атрибуция к участнику сеанса, см. ADR-002) |
track_sid |
VARCHAR(64) | ID трека в LiveKit (уникален в пределах сеанса с session_id) |
egress_id |
VARCHAR(64) | ID egress-задачи в LiveKit; заполняется при старте egress; может быть NULL |
file_path |
TEXT | Абсолютный путь к файлу на диске (например, /recordings/{session_id}/{participant_id}_{track_sid}.ogg); заполняется при завершении egress |
status |
ENUM | Статус трека: recording (egress активен) → recorded (файл готов) → transcribed (Whisper завершил) / failed (ошибка на любом шаге) |
started_at |
TIMESTAMPTZ | UTC-время старта egress (из EgressInfo) |
ended_at |
TIMESTAMPTZ | UTC-время завершения egress; NULL пока трек в статусе recording |
segments |
JSONB | Список объектов {start: float, end: float, text: string} — результат faster-whisper для этого трека; NULL пока не транскрибирован |
Ограничения:
UNIQUE(session_id, track_sid)— один трек на сеанс; дедупликация при повторныхtrack_published- Индексы:
(session_id),(session_id, status)для поиска активных и готовых треков
Инвариант:
- Независимый статус трека (отдельно от
conference_sessions.pipeline_status) - Тайминги сегментов в
segments— относительно начала файла (не UTC); абсолютный расчёт:session.t_start + timedelta(seconds=segment.start)
phrases
Восстановленные транскрипт-фразы (результат faster-whisper + реконструкция по алгоритму ТЗ §1.3).
Изменение: Колонка user_id заменена на participant_id (FK → conference_participants), чтобы атрибутировать фразы гостям без user_id (ADR-002).
| Поле | Тип | Описание |
|---|---|---|
id |
BIGINT | Primary key, Identity(always=True) |
participant_id |
UUID | FK → conference_participants; CASCADE удаление; NOT NULL; атрибуция к участнику сеанса |
session_id |
UUID | FK → conference_sessions; CASCADE удаление |
data |
TEXT | Текст фразы (результат build_phrases() — может быть несколько сегментов Whisper одного спикера подряд) |
t_start |
TIMESTAMPTZ | Начало фразы (UTC); рассчитывается как session.t_start + (segment.start + track_offset) |
t_end |
TIMESTAMPTZ | Конец фразы (UTC) |
Индексы:
(session_id, t_start)для хронологического поиска фраз в сеансе
Как получить пользователя/гостя фразы:
SELECT p.data, u.name_user, g.display_name
FROM phrases p
JOIN conference_participants cp ON p.participant_id = cp.id
LEFT JOIN users u ON cp.user_id = u.id
LEFT JOIN guest_access g ON cp.guest_id = g.id
WHERE p.session_id = $1
ORDER BY p.t_start;
chat_messages
Текстовые сообщения, отправленные во время сеанса конференции (WS чат).
Миграции:
f418dd65e7b1: колонкаconference_idпереименована вsession_id504791847d4f: добавленыguest_access_idиauthor_nameдля поддержки гостевых авторов
| Поле | Тип | Описание |
|---|---|---|
id |
BIGINT | Primary key, Identity(always=True), уникален в пределах инстанса |
session_id |
UUID | FK → conference_sessions; CASCADE удаление |
user_id |
UUID | FK → users; nullable; заполнено для авторов-пользователей |
guest_access_id |
UUID | FK → guest_access; nullable; CASCADE удаление; заполнено для авторов-гостей |
author_name |
VARCHAR(255) | Снапшот отображаемого имени автора из LiveKit access-токена на момент отправки; NOT NULL (переживает переименование пользователя или удаление гостевой записи) |
text |
TEXT | Текст сообщения (1..2000 символов) |
created_at |
TIMESTAMPTZ | UTC-время создания |
Ограничения:
- CHECK:
user_id IS NOT NULL OR guest_access_id IS NOT NULL— ровно один из user_id/guest_access_id должен быть заполнен (аналогичноconference_participants, ADR-001 п.6)
Индексы:
(session_id, created_at)для хронологического поиска сообщений в сеансе
Назначение: Хранение истории чата конференции для отдачи последних N сообщений при подключении нового клиента; публикация в Redis pub/sub для broadcast остальным клиентам
instance_settings
Настройки инстанса как key-value хранилище (JSONB). Бутстрап из config/plugins.yaml при старте backend'а (однократный, идемпотентный импорт).
| Поле | Тип | Описание |
|---|---|---|
key |
TEXT | Primary key; имя настройки (transcriber, summarizer, chat, ai_level, summary_recipients, display_timezone, registration_team_choice, registration_email_domain) |
value |
JSONB | Значение настройки (структура зависит от ключа) |
updated_at |
TIMESTAMPTZ | UTC-время последнего обновления |
Назначение:
- Централизованное хранилище конфигурации инстанса (переносит дефолты из
config/plugins.yamlв БД) - Административный интерфейс может менять настройки без рестарта приложения
- Воркеры Celery читают конфигурацию на старте каждой задачи (fallback на YAML при пустой таблице)
Ключи и типы значений (примеры):
{
"transcriber": {"enabled": true, "provider": "faster-whisper-cpu", "language": "ru", ...},
"summarizer": {"enabled": true, "provider": "qwen-local", "chunk_minutes": 20, ...},
"chat": {"enabled": false},
"ai_level": {"level": "min"},
"summary_recipients": {"mode": "all"},
"display_timezone": {"tz": "Europe/Moscow"},
"registration_team_choice": {"enabled": true},
"registration_email_domain": {"enabled": true, "domain": "example.com"}
}
email_deliveries
Журнал отправленных писем (саммари и приглашения). Идемпотентность рассылки саммари и отслеживание доставки.
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Primary key, генерируется как gen_random_uuid() |
session_id |
UUID | FK → conference_sessions; NULL для приглашений; заполняется для саммари |
conference_id |
UUID | FK → conferences; NULL для саммари; заполняется для приглашений |
recipient_email |
VARCHAR(320) | Email получателя (хранится в lower() — нормализованный) |
kind |
VARCHAR(16) | Тип письма: 'summary' (саммари сеанса), 'invitation' (.ics приглашение) |
sent_at |
TIMESTAMPTZ | UTC-время отправки (server_default=now()) |
Ограничения:
- CHECK:
kind IN ('summary', 'invitation') - CHECK:
(kind = 'summary') = (session_id IS NOT NULL)— саммари обязана иметь session_id - CHECK:
(kind = 'invitation') = (conference_id IS NOT NULL)— приглашение обязано иметь conference_id - UNIQUE INDEX
uq_email_deliveries_summaryна(session_id, recipient_email)WHEREkind = 'summary'— гарантирует, что один адресат получит саммри сеанса один раз (идемпотентность повторной отправки) - INDEX
ix_email_deliveries_conferenceнаconference_idдля быстрого поиска истории приглашений
Назначение:
- Для саммари: гарантирует идемпотентность — повторный запуск
notify_sessionотправляет письмо только тем адресатам, которые ещё не в таблице - Для приглашений: журнал, без unique (изменение расписания конференции переслёт приглашение, допуская дублирование в истории)
livekit_webhook_events
Журнал идемпотентной доставки webhook-событий от LiveKit.
| Поле | Тип | Описание |
|---|---|---|
event_id |
VARCHAR(255) | Primary key (уникальный ID события от LiveKit) |
event_type |
VARCHAR(64) | Тип события: room_started, participant_joined, participant_left, room_finished и др. |
received_at |
TIMESTAMPTZ | UTC-время получения события |
Назначение:
- Дедупликация: каждое webhook-событие от LiveKit имеет уникальный
event_id - При получении события выполняется
INSERT ... ON CONFLICT DO NOTHINGпо primary keyevent_id - Повторная доставка того же события отклоняется на уровне БД; ответ 200 всё равно отправляется (идемпотентность)
Ключевые решения архитектуры
UUID как primary key
Все таблицы, кроме phrases и chat_messages, используют UUID генерируемые функцией gen_random_uuid(). Это обеспечивает:
- Глобальную уникальность без синхронизации
- Предсказуемую нагрузку на индексы
- Возможность распределённых вставок
BIGINT Identity для phrases и chat_messages
Высокочастотные таблицы используют BIGINT Identity(always=True) — быстрее и проще для очередей обработки фраз/сообщений.
Номер и slug конференции (9 цифр + base64url)
- Номер: 9 десятичных цифр, первая 1..9 (энтропия ≈2^29.75); генерируется с retry при коллизии; задача перебора при rate limit ≈60+ дней; резолв отвечает единообразным 404 (см. ADR-001, п.4)
- Slug:
secrets.token_urlsafe(8)— 11 base64url-символов (64 бита энтропии); используется как имя комнаты в LiveKit и часть URL/j/{slug}
Расширение btree_gist (не используется)
Миграция f418dd65e7b1: расширение btree_gist больше не используется (EXCLUDE constraint на room_bookings снят с удалением таблицы, см. ADR-001). Расширение остаётся установленным в БД (безвредно, миграция обратима).
Все временные метки в UTC
Все столбцы DateTime(timezone=True) хранят время в UTC на сервере. Конвертация в локальное время происходит на клиенте на основе часового пояса браузера. iCalendar-экспорт (.ics) включает VTIMEZONE.
Enum pipeline_status
Статус пост-обработки сеанса (единица пайплайна) отслеживает жизненный цикл от записи через транскрибацию и суммаризацию до уведомления или ошибки.
Состояния машины:
recording → transcribing → summarizing → notified / failed
| Статус | Описание | Переход | Ответственный |
|---|---|---|---|
recording |
Сеанс идёт, LiveKit Egress записывает треки | Автоматический при t_start сеанса |
Backend (создание сеанса) |
transcribing |
Транскрибация в процессе | При run_pipeline(), после завершения egress |
Воркер run_pipeline |
summarizing |
Суммаризация в процессе | После успешной транскрибации и реконструкции фраз | Воркер run_pipeline |
notified |
Уведомление отправлено | После успешной суммаризации и отправки email | Воркер уведомлений |
failed |
Ошибка на одном из шагов (пайплайн остановлен) | При сбое воркера или если все треки провалены | Воркер run_pipeline |
Идемпотентность: каждый шаг пайплайна проверяет текущий pipeline_status перед началом и пропускает уже завершённые шаги (guard от повторных запусков).
Пример восстановления после сбоя:
- Воркер транскрибации упал посеридине обработки треков
- При повторном запуске
run_pipeline(session_id)проверяетpipeline_status=transcribing - Пропускает уже транскрибированные треки (у которых
segments IS NOT NULL) - Завершает оставшиеся треки и переходит в
summarizing
Идемпотентность через статусы
Каждый шаг пайплайна (транскрибация, суммаризация, уведомление) должен быть идемпотентным: повторное выполнение не приведёт к дублированию или ошибкам. Это достигается через проверку текущего pipeline_status перед началом шага.
Миграции
Миграция d87681e12784
Участники конференций и аватары пользователей:
-
Новая таблица
conference_invitees(ADR-003) — приглашённые участники конференцииid(UUID PK),conference_id(FK),user_id(nullable FK),email(nullable VARCHAR),created_at- CHECK: ровно одна identity (
user_idИЛИemail) - Частичные UNIQUE индексы по (conference_id, user_id) и (conference_id, lower(email))
- Организатор не хранится в таблице (выводится из
conferences.owner_id)
-
Колонка в
users:avatar_path(VARCHAR(512), nullable) — путь к загруженному аватару- Пример:
avatars/550e8400-e29b-41d4-a716-446655440000.jpg - NULL → заглушка с инициалами на фронте
- Удаление аватара вычищает файл с диска и обнуляет поле
- Пример:
SQL:
CREATE TABLE conference_invitees (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conference_id UUID NOT NULL REFERENCES conferences(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
email VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK ((user_id IS NOT NULL)::int + (email IS NOT NULL)::int = 1)
);
CREATE UNIQUE INDEX uq_conference_invitees_user
ON conference_invitees(conference_id, user_id) WHERE user_id IS NOT NULL;
CREATE UNIQUE INDEX uq_conference_invitees_email
ON conference_invitees(conference_id, lower(email)) WHERE email IS NOT NULL;
ALTER TABLE users ADD COLUMN avatar_path VARCHAR(512);
Миграция 504791847d4f
Поддержка гостевых авторов в чате:
-
Колонки в
chat_messages:guest_access_id(UUID, nullable) — FK →guest_access.id,ON DELETE CASCADE; заполнено для авторов-гостейauthor_name(VARCHAR(255), NOT NULL) — снапшот имени автора из LiveKit access-токена на момент отправки; добавляется nullable, backfill изusers.name_userдля уже существующих строк (все они сuser_id), затем ужесточается доNOT NULL
-
Изменение
chat_messages.user_id:- Переходит в nullable (вместо NOT NULL), т.к. автором может быть гость
-
Check-констрейнт:
ck_chat_messages_author—user_id IS NOT NULL OR guest_access_id IS NOT NULL(ровно один из двух должен быть заполнен)
Назначение: WS-чат поддерживает как пользователей, так и гостей как авторов сообщений, с сохранением снапшота имени (для истории и чата).
Миграция 9d37822e4513
Справочник команд:
- Новая таблица
teams—id(UUID PK),name(уникально),created_at - Колонка в
users:team_id(UUID, nullable) — FK →teams.id,ON DELETE SET NULL
SQL:
CREATE TABLE teams (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE users ADD COLUMN team_id UUID;
ALTER TABLE users ADD CONSTRAINT fk_users_team_id_teams
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL;
Миграция 5970bf64fc43
Настройки инстанса и уведомления:
- Новая таблица
instance_settings— key-value хранилище настроек (JSONB) с бутстрапом изconfig/plugins.yaml - Новая таблица
email_deliveries— идемпотентность саммари (уникальный частичный индекс по(session_id, recipient_email)) и журнал приглашений - Колонки в
conferences:summary_recipients(nullable TEXT, CHECK) — переопределение рассылки ('all', 'owner', или NULL=дефолт)ics_sequence(INT, default 0) — счётчик для VEVENT SEQUENCE
- Колонка в
users:is_blocked(BOOLEAN, default false) — блокировка администратором
SQL:
CREATE TABLE instance_settings (key TEXT PRIMARY KEY, value JSONB, updated_at TIMESTAMPTZ DEFAULT now());
CREATE TABLE email_deliveries (...);
ALTER TABLE conferences ADD COLUMN summary_recipients VARCHAR(16) CHECK (...);
ALTER TABLE conferences ADD COLUMN ics_sequence INT DEFAULT 0;
ALTER TABLE users ADD COLUMN is_blocked BOOLEAN DEFAULT false;
Миграция 88aa676ac140
Таблица session_audio_tracks и атрибуция фраз (ADR-002):
- Новая таблица
session_audio_tracks— аудиотреки сеанса с независимым статусом и сегментами Whisper - Изменение
phrases:user_id→participant_id— атрибуция к участнику сеанса (FKconference_participants.id), чтобы работать с гостями без user_id
Данные (в продакшене): сохраняются через данные участников (conference_participants) и сеансов.
Миграция f418dd65e7b1
Переход на динамические конференции (ADR-001):
-
Таблица
conferences→conference_sessions(переименование + новые колонки)- Сохранены все данные
- Добавлена
conference_id(FK → новая таблица conferences) pipeline_statusостаётся (семантика не меняется)
-
Новая таблица
conferences- Постоянные сущности (номер, slug, владелец, статус, recurrence)
- Связь с
conference_sessions(один ко многим, CASCADE удаление)
-
Новая таблица
guest_access- Гости, представившиеся при входе
- display_name обязателен, email опционален
-
Колонки фраз и сообщений:
conference_id→session_idphrases.conference_id→phrases.session_idchat_messages.conference_id→chat_messages.session_idconference_participants.conference_id→conference_participants.session_id
-
Таблицы удалены
rooms(комнаты больше не предустановленные)room_bookings(бронирования заменены динамическими конференциями)booking_participants(доступ теперь по паролю, не по списку)
-
CHECK-ограничения в новой таблице conferences
is_closed = false OR password_hash IS NOT NULLrecurrence IS NULL OR is_pinned = true
-
CHECK-ограничение в conference_participants
(user_id IS NOT NULL)::int + (guest_id IS NOT NULL)::int = 1— ровно один из user_id/guest_id
-
Индексы
(conference_id, t_start)на conference_sessions(pipeline_status)на conference_sessions(session_id)на conference_participants