Files
vidconf/backend/schemas/admin.py
Max Ronzhin 173d384f06
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
feat(admin): рычаги нагрузки медиа — потолок качества публикации и лимит плиток
instance_settings.media_limits (publish_quality_cap: off/720p/360p/180p,
stage_max_tiles: 4/9/16/25) — новая вкладка «Нагрузка» в админке, дефолты
(off/25) сохраняют текущее поведение существующих инсталляций.

Настройка отдаётся не только GET /admin/settings, но и в join-ответе
(JoinOut) — участнику нужно иметь её на руках ДО публикации трека, а
/admin/settings доступен только администратору.

Потолок качества применяется через RoomOptions.publishDefaults
(videoEncoding + videoSimulcastLayers на пресетах VideoPresets LiveKit) —
режет битрейт верхнего слоя симулкаста, реальное разрешение WebRTC
подстраивает сам. Лимит плиток — фильтрация STAGE_GRID_LAYOUTS по
columns*rows в StageGrid, лишние участники уходят на страницу пагинации
вместо подписки.

Значение приезжает в joinState вместе с токеном ДО первого рендера
LiveKitRoom (RoomPage не рендерит его, пока joinState не заполнен целиком),
поэтому смена настройки не переподключает уже вошедшего участника —
roomOptions пересчитывается по стабильной ссылке на joinState, которая
после подключения не меняется.

Проверено вживую на локальном стенде (docker compose --profile media):
сохранение/персист настроек, join отдаёт актуальные значения, уже
подключённый участник не разрывается при смене настройки в другом окне.
2026-08-02 19:53:23 +03:00

188 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pydantic-схемы админ-API (`/api/v1/admin/*`)."""
import uuid
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, ConfigDict, EmailStr, Field
from core.plugins.config import AiLevel, PublishQualityCap, StageMaxTiles, SummaryRecipientsMode
from schemas.conferences import ConferenceOut
from services.ai_levels import AiLevelStatus
class AdminConferenceOut(ConferenceOut):
"""Конференция в ответах админ-API — `ConferenceOut` + данные владельца.
`owner_name`/`owner_email` — `None` для конференции без владельца
(`conferences.owner_id IS NULL`, ADR-001) — колонка «Владелец» в таблице
админки (`frontend/src/api/admin.ts`).
"""
owner_name: str | None = None
owner_email: str | None = None
class AdminConferenceListOut(BaseModel):
"""Страница списка конференций (`GET /admin/conferences`)."""
items: list[AdminConferenceOut]
total: int
class AdminUserOut(BaseModel):
"""Пользователь в ответах админ-API (профиль + служебные поля модерации).
`avatar_url`/`team_name` заполняются явно роутером (не через
`from_attributes` — оба поля вычисляемые, не хранятся как атрибут ORM),
см. `api/admin.py::_to_admin_user_out`.
"""
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
email: str
name_user: str
role: str
is_blocked: bool
email_verified: bool
created_at: datetime
team_id: uuid.UUID | None = None
avatar_url: str | None = None
team_name: str | None = None
class AdminUserListOut(BaseModel):
"""Страница списка пользователей (`GET /admin/users`)."""
items: list[AdminUserOut]
total: int
class AdminUserCreateIn(BaseModel):
"""Тело создания пользователя администратором (`POST /admin/users`).
Та же политика пароля, что при самостоятельной регистрации
(`RegisterIn.password`, min 8 символов). `team_id` — опциональная
привязка к команде, в отличие от публичной регистрации не завязана на
настройку `registration_team_choice` (админ назначает команду всегда).
"""
name_user: str = Field(min_length=1, max_length=255)
email: EmailStr
password: str = Field(min_length=8)
team_id: uuid.UUID | None = None
class AdminUserUpdateIn(BaseModel):
"""Тело правки пользователя администратором — роль, блокировка, ФИО и/или команда.
`team_id` не различает «поле не передано» и «явный `null`» через
сравнение с `None` — роутер читает `model_fields_set`, чтобы явный сброс
команды (`{"team_id": null}`) отличался от отсутствия поля в запросе
(тот же паттерн, что `summary_recipients` в `schemas/conferences.py`).
`name_user` — та же правка, что доступна пользователю в своём профиле
(редактирование по тем же правилам).
"""
role: Literal["admin", "user"] | None = None
is_blocked: bool | None = None
team_id: uuid.UUID | None = None
name_user: str | None = Field(default=None, min_length=1, max_length=255)
class TeamOut(BaseModel):
"""Команда в ответах админ-API."""
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
created_at: datetime
class TeamListOut(BaseModel):
"""Список команд (`GET /admin/teams`), отсортирован по названию."""
items: list[TeamOut]
total: int
class TeamCreateIn(BaseModel):
"""Тело создания команды; `name` обрезается от пробелов до проверки длины."""
model_config = ConfigDict(str_strip_whitespace=True)
name: str = Field(min_length=1, max_length=255)
class TeamUpdateIn(BaseModel):
"""Тело переименования команды; `name` обрезается от пробелов до проверки длины."""
model_config = ConfigDict(str_strip_whitespace=True)
name: str = Field(min_length=1, max_length=255)
class InvitationsSendIn(BaseModel):
"""Тело ручной рассылки .ics-приглашений (`POST /admin/conferences/{id}/invitations`).
Пустой список получателей не отличается от отсутствующего поля — оба
трактуются как «получатели по умолчанию» (см. `workers/tasks/invitations.py`).
"""
emails: list[EmailStr] | None = None
class SettingsOut(BaseModel):
"""Эффективные настройки инстанса для отображения в админке.
`transcription_queue_served` — обслуживается ли очередь `transcription`
хотя бы одним воркером Celery
прямо сейчас (`services.pipeline_producer.transcription_queue_served`);
`False` при включённой транскрибации — сигнал админке показать
предупреждение рядом с чекбоксом AI («модуль включён, но задачи некому
обрабатывать»), не связано с доступностью уровня AI (`ai_levels`,
которая смотрит только на железо/скачанные модели).
"""
chat_enabled: bool
transcription_enabled: bool
ai_level: AiLevel
ai_levels: list[AiLevelStatus]
transcription_queue_served: bool
summary_recipients: SummaryRecipientsMode
display_timezone: str
registration_team_choice: bool
registration_email_domain_enabled: bool
registration_email_domains: list[str] = Field(default_factory=list)
contact_email_enabled: bool
contact_email: str | None = None
# Рычаги нагрузки медиа (`InstanceConfig.media_limits`) — потолок
# качества публикации и максимум плиток сцены, см. `core/plugins/config.py`.
publish_quality_cap: PublishQualityCap
stage_max_tiles: StageMaxTiles
class TestEmailIn(BaseModel):
"""Тело запроса тестовой отправки (`POST /admin/settings/test-email`).
`to` не задан — получатель по умолчанию: email текущего администратора
(см. `api/admin.py::send_test_email`).
"""
to: EmailStr | None = None
class TestEmailOut(BaseModel):
"""Результат тестовой отправки — понятный админу успех/ошибка транспорта.
`smtp_host`/`smtp_port` — только хост и порт (без логина/пароля, они не
покидают `.env`), заполняются лишь при `EMAIL_BACKEND=smtp`.
"""
success: bool
message: str
smtp_host: str | None = None
smtp_port: int | None = None