Files
vidconf/docs/db/schema.md
Max Ronzhin 8757bec8ac
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
first commit
2026-07-23 02:38:05 +03:00

41 KiB
Raw Blame History

Схема БД

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 Статус пайплайна: recordingtranscribingsummarizingnotified / 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) WHERE user_id IS NOT NULL — один приглашённый пользователь на конференцию
  • UNIQUE INDEX uq_conference_invitees_email на (conference_id, lower(email)) WHERE email 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_id
  • 504791847d4f: добавлены 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) WHERE kind = '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 key event_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

Участники конференций и аватары пользователей:

  1. Новая таблица 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)
  2. Колонка в 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

Поддержка гостевых авторов в чате:

  1. Колонки в 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
  2. Изменение chat_messages.user_id:

    • Переходит в nullable (вместо NOT NULL), т.к. автором может быть гость
  3. Check-констрейнт:

    • ck_chat_messages_authoruser_id IS NOT NULL OR guest_access_id IS NOT NULL (ровно один из двух должен быть заполнен)

Назначение: WS-чат поддерживает как пользователей, так и гостей как авторов сообщений, с сохранением снапшота имени (для истории и чата).

Миграция 9d37822e4513

Справочник команд:

  1. Новая таблица teamsid (UUID PK), name (уникально), created_at
  2. Колонка в 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

Настройки инстанса и уведомления:

  1. Новая таблица instance_settings — key-value хранилище настроек (JSONB) с бутстрапом из config/plugins.yaml
  2. Новая таблица email_deliveries — идемпотентность саммари (уникальный частичный индекс по (session_id, recipient_email)) и журнал приглашений
  3. Колонки в conferences:
    • summary_recipients (nullable TEXT, CHECK) — переопределение рассылки ('all', 'owner', или NULL=дефолт)
    • ics_sequence (INT, default 0) — счётчик для VEVENT SEQUENCE
  4. Колонка в 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):

  1. Новая таблица session_audio_tracks — аудиотреки сеанса с независимым статусом и сегментами Whisper
  2. Изменение phrases: user_idparticipant_id — атрибуция к участнику сеанса (FK conference_participants.id), чтобы работать с гостями без user_id

Данные (в продакшене): сохраняются через данные участников (conference_participants) и сеансов.

Миграция f418dd65e7b1

Переход на динамические конференции (ADR-001):

  1. Таблица conferencesconference_sessions (переименование + новые колонки)

    • Сохранены все данные
    • Добавлена conference_id (FK → новая таблица conferences)
    • pipeline_status остаётся (семантика не меняется)
  2. Новая таблица conferences

    • Постоянные сущности (номер, slug, владелец, статус, recurrence)
    • Связь с conference_sessions (один ко многим, CASCADE удаление)
  3. Новая таблица guest_access

    • Гости, представившиеся при входе
    • display_name обязателен, email опционален
  4. Колонки фраз и сообщений: conference_idsession_id

    • phrases.conference_idphrases.session_id
    • chat_messages.conference_idchat_messages.session_id
    • conference_participants.conference_idconference_participants.session_id
  5. Таблицы удалены

    • rooms (комнаты больше не предустановленные)
    • room_bookings (бронирования заменены динамическими конференциями)
    • booking_participants (доступ теперь по паролю, не по списку)
  6. CHECK-ограничения в новой таблице conferences

    • is_closed = false OR password_hash IS NOT NULL
    • recurrence IS NULL OR is_pinned = true
  7. CHECK-ограничение в conference_participants

    • (user_id IS NOT NULL)::int + (guest_id IS NOT NULL)::int = 1 — ровно один из user_id/guest_id
  8. Индексы

    • (conference_id, t_start) на conference_sessions
    • (pipeline_status) на conference_sessions
    • (session_id) на conference_participants