71 KiB
api-copp
Платформа ЦОПП Челябинской области для проведения профориентационных тестирований. Включает REST API на FastAPI и пользовательский интерфейс на Flet. Поддерживает тесты Голланда, Климова и Гломштока с паутинными диаграммами результатов.
Содержание
- Архитектура
- Быстрый старт
- Переменные окружения
- Структура проекта
- База данных
- API
- Тест Голланда
- Паутинная диаграмма
- Фронтенд
- Дизайн-система
- Деплой с внешним прокси
- Известные ошибки и просчёты
Архитектура
Пользователь (браузер)
│
│ :80
[ Внешний прокси / nginx ] ← необязателен для локальной сети
│
│ :80
[ web — Flet ASGI ] ← единственный публичный сервис
│
│ http://api:8000 (только внутри Docker-сети)
[ api — FastAPI ]
│ │
[ PostgreSQL :5432 ] [ Redis :6379 ]
Важно: сервис api не публикует порты наружу — он доступен только контейнеру web внутри Docker-сети. Пользователь никогда не обращается к API напрямую.
Сетевая топология Docker: PostgreSQL и Redis запускаются отдельным compose-файлом docker-compose_db.yml и подключаются к той же Docker network.
Быстрый старт
1. Инфраструктура (PostgreSQL + Redis)
docker compose -f docker-compose_db.yml up -d
2. Приложение (API + Web)
docker compose up -d --build
После запуска:
| Адрес | Что |
|---|---|
http://localhost |
Веб-интерфейс (Flet) |
http://localhost:8000/docs |
Swagger UI — только с хоста сервера, порт не опубликован |
http://localhost:8000/redoc |
ReDoc |
В продакшне API недоступен снаружи. Для работы с
/docsиспользуйте SSH-туннель:ssh -L 8000:localhost:8000 user@your-server
Доступ к документации — логин/пароль из переменных DOCS_USERNAME / DOCS_PASSWORD.
3. Инициализация БД
После первого запуска выполните (с хоста сервера или через туннель):
# Создать таблицы
curl -X POST http://localhost:8000/db/create-tables \
-H "X-Admin-Key: <ADMIN_KEY>"
# Применить миграции (добавляет колонки, если БД уже была создана ранее)
curl -X POST http://localhost:8000/db/migrate \
-H "X-Admin-Key: <ADMIN_KEY>"
4. Загрузка теста Голланда
# Создать тест (возвращает poll_id)
curl -X POST http://localhost:8000/polls/holland \
-H "X-Admin-Key: <ADMIN_KEY>"
# Загрузить ключ расшифровки (42 пары → 6 типов × 14 баллов)
curl -X POST http://localhost:8000/polls/holland/<poll_id>/seed-scores \
-H "X-Admin-Key: <ADMIN_KEY>"
Переменные окружения
Важно: перед деплоем в продакшн обязательно смените
SECRET_KEY,ADMIN_KEY,DOCS_PASSWORDи пароли БД.
| Переменная | Описание | Пример |
|---|---|---|
APP_PATH |
Абсолютный путь к директории приложения внутри контейнера | /app |
DB_HOST |
Хост PostgreSQL | postgres |
DB_PORT |
Порт PostgreSQL | 5432 |
DB_USER |
Пользователь БД | postgres |
DB_PASS |
Пароль БД | postgres |
DB_NAME |
Имя базы данных | profi |
REDIS_HOST |
Хост Redis | redis |
REDIS_PORT |
Порт Redis | 6379 |
REDIS_DB |
Номер БД Redis | 0 |
REDIS_PASSWORD |
Пароль Redis | — |
SECRET_KEY |
Секрет для подписи JWT-токенов (min 32 символа) | python -c "import secrets; print(secrets.token_hex(32))" |
ADMIN_KEY |
Ключ для защищённых admin-эндпоинтов | — |
DOCS_USERNAME |
Логин для доступа к /docs и /redoc |
admin |
DOCS_PASSWORD |
Пароль для доступа к /docs и /redoc |
— |
API_URL |
Внутренний URL API для фронтенда (только Docker-сеть) | http://api:8000 |
Файл docker-compose.yml содержит демо-значения — не используйте их в продакшн.
Структура проекта
api-copp/
├── main.py # точка входа FastAPI, регистрация роутеров
├── requirements.txt # зависимости API
├── pyproject.toml # метаданные проекта
├── Dockerfile.api # образ API (python:3.14-slim)
├── Dockerfile.web # образ фронтенда (python:3.11-slim + flet), порт 80
├── docker-compose.yml # API (без портов) + Web (порт 80)
├── docker-compose_db.yml # PostgreSQL + Redis
├── nginx.conf # пример конфига nginx (для внешнего прокси)
│
├── bd/ # слой доступа к данным
│ ├── __init__.py # Settings (pydantic), make_engine()
│ └── tables/
│ ├── simple_base.py # общий Base для SQLAlchemy
│ ├── users.py # User
│ ├── organization.py # Organization
│ ├── group.py # Group
│ ├── poll.py # Poll
│ ├── question.py # Question
│ ├── choice.py # Choice
│ ├── response.py # Response, Answer
│ ├── scale.py # ScaleDimension, ChoiceScore
│ └── radar_result.py # RadarResult, RadarResultItem
│
├── route/ # FastAPI-роутеры
│ ├── auth_utils.py # JWT, bcrypt, require_admin_key
│ ├── auth.py # /auth/register, /auth/login
│ ├── base.py # /health, /version
│ ├── init_data_base.py # /db/create-tables, /db/migrate
│ ├── users_crud.py # CRUD пользователей
│ ├── groups_crud.py # CRUD групп
│ ├── organizations_crud.py# CRUD организаций
│ ├── poll_crud.py # CRUD опросов
│ ├── question_crud.py # CRUD вопросов
│ ├── choice_crud.py # CRUD вариантов ответов
│ ├── response_crud.py # приём ответов + авто-расчёт радара
│ ├── holland_crud.py # генерация теста Голланда + seed-scores
│ ├── glomshtok_crud.py # генерация теста Гломштока
│ ├── klimov_crud.py # генерация теста Климова
│ ├── scale_crud.py # CRUD осей и расшифровок
│ ├── radar_crud.py # расчёт и чтение радарных результатов
│ └── radar_svg_gen.py # серверная генерация SVG-диаграмм
│
├── web/ # Flet-фронтенд
│ ├── main.py # точка входа Flet ASGI + применение темы
│ ├── router.py # маршрутизация страниц
│ ├── api_client.py # httpx-обёртки над API (API_URL из env)
│ ├── designer.py # дизайн-система: colors, компоненты
│ ├── theme_manager.py # менеджер тем, сохранение в client_storage
│ ├── radar_chart.py # виджет RadarChart (flet.canvas)
│ ├── radar_svg.py # генератор SVG (устаревший, не используется)
│ └── views/
│ ├── login_view.py
│ ├── reg_view.py
│ ├── user_view.py
│ ├── profile_view.py
│ ├── poll_view.py
│ ├── response_view.py
│ ├── my_tests_view.py
│ ├── take_test_view.py
│ └── content_users/
│ ├── main_page.py # главная: «Все тесты» / «Мои тесты»
│ ├── poll_page.py # прохождение теста
│ ├── response_page.py # результат с паутинной диаграммой
│ ├── my_tests_page.py # список пройденных тестов
│ ├── take_test_page.py # список доступных тестов
│ └── profile_page.py # редактирование профиля + выбор темы
│
└── data/ # смонтированный том Docker (./data:/app/data)
├── postgres/ # данные PostgreSQL
└── radar/ # сохранённые SVG-файлы диаграмм
База данных
Схема таблиц
organizations ──< groups ──< users
│
polls ──< questions ──< choices ──< choice_scores
│ │
└──< responses ──< answers │
│ │
└──< radar_results ──< radar_result_items
(image_path) └── (dimension_id FK → scale_dimensions)
↑
polls ──< scale_dimensions ──────────────────-─┘
Ключевые таблицы
| Таблица | Описание |
|---|---|
users |
Испытуемые: username, hashed_password, first_name, last_name, FK на groups и organizations |
polls |
Тест/опрос: title, description |
questions |
Вопросы теста с type (single / multiple) |
choices |
Варианты ответов к вопросам |
responses |
Один факт прохождения теста (user + poll + timestamp) |
answers |
Конкретные ответы внутри прохождения |
scale_dimensions |
Оси радарной диаграммы: name, color, position, FK на polls |
choice_scores |
Расшифровка: какой вариант ответа сколько баллов вносит в какую ось |
radar_results |
Итоговый результат по осям + image_path (путь к SVG-файлу) |
radar_result_items |
Снимок значений по каждой оси на момент расчёта |
Миграции
Таблицы создаются через POST /db/create-tables. Для уже существующих БД применяются дополнительные ALTER-миграции через POST /db/migrate. Текущие миграции:
- Добавление колонок
username/hashed_passwordвusers - Добавление колонки
image_pathвradar_results
API
Аутентификация
POST /auth/register — регистрация (username ≥ 3 симв., password ≥ 8 симв.)
POST /auth/login — получить JWT-токен (OAuth2 password flow)
JWT-токен передаётся заголовком Authorization: Bearer <token>. Admin-операции требуют заголовок X-Admin-Key.
Основные эндпоинты
# Тесты
GET /polls — список тестов
POST /polls — создать тест
GET /polls/{id} — тест с вопросами и вариантами
DELETE /polls/{id} — удалить тест
# Вопросы / Варианты ответов
POST /polls/{id}/questions
POST /questions/{id}/choices
# Прохождение
POST /polls/{poll_id}/responses — сдать тест (автоматически считает радар)
GET /polls/users/{user_id}/responses — история пользователя
GET /responses/{id} — конкретный результат
# Результаты (радар)
POST /responses/{id}/radar/compute — (пере)считать радарный результат
GET /responses/{id}/radar — данные для диаграммы
GET /responses/{id}/radar/image — SVG-файл диаграммы
GET /users/{user_id}/radar-results — история радарных результатов
# Оси и расшифровки
POST /polls/{poll_id}/dimensions — создать ось
GET /polls/{poll_id}/dimensions — список осей
PUT /dimensions/{id}
DELETE /dimensions/{id}
POST /dimensions/{id}/scores — задать расшифровку варианта ответа
GET /choices/{choice_id}/scores
# Готовые тесты (admin)
POST /polls/holland — создать тест Голланда (42 пары)
POST /polls/holland/{poll_id}/seed-scores — загрузить ключ расшифровки Голланда
POST /polls/glomshtok — создать тест Гломштока
POST /polls/klimov — создать тест Климова
# База данных (admin)
POST /db/create-tables
POST /db/migrate
# Служебные
GET /health
GET /version
Тест Голланда
Реализована модификация Г.В. Резапкиной теста профессиональной ориентации Дж. Голланда. 42 пары профессий, испытуемый выбирает предпочтительную в каждой паре.
Типы личности
| Код | Название | Цвет |
|---|---|---|
| Р | Реалистичный | #FF8C00 |
| И | Интеллектуальный | #4169E1 |
| С | Социальный | #32CD32 |
| К | Конвенциональный | #9370DB |
| П | Предприимчивый | #DC143C |
| А | Артистический | #FF69B4 |
Каждый тип получает ровно 14 вопросов (= 14 максимально возможных баллов). Результат — паутинная диаграмма с 6 осями.
Процедура загрузки
POST /polls/holland— создаёт тест с 42 вопросами-парамиPOST /polls/holland/{poll_id}/seed-scores— создаёт 6ScaleDimensionи 84ChoiceScore
Паутинная диаграмма
Как работает
При сдаче теста (POST /polls/{id}/responses) сервер автоматически:
- Суммирует баллы по каждой оси (
ChoiceScore.score) для выбранных вариантов - Сохраняет результат в
radar_results+radar_result_items - Генерирует SVG-файл и сохраняет в
{APP_PATH}/data/radar/{result_id}.svg - Записывает относительный путь в
image_path— всё в одномcommit()
Скачивание SVG
Фронтенд получает SVG-байты с API через внутреннюю Docker-сеть (api_client.get_radar_svg_bytes()), кодирует в data:application/octet-stream;base64,... и передаёт в page.launch_url(). Браузер скачивает файл напрямую из data URI — никаких запросов к API из браузера.
Генерация SVG
SVG генерируется чистым Python без зависимостей (route/radar_svg_gen.py):
- 5 концентрических сеток
- Подписи с именем типа и баллом
- Размер холста: 520×520 пикселей
Фронтенд
Написан на Flet — Python-фреймворке поверх Flutter. Работает как ASGI-приложение через uvicorn на порту 80.
Маршруты
| Путь | Страница |
|---|---|
/login |
Вход |
/reg |
Регистрация |
/user/{url_key} |
Главная страница пользователя |
/take_test/{url_key} |
Список тестов для прохождения |
/poll/{url_key}/{poll_id} |
Прохождение теста |
/response/{url_key}/{response_id} |
Результат с диаграммой |
/my_tests/{url_key} |
Мои результаты |
/profile/{url_key} |
Профиль пользователя + выбор темы |
{url_key} — одноразовый 32-символьный hex-ключ, генерируется при успешном входе (secrets.token_hex(16)). Обращение к защищённому маршруту с чужим ключом перенаправляет на /login.
Варианты ответов в тесте
Вместо стандартного ft.RadioGroup / ft.Radio (лейбл которого не переносится) используются кастомные кликабельные контейнеры (poll_page.py). При выборе варианта:
- фон меняется с
surface_alt→primary_light - иконка меняется с
RADIO_BUTTON_UNCHECKED→CHECK_CIRCLE
Дизайн-система
Все UI-компоненты сосредоточены в web/designer.py. Управление темой — в web/theme_manager.py.
Глобальный масштаб
SCALE: float = 1.7 # 1.0 = 100%, меняйте только эту константу
s(14) # → round(14 * 1.7) = 24px
Цветовая палитра (стиль copp74.ru)
| Имя | HEX | Применение |
|---|---|---|
colors.primary |
#F85A40 |
Кнопки, акценты, логотип |
colors.primary_dark |
#D94530 |
Hover-состояние кнопок |
colors.primary_light |
#FFEAB7 |
Выбранный вариант ответа |
colors.background |
#F4F6F8 |
Фон страниц |
colors.surface |
#FFFFFF |
Карточки, формы |
colors.surface_alt |
#F0F2F5 |
Невыбранные варианты, значки |
colors.accent |
#FFC845 |
Жёлтый акцент |
colors.info |
#007FBD |
Синие кнопки скачивания |
colors.success |
#85C446 |
Зелёный (успех) |
colors.text_primary |
#1E2A3A |
Основной текст |
colors.text_secondary |
#5A6779 |
Вторичный текст, плейсхолдеры |
colors.text_on_primary |
#FFFFFF |
Текст на оранжевых кнопках |
colors.border |
#DDE3EA |
Границы полей |
Компоненты
| Класс | Наследует | Описание |
|---|---|---|
CastomText |
ft.Text |
Текст с масштабированием |
CastomTextField_input |
ft.TextField |
Поле ввода, белый фон, оранжевый фокус |
CastomButton |
ft.Button |
Оранжевая кнопка с белым текстом |
CastomIconButton |
ft.Button |
Синяя кнопка с иконкой |
CastomDropdown |
ft.Dropdown |
Выпадающий список |
CastomSwitch |
ft.Switch |
Переключатель |
AlterDiaalogApproval |
ft.AlertDialog |
Диалог пользовательского соглашения |
Темы интерфейса
Пользователь может выбрать тему в профиле. Тема сохраняется в client_storage браузера и восстанавливается при следующем входе.
| ID | Название | Фон |
|---|---|---|
default |
ЦОПП (orange) | #F4F6F8 |
light |
Светлая | #FFFFFF |
dark_blue |
Синяя | #E8F4FD |
green |
Зелёная | #EDF7E7 |
Деплой с внешним прокси
В репозитории есть готовый файл nginx.conf для случая, когда nginx развёрнут на отдельном сервере или уже управляет несколькими сайтами.
Минимальная конфигурация upstream в вашем nginx:
location / {
proxy_pass http://<server-ip>:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
Заголовки Upgrade / Connection обязательны — Flet использует WebSocket.
Известные ошибки и просчёты
1. Лишнее дублирование генерации SVG
Проблема: SVG-генератор написан дважды — route/radar_svg_gen.py (сервер) и web/radar_svg.py (клиент).
Решение: Кнопка «Сохранить» скачивает SVG через api_client.get_radar_svg_bytes(). web/radar_svg.py больше не используется, но файл остался — можно удалить при следующем рефакторинге.
2. Несовместимость Python 3.14 и psycopg ✅ исправлено
Проблема: pyproject.toml указывал psycopg>=3.3.3, но реально используется синхронный pg8000.
Решение: Зависимость заменена на pg8000>=1.29.0.
3. Нет транзакции при сохранении SVG ✅ исправлено
Проблема: image_path обновлялся отдельным commit() после основного — при падении между ними запись оставалась без пути к файлу.
Решение: SVG генерируется до коммита, image_path устанавливается до коммита. Один атомарный session.commit().
4. Перенос подписей в диаграмме по буквам ✅ исправлено
Решение: _W_LBL увеличен до 130, добавлен max_lines=2, no_wrap=False.
5. Текст вариантов ответов обрезался ✅ исправлено
Проблема: ft.Radio(label=...) в Flet не переносит текст метки — длинные варианты ответов обрезались.
Решение: Реализованы кастомные кликабельные контейнеры с ft.Text(expand=True, no_wrap=False).
6. Ошибка module 'designer' has no attribute 'clolors' ✅ исправлено
Проблема: Класс цветов был назван clolors с опечаткой. После переименования в colors все ссылки в 10+ файлах указывали на старое имя.
Решение: Класс переименован в colors, все ссылки во всех view-файлах обновлены.
Содержание
- Архитектура
- Быстрый старт
- Переменные окружения
- Структура проекта
- База данных
- API
- Тест Голланда
- Паутинная диаграмма
- Фронтенд
- Деплой с внешним прокси
- Известные ошибки и просчёты
Архитектура
Пользователь (браузер)
│
│ :80
[ Внешний прокси / nginx ] ← необязателен для локальной сети
│
│ :80
[ web — Flet ASGI ] ← единственный публичный сервис
│
│ http://api:8000 (только внутри Docker-сети)
[ api — FastAPI ]
│ │
[ PostgreSQL :5432 ] [ Redis :6379 ]
Важно: сервис api не публикует порты наружу — он доступен только контейнеру web внутри Docker-сети. Пользователь никогда не обращается к API напрямую.
Сетевая топология Docker: PostgreSQL и Redis запускаются отдельным compose-файлом docker-compose_db.yml и подключаются к той же Docker network.
Быстрый старт
1. Инфраструктура (PostgreSQL + Redis)
docker compose -f docker-compose_db.yml up -d
2. Приложение (API + Web)
docker compose up -d --build
После запуска:
| Адрес | Что |
|---|---|
http://localhost |
Веб-интерфейс (Flet) |
http://localhost:8000/docs |
Swagger UI — только с хоста сервера, порт не опубликован |
http://localhost:8000/redoc |
ReDoc |
В продакшне API недоступен снаружи. Для работы с
/docsиспользуйте SSH-туннель:ssh -L 8000:localhost:8000 user@your-server
Доступ к документации — логин/пароль из переменных DOCS_USERNAME / DOCS_PASSWORD.
3. Инициализация БД
После первого запуска выполните (с хоста сервера или через туннель):
# Создать таблицы
curl -X POST http://localhost:8000/db/create-tables \
-H "X-Admin-Key: <ADMIN_KEY>"
# Применить миграции (добавляет колонки, если БД уже была создана ранее)
curl -X POST http://localhost:8000/db/migrate \
-H "X-Admin-Key: <ADMIN_KEY>"
4. Загрузка теста Голланда
# Создать тест (возвращает poll_id)
curl -X POST http://localhost:8000/polls/holland \
-H "X-Admin-Key: <ADMIN_KEY>"
# Загрузить ключ расшифровки (42 пары → 6 типов × 14 баллов)
curl -X POST http://localhost:8000/polls/holland/<poll_id>/seed-scores \
-H "X-Admin-Key: <ADMIN_KEY>"
Переменные окружения
Важно: перед деплоем в продакшн обязательно смените
SECRET_KEY,ADMIN_KEY,DOCS_PASSWORDи пароли БД.
| Переменная | Описание | Пример |
|---|---|---|
APP_PATH |
Абсолютный путь к директории приложения внутри контейнера | /app |
DB_HOST |
Хост PostgreSQL | postgres |
DB_PORT |
Порт PostgreSQL | 5432 |
DB_USER |
Пользователь БД | postgres |
DB_PASS |
Пароль БД | postgres |
DB_NAME |
Имя базы данных | profi |
REDIS_HOST |
Хост Redis | redis |
REDIS_PORT |
Порт Redis | 6379 |
REDIS_DB |
Номер БД Redis | 0 |
REDIS_PASSWORD |
Пароль Redis | — |
SECRET_KEY |
Секрет для подписи JWT-токенов (min 32 символа) | python -c "import secrets; print(secrets.token_hex(32))" |
ADMIN_KEY |
Ключ для защищённых admin-эндпоинтов | — |
DOCS_USERNAME |
Логин для доступа к /docs и /redoc |
admin |
DOCS_PASSWORD |
Пароль для доступа к /docs и /redoc |
— |
API_URL |
Внутренний URL API для фронтенда (только Docker-сеть) | http://api:8000 |
Файл docker-compose.yml содержит демо-значения — не используйте их в продакшн.
Структура проекта
api-copp/
├── main.py # точка входа FastAPI, регистрация роутеров
├── requirements.txt # зависимости API
├── pyproject.toml # метаданные проекта
├── Dockerfile.api # образ API (python:3.14-slim)
├── Dockerfile.web # образ фронтенда (python:3.11-slim + flet), порт 80
├── docker-compose.yml # API (без портов) + Web (порт 80)
├── docker-compose_db.yml # PostgreSQL + Redis
├── nginx.conf # пример конфига nginx (для внешнего прокси)
│
├── bd/ # слой доступа к данным
│ ├── __init__.py # Settings (pydantic), make_engine()
│ └── tables/
│ ├── simple_base.py # общий Base для SQLAlchemy
│ ├── users.py # User
│ ├── organization.py # Organization
│ ├── group.py # Group
│ ├── poll.py # Poll
│ ├── question.py # Question
│ ├── choice.py # Choice
│ ├── response.py # Response, Answer
│ ├── scale.py # ScaleDimension, ChoiceScore
│ └── radar_result.py # RadarResult, RadarResultItem
│
├── route/ # FastAPI-роутеры
│ ├── auth_utils.py # JWT, bcrypt, require_admin_key
│ ├── auth.py # /auth/register, /auth/login
│ ├── base.py # /health, /version
│ ├── init_data_base.py # /db/create-tables, /db/migrate
│ ├── users_crud.py # CRUD пользователей
│ ├── groups_crud.py # CRUD групп
│ ├── organizations_crud.py# CRUD организаций
│ ├── poll_crud.py # CRUD опросов
│ ├── question_crud.py # CRUD вопросов
│ ├── choice_crud.py # CRUD вариантов ответов
│ ├── response_crud.py # приём ответов + авто-расчёт радара
│ ├── holland_crud.py # генерация теста Голланда + seed-scores
│ ├── scale_crud.py # CRUD осей и расшифровок
│ ├── radar_crud.py # расчёт и чтение радарных результатов
│ └── radar_svg_gen.py # серверная генерация SVG-диаграмм
│
├── web/ # Flet-фронтенд
│ ├── main.py # точка входа Flet ASGI
│ ├── router.py # маршрутизация страниц
│ ├── api_client.py # httpx-обёртки над API (API_URL из env)
│ ├── designer.py # компоненты дизайн-системы
│ ├── radar_chart.py # виджет RadarChart (flet.canvas)
│ ├── radar_svg.py # генератор SVG-data-URI (на клиенте, не используется)
│ └── views/
│ ├── login_view.py
│ ├── reg_view.py
│ ├── user_view.py
│ ├── profile_view.py
│ ├── poll_view.py
│ ├── response_view.py
│ ├── my_tests_view.py
│ ├── take_test_view.py
│ └── content_users/
│ ├── main_page.py # главная: «Все тесты» / «Мои тесты»
│ ├── poll_page.py # прохождение теста (кастомные кнопки выбора)
│ ├── response_page.py # результат с паутинной диаграммой
│ ├── my_tests_page.py # список пройденных тестов
│ ├── take_test_page.py # список доступных тестов
│ └── profile_page.py # редактирование профиля
│
└── data/ # смонтированный том Docker (./data:/app/data)
├── postgres/ # данные PostgreSQL
└── radar/ # сохранённые SVG-файлы диаграмм
База данных
Схема таблиц
organizations ──< groups ──< users
│
polls ──< questions ──< choices ──< choice_scores
│ │
└──< responses ──< answers │
│ │
└──< radar_results ──< radar_result_items
(image_path) └── (dimension_id FK → scale_dimensions)
↑
polls ──< scale_dimensions ──────────────────-─┘
Ключевые таблицы
| Таблица | Описание |
|---|---|
users |
Испытуемые: username, hashed_password, first_name, last_name, FK на groups и organizations |
polls |
Тест/опрос: title, description |
questions |
Вопросы теста с type (single / multiple) |
choices |
Варианты ответов к вопросам |
responses |
Один факт прохождения теста (user + poll + timestamp) |
answers |
Конкретные ответы внутри прохождения |
scale_dimensions |
Оси радарной диаграммы: name, color, position, FK на polls |
choice_scores |
Расшифровка: какой вариант ответа сколько баллов вносит в какую ось |
radar_results |
Итоговый результат по осям + image_path (путь к SVG-файлу) |
radar_result_items |
Снимок значений по каждой оси на момент расчёта |
Миграции
Таблицы создаются через POST /db/create-tables. Для уже существующих БД применяются дополнительные ALTER-миграции через POST /db/migrate. Текущие миграции:
- Добавление колонок
username/hashed_passwordвusers - Добавление колонки
image_pathвradar_results
API
Аутентификация
POST /auth/register — регистрация (username ≥ 3 симв., password ≥ 8 симв.)
POST /auth/login — получить JWT-токен (OAuth2 password flow)
JWT-токен передаётся заголовком Authorization: Bearer <token>. Admin-операции требуют заголовок X-Admin-Key.
Основные эндпоинты
# Тесты
GET /polls — список тестов
POST /polls — создать тест
GET /polls/{id} — тест с вопросами и вариантами
DELETE /polls/{id} — удалить тест
# Вопросы / Варианты ответов
POST /polls/{id}/questions
POST /questions/{id}/choices
# Прохождение
POST /polls/{poll_id}/responses — сдать тест (автоматически считает радар)
GET /polls/users/{user_id}/responses — история пользователя
GET /responses/{id} — конкретный результат
# Результаты (радар)
POST /responses/{id}/radar/compute — (пере)считать радарный результат
GET /responses/{id}/radar — данные для диаграммы
GET /responses/{id}/radar/image — SVG-файл диаграммы
GET /users/{user_id}/radar-results — история радарных результатов
# Оси и расшифровки
POST /polls/{poll_id}/dimensions — создать ось
GET /polls/{poll_id}/dimensions — список осей
PUT /dimensions/{id}
DELETE /dimensions/{id}
POST /dimensions/{id}/scores — задать расшифровку варианта ответа
GET /choices/{choice_id}/scores
# Тест Голланда
POST /polls/holland — создать тест с 42 парами
POST /polls/holland/{poll_id}/seed-scores — загрузить ключ расшифровки
# База данных (admin)
POST /db/create-tables
POST /db/migrate
# Служебные
GET /health
GET /version
Тест Голланда
Реализована модификация Г.В. Резапкиной теста профессиональной ориентации Дж. Голланда. 42 пары профессий, испытуемый выбирает предпочтительную в каждой паре.
Типы личности
| Код | Название | Цвет |
|---|---|---|
| Р | Реалистичный | #FF8C00 |
| И | Интеллектуальный | #4169E1 |
| С | Социальный | #32CD32 |
| К | Конвенциональный | #9370DB |
| П | Предприимчивый | #DC143C |
| А | Артистический | #FF69B4 |
Каждый тип получает ровно 14 вопросов (= 14 максимально возможных баллов). Результат — паутинная диаграмма с 6 осями.
Процедура загрузки
POST /polls/holland— создаёт тест с 42 вопросами-парамиPOST /polls/holland/{poll_id}/seed-scores— создаёт 6ScaleDimensionи 84ChoiceScore
Паутинная диаграмма
Как работает
При сдаче теста (POST /polls/{id}/responses) сервер автоматически:
- Суммирует баллы по каждой оси (
ChoiceScore.score) для выбранных вариантов - Сохраняет результат в
radar_results+radar_result_items - Генерирует SVG-файл и сохраняет в
{APP_PATH}/data/radar/{result_id}.svg - Записывает относительный путь в
image_path— всё в одномcommit()
Скачивание SVG
Фронтенд получает SVG-байты с API через внутреннюю Docker-сеть (api_client.get_radar_svg_bytes()), кодирует в data:application/octet-stream;base64,... и передаёт в page.launch_url(). Браузер скачивает файл напрямую из data URI — никаких запросов к API из браузера, никакой зависимости от публичности API.
Генерация SVG
SVG генерируется чистым Python без зависимостей (route/radar_svg_gen.py):
- Фон
#2F184B, полигонrgba(155,114,207,0.55) - 5 концентрических сеток
- Подписи с именем типа и баллом
- Размер холста: 520×520 пикселей
Фронтенд
Написан на Flet — Python-фреймворке поверх Flutter. Работает как ASGI-приложение через uvicorn на порту 80.
Адаптация под мобильные устройства
Интерфейс адаптирован для экранов с малым горизонтальным разрешением:
- Формы входа и регистрации растягиваются на всю ширину экрана (без фиксированной ширины
360px) - Карточки навигации («Все тесты» / «Мои тесты») переносятся на следующую строку при нехватке места (
ft.Row(wrap=True)) - Варианты ответов в тесте реализованы кастомными контейнерами вместо
ft.Radio— текст длинных вариантов переносится на следующую строку - Заголовок страницы пользователя не выталкивает кнопку профиля
Варианты ответов в тесте
Вместо стандартного ft.RadioGroup / ft.Radio (лейбл которого не переносится) используются кастомные кликабельные контейнеры (poll_page.py: _build_choice_group). При выборе варианта:
- фон меняется с
laer3→laer1 - появляется белая рамка
- иконка меняется с
RADIO_BUTTON_UNCHECKED→CHECK_CIRCLE
Маршруты
| Путь | Страница |
|---|---|
/login |
Вход |
/reg |
Регистрация |
/user/{url_key} |
Главная страница пользователя |
/take_test/{url_key} |
Список тестов для прохождения |
/poll/{url_key}/{poll_id} |
Прохождение теста |
/response/{url_key}/{response_id} |
Результат с диаграммой |
/my_tests/{url_key} |
Мои результаты |
/profile/{url_key} |
Профиль пользователя |
{url_key} — одноразовый 32-символьный hex-ключ, генерируется при успешном входе (secrets.token_hex(16)). Обращение к защищённому маршруту с чужим ключом перенаправляет на /login.
Деплой с внешним прокси
В репозитории есть готовый файл nginx.conf для случая, когда nginx развёрнут на отдельном сервере или уже управляет несколькими сайтами.
Минимальная конфигурация upstream в вашем nginx:
location / {
proxy_pass http://<server-ip>:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
Заголовки Upgrade / Connection обязательны — Flet использует WebSocket.
Известные ошибки и просчёты
1. Лишнее дублирование генерации SVG
Проблема: SVG-генератор написан дважды — route/radar_svg_gen.py (сервер) и web/radar_svg.py (клиент).
Решение: Кнопка «Сохранить» скачивает SVG через api_client.get_radar_svg_bytes(). web/radar_svg.py больше не используется, но файл остался — можно удалить при следующем рефакторинге.
2. Несовместимость Python 3.14 и psycopg ✅ исправлено
Проблема: pyproject.toml указывал psycopg>=3.3.3, но реально используется синхронный pg8000.
Решение: Зависимость заменена на pg8000>=1.29.0.
3. Нет транзакции при сохранении SVG ✅ исправлено
Проблема: image_path обновлялся отдельным commit() после основного — при падении между ними запись оставалась без пути к файлу.
Решение: SVG генерируется до коммита, image_path устанавливается до коммита. Один атомарный session.commit().
4. Перенос подписей в диаграмме по буквам ✅ исправлено
Решение: _W_LBL увеличен до 130, добавлен max_lines=2, no_wrap=False.
5. Текст вариантов ответов обрезался ✅ исправлено
Проблема: ft.Radio(label=...) в Flet не переносит текст метки — длинные варианты ответов обрезались.
Решение: Реализованы кастомные кликабельные контейнеры с ft.Text(expand=True, no_wrap=False).
6. Браузер не может обратиться к http://api:8000 ✅ исправлено
Проблема: Внутреннее Docker-имя api недоступно из браузера пользователя.
Решение: Фронтенд скачивает SVG через httpx (внутри Docker-сети) и передаёт браузеру как data:application/octet-stream;base64,....
7. image_path добавлен после создания таблиц ✅ исправлено
Решение: Добавлена явная миграция:
ALTER TABLE radar_results ADD COLUMN IF NOT EXISTS image_path VARCHAR(512);
8. Документация API доступна без авторизации ✅ исправлено
Решение: Стандартные маршруты /docs, /redoc, /openapi.json отключены. Зарегистрированы собственные с HTTP Basic Auth через secrets.compare_digest.
Содержание
- Архитектура
- Быстрый старт
- Переменные окружения
- Структура проекта
- База данных
- API
- Тест Голланда
- Паутинная диаграмма
- Фронтенд
- Известные ошибки и просчёты
Архитектура
┌─────────────────────────────────────────────┐
│ Браузер пользователя :80 │
│ (Flet Web App) │
└────────────────────┬────────────────────────┘
│ HTTP / REST
┌────────────────────▼────────────────────────┐
│ FastAPI API :8000 │
│ - аутентификация (JWT) │
│ - управление тестами │
│ - сбор ответов │
│ - расчёт радарных результатов │
│ - генерация SVG-диаграмм │
└──────┬─────────────────────┬────────────────┘
│ │
┌──────▼──────┐ ┌────────▼────────┐
│ PostgreSQL │ │ Redis │
│ :5432 │ │ :6379 │
│ (основная │ │ (сессии / │
│ БД) │ │ кэш) │
└─────────────┘ └─────────────────┘
Сетевая топология Docker: все три сервиса (api, web, postgres, redis) работают в одной Docker-сети. Фронтенд (web) обращается к API по внутреннему имени http://api:8000. PostgreSQL и Redis запускаются отдельным compose-файлом docker-compose_db.yml.
Быстрый старт
1. Инфраструктура (PostgreSQL + Redis)
docker compose -f docker-compose_db.yml up -d
2. Приложение (API + Web)
docker compose up -d --build
После запуска:
| Адрес | Что |
|---|---|
http://localhost |
Веб-интерфейс (Flet) |
http://localhost:8000/docs |
Swagger UI (HTTP Basic Auth) |
http://localhost:8000/redoc |
ReDoc |
Доступ к документации — логин/пароль из переменных DOCS_USERNAME / DOCS_PASSWORD.
3. Инициализация БД
После первого запуска выполните:
# Создать таблицы
curl -X POST http://localhost:8000/db/create-tables \
-H "X-Admin-Key: <ADMIN_KEY>"
# Применить миграции (добавляет колонки, если БД уже была создана ранее)
curl -X POST http://localhost:8000/db/migrate \
-H "X-Admin-Key: <ADMIN_KEY>"
4. Загрузка теста Голланда
# Создать тест (возвращает poll_id)
curl -X POST http://localhost:8000/polls/holland \
-H "X-Admin-Key: <ADMIN_KEY>"
# Загрузить ключ расшифровки (42 пары → 6 типов × 14 баллов)
curl -X POST http://localhost:8000/polls/holland/<poll_id>/seed-scores \
-H "X-Admin-Key: <ADMIN_KEY>"
Переменные окружения
Важно: перед деплоем в продакшн обязательно смените
SECRET_KEY,ADMIN_KEY,DOCS_PASSWORDи пароли БД.
| Переменная | Описание | Пример |
|---|---|---|
APP_PATH |
Абсолютный путь к директории приложения внутри контейнера | /home/user/api-copp |
DB_HOST |
Хост PostgreSQL | postgres |
DB_PORT |
Порт PostgreSQL | 5432 |
DB_USER |
Пользователь БД | postgres |
DB_PASS |
Пароль БД | postgres |
DB_NAME |
Имя базы данных | profi |
REDIS_HOST |
Хост Redis | redis |
REDIS_PORT |
Порт Redis | 6379 |
REDIS_DB |
Номер БД Redis | 0 |
REDIS_PASSWORD |
Пароль Redis | — |
SECRET_KEY |
Секрет для подписи JWT-токенов (min 32 символа) | python -c "import secrets; print(secrets.token_hex(32))" |
ADMIN_KEY |
Ключ для защищённых admin-эндпоинтов | — |
DOCS_USERNAME |
Логин для доступа к /docs и /redoc |
admin |
DOCS_PASSWORD |
Пароль для доступа к /docs и /redoc |
— |
API_URL |
Внутренний URL API для фронтенда | http://api:8000 |
Файлы docker-compose.yml содержат демо-значения — не используйте их в продакшн.
Структура проекта
api-copp/
├── main.py # точка входа FastAPI, регистрация роутеров
├── requirements.txt # зависимости API
├── pyproject.toml # метаданные проекта
├── Dockerfile.api # образ API (python:3.14-slim)
├── Dockerfile.web # образ фронтенда (python:3.11-slim + flet)
├── docker-compose.yml # API + Web
├── docker-compose_db.yml # PostgreSQL + Redis
├── docker-compose_web.yml # отдельный запуск только Web
│
├── bd/ # слой доступа к данным
│ ├── __init__.py # Settings (pydantic), make_engine()
│ └── tables/
│ ├── simple_base.py # общий Base для SQLAlchemy
│ ├── users.py # User
│ ├── organization.py # Organization
│ ├── group.py # Group
│ ├── poll.py # Poll
│ ├── question.py # Question
│ ├── choice.py # Choice
│ ├── response.py # Response, Answer
│ ├── scale.py # ScaleDimension, ChoiceScore
│ └── radar_result.py # RadarResult, RadarResultItem
│
├── route/ # FastAPI-роутеры
│ ├── auth_utils.py # JWT, bcrypt, require_admin_key
│ ├── auth.py # /auth/register, /auth/login
│ ├── base.py # /health, /version
│ ├── init_data_base.py # /db/create-tables, /db/migrate
│ ├── users_crud.py # CRUD пользователей
│ ├── groups_crud.py # CRUD групп
│ ├── organizations_crud.py# CRUD организаций
│ ├── poll_crud.py # CRUD опросов
│ ├── question_crud.py # CRUD вопросов
│ ├── choice_crud.py # CRUD вариантов ответов
│ ├── response_crud.py # приём ответов + авто-расчёт радара
│ ├── holland_crud.py # генерация теста Голланда + seed-scores
│ ├── scale_crud.py # CRUD осей и расшифровок
│ ├── radar_crud.py # расчёт и чтение радарных результатов
│ └── radar_svg_gen.py # серверная генерация SVG-диаграмм
│
├── web/ # Flet-фронтенд
│ ├── main.py # точка входа Flet ASGI
│ ├── router.py # маршрутизация страниц
│ ├── api_client.py # httpx-обёртки над API
│ ├── designer.py # компоненты дизайн-системы
│ ├── radar_chart.py # виджет RadarChart (flet.canvas)
│ ├── radar_svg.py # генератор SVG-data-URI (на клиенте)
│ └── views/
│ └── content_users/
│ ├── poll_page.py # прохождение теста + диаграмма сразу после
│ ├── response_page.py # страница результата с диаграммой
│ ├── my_tests_page.py # список пройденных тестов
│ └── ...
│
└── data/ # смонтированный том Docker (./data:/app/data)
├── postgres/ # данные PostgreSQL
└── radar/ # сохранённые SVG-файлы диаграмм (создаётся автоматически)
База данных
Схема таблиц
organizations ──< groups ──< users
│
polls ──< questions ──< choices ──< choice_scores
│ │
└──< responses ──< answers │
│ │
└──< radar_results ──< radar_result_items
(image_path) └── (dimension_id FK → scale_dimensions)
↑
polls ──< scale_dimensions ──────────────────-─┘
Ключевые таблицы
| Таблица | Описание |
|---|---|
users |
Испытуемые: username, hashed_password, first_name, last_name, FK на groups и organizations |
polls |
Тест/опрос: title, description |
questions |
Вопросы теста с type (single / multiple) |
choices |
Варианты ответов к вопросам |
responses |
Один факт прохождения теста (user + poll + timestamp) |
answers |
Конкретные ответы внутри прохождения |
scale_dimensions |
Оси радарной диаграммы: name, color, position, FK на polls |
choice_scores |
Расшифровка: какой вариант ответа сколько баллов вносит в какую ось |
radar_results |
Итоговый результат по осям + image_path (путь к SVG-файлу) |
radar_result_items |
Снимок значений по каждой оси на момент расчёта |
Миграции
Таблицы создаются через POST /db/create-tables. Для уже существующих БД применяются дополнительные ALTER-миграции через POST /db/migrate. Текущие миграции:
- Добавление колонок
username/hashed_passwordвusers - Добавление колонки
image_pathвradar_results
API
Аутентификация
POST /auth/register — регистрация (username ≥ 3 симв., password ≥ 8 симв.)
POST /auth/login — получить JWT-токен (OAuth2 password flow)
JWT-токен передаётся заголовком Authorization: Bearer <token>. Admin-операции требуют заголовок X-Admin-Key.
Основные эндпоинты
# Тесты
GET /polls — список тестов
POST /polls — создать тест
GET /polls/{id} — тест с вопросами и вариантами
DELETE /polls/{id} — удалить тест
# Вопросы / Варианты ответов
POST /polls/{id}/questions
POST /questions/{id}/choices
# Прохождение
POST /polls/{poll_id}/responses — сдать тест (автоматически считает радар)
GET /polls/users/{user_id}/responses — история пользователя
GET /responses/{id} — конкретный результат
# Результаты (радар)
POST /responses/{id}/radar/compute — (пере)считать радарный результат
GET /responses/{id}/radar — данные для диаграммы
GET /responses/{id}/radar/image — SVG-файл диаграммы
GET /users/{user_id}/radar-results — история радарных результатов
# Оси и расшифровки
POST /polls/{poll_id}/dimensions — создать ось
GET /polls/{poll_id}/dimensions — список осей
PUT /dimensions/{id}
DELETE /dimensions/{id}
POST /dimensions/{id}/scores — задать расшифровку варианта ответа
GET /choices/{choice_id}/scores
# Тест Голланда
POST /polls/holland — создать тест с 42 парами
POST /polls/holland/{poll_id}/seed-scores — загрузить ключ расшифровки
# База данных (admin)
POST /db/create-tables
POST /db/migrate
# Служебные
GET /health
GET /version
Тест Голланда
Реализована модификация Г.В. Резапкиной теста профессиональной ориентации Дж. Голланда. 42 пары профессий, испытуемый выбирает предпочтительную в каждой паре.
Типы личности
| Код | Название | Цвет |
|---|---|---|
| Р | Реалистичный | #FF8C00 |
| И | Интеллектуальный | #4169E1 |
| С | Социальный | #32CD32 |
| К | Конвенциональный | #9370DB |
| П | Предприимчивый | #DC143C |
| А | Артистический | #FF69B4 |
Каждый тип получает ровно 14 вопросов (= 14 максимально возможных баллов). Результат — паутинная диаграмма с 6 осями.
Процедура загрузки
POST /polls/holland— создаёт тест с 42 вопросами-парамиPOST /polls/holland/{poll_id}/seed-scores— создаёт 6ScaleDimensionи 84ChoiceScore(по 2 на каждую пару: вариант A → тип X, вариант B → тип Y)
Паутинная диаграмма
Как работает
При сдаче теста (POST /polls/{id}/responses) сервер автоматически:
- Суммирует баллы по каждой оси (
ChoiceScore.score) для выбранных вариантов - Сохраняет результат в
radar_results+radar_result_items - Генерирует SVG-файл и сохраняет в
{APP_PATH}/data/radar/{result_id}.svg - Записывает относительный путь
data/radar/{result_id}.svgв колонкуimage_path
Скачивание SVG
Фронтенд скачивает SVG с API через внутреннюю сеть (GET /responses/{id}/radar/image), кодирует в base64 data URI и открывает в браузере как data:image/svg+xml;base64,... — файл сохраняется на устройство пользователя без необходимости публичного URL к серверу.
Генерация SVG
SVG генерируется чистым Python без зависимостей (route/radar_svg_gen.py). Серверный SVG идентичен клиентскому (web/radar_svg.py) по разметке, но предназначен для долгосрочного хранения. Параметры:
- Фон
#2F184B, полигонrgba(155,114,207,0.55) - 5 концентрических сеток
- Подписи с именем типа и баллом
- Размер холста: 520×520 пикселей
Фронтенд
Написан на Flet — Python-фреймворке поверх Flutter. Работает как ASGI-приложение через uvicorn, отдаётся браузеру как веб-приложение.
Маршруты
| Путь | Страница |
|---|---|
/login |
Вход |
/reg |
Регистрация |
/user/{url_key} |
Главная страница пользователя |
/take_test/{url_key} |
Список тестов для прохождения |
/poll/{url_key}/{poll_id} |
Прохождение теста |
/response/{url_key}/{response_id} |
Результат с диаграммой |
/my_tests/{url_key} |
Мои результаты |
/profile/{url_key} |
Профиль пользователя |
{url_key} — одноразовый 32-символьный hex-ключ, генерируется при успешном входе (secrets.token_hex(16)) и сохраняется в сессии. Обращение к защищённому маршруту с чужим или отсутствующим ключом автоматически сбрасывает сессию и перенаправляет на /login.
Диаграмма после теста
После сдачи теста пользователь сразу видит паутинную диаграмму (виджет RadarChart) и кнопку «Сохранить диаграмму», которая загружает SVG с сервера и открывает его в новой вкладке для сохранения.
Известные ошибки и просчёты
1. Лишнее дублирование генерации SVG
Проблема: SVG-генератор написан дважды — route/radar_svg_gen.py (сервер) и web/radar_svg.py (клиент). Кнопка «Сохранить» изначально создавала SVG на клиенте заново вместо использования уже сохранённого серверного файла.
Решение: Кнопка «Сохранить» теперь скачивает SVG через api_client.get_radar_svg_bytes(), а не регенерирует его. Серверный файл — источник истины.
Оставшийся долг: web/radar_svg.py больше не используется кнопками сохранения, но файл остался. Можно удалить при следующем рефакторинге.
2. Несовместимость Python 3.14 и psycopg ✅ исправлено
Проблема: pyproject.toml указывал requires-python = ">=3.14" и зависимость psycopg>=3.3.3, однако Dockerfile.api использует python:3.14-slim. psycopg (асинхронный адаптер) на 3.14 может не иметь собранных wheels. В requirements.txt вместо psycopg используется pg8000==1.29.0 — синхронный pure-Python драйвер.
Решение: Все запросы к БД синхронные (make_engine() возвращает синхронный create_engine с postgresql+pg8000://...). Фактически psycopg не используется. Ошибочная зависимость psycopg заменена на pg8000>=1.29.0 в pyproject.toml.
3. Нет транзакции при сохранении SVG ✅ исправлено
Проблема: После основного session.commit() (сохранение RadarResult и RadarResultItem) шло отдельное обновление result.image_path. Если процесс падал между первым и вторым commit — в БД оставалась запись без image_path, хотя файл уже существовал на диске.
Последствие: При повторном прохождении теста пересчёт (compute_radar) удалял старый RadarResult и создавал новый, что восстанавливало корректность. Но исторический файл .svg «осиротевал» на диске.
Решение: items_data теперь строится непосредственно из scores_map/dimensions (данные уже в памяти после flush()), SVG генерируется до коммита, result.image_path устанавливается до коммита. Итого один session.commit() атомарно сохраняет RadarResult, RadarResultItem и путь к SVG.
4. Перенос подписей в диаграмме по буквам (устранено)
Проблема: В виджете RadarChart (web/radar_chart.py) подписи осей переносились по буквам из-за слишком малой ширины контейнера (_W_LBL=90).
Решение: Увеличено до _W_LBL=130, добавлен max_lines=2, no_wrap=False — перенос только по словам.
5. Первоначально неверные пары профессий
Проблема: В первой версии holland_crud.py использовались произвольные пары профессий, не соответствующие оригинальной методике. Ключ расшифровки (CHOICE_KEY) не был реализован — загрузка в БД не работала.
Решение: Заменены на 42 официальные пары из модификации Резапкиной с корректным CHOICE_KEY — каждый из 6 типов (Р/И/С/К/П/А) получает ровно 14 баллов-вопросов.
6. Браузер не может напрямую обратиться к http://api:8000
Проблема: Внутреннее Docker-имя api недоступно из браузера пользователя. Если бы кнопка «Сохранить» формировала URL вида http://api:8000/responses/.../radar/image и передавала его в page.launch_url() — браузер получал бы ошибку DNS.
Решение: Фронтенд скачивает SVG-байты с API через httpx (внутренняя сеть), конвертирует в base64 data URI и передаёт его в page.launch_url(). Браузер открывает data URI напрямую, без обращения к серверу.
7. image_path добавлен после создания таблиц (требует миграции)
Проблема: Колонка image_path была добавлена в RadarResult после того, как таблицы уже создавались через POST /db/create-tables. create_all не изменяет существующие таблицы.
Решение: Добавлена явная миграция в route/init_data_base.py:
ALTER TABLE radar_results ADD COLUMN IF NOT EXISTS image_path VARCHAR(512);
Применяется вызовом POST /db/migrate.
8. Документация API доступна без авторизации по умолчанию у FastAPI (устранено)
Проблема: FastAPI по умолчанию открывает /docs и /openapi.json без каких-либо ограничений.
Решение: docs_url=None, openapi_url=None, redoc_url=None — стандартные маршруты отключены. Вместо них зарегистрированы собственные /docs, /redoc, /openapi.json с HTTP Basic Auth через secrets.compare_digest (защита от timing-атак).