# Изменения

Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
проект придерживается [семантического версионирования](https://semver.org/lang/ru/).

## [0.0.39] — 2026-08-11

### Добавлено
- Вкладка «Зайти как гость» на экранах входа и регистрации — ведёт на
  `/join`, чтобы незалогиненный пользователь мог подключиться по номеру
  конференции, не выбирая между входом и регистрацией. На узких экранах
  (375/360px) подпись сокращается до «Гость» (та же схема коротких
  подписей, что у «Мини-окно»/«Мини» в тулбаре комнаты).

### Исправлено
- Дата и время в прошлом были запрещены сервером для ЛЮБОЙ конференции
  при создании, включая закреплённые — хотя закреплённая конференция не
  разовое событие, а постоянная комната-ссылка, и прошедшая дата для неё
  осмысленна (например, серия «каждый день с такого-то числа», уже
  наступившего). Запрет прошлого теперь действует только для обычных
  конференций.
- Поле «Длительность, мин» в форме конференции: числовое состояние
  (`Number(value) || 0`) не давало очистить поле — стирание тут же
  превращалось обратно в 0, и следующая цифра визуально дописывалась
  к нему вместо замены. Состояние переведено на строку, плюс выделение
  содержимого по фокусу.

## [0.0.38] — 2026-08-11

### Исправлено
- Safari: при выключении виртуального фона в комнате чернело собственное
  видео (остальные участники видели картинку нормально). Причина —
  внутренняя охрана livekit-client от мерцания сравнивала `srcObject` с
  тем же объектом `MediaStream` и из-за этого не давала сработать
  собственному обходному пути библиотеки для чёрного кадра в Safari.
  Теперь после остановки процессора фона элементы принудительно
  переприцепляются (`detach`/`attach`), пересоздавая `MediaStream`.

## [0.0.37] — 2026-08-10

Закрытие технического долга. Без изменений в продуктовой логике, кроме
исправления лишнего отказа при первом входе в комнату (см. «Исправлено»).

### Исправлено
- Тесты бэкенда были не изолированы от состояния `instance_settings`
  (`chat`, `hand_queue` и др.) в общей dev-БД: выключение любого модуля в
  админке локального стенда роняло не связанные с ним тесты. Изоляция —
  через фикстуру `clean_instance_settings`, явно подключаемую в тестах,
  которым нужен «чистый стол» по умолчанным значениям.
- `RoomPage.tsx`: сохранённый выбор устройства «по умолчанию»
  (`usePersistentUserChoices`, литерал `"default"`) не отсекался
  `|| undefined` — в Chrome это давало лишний `OverconstrainedError` на
  первом визите (то же самое уже было исправлено для страницы проверки
  устройств в 0.0.33).
- Локальный контейнерный стенд (`docker compose --profile media up`) не
  поднимался «как есть» — `DATABASE_URL` из корневого `.env` (указывает на
  `localhost`, нужен только для запуска backend на хосте) утекал внутрь
  контейнеров и ronял подключение к БД. Теперь compose всегда собирает
  строку подключения сам из `POSTGRES_USER`/`PASSWORD`/`DB` с хостом
  `postgres`, как уже было сделано для `REDIS_URL`.
- `vidconf_pipeline_sessions` в `/metrics` читалась через основной пул БД —
  на нагрузке могла отвалиться вместе с остальным API ровно тогда, когда
  метрика нужнее всего (см. инцидент 07.08). Теперь читается через отдельный
  движок, как `vidconf_db_up`/`vidconf_db_pool_*`.

### Изменено
- `.env.example`: убраны неиспользуемые `MEDIA_URL`/`VITE_API_URL`,
  добавлены живые `MEDIA_ROOT`/`EGRESS_START_TIMEOUT_S`/`LIVEKIT_WS_URL`.
- 13 предупреждений `ruff` (длина строки): реальный код переформатирован,
  текст регламента персональных данных вынесен в отдельный модуль
  (`services/consent_policy_text.py`) — как и вёрстка email-писем, это
  прозаический текст, а не код.

## [0.0.36] — 2026-08-10

Исправление к 0.0.35: замена фона не появлялась в Safari и Firefox.

### Исправлено
- Проверка поддержки замены фона была строже, чем требует библиотека, и
  молча отрезала два браузера — кнопки выбора фона не было ни в превью, ни
  в комнате. У библиотеки два конвейера обработки кадров: современный
  (только Chrome и производные) и запасной через `canvas.captureStream()`,
  который работает и в Safari, и в Firefox; проверка требовала именно
  современный. Теперь условие буквально повторяет проверку самой библиотеки.

## [0.0.35] — 2026-08-10

Замена фона видео на картинку — отключаемый модуль (по умолчанию выключен).
Три готовые сцены и свои картинки (до 10 штук, загружаются в профиле).
**Только на компьютере:** сегментацию силуэта считает нейросеть на каждом
кадре, и на телефоне это греет устройство, ест батарею и просаживает FPS
в самой встрече — портить основное ради украшения нельзя.

### Добавлено
- Кнопка «Фон» в тулбаре комнаты и выбор фона в превью на входе. Выбранный
  до входа фон применяется к публикуемому треку сразу в конференции.
- Три дефолтные сцены (офис, пляж с пальмами, космическая станция) —
  собственные векторные рисунки (`design/backgrounds/`), а не фотографии
  из интернета: у нарисованной сцены нет чужой лицензии, а продукт
  расходится по инсталляциям, и проверять права на каждую копию некому.
- Раздел «Свои фоны» в профиле: загрузка (не больше 10 штук) и удаление.
  Картинка уменьшается до 1280px по длинной стороне и переводится в WebP
  прямо в браузере перед отправкой — «чтобы не грузили БД». Файлы лежат
  на диске, в БД только пути, как у аватаров.
- Тумблер «Замена фона видео» в админке. Лимит на число картинок и формат
  файла проверяются на сервере, а не только в интерфейсе.

### Особенности реализации
- **Модель сегментации и wasm-рантайм отдаются со своего домена.** По
  умолчанию библиотека тянет их с внешних CDN (jsdelivr и
  storage.googleapis.com), и в закрытом контуре фича молча не работала бы.
  Модель (Apache 2.0) лежит в репозитории, рантайм копируется из
  node_modules при сборке; nginx отдаёт их сжатыми и с долгим кэшем.
- Библиотека и модель грузятся **лениво**, только когда фон реально
  включают: вход в конференцию не стал медленнее, основной бандл не вырос.
- «Десктоп» определяется по возможностям устройства (точный указатель +
  hover + число точек касания), а НЕ по ширине окна: узкое окно на
  десктопе — всё ещё десктоп, а широкий планшет — всё ещё планшет.
  На мобильном кнопки нет вовсе, а не задизейбленной.
- Фон переживает выключение и включение камеры и смену устройства съёмки.
  Удаление применённой сейчас картинки сбрасывает выбор на «без фона».

## [0.0.34] — 2026-08-09

Правка к проверке устройств на входе (0.0.33): превью и кнопки
камеры/микрофона теперь и на шаге «Подключиться к конференции», не только
на карточке «Как вас зовут?». Закрывает реальный пробел — авторизованный
пользователь, входящий через `/join`, карточку «Как вас зовут?» не проходит
вовсе (сразу подключение) и раньше проверку устройств не видел никогда.

### Исправлено
- Превью камеры и кнопки вкл/выкл — на шаге ввода ссылки/номера конференции,
  тем же компонентом, что и на карточке гостя. Поток живёт непрерывно на
  обоих шагах — переход между ними не гасит и не переоткрывает камеру.
- Состояние кнопок «войти с камерой/микрофоном» теперь доезжает до комнаты
  и для гостя (`input → guest-info → комната`), и для авторизованного
  пользователя (`input → connecting → комната`).
- Починена грабля с превью между шагами: `<video>` на разных шагах —
  разные DOM-узлы, обычный `ref` не переживал переезд между ними (картинка
  гасла в чёрный прямоугольник, хотя поток оставался жив). `videoRef` хука
  стал callback-ref, переподключающим поток к новому узлу автоматически.

## [0.0.33] — 2026-08-09

Проверка устройств на входе — отключаемый модуль (по умолчанию выключен).
Раньше при первом включении микрофона/камеры уже внутри конференции у
участника всплывал системный диалог разрешения браузера; теперь его можно
пройти заранее, на странице логина и в карточке «Как вас зовут?» при
гостевом входе, вместе с живым превью камеры.

### Добавлено
- Запрос доступа к камере и микрофону на `LoginPage` и в карточке
  гостевого входа `JoinPage` — привязан к первому жесту на карточке
  (клик/тап/клавиша), не спрашивается повторно, если браузер уже помнит
  разрешение (`navigator.permissions`, с фолбэком на жест там, где API
  недоступен, например в Safari). Камера и микрофон запрашиваются
  независимо — отказ в одном не блокирует другой.
- Живое зеркальное превью камеры на `JoinPage` (карточка «Как вас
  зовут?») с кнопками-пиктограммами «микрофон»/«камера»: реально
  останавливают и перезапускают поток, а не просто прячут картинку.
  На `JoinPage` кнопки определяют, с чем гость войдёт в конференцию
  (передаётся в `RoomPage`); на `LoginPage` — только пре-авторизация
  разрешения без интерфейса (страница ведёт в лобби, а не в конкретную
  конференцию, применить выбор там негде).
- Публичный `GET /api/v1/public/settings` — флаг модуля, нужный обеим
  страницам до аутентификации.
- Тумблер «Проверка устройств на входе» в админке (Настройки → Модули).

### Изменено
- Отказ в доступе не блокирует ни вход в систему, ни в конференцию —
  показывается понятная подсказка, дальше можно идти как раньше.
- Камера гарантированно освобождается при уходе с карточки, сабмите
  формы, размонтировании и любой ошибке — включая явный `release()`
  перед переходом в комнату, чтобы устройство не досталось LiveKit
  «занятым».

## [0.0.32] — 2026-08-09

Метрики состояния БД и пулов соединений + алерты в Prometheus — по решению
оператора на отказ БД реагируем сигналом, а не автолечением (перезапуск
контейнера при недоступной БД оборвал бы WS у всех, кто в конференциях).
См. разбор инцидента 07.08.2026 (0.0.31): `/api/health` во время отказа
отдавал 200 с `db: false`, а Prometheus скрейпит `/metrics`, где метрик
состояния БД не было вообще — строить алерт было не на чем.

### Добавлено
- **`vidconf_db_up`** — доступность БД (1/0), проверяется отдельным от
  основного пула соединением с коротким таймаутом. Позволяет отличить
  «БД лежит» от «основной пул занят под нагрузкой» — это два разных
  состояния, и до этого релиза их нечем было различить.
- **`vidconf_db_pool_size`/`_max_overflow`/`_checked_out`** — конфигурация
  и занятость основного пула SQLAlchemy. Читаются синхронно из объекта
  пула (`engine.pool`), без единого запроса к БД — это единственный
  способ получить сигнал именно в момент, когда пул исчерпан.
- **`vidconf_redis_pool_in_use`/`_max_connections`** — занятость пула
  Redis (второй потолок того же рода, закрыт в 0.0.31).
- Алерты `deploy/monitoring/alerts.yml` (группа `vidconf-db`):
  `DatabaseUnavailable` (`vidconf_db_up == 0`, `for: 30s`, critical) и
  `DbConnectionPoolNearExhaustion`/`RedisConnectionPoolNearExhaustion`
  (занято > 80% дольше минуты, warning) — ранний сигнал: в инциденте
  07.08 пул заполнялся постепенно по мере входа участников в комнату,
  а не рывком от HTTP-нагрузки.
- Дашборд Grafana **«БД и пулы соединений»**
  (`deploy/monitoring/grafana/dashboards/db-pool.json`).

### Технические детали
- `GET /metrics` больше не падает и не виснет при недоступности основного
  пула БД: gauge'и о состоянии пула читаются первыми и не зависят от него
  (отдельное NullPool-соединение для `db_up`, синхронный снимок для
  занятости пула), а зависящий от основного пула `vidconf_pipeline_sessions`
  обёрнут таймаутом (2с) — при недоступности оставляет прежнее значение,
  не роняя остальные метрики. Полностью развести его с основным пулом не
  стали: тестовый харнесс подменяет `get_session` на savepoint-сессию
  (`tests/conftest.py`), отдельное соединение не увидело бы несознанные
  тестом данные — тот же компромисс, что и в 0.0.31 для `api/chat.py`.
- Проверено вживую на локальном стенде (не только по синтаксису конфига):
  остановка Postgres → `vidconf_db_up` = 0, `/metrics` продолжает отвечать,
  алерт `DatabaseUnavailable` переходит в `firing`; временно урезанный
  пул под нагрузкой → `DbConnectionPoolNearExhaustion` переходит в
  `firing` ровно через заявленный `for: 1m`; снятие нагрузки/восстановление
  БД — алерты гаснут.

## [0.0.31] — 2026-08-09

Разбор провала входа на нагрузочном тесте 07.08.2026: комната держала
соединения с БД и Redis на каждого участника.

### Исправлено
- **Вход в систему переставал работать, когда в конференции набиралось
  около сорока человек.** WS-подключение комнаты (чат и очередь рук)
  держало занятым одно соединение с БД всё время, пока участник сидел
  в конференции: SELECT'ы хендшейка открывали транзакцию, а закрыть её
  было некому. Пул — 20 соединений на воркер (40 на инстанс), поэтому
  сороковой вошедший выгребал его досуха, и все остальные запросы —
  резолв конференции, гостевой вход, логин, обновление токена — начинали
  отвечать 500. Теперь соединение возвращается в пул сразу после
  хендшейка; на локальном стенде 120 участников на одном воркере не
  занимают ни одного соединения в простое (было: 20 из 20 при 20
  участниках, дальше вход не работал вовсе).
- **Пользователя выкидывало из системы, когда серверу было плохо.**
  Фоновое обновление access-токена считало неудачей любой отрицательный
  ответ и на каждую такую неудачу сбрасывало сессию с переходом на
  страницу входа. Ответ 5xx (и обрыв сети) теперь означает «сервер
  временно недоступен»: сессия сохраняется, пользователь остаётся
  в системе и получает обычную ошибку запроса. Разлогинивание осталось
  только там, где backend прямо сказал, что сессия недействительна.
  Восстановление сессии при старте приложения повторяет попытку трижды,
  прежде чем показать страницу входа.

### Технические детали
- Размер пула соединений с Redis задан явно (`REDIS_MAX_CONNECTIONS`,
  по умолчанию 500): на нём висят долгоживущие pub/sub-подписки комнаты —
  по одной на участника, — а дефолт redis-py 8 (100) упирался в потолок
  примерно на сотом участнике на воркер. Второй потолок того же рода,
  что и пул БД; найден при проверке правки выше на 120 участниках.
- Размеры пулов БД (`DB_POOL_SIZE`/`DB_MAX_OVERFLOW`) не менялись
  осознанно: соединение больше не удерживается впустую, поэтому
  расширение пула лечило бы симптом и лишь отодвинуло порог.

## [0.0.30] — 2026-08-04

Согласие на обработку персональных данных при регистрации + отключаемый модуль.

### Добавлено
- На форме регистрации — галочка согласия на обработку персональных данных
  со ссылкой на публичную страницу регламента (`/legal/personal-data-consent`).
  Кнопка регистрации неактивна, пока галочка не отмечена; сервер тоже
  отказывает без согласия (`POST /auth/register` → 400 `consent_required`,
  второй эшелон проверки — тот же принцип, что у `hand_queue_disabled`).
- Текст регламента — настройка инстанса, редактируемая в админке
  (вкладка «Настройки» → карточка «Согласие на обработку персональных
  данных»): текстовое поле + тумблер «требовать согласие при регистрации».
  Дефолтный текст — типовой шаблон с плейсхолдерами под организацию
  (наименование оператора, адрес, контакты, цели и срок обработки),
  **не проходил юридическую проверку** — в карточке администратора
  об этом явное предупреждение.
- Номер редакции текста растёт автоматически при каждой правке —
  у каждого пользователя, давшего согласие, в БД фиксируется и версия
  документа, и дата согласия (`users.consent_version`, `consent_given_at`).
- Модуль отключаем (`instance_settings.consent_policy`), по умолчанию
  выключен — поведение существующих инсталляций не меняется. У уже
  зарегистрированных пользователей согласие не запрашивалось и не
  запрашивается задним числом, вход не блокируется.

### Технические детали
- Миграция Alembic добавляет `users.consent_version`/`consent_given_at`
  (nullable — `NULL` означает «согласие не запрашивалось»).
- Публичный `GET /auth/registration-options` (уже существующий, без нового
  эндпоинта) дополнен полями `consent_required`/`consent_text`/`consent_version` —
  тем же ответом пользуется и страница регламента, доступная всегда,
  независимо от того, включён ли модуль.

## [0.0.29] — 2026-08-04

Кнопка «демонстрация экрана» в мини-окне конференции.

### Добавлено
- В мини-окне (Document PiP) рядом с кнопкой микрофона появилась кнопка
  демонстрации экрана: начать и остановить показ можно, не разворачивая
  основное окно. Кнопка и кнопка основного тулбара отражают одно
  состояние — обе читают его из комнаты, а не из разметки.
- Своя демонстрация показывается в мини-окне по обычным правилам сцены:
  забирает крупную плитку при старте, держится, пока говорят другие, и
  уступает говорящему либо закреплённому участнику после остановки.

### Примечания
- Кнопка есть только там, где мини-окно — настоящее окно со своей
  разметкой, то есть в Chrome и Edge. В Safari мини-окно выводится
  средствами системы (нативный «картинка в картинке»), собственных
  кнопок в нём быть не может; в Firefox мини-окна нет вовсе. Это
  ограничение браузеров, а не недоработка.
- При выборе «весь экран» мини-окно попадает в собственную
  демонстрацию — как и у всех остальных участников. Чтобы этого
  избежать, показывайте конкретное окно, а не экран целиком.

## [0.0.28] — 2026-08-04

Очередь поднятых рук видна всем участникам + отключаемый модуль.

### Добавлено
- Очередь поднятых рук больше не спрятана от обычных участников — её
  видит любой, кто в конференции, не только организатор. Опустить чужую
  руку по-прежнему может только организатор: сервер это уже проверял
  (`api/chat.py`, задача B1), правки — только на фронте, кнопка
  «Опустить» показывается у записи, если это своя рука либо пользователь
  сам организатор.
- «Поднятие руки» (кнопка «Рука» + очередь целиком) — отключаемый модуль
  в админке (`instance_settings.hand_queue`, дефолт `enabled=true` —
  поведение существующих инсталляций не меняется). Настройка едет
  участнику в `JoinOut` (как `chat_enabled`) до входа в комнату.
  Выключенный модуль гасит кнопки на фронте и отклоняет
  `raise_hand`/`lower_hand` на сервере кодом `hand_queue_disabled` —
  вторая линия защиты для клиента со старым `JoinOut` на руках.
  Переключение применяется со следующего входа в комнату (та же
  застылость на время жизни соединения, что и у `chat_enabled`).

## [0.0.27] — 2026-08-04

Разрыв связи больше не выбрасывает участника из конференции.

### Исправлено
- Телефон с погасшим экраном выпадал из конференции, а при возвращении
  мог оказаться в лобби вместо комнаты. Причина установлена по логам
  прода: Chrome срезает ресурсы фоновой вкладке, телефон перестаёт
  отвечать по ICE, и LiveKit закрывает участника через 5 с после потери
  соединения (замерено: 37 с после блокировки экрана, 23 с после
  сворачивания браузера — то есть достаточно просто убрать вкладку в
  фон, гасить экран не обязательно). Восстановить сессию после этого
  нельзя — участника на сервере уже нет; нужен полный повторный вход,
  и `livekit-client` пытается сделать его сам, но его бюджет повторов
  (10 попыток за ~44 с) в фоновой вкладке успевает сгореть. Тогда
  приходило событие `Disconnected`, и страница комнаты уводила
  пользователя в лобби.

  Теперь непреднамеренный разрыв не уводит со страницы: сбрасывается
  состояние входа, и работает уже имевшийся путь авто-перезахода —
  резолв конференции, свежий токен, вход заново. Намеренный выход
  отличается по нажатию кнопки «Выйти», а не по коду причины: причину
  `CLIENT_INITIATED` присылает и кнопка, и сам `livekit-client`, который
  при заморозке вкладки (событие `freeze`) вызывает `disconnect()`
  самостоятельно. Разрывы, после которых возвращаться нельзя (участника
  выгнал организатор, конференция закрыта, вход той же личностью с
  другого устройства), уводят в лобби как раньше. От цикла «вошёл —
  сразу выбросило» защищает лимит в 5 перезаходов подряд; соединение,
  прожившее дольше 30 с, счётчик обнуляет.

## [0.0.26] — 2026-08-04

Мобильная комната: свайп шторки настроек и полноэкранный режим.

### Исправлено
- Шторка «Настройки» на мобильном не закрывалась свайпом вниз почти никогда:
  обработчики висели только на ручке-волоске (`.room-sheet-handle`,
  40×4px) — палец в неё практически невозможно попасть, и палец почти
  всегда приземлялся на панель, где обработчиков не было вовсе. Теперь
  свайп закрывает шторку при жесте по любому месту панели, но только
  когда её содержимое проскроллено в самый верх (`scrollTop === 0` на
  начало жеста) — иначе свайп вниз листает список устройств, как и должен
  стандартный bottom sheet.

### Добавлено
- Полноэкранный режим комнаты — на мобильном кнопка «Экран» в тулбаре (как
  на десктопе), при активации топбар и тулбар уходят из потока и лежат
  оверлеем поверх сцены: показываются по тапу/клику по сцене вне элементов
  управления и сами прячутся через несколько секунд бездействия. На
  десктопе — та же логика (топбар/тулбар тоже прячутся в полноэкранном
  режиме), дополнительный способ вернуть их — навести мышь в нижнюю полосу
  экрана.

### Изменено
- Кнопка настроек устройств в тулбаре на мобильном подписана «Настройки»
  вместо «Устройства».
- Раскладка кнопок мобильного тулбара, когда они не помещаются в один ряд
  (7 и больше — с полноэкранным режимом и «Очередью» у организатора это
  обычный случай), стала равномерной сеткой на 4 колонки (7 → 4+3,
  8 → 4+4) вместо переноса «как получится» через `flex-wrap`.
- `deploy/render-templates.sh` умеет читать значения из файла, заданного
  переменной `ENV_FILE`, а не только из корневого `.env` (см. коммит
  `3847476`, вошёл в этот релиз) — для локальных стендов, где корневой
  `.env` указывает на боевые адреса.

## [0.0.25] — 2026-08-03

Два дефекта мини-окна конференции (Document PiP), оба видны только в Chrome.

### Исправлено
- В Chrome пропадал звук других участников после возврата сцены из
  мини-окна в основное. `RoomAudioRenderer` жил внутри `RoomStage` и
  рендерился в обеих её ветках, поэтому скрытые `<audio>` с чужими
  аудиотреками физически переезжали в документ PiP-окна и обратно.
  После такого переезда Chrome теряет аудиовыход у remote-трека: пакеты
  продолжают приходить (`packetsReceived` растёт), а
  `totalSamplesDuration` и `totalAudioEnergy` замирают, и трек молчит
  даже в свежесозданном `<audio>` со свежим `MediaStream`. Тот же цикл
  detach/attach в пределах одного документа безвреден — дело именно в
  переезде между документами. Рендерер вынесен в `RoomPage`: один
  экземпляр, всегда в основном документе, в PiP-окне аудиоэлементов нет
  вовсе. В Safari бага не было — там Document PiP не используется
  (video-фолбэк), сцена из основного окна не уезжает.
- В мини-окне не действовало закрепление участника, а демонстрация
  экрана слетала на говорящего от любой чужой реплики. Мини-плеер
  намеренно ходил с упрощёнными правилами выбора фокуса (без
  `holdScreenShare`, без приоритета говорящего с камерой, без
  антидребезга) и с собственным локальным состоянием закрепления.
  Теперь `pickStageFocus` получает одинаковые правила в обоих вариантах
  сцены, а закрепление переезжает между окнами тем же мостиком через
  `RoomPage`, что и фокус. Сознательно оставлено одно отличие —
  фолбэк «показать себя» (`localKey`) только у мини-плеера: это защита
  от дефекта 0.0.11, когда мини-окно открывалось на самом пользователе.
- Закрепление сбрасывалось при каждом монтировании сцены: на первом
  рендере `useTracks` отдаёт пустой набор треков, и правило «закреплённый
  вышел из комнаты» принимало это за уход участника.

### Изменено
- Кнопка-булавка на плитке подписана «Закрепить» вместо «Закрепить в
  основном окне» — закрепление больше не ограничено основным окном.

## [0.0.24] — 2026-08-03

Три артефакта вёрстки, вылезающие за границы блоков (эмодзи-поповер чата,
заголовок брендовой панели, чат в Firefox) + аудит похожих мест.

### Исправлено
- Заголовок «Ваша инфраструктура» на странице входа вылезал за край
  брендовой панели на широком окне с узкой панелью (`.layout` — flex
  42/58) — старый фикс уменьшал кегль только по ширине ОКНА (`@media`),
  а не панели. Кегль `.brand-headline` теперь считается через container
  query (`container-type: inline-size` + `cqw`) — тот же приём, что у
  аватара участника в комнате. Тем же механизмом обрезались плашки
  статистики («AI-саммари») — `.brand-stats` теперь переносит их на
  мобильном и десктопе одинаково.
- Эмодзи-поповер в чате комнаты: последняя (5-я) колонка вылезала за
  правый край поповера. Причина — гонка CSS-специфичности: правило
  круглой кнопки «Отправить» (`.chat-input-row button`, 42×42px)
  продолжало красить размер и кнопкам эмодзи внутри поповера (та же
  гонка чинилась для цвета в 0.0.19, но не для размера). Добавлены явные
  `width`/`height: auto` нужной специфичности + `max-width` на попап как
  общая страховка.
- Чат комнаты вылезал за границы панели в Firefox: `.chat-messages`
  (flex-колонка) без `min-height: 0` не сжималась в Firefox, `textarea`
  поля ввода без `min-width: 0` упиралась в автоматическую минимальную
  ширину (Firefox считает её от атрибута `cols`, жёстче Chrome/Safari).

## [0.0.23] — 2026-08-02

Документация по сайзингу под медиа-нагрузку + видимость TURN-аллокаций в логах.

### Добавлено
- `docs/deploy/hardware-sizing.md` — таблица «профиль нагрузки → CPU/RAM/
  полоса» для медиа (видеоконференции), на реальных боевых замерах
  28.07 и 31.07.2026, с формулой для расчёта под свой сценарий.

### Исправлено
- coturn: включено verbose-логирование — дефолтный уровень не писал
  построчно `ALLOCATE`/`CreatePermission`/`Refresh` даже при рабочем
  relay-соединении (найдено на релизе 0.0.22 — звонок через TURN работал,
  а `grep -ci allocate` по логам coturn показывал 0).

## [0.0.22] — 2026-08-02

TURN over TLS (5349) — для клиентов из сетей, где наружу открыт только 443.

### Добавлено
- coturn слушает TLS на `5349` (сертификат Let's Encrypt, обновляется
  автоматически). Включается одним ключом `TURN_TLS_HOST` в `.env` (домен
  сертификата, НЕ IP) — пусто оставляет прежнее поведение без единого следа
  в рендеренных конфигах.
- LiveKit объявляет клиентам запись `protocol: tls` в `rtc.turn_servers`
  (последней в списке, как самый дорогой фолбэк) — без анонса включённый
  TLS был бы бесполезен: именно так уже случалось с обычным TURN до 0.0.14
  (сервис работал healthy, но не обслужил ни одной аллокации).
- Новый init-контейнер `coturn-certs-init`: копирует fullchain/privkey из
  `/etc/letsencrypt` в отдельный volume под правами `644` — coturn
  (`nobody:nogroup`, без root-фазы в entrypoint) не может прочитать
  оригинальный ключ (`root`, `0600`), а права на хосте ослаблять нельзя.
- Deploy-hook certbot дополнительно перекопирует сертификат и перезапускает
  `coturn` при продлении — без этого TLS-TURN тихо остановился бы
  обслуживать новые TLS-хендшейки примерно через 60 дней.

### Не сделано
- TURN over `443` — недостижимо без SNI-мультиплексора: порт уже занят
  nginx (Docker port-publish), а coturn слушает в `network_mode: host` и не
  может разделить с ним один и тот же сокет.

## [0.0.21] — 2026-08-02

Рычаги нагрузки медиа в админке: потолок качества публикации и лимит плиток.

### Добавлено
- Настройка инстанса «Потолок качества публикации видео» (без ограничения /
  720p / 360p / 180p) — режет битрейт исходящего видео публикующего через
  `publishDefaults` LiveKit, снижает нагрузку на его канал и устройство.
  Дефолт — без ограничения, поведение существующих инсталляций не меняется.
- Настройка инстанса «Максимум плиток на экране» (25 / 16 / 9 / 4) — участники
  сверх лимита уходят на следующую страницу сетки вместо подписки на их
  видеотреки, меньше одновременных видеопотоков на канал и экран участника.
  Дефолт — 25 (текущий максимум сетки 5×5), без изменений.
- Обе настройки доступны в новой карточке «Нагрузка» вкладки «Настройки»
  админки и отдаются участнику вместе с токеном входа в конференцию — ещё до
  подключения к комнате, чтобы применяться до публикации трека и не вызывать
  переподключение уже вошедших участников при смене настройки.

## [0.0.20] — 2026-08-02

Сеть LiveKit: один UDP-порт вместо диапазона на 101 порт.

### Исправлено
- Весь медиа-трафик конференций шёл через userland-прокси Docker: диапазон
  `54000-54100/udp` заставлял поднимать по отдельному процессу `docker-proxy`
  на каждый порт. LiveKit переведён на `rtc.udp_port` (один порт,
  мультиплексирование ICE-сессий по ufrag внутри самого сервера) — проброс
  портов схлопнут до одного, TURN не затронут. Проверено локально
  синтетической нагрузкой (`lk load-test`, 2 видео + 2 аудио publisher'а,
  2 subscriber'а) — 0% потерь пакетов, ICE во всех сессиях выбирает новый
  единственный порт.

## [0.0.19] — 2026-08-02

Правки по замечаниям к части B (комната конференции).

### Исправлено
- Очередь поднятых рук открывалась боковой панелью во весь экран на
  мобильном — теперь компактный поповер над кнопкой (как «Вид»), размер
  подстраивается под число записей, после ~10 строк список скроллится.
- Кнопки нижнего тулбара при сужении окна вылезали за края блока (задачи
  B1/B2 добавили «Рука»/«Очередь», в тулбаре стало до 11 кнопок вместо
  восьми) — теперь плавно уменьшаются на диапазоне 1200–600px вместо
  жёсткого скачка на мобильный вид.
- Подпись «Мини-окно» на промежуточных ширинах переносилась на 2 строки и
  делала эту кнопку выше соседних — ниже 1200px показывается короткое
  «Мини».
- Эмодзи-поповер в чате красил все свои кнопки в зелёный цвет кнопки
  «Отправить» (гонка специфичности CSS-селекторов) — исправлено; заодно
  сетка приведена к ровным 5×6 без неполной строки, добавлены
  🦾 🚀 🦞 💯 🤷‍♂️.

## [0.0.18] — 2026-08-01

Ограничение частоты запросов больше не блокирует вход целой конференции.

### Исправлено
- Лимит на резолв конференции и гостевой вход считался по адресу контейнера
  nginx, а не клиента, — то есть «10 запросов в минуту» действовали на весь
  сервер разом. Одиннадцатый человек, открывший ссылку в течение минуты,
  получал отказ и видел «Не удалось найти конференцию» для существующей и
  активной конференции. Теперь адрес берётся из заголовка, который nginx уже
  передаёт.
- Счётчик считает только неудачные попытки — конференция не найдена или
  пароль неверен. Именно так выглядит перебор номера, от которого защищает
  ограничение; массовый вход по рабочей ссылке к нему отношения не имеет.
  На общий поток с адреса оставлен потолок в 300 запросов в минуту.

## [0.0.17] — 2026-08-01

Массовый вход в систему и в конференцию перестаёт упираться в проверку пароля.

### Исправлено
- Проверка пароля больше не останавливает весь backend. Хэширование argon2 —
  это десятки миллисекунд счёта, и выполнялось оно синхронно внутри
  асинхронного обработчика: пока считался один пароль, процесс не обслуживал
  ничего другого. На нагрузочном тесте 31.07 с примерно семью десятками
  одновременных входов это дало p95 логина 7.28 секунды, p95 входа в
  конференцию 7.06 секунды и 33 соединения к базе, висящих в открытой
  транзакции при одном активном запросе. Страдали и посторонние запросы —
  вход в конференцию отказывал, хотя пароль там не проверялся вовсе.
  Теперь хэширование считается в пуле потоков.
- Параметры argon2id приведены к рекомендации OWASP (t=2, m=19 МБ, p=1)
  вместо дефолтов библиотеки (t=3, m=64 МБ, p=4): 95 мс против 42 мс на
  проверку. Прежнее значение `parallelism=4` вдобавок занимало все четыре
  ядра сервера — те же, на которых работает медиа-сервер.
- Пароли, сохранённые со старыми параметрами, продолжают работать и
  перевыпускаются автоматически при первом успешном входе.

## [0.0.16] — 2026-08-01

Роль организатора в комнате: поднятие руки с очередью и принудительный мьют.

### Добавлено
- Роль организатора прокинута в комнату — метаданные LiveKit-токена несут
  подсказку `is_organizer` для UI (владелец конференции/администратор);
  любое серверное действие организатора перепроверяется по владельцу
  конференции в БД, метаданным токена для авторизации не доверяем.
- Поднятие руки: кнопка «Рука» в тулбаре (у всех участников, с общим
  счётчиком), бейдж на плитке говорящего, видимый всем, и панель «Очередь»
  для организатора — участники в порядке поднятия руки, с возможностью
  опустить любую руку. Состояние — в Redis (не в БД): очередь существует
  ровно во время звонка. Участник, вышедший из конференции, автоматически
  исчезает из очереди; переподключение место не теряет.
- Принудительный мьют: организатор может выключить микрофон или камеру
  любого участника — кнопки на его плитке. Участник получает уведомление,
  что его выключил организатор, и может включить себя обратно сразу тем же
  тулбаром.
- Эмодзи скачущего коня 🐎 в наборе смайликов чата.

### Изменено
- Транспорт чата (`WS /conferences/{id}/chat`) расширен: очередь поднятых
  рук и уведомления о мьюте едут по тому же аутентифицированному
  соединению, что и сообщения чата — без нового эндпоинта.

Миграций БД нет — новое состояние (очередь поднятых рук) хранится в Redis,
не в Postgres.

## [0.0.15] — 2026-07-30

Шесть доработок UI комнаты конференции.

### Добавлено
- Выбор эмодзи в чате — поповер с небольшим собственным набором популярных
  эмодзи (без библиотеки-пикера), вставка в позицию курсора.
- Кнопка микрофона в мини-окне (Document PiP) — раньше в PiP не было
  никакого способа замьютить себя, не разворачивая основное окно.
- Закрепление участника доступно из любого режима показа («Стандарт»,
  «Плитки», «Живые плитки») — раньше булавка рендерилась только в
  «Стандарте». Само закрепление автоматически переключает вид на «Стандарт»
  (закреплённого иначе негде показать крупно); открепление вид назад не
  меняет.

### Изменено
- Участник заходит в конференцию с выключенными микрофоном и камерой,
  включает их сам уже в комнате.
- В топбаре комнаты вместо технического `room_name` LiveKit (вида
  `hC-Dmos9KEM`) показывается название конференции; при его отсутствии —
  «Конференция без названия». Номер и ссылка-приглашение не задеты.

## [0.0.14] — 2026-07-28

TURN-фолбэк для участников из сетей с жёстким NAT.

### Исправлено
- LiveKit теперь анонсирует клиентам внешний TURN-сервер (`rtc.turn_servers`
  в конфигурации, UDP и TCP на 3478). Раньше coturn поднимался и был healthy,
  но клиенты о нём не знали: внешний TURN в конфиге объявлен не был, встроенный
  выключен, а фронтенд `iceServers` не задаёт. За всё время работы в логах
  coturn не было ни одной аллокации — то есть relay не использовался никогда,
  и участники из сетей, где прямое UDP-соединение не проходит, теряли связь
  (`PEER_CONNECTION_DISCONNECTED`). На нагрузочном тесте 28.07 все такие
  разрывы пришлись на внешних участников и ни одного — на офисных.
- Credentials TURN генерируются по механизму TURN REST API из общего
  `TURN_STATIC_AUTH_SECRET`, то есть тот же секрет, что и у coturn.

### Изменено
- `deploy/render-templates.sh` подставляет `TURN_EXTERNAL_IP` и
  `TURN_STATIC_AUTH_SECRET` также в конфигурацию LiveKit.
- Раздел 8 руководства по развёртыванию переписан: TURN больше не «план на
  будущее», а рабочая конфигурация. Отдельно описано правило ufw для
  relay-диапазона `49160:49200/udp` — без него TURN отвечает на запросы, но
  релей не работает, причём в логах coturn при этом тишина.

TURN over TLS (5349/443) по-прежнему не настроен: требует монтирования
сертификата в coturn. Неработающий `turns:` намеренно не анонсируется, иначе
клиент ждал бы таймаута перед переходом к рабочему кандидату.

## [0.0.13] — 2026-07-28

Снижение нагрузки на сеть: клиент перестаёт получать полное качество всех
чужих камер независимо от того, какого размера плитка на экране.

### Изменено
- Включены `adaptiveStream` и `dynacast` в опциях комнаты. Оба флага в LiveKit
  выключены по умолчанию, из-за чего каждый участник был подписан на полное
  качество всех чужих треков, а каждый паблишер слал все слои симулкаста, даже
  когда их никто не смотрит. Теперь качество подписки выбирается по фактическому
  размеру плитки, а неотрисованные треки уходят в паузу. На локальном стенде
  (9 участников, паблишеры 720p) входящий поток одного клиента упал с
  11 110 до 778 кбит/с.

  Вместе с этим начинает экономить уже написанный код, который до сих пор не
  давал выигрыша: «скрыть остальных» не рендерит карусель (исходящий трафик
  LiveKit 0.76 → 0.03 Мбит/с), пагинация сетки участников рендерит только
  текущую страницу, а пауза чужого видео в свёрнутой вкладке работает лишь
  при включённом `adaptiveStream`.
- Контейнеры больше не пересобирают окружение Python при запуске: во все
  вызовы `uv run` в прод-путях (CMD образа, `command`/`entrypoint`/`healthcheck`
  сервисов, миграции и seed в `install.sh`) добавлен `--no-sync`. Раньше
  окружение, собранное на этапе build с `--no-dev`, при каждом старте
  синхронизировалось заново и подтягивало dev-группу: ~26 МБ загрузок,
  замедленный старт и ruff/mypy/pytest в рантайме. Healthcheck'и делали то же
  самое каждые 15 секунд всю жизнь контейнера. Размер `/app/.venv` после
  запуска: 574 → 456 МБ. Локальная разработка не затронута — там dev-группа
  нужна и ставится как раньше.

## [0.0.12] — 2026-07-28

Разблокировка backend под нагрузкой: вход в конференцию перестаёт отваливаться,
когда участники включают микрофоны.

### Исправлено
- Обработчик webhook `track_published` больше не запускает Track Egress внутри
  своей транзакции. Раньше каждый опубликованный микрофон уходил в сетевой
  вызов, а на инстансе без профиля `transcribe` (egress-сервиса в деплое нет)
  этот вызов висел 20–25 секунд, всё это время удерживая соединение с БД. На
  нагрузочном тесте с 19 участниками пул соединений выгребался за секунды, и
  вход в конференцию начинал отвечать 500. Теперь запуск уходит в фоновую
  задачу со своей сессией, а webhook отвечает сразу — LiveKit перестаёт копить
  очередь доставки и терять события.
- Track Egress не запускается вовсе, если транскрибация выключена в настройках
  инстанса — тот же guard, что уже был в обработчике `room_finished`.
- Запуск Track Egress ограничен таймаутом (по умолчанию 3 секунды) вместо
  ожидания собственного таймаута LiveKit.

### Изменено
- Пул соединений с БД задаётся явно (`DB_POOL_SIZE`, `DB_MAX_OVERFLOW`,
  `DB_POOL_TIMEOUT`) вместо дефолта SQLAlchemy 5 + 10. Считайте бюджет на весь
  инстанс: каждый воркер держит свой пул, и сумма должна оставаться заметно
  ниже `max_connections` у Postgres.
- Backend запускается с несколькими процессами uvicorn (`UVICORN_WORKERS`,
  по умолчанию 2). Одиночный процесс означал, что любой блокирующий вызов
  останавливает и параллельные запросы, и WS-чат всех участников. Дефолт 2, а
  не по числу ядер: на четырёхъядерном сервере ядра делятся с LiveKit.

## [0.0.11] — 2026-07-28

Выбор режима показа участников в конференции, скрытие остальных и круглая
иконка участника при выключенной камере.

### Добавлено
- Выбор режима показа участников на сцене вместо автоматической раскладки:
  «Стандарт» (крупная плитка плюс карусель остальных сбоку, как было),
  «Плитки» (все участники равными плитками) и «Живые плитки» (плитками —
  только те, у кого включена камера, остальные в карусели). Кто именно
  показан крупно в «Стандарте», по-прежнему решает прежняя логика фокуса
  (новая демонстрация → закреплённый → говорящий с видео → говорящий).
  Выбор сохраняется между заходами в комнату.
- Скрытие остальных участников — на сцене остаётся только основная плитка.
  Скрыть можно из меню «Вид», из шторки настроек на мобильном и кнопкой над
  колонкой миниатюр; вернуть — кнопкой «Показать остальных (N)», которая
  видна всё время, пока кто-то скрыт. В режиме «Плитки» скрывать нечего,
  переключатель там заблокирован.
- Переключатель вида: кнопка «Вид» с поповером в тулбаре на широком экране;
  на мобильном — секция «Вид» в шторке настроек, чтобы не добавлять шестую
  кнопку в уже ужатый тулбар.

### Изменено
- Демонстрация экрана временно перебивает выбранный режим: пока идёт шэр,
  сцена ведёт себя как «Стандарт», а по его завершении возвращается к
  выбранному виду.
- Равномерная сетка плиток стала пригодна для телефона: 2 колонки от 360px
  ширины и портретная раскладка 2×3 вместо двух плиток на страницу, которые
  давал набор раскладок библиотеки. Лишние участники уходят на следующую
  страницу — листается свайпом и стрелками.

### Исправлено
- Иконка участника при выключенной камере больше не превращается в эллипс:
  размер считается от узкой стороны плитки (container-запросы), а не как
  проценты от разных сторон. В крупной плитке иконка занимает примерно её
  половину, в миниатюрах карусели — всю узкую сторону с отступом 5px; на
  большой плитке фокуса иконка больше не упирается в прежний потолок 96px.
- Мини-плеер в Chrome открывался на самом пользователе вместо того, что он
  видел крупно: сцена в PiP-окне — отдельный экземпляр компонента и выбирала
  фокус с нуля, доходя до фолбэка «показать себя». Теперь ключ фокуса
  переживает переезд в мини-окно и обратно. В Safari (video-фолбэк вместо
  Document PiP) поведение не менялось.

## [0.0.10] — 2026-07-28

Разбивка метрик по контейнерам в Grafana и плашка с характеристиками сервера.

### Добавлено
- Метрики по контейнерам (CPU/память/сеть каждого отдельно) — замена
  `cAdvisor`, несовместимого с containerd-снапшоттером Docker Engine на
  сервере `1gb` (убран в `0.0.9`). Свой минимальный сервис
  `container-exporter` (`deploy/monitoring/container-exporter/`) опрашивает
  Docker Engine API параллельно в фоновой задаче, независимо от
  storage-driver'а. Доступ к докер-сокету изолирован через
  `docker-socket-proxy` — экспортеру разрешены только `GET`-запросы по
  контейнерам, любые изменяющие запросы блокируются на уровне прокси.
  Панели «Топ контейнеров по CPU/памяти» вернулись в дашборд
  «Хост и контейнеры».
- Плашка «Характеристики сервера» в том же дашборде: живые «Ядра CPU» /
  «Объём RAM» / «Диск (/)» из `node-exporter`, GPU — из новой info-метрики
  backend `vidconf_host_info` (данные `install.sh`, `HW_*` в `.env`).

## [0.0.9] — 2026-07-28

Метрики CPU/RAM/диска хоста в Grafana; дефолтные подвкладки конференций и
пользователей в админке.

### Добавлено
- Мониторинг железа: `node-exporter` (CPU/RAM/диск/сеть/load average хоста)
  — профиль compose `monitoring`, без публикации портов наружу. Новый
  дашборд Grafana «Хост и контейнеры». Три новых алерта Prometheus:
  `HostMemoryLow`, `HostDiskLow`, `HostCpuHigh` (пороги — под сервер `1gb`:
  8 ГБ RAM, 4 CPU, 50 ГБ диска). `cAdvisor` (разбивка по контейнерам)
  сознательно не включён — несовместим с containerd-снапшоттером Docker
  Engine на `1gb`, см. `docs/deploy/monitoring.md`.

### Изменено
- Админка, вкладка «Конференции»: вкладка «Все» перенесена в конец списка
  фильтров (после «Завершённые»), по умолчанию открываются «Запланированные».
- Админка, вкладка «Пользователи»: по умолчанию открываются «Активные»
  (было «Все»).

## [0.0.8] — 2026-07-27

Админка: несколько mail-доменов для регистрации, фильтр и поиск по команде в
списке пользователей, исправлен баг сохранения настроек.

### Исправлено
- Сохранение любой настройки инстанса блокировалось ошибкой «уровень AI
  недоступен», даже если AI-модуль (транскрибация/суммаризация) выключен и
  пользователь уровень не трогал — фронт отправлял в `PUT /admin/settings`
  весь набор полей целиком, включая уже сохранённый `ai_level`. Теперь
  доступность уровня проверяется только при его фактической смене; фронт
  отправляет в запросе лишь реально изменённые поля.

### Добавлено
- «Эталон mail-домена» в настройках инстанса поддерживает несколько
  доменов вместо одного — регистрация проходит с email с любого домена из
  списка. Старое значение из БД (один домен) читается без миграции и
  переписывается в новый формат при первом же сохранении.
- В списке пользователей админки — вкладки «Активные»/«Заблокированные»/
  «Все» перед полем поиска.
- Поиск по пользователям учитывает и название команды — пользователи без
  команды по-прежнему остаются в общей выдаче.

## [0.0.7] — 2026-07-27

Настройки устройств: выбор аудиовывода и компактная шторка на мобильном.

### Добавлено
- Выбор устройства аудиовывода (динамики/наушники/Bluetooth-гарнитура) в
  «Настройках устройств» рядом с микрофоном и камерой. Выбор переживает
  перезаход в комнату. Список показывается только там, где браузер умеет
  им управлять (Android Chrome и другие Chromium на десктопе/мобильном);
  в iOS Safari поддержки нет вовсе — вместо списка короткая подсказка, что
  переключение звука там на стороне системы.
- На мобильной ширине «Настройки устройств» открываются компактной шторкой
  снизу вместо модалки по центру экрана — закрывается свайпом вниз, тапом
  вне панели или Escape. На десктопе поведение не изменилось.

### Примечания по эксплуатации
- Управлять маршрутизацией звука (принудительно включить громкую связь или
  перекинуть звук на Bluetooth) из браузера нельзя в принципе — это делает
  ОС, веб-страница может только выбрать устройство вывода там, где браузер
  это позволяет (`setSinkId`).

## [0.0.6] — 2026-07-27

Управление фокусом основного окна конференции: участника можно закрепить, а
без закрепления окно само следует за говорящим. Плюс общий футер на всех
страницах сервиса.

### Добавлено
- Закрепление участника в основном окне — кнопка-булавка на плитке.
  Закреплённый участник держит фокус, даже когда говорят другие; повторное
  нажатие снимает закрепление, как и выход закреплённого из комнаты.
  Закреплённая плитка помечена рамкой и подсвеченной булавкой; на
  тач-устройствах булавка видна без наведения.
- Общий футер на всех страницах, кроме комнаты конференции: версия инстанса
  и копирайт со ссылкой на автора.

### Изменения
- Основное окно конференции следует за активным говорящим, а не удерживает
  выбранного один раз участника. Фокус не переключается на короткие реплики
  (удержание 1.2 с) и не уводится с идущей демонстрации экрана. Среди
  нескольких одновременно говорящих крупно показывается тот, у кого включена
  камера. Мини-плеер (Document PiP) работает как раньше.
- Демонстрация экрана по-прежнему главнее всего: она перебивает и
  закрепление, а после её завершения фокус возвращается на закреплённого
  участника.
- Футер админки заменён общим компонентом; в брендовой панели экранов входа
  строка копирайта с захардкоженным годом убрана — год теперь берётся
  текущий.

### Исправлено
- Календарь: «г.» после года убрана и из подписей дней в самой сетке (видны
  как всплывающая подсказка и в accessibility-дереве) — в недельном и
  месячном виде. В 0.0.4 это было исправлено только для подписи периода.

## [0.0.5] — 2026-07-27

Контактный адрес инстанса: настраиваемый адрес для ответов на письма
(уходят от `no-reply@`) и кнопка проверки почтовой конфигурации из
админки.

### Добавлено
- Настройка «Контактный адрес» в админке (вкладка «Настройки») — email
  подставляется в заголовок `Reply-To` писем подтверждения регистрации,
  приглашений на конференции и саммари; при выключенной настройке
  `Reply-To` не проставляется, поведение как раньше.
- Кнопка «Отправить тестовое письмо» — синхронная проверка почтовой
  конфигурации без регистрации фиктивного пользователя. Получатель по
  умолчанию — email текущего администратора, можно указать другой.
  Результат виден сразу: успех либо текст ошибки транспорта (сервер
  недоступен/получатель отклонён), без логина и пароля SMTP в ответе.

## [0.0.4] — 2026-07-26

Календарь конференций: перестроен в раскладку «календарь во всю ширину плюс
модальные окна» — карточки событий разгружены, подробности вынесены во
всплывающее окно.

### Изменения
- Форма создания и редактирования конференции больше не занимает постоянную
  колонку справа, а открывается в модальном окне — по кнопке «Новая
  конференция», по карандашику на карточке события или из информационного
  окна. Добавлен общий хук `useModalDismiss` (закрытие по Escape и возврат
  фокуса на элемент, из которого окно открыли).
- Блок календаря занимает всю ширину страницы.
- Карточка события в сетке содержит только название (обрезается многоточием и
  больше не вылезает за границы ячейки) и — у организатора — кнопку
  редактирования. Время, номер и статус переехали в информационное окно.
- Клик по карточке события всегда открывает информационное окно, в том числе
  для своих конференций: редактирование стало отдельным действием.
- Информационное окно вхождения дополнено статус-бейджем и кнопкой
  «Редактировать» для организатора.

### Исправления
- Подпись периода в недельном и месячном виде больше не содержит «г.»
  (форматирование через `Intl.DateTimeFormat` вместо локали FullCalendar).

## [0.0.3] — 2026-07-26

Мобильная версия: адаптивная вёрстка для узких экранов на ключевых экранах
сервиса, найденная и поправленная по итогам ручного прохода по мобильному
интерфейсу.

### Исправления
- Вход/регистрация (`AuthLayout`): заголовок «Ваша инфраструктура» вылезал за
  край брендовой панели на узких экранах — кегль теперь адаптивно уменьшается
  (`clamp()`) вместо обрезки.
- Комната конференции: топбар и тулбар не помещались на мобильной ширине
  (тулбар с 8 кнопками физически шире экрана, крайние кнопки были обрезаны без
  возможности до них докрутить). На мобильном тулбар сокращён до
  микрофона/камеры/устройств/чата/выхода (демонстрация экрана,
  полноэкранный режим и мини-окно скрыты — редко нужны с телефона), в топбаре
  скрыты название конференции и строка статуса, а чип «№ … / Пригласить»
  оставлен — с телефона он нужнее.
- Админка: заголовок и 4 вкладки раздела не помещались в ширину (вкладки
  переносятся в 2 ряда, поясняющий текст под заголовком скрыт на мобильном);
  таблицы «Конференции» и «Пользователи» обрезались вбок — на мобильном в них
  остаются только ключевые колонки, название конференции стало кнопкой,
  открывающей то же окно редактирования, что и раньше (туда же добавлена
  кнопка удаления — на мобильном столбец действий скрыт).
- Подключение к конференции (`/join`): кнопка «Ввести другую ссылку или
  номер» на экране завершённой конференции вылезала за карточку на узких
  экранах (единственная кнопка на этом флоу без формы-обёртки, задающей ей
  ширину, а `.btn` держит `white-space: nowrap`).

## [0.0.2] — 2026-07-26

Релиз воспроизводимого боевого развёртывания: конфигурация деплоя перенесена в
репозиторий как источник истины, устранён инцидент безопасности, добавлена
пошаговая инструкция для девопсов.

### Безопасность
- `postgres` и `redis` сняты с публикации на `0.0.0.0` и привязаны к `127.0.0.1`
  (docker писал DNAT в обход `ufw`, порты были открыты в интернет).
- Порты `backend`, `livekit`-signaling, `prometheus`, `grafana`, `llm` также
  переведены на loopback — наружу остаются только `80/443/7881` (+ UDP `54000-54100`).
- Для `redis` включена обязательная аутентификация (`--requirepass`); пароль
  проброшен во всех потребителей (backend, worker, exporter, livekit, egress).

### Развёртывание и инфраструктура
- `nginx`/`coturn`/`livekit`: TLS на 443, http→https редирект, ACME-webroot;
  конфиги вынесены в `*.template` и рендерятся через `deploy/render-templates.sh`
  (`envsubst`). Секреты — только из `.env`, обязательны (`${VAR:?}`), без
  небезопасных дефолтов.
- `--env-file .env` добавлен во все вызовы `docker compose` (без него compose не
  подхватывал корневой `.env` и брал dev-дефолты — корень бага «конференции не
  работают у других» и `WARN: LIVEKIT_API_KEY not set`).
- Мониторинг развязан от опционального LLM: `job=llm`/алерт `LlmDown`
  закомментированы для профилей без `llm`/`llm-gpu`.
- `install.sh`: fail-fast валидация — установка останавливается с понятной
  ошибкой, если на боевом домене остались dev-плейсхолдеры (`example.com`,
  `ws://localhost`, `devkey` и т.п.), вместо тихого подъёма нерабочего прода.
- `install.sh`: флаг `--monitoring` (Grafana/Prometheus раньше не поднимались
  ни в одном пресете).
- `.env.example` покрывает все параметры деплоя; дефолтные профили —
  `media,monitoring`.

### Исправления
- `nginx`: `/openapi.json` больше не перехватывается SPA-фолбэком, а отдаётся
  схемой backend.
- Предупреждение в `.env.example`: `LIVEKIT_PUBLIC_URL` в HTTPS-проде обязан
  быть `wss://`.

### Документация
- Добавлено `docs/deploy/DEPLOYMENT.md` — полное руководство от голой Ubuntu 24
  до рабочего `https://<домен>`: SSL, `.env`, профили, траблшутинг, доступ к
  мониторингу через ssh-туннель, описание панелей, runbook включения
  транскрибации/суммаризации. Ссылки добавлены в `README.md`.
- `docs/deploy/install.md`: задокументирован флаг `--monitoring`.

### Примечания по эксплуатации
- На боевом сервере исправлен certbot deploy-hook (`nginx -s reload` → `restart`):
  без этого после автопродления сертификата nginx продолжал бы отдавать старый.
- TURN-раздача клиентам (`rtc.turn_servers` + TURN/TLS на 443) остаётся
  опциональной — кросс-сетевое медиа проверено вживую и работает без неё;
  нужна только для экстремального NAT (CGNAT/жёсткий корпоративный firewall).

## [0.0.1]

Первоначальная версия VidConf.

[0.0.10]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.10
[0.0.9]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.9
[0.0.8]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.8
[0.0.7]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.7
[0.0.6]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.6
[0.0.5]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.5
[0.0.4]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.4
[0.0.3]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.3
[0.0.2]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.2
[0.0.1]: https://github.com/MaxRonzhin/vidconf_rel/releases/tag/v0.0.1
