каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
"""Настройки. Нормативы ГОСТ — в миллисекундах и из конфига, не из кода."""
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
import platform
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
from functools import lru_cache
|
2026-09-26 18:12:27 +03:00
|
|
|
|
from typing import Literal
|
|
|
|
|
|
from urllib.parse import unquote, urlsplit
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
2026-09-17 13:43:15 +03:00
|
|
|
|
from pydantic import AliasChoices, Field
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
|
|
|
|
|
|
from app.domain.timers import NORMATIVES, TimerCode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
|
|
|
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
# `production` activates fail-closed checks for session signing and cookies.
|
|
|
|
|
|
app_env: Literal["development", "production"] = "development"
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
# Данные
|
|
|
|
|
|
database_url: str = "postgresql+asyncpg://lct:lct@localhost:5432/lct"
|
2026-09-26 18:12:27 +03:00
|
|
|
|
# HTTP auth and WebSocket handshakes both validate against PostgreSQL.
|
|
|
|
|
|
# Keep enough warm slots for concurrent classroom joins without tying up
|
|
|
|
|
|
# the whole server's PostgreSQL connection budget.
|
|
|
|
|
|
db_pool_size: int = Field(default=20, ge=1, le=100)
|
|
|
|
|
|
db_pool_max_overflow: int = Field(default=10, ge=0, le=100)
|
|
|
|
|
|
# Unique and stable per backend process/container. Cluster deployments
|
|
|
|
|
|
# should set this explicitly so a restart retains its session ownership.
|
|
|
|
|
|
backend_node_id: str = Field(
|
|
|
|
|
|
default_factory=lambda: platform.node() or "local",
|
|
|
|
|
|
min_length=1,
|
|
|
|
|
|
max_length=128,
|
|
|
|
|
|
)
|
2026-09-21 19:56:13 +03:00
|
|
|
|
# Только локальная демонстрация: живые занятия и отчёты в памяти, без
|
|
|
|
|
|
# Postgres и без долговременного журнала. Не включать на учебном стенде.
|
|
|
|
|
|
demo_no_db: bool = False
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
|
|
|
|
|
# Голосовой контур
|
|
|
|
|
|
models_dir: str = "models"
|
2026-09-24 01:10:49 +03:00
|
|
|
|
stt_model: str = "whisper-small"
|
|
|
|
|
|
# Локальный файл GGML Whisper small для whisper.cpp. Имя модели из сети
|
|
|
|
|
|
# не принимается: учебный контур не скачивает веса при запуске.
|
|
|
|
|
|
whisper_model_path: str = "models/whisper-small"
|
|
|
|
|
|
whisper_server_url: str = "http://127.0.0.1:18082"
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
endpointing_ms: int = 600
|
|
|
|
|
|
mic_sample_rate: int = 16_000
|
|
|
|
|
|
tts_sample_rate: int = 24_000
|
|
|
|
|
|
offline: bool = False
|
2026-09-24 01:10:49 +03:00
|
|
|
|
# STT/голос не входят в MVP ДДС; включаются лишь для отдельного эксперимента.
|
|
|
|
|
|
voice_enabled: bool = False
|
|
|
|
|
|
# Запись хранится локально и отдаётся только через авторизованный API.
|
|
|
|
|
|
record_calls: bool = True
|
|
|
|
|
|
recordings_dir: str = "recordings"
|
|
|
|
|
|
# Автокопирование полного стенда. Интервал по умолчанию ровно сутки;
|
|
|
|
|
|
# небольшой grace нужен только для отображения просрочки в мониторинге.
|
|
|
|
|
|
backup_interval_seconds: int = 86_400
|
|
|
|
|
|
backup_retry_seconds: int = 300
|
|
|
|
|
|
backup_keep: int = 14
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
|
|
|
|
|
# LLM. Провайдер меняется значением, не кодом (docs/arch/STACK.md).
|
2026-09-17 13:43:15 +03:00
|
|
|
|
# Имена COMPAT_MODEL_* принимаются тоже — так их выставляет командный сниппет.
|
2026-09-24 01:10:49 +03:00
|
|
|
|
llm_provider: str = "local"
|
2026-09-17 13:43:15 +03:00
|
|
|
|
llm_base_url: str = Field(
|
2026-09-24 01:10:49 +03:00
|
|
|
|
default="http://127.0.0.1:18080/v1",
|
|
|
|
|
|
validation_alias=AliasChoices("llm_base_url", "compat_model_url"),
|
2026-09-17 13:43:15 +03:00
|
|
|
|
)
|
|
|
|
|
|
llm_api_key: str = Field(
|
|
|
|
|
|
default="", validation_alias=AliasChoices("llm_api_key", "compat_model_api_key"),
|
|
|
|
|
|
repr=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
llm_model_caller: str = Field(
|
2026-09-24 01:10:49 +03:00
|
|
|
|
default="Qwen3-1.7B", validation_alias=AliasChoices("llm_model_caller", "compat_model_name")
|
2026-09-17 13:43:15 +03:00
|
|
|
|
)
|
|
|
|
|
|
llm_model_judge: str = Field(
|
|
|
|
|
|
default="", validation_alias=AliasChoices("llm_model_judge", "compat_model_name")
|
|
|
|
|
|
)
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
judge_temperature: float = 0.0
|
2026-09-24 01:10:49 +03:00
|
|
|
|
# Vikhr — отдельный русскоязычный контрольный профиль диалога. Никакая
|
|
|
|
|
|
# модель не решает, какие факты раскрыть, как маршрутизировать карточку или
|
|
|
|
|
|
# какой балл поставить: это делает протокол и оценка в коде.
|
|
|
|
|
|
llm_control_base_url: str = "http://127.0.0.1:18081/v1"
|
|
|
|
|
|
llm_model_control: str = "Vikhr-1B"
|
|
|
|
|
|
grammar_llm_enabled: bool = False
|
2026-09-26 18:12:27 +03:00
|
|
|
|
assessment_feedback_enabled: bool = True
|
2026-09-24 01:10:49 +03:00
|
|
|
|
dialogue_model_mode: str = "dialogue" # dialogue | russian_control
|
|
|
|
|
|
# Docker Desktop даёт контейнеру специальное имя хоста. Оно разрешается
|
|
|
|
|
|
# только явным флагом: обычный OFFLINE по-прежнему принимает лишь literal
|
|
|
|
|
|
# loopback и не может незаметно уйти во внешний API.
|
|
|
|
|
|
allow_docker_host_models: bool = False
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
feat: вход, роли и аудит действий (lct-23)
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
|
|
|
|
# Вход и роли (lct-23). `session_secret` обязателен на рабочем стенде:
|
|
|
|
|
|
# значение по умолчанию годится только для разработки, и администратор
|
|
|
|
|
|
# видит в состоянии сервисов, что оно не заменено.
|
|
|
|
|
|
session_secret: str = "dev-secret-поменять-на-стенде"
|
2026-09-24 01:10:49 +03:00
|
|
|
|
# В TLS-профиле cookie уходит только по HTTPS/WSS. Для нативной разработки
|
|
|
|
|
|
# на loopback флаг выключен, иначе браузер не сможет войти по HTTP.
|
|
|
|
|
|
secure_cookies: bool = False
|
feat: вход, роли и аудит действий (lct-23)
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
|
|
|
|
#: Вход без пароля для `make lesson` и тестов. На стенде выключен.
|
|
|
|
|
|
dev_auth_bypass: bool = False
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
# Необязательная интеграция с локальным AD/LDAP-каталогом. Пароль
|
|
|
|
|
|
# сервисной учётки никогда не показывается в диагностиках/config repr.
|
|
|
|
|
|
# LDAP без TLS намеренно не поддерживается: используйте ldaps:// либо
|
|
|
|
|
|
# ldap:// с обязательным StartTLS.
|
|
|
|
|
|
ldap_enabled: bool = False
|
|
|
|
|
|
ldap_url: str = ""
|
|
|
|
|
|
ldap_base_dn: str = ""
|
|
|
|
|
|
ldap_bind_dn: str = ""
|
|
|
|
|
|
ldap_bind_password: str = Field(default="", repr=False)
|
|
|
|
|
|
ldap_user_filter: str = "(objectClass=person)"
|
|
|
|
|
|
ldap_login_attribute: str = "sAMAccountName"
|
|
|
|
|
|
ldap_role_groups: dict[str, str] = {}
|
|
|
|
|
|
ldap_service_groups: dict[str, str] = {}
|
|
|
|
|
|
ldap_ca_certs_file: str = ""
|
|
|
|
|
|
ldap_connect_timeout_seconds: int = Field(default=5, ge=1, le=30)
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
# Нормативы: переопределяют значения по умолчанию из domain/timers.py
|
|
|
|
|
|
timer_limits_ms: dict[TimerCode, int] = {}
|
|
|
|
|
|
|
|
|
|
|
|
def limit_ms(self, code: TimerCode) -> int:
|
|
|
|
|
|
return self.timer_limits_ms.get(code, NORMATIVES[code].limit_ms)
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
def validate_deployment_security(self) -> None:
|
|
|
|
|
|
"""Reject known development authentication defaults on a production app."""
|
|
|
|
|
|
if self.app_env != "production":
|
|
|
|
|
|
return
|
|
|
|
|
|
problems: list[str] = []
|
|
|
|
|
|
if self.demo_no_db:
|
|
|
|
|
|
problems.append("DEMO_NO_DB must be disabled")
|
|
|
|
|
|
if self.dev_auth_bypass:
|
|
|
|
|
|
problems.append("DEV_AUTH_BYPASS must be disabled")
|
|
|
|
|
|
if not self.offline:
|
|
|
|
|
|
problems.append("OFFLINE must be enabled for the local training deployment")
|
|
|
|
|
|
if self.llm_provider != "local":
|
|
|
|
|
|
problems.append("LLM_PROVIDER must be local for the local training deployment")
|
|
|
|
|
|
if self.session_secret == "dev-secret-поменять-на-стенде" or len(self.session_secret) < 32:
|
|
|
|
|
|
problems.append("SESSION_SECRET must be a unique value of at least 32 characters")
|
|
|
|
|
|
database_password = unquote(urlsplit(self.database_url).password or "")
|
|
|
|
|
|
if (len(database_password) < 32
|
|
|
|
|
|
or any(char not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._~-"
|
|
|
|
|
|
for char in database_password)):
|
|
|
|
|
|
problems.append("PostgreSQL password must contain at least 32 URL-safe characters")
|
|
|
|
|
|
if not self.secure_cookies:
|
|
|
|
|
|
problems.append("SECURE_COOKIES must be enabled (HTTPS/WSS required)")
|
|
|
|
|
|
if problems:
|
|
|
|
|
|
raise ValueError("unsafe production security configuration: " + "; ".join(problems))
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
|
|
|
|
|
@lru_cache
|
|
|
|
|
|
def get_settings() -> Settings:
|
|
|
|
|
|
return Settings()
|