598 lines
27 KiB
Markdown
598 lines
27 KiB
Markdown
# api-copp
|
||
|
||
Платформа ЦОПП Челябинской области для проведения профориентационных тестирований. REST API на FastAPI + веб-интерфейс на Flet. Поддерживает тесты Голланда, Климова и Гломштока с паутинными диаграммами результатов.
|
||
|
||
---
|
||
|
||
## Содержание
|
||
|
||
- [Архитектура](#архитектура)
|
||
- [Быстрый старт](#быстрый-старт)
|
||
- [Переменные окружения](#переменные-окружения)
|
||
- [Структура проекта](#структура-проекта)
|
||
- [База данных](#база-данных)
|
||
- [API](#api)
|
||
- [Перенос данных между серверами](#перенос-данных-между-серверами)
|
||
- [Паутинная диаграмма](#паутинная-диаграмма)
|
||
- [Redis и кэширование](#redis-и-кэширование)
|
||
- [Деплой с внешним прокси](#деплой-с-внешним-прокси)
|
||
- [Известные ошибки и решения](#известные-ошибки-и-решения)
|
||
|
||
---
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
Браузер пользователя
|
||
│ :80
|
||
[ web — Flet ASGI ] ← единственный публичный сервис
|
||
│ http://api:8000 (только Docker-сеть)
|
||
[ api — FastAPI :8000 ]
|
||
│ │
|
||
[ PostgreSQL :5432 ] [ Redis :6379 ]
|
||
```
|
||
|
||
Сервис `api` **не публикует порты наружу** — доступен только контейнеру `web` внутри Docker-сети. Пользователь никогда не обращается к API напрямую.
|
||
|
||
PostgreSQL и Redis могут запускаться как:
|
||
- отдельным compose-файлом `docker-compose_db.yml` (локально в Docker)
|
||
- внешними сервисами (отдельный сервер, managed service) — достаточно прописать нужные `REDIS_HOST`/`DB_HOST` в переменных окружения
|
||
|
||
---
|
||
|
||
## Быстрый старт
|
||
|
||
### 1. Инфраструктура (PostgreSQL + Redis)
|
||
|
||
```bash
|
||
docker compose -f docker-compose_db.yml up -d
|
||
```
|
||
|
||
### 2. Приложение
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
| Адрес | Что |
|
||
|---|---|
|
||
| `http://localhost` | Веб-интерфейс (Flet) |
|
||
| `http://localhost:8000/docs` | Swagger UI (HTTP Basic Auth) |
|
||
| `http://localhost:8000/redoc` | ReDoc |
|
||
| `http://localhost:8081` | Дашборд статистики |
|
||
|
||
### 3. Инициализация БД (первый запуск)
|
||
|
||
```bash
|
||
# Создать таблицы
|
||
curl -X POST http://localhost:8000/db/create-tables \
|
||
-H "X-Admin-Key: <ADMIN_KEY>"
|
||
|
||
# Применить все миграции (безопасно запускать повторно — IF NOT EXISTS)
|
||
curl -X POST http://localhost:8000/db/migrate \
|
||
-H "X-Admin-Key: <ADMIN_KEY>"
|
||
|
||
# Миграция FK: responses.user_id → users.id ON DELETE CASCADE
|
||
curl -X POST http://localhost:8000/db/migrate-cascade-user \
|
||
-H "X-Admin-Key: <ADMIN_KEY>"
|
||
```
|
||
|
||
> **Важно:** `db/migrate-cascade-user` нужно выполнить **один раз** на каждом сервере с уже существующей БД (созданной до апреля 2026). На новых базах — уже включено автоматически через `create-tables`.
|
||
|
||
### 4. Загрузка тестов
|
||
|
||
```bash
|
||
# Голланд (42 пары профессий)
|
||
curl -X POST http://localhost:8000/polls/holland -H "X-Admin-Key: <ADMIN_KEY>"
|
||
curl -X POST http://localhost:8000/polls/holland/<poll_id>/seed-scores -H "X-Admin-Key: <ADMIN_KEY>"
|
||
|
||
# Климов (ДДО)
|
||
curl -X POST http://localhost:8000/polls/klimov -H "X-Admin-Key: <ADMIN_KEY>"
|
||
curl -X POST http://localhost:8000/polls/klimov/<poll_id>/seed-scores -H "X-Admin-Key: <ADMIN_KEY>"
|
||
|
||
# Гломшток (Карта интересов)
|
||
curl -X POST http://localhost:8000/polls/glomshtok -H "X-Admin-Key: <ADMIN_KEY>"
|
||
curl -X POST http://localhost:8000/polls/glomshtok/<poll_id>/seed-scores -H "X-Admin-Key: <ADMIN_KEY>"
|
||
```
|
||
|
||
---
|
||
|
||
## Переменные окружения
|
||
|
||
> Файл `docker-compose.yml` содержит **демо-значения**. Перед продакшн-деплоем обязательно смените все пароли и ключи.
|
||
|
||
| Переменная | Описание | Пример |
|
||
|---|---|---|
|
||
| `APP_PATH` | Путь к директории приложения внутри контейнера | `/app` |
|
||
| `DB_HOST` | Хост PostgreSQL | `postgres` |
|
||
| `DB_PORT` | Порт PostgreSQL | `5432` |
|
||
| `DB_USER` | Пользователь БД | `postgres` |
|
||
| `DB_PASS` | Пароль БД | — |
|
||
| `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-эндпоинтов (`X-Admin-Key`) | — |
|
||
| `DOCS_USERNAME` | Логин для `/docs` и `/redoc` | `admin` |
|
||
| `DOCS_PASSWORD` | Пароль для `/docs` и `/redoc` | — |
|
||
| `API_URL` | URL API для фронтенда (только Docker-сеть) | `http://api:8000` |
|
||
|
||
---
|
||
|
||
## Структура проекта
|
||
|
||
```
|
||
api-copp/
|
||
├── main.py # точка входа FastAPI, регистрация роутеров
|
||
├── requirements.txt # зависимости API
|
||
├── Dockerfile.api # образ API
|
||
├── Dockerfile.web # образ фронтенда (Flet)
|
||
├── Dockerfile.stats # образ дашборда статистики
|
||
├── docker-compose.yml # API + Web + Stats
|
||
├── docker-compose_db.yml # PostgreSQL + Redis
|
||
│
|
||
├── bd/
|
||
│ ├── __init__.py # Settings (pydantic), make_engine()
|
||
│ └── tables/
|
||
│ ├── simple_base.py
|
||
│ ├── users.py # User (username, hashed_password, group_id, org_id)
|
||
│ ├── organization.py # Organization
|
||
│ ├── group.py # Group
|
||
│ ├── poll.py # Poll (title, description, is_active)
|
||
│ ├── question.py # Question (type: single/multiple)
|
||
│ ├── choice.py # Choice (варианты ответов)
|
||
│ ├── response.py # Response, Answer
|
||
│ ├── scale.py # ScaleDimension, ChoiceScore
|
||
│ └── radar_result.py # RadarResult, RadarResultItem
|
||
│
|
||
├── route/
|
||
│ ├── auth_utils.py # JWT, bcrypt, require_admin_key
|
||
│ ├── auth.py # /auth/register, /auth/login, /auth/logout
|
||
│ ├── base.py # /health, /version
|
||
│ ├── init_data_base.py # /db/create-tables, /db/migrate, /db/migrate-cascade-user
|
||
│ ├── 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 # тест Голланда
|
||
│ ├── klimov_crud.py # тест Климова (ДДО)
|
||
│ ├── glomshtok_crud.py # тест Гломштока (Карта интересов)
|
||
│ ├── scale_crud.py # CRUD осей и расшифровок
|
||
│ ├── radar_crud.py # расчёт и чтение радарных результатов
|
||
│ ├── radar_svg_gen.py # серверная генерация SVG-диаграмм
|
||
│ ├── stats_crud.py # статистика (кэшируется в Redis, TTL 5 мин)
|
||
│ └── transfer_crud.py # экспорт/импорт/бэкап/восстановление БД
|
||
│
|
||
├── web/ # Flet-фронтенд (порт 80)
|
||
│ ├── main.py
|
||
│ ├── router.py
|
||
│ ├── api_client.py
|
||
│ ├── designer.py # дизайн-система
|
||
│ ├── radar_chart.py # виджет RadarChart
|
||
│ └── views/content_users/
|
||
│
|
||
└── data/ # том Docker (./data:/app/data)
|
||
├── postgres/ # данные PostgreSQL
|
||
└── radar/ # SVG-файлы диаграмм (структура: <тест>/<группа>/<user_id>.svg)
|
||
```
|
||
|
||
---
|
||
|
||
## База данных
|
||
|
||
### Схема связей
|
||
|
||
```
|
||
organizations ──< groups ──< users
|
||
│ CASCADE DELETE
|
||
polls ──< questions ──< choices ──< choice_scores
|
||
│
|
||
└──< responses ──< answers (CASCADE: poll→responses, responses→answers)
|
||
│
|
||
└──< radar_results ──< radar_result_items (CASCADE)
|
||
│
|
||
image_path → data/radar/...svg
|
||
|
||
polls ──< scale_dimensions ──< choice_scores
|
||
│
|
||
└── radar_result_items.dimension_id (SET NULL при удалении оси)
|
||
```
|
||
|
||
### Каскадное удаление
|
||
|
||
При удалении **пользователя** автоматически удаляется:
|
||
- все его `responses` → все `answers` внутри → все `radar_results` → все `radar_result_items`
|
||
|
||
`organization` и `group`, к которым был привязан пользователь, **не удаляются** — только обнуляются ссылки (`SET NULL`).
|
||
|
||
При удалении **теста (poll)**:
|
||
- все `questions` → все `choices` → все `choice_scores`
|
||
- все `responses` → все `answers` → все `radar_results` → все `radar_result_items`
|
||
- все `scale_dimensions` → все `choice_scores`
|
||
|
||
### Миграции
|
||
|
||
Применяются через admin-эндпоинты (безопасно запускать повторно — используют `IF NOT EXISTS`/`IF EXISTS`):
|
||
|
||
| Эндпоинт | Что делает |
|
||
|---|---|
|
||
| `POST /db/create-tables` | Создаёт все таблицы по моделям SQLAlchemy |
|
||
| `POST /db/migrate` | Добавляет колонки `username`/`hashed_password` в users, `image_path` в radar_results |
|
||
| `POST /db/migrate-cascade-user` | Добавляет FK `responses.user_id → users(id) ON DELETE CASCADE` |
|
||
|
||
---
|
||
|
||
## API
|
||
|
||
### Аутентификация
|
||
|
||
```
|
||
POST /auth/register — регистрация (username ≥ 3 симв., password ≥ 8 симв.)
|
||
POST /auth/login — JWT-токен (OAuth2 password flow)
|
||
POST /auth/logout — отзыв токена (добавляет в Redis-блэклист)
|
||
```
|
||
|
||
Токен: `Authorization: Bearer <token>`
|
||
Admin-операции: `X-Admin-Key: <ADMIN_KEY>`
|
||
|
||
### Тесты и вопросы
|
||
|
||
```
|
||
GET /polls — список тестов
|
||
POST /polls — создать тест [admin]
|
||
GET /polls/{id} — тест с вопросами и вариантами
|
||
DELETE /polls/{id} — удалить тест [admin]
|
||
POST /polls/{id}/questions — добавить вопрос [admin]
|
||
POST /questions/{id}/choices — добавить вариант ответа [admin]
|
||
```
|
||
|
||
### Прохождение теста
|
||
|
||
```
|
||
POST /polls/{poll_id}/responses — сдать тест (автоматически считает радар + генерирует SVG)
|
||
GET /polls/users/{user_id}/responses — история пользователя
|
||
GET /responses/{id} — конкретный результат с ответами
|
||
```
|
||
|
||
### Результаты (паутинная диаграмма)
|
||
|
||
```
|
||
GET /responses/{id}/radar — данные диаграммы (оси + баллы)
|
||
GET /responses/{id}/radar/image — SVG-файл диаграммы
|
||
POST /responses/{id}/radar/compute — пересчитать диаграмму [admin]
|
||
GET /users/{user_id}/radar-results — история всех радарных результатов пользователя
|
||
```
|
||
|
||
### Оси и расшифровки
|
||
|
||
```
|
||
POST /polls/{poll_id}/dimensions — создать ось [admin]
|
||
GET /polls/{poll_id}/dimensions — список осей
|
||
PUT /dimensions/{id} [admin]
|
||
DELETE /dimensions/{id} [admin]
|
||
POST /dimensions/{id}/scores — задать расшифровку варианта ответа [admin]
|
||
GET /choices/{choice_id}/scores
|
||
```
|
||
|
||
### Готовые тесты
|
||
|
||
```
|
||
POST /polls/holland — создать тест Голланда (42 пары) [admin]
|
||
POST /polls/holland/{poll_id}/seed-scores — загрузить ключ расшифровки [admin]
|
||
POST /polls/klimov — создать тест Климова (ДДО) [admin]
|
||
POST /polls/klimov/{poll_id}/seed-scores [admin]
|
||
POST /polls/glomshtok — создать тест Гломштока [admin]
|
||
POST /polls/glomshtok/{poll_id}/seed-scores [admin]
|
||
```
|
||
|
||
### Пользователи / Группы / Организации
|
||
|
||
```
|
||
POST /users/ — создать пользователя [admin]
|
||
GET /users/ — список пользователей [admin]
|
||
DELETE /users/{id} — удалить пользователя (каскадно удаляет все данные)
|
||
GET /users/me — текущий пользователь
|
||
PUT /users/me — обновить профиль
|
||
|
||
GET /groups/ — список групп
|
||
POST /groups/ [admin]
|
||
...
|
||
|
||
GET /organizations/ — список организаций
|
||
POST /organizations/ [admin]
|
||
...
|
||
```
|
||
|
||
### Статистика
|
||
|
||
```
|
||
GET /stats/summary — общая сводка (total_responses, total_polls, ...)
|
||
GET /stats/responses?group_by=poll — ответы по тестам/организациям/группам/месяцам
|
||
GET /stats/results/avg-by-dimension — средний балл по осям
|
||
GET /stats/results/distribution — распределение баллов
|
||
GET /stats/years — список лет с активностью
|
||
```
|
||
|
||
Фильтры: `poll_id`, `org_id`, `group_id`, `year`. Все ответы кэшируются в Redis (TTL 5 минут). Кэш автоматически сбрасывается при удалении пользователя.
|
||
|
||
### База данных
|
||
|
||
```
|
||
POST /db/create-tables [admin]
|
||
POST /db/migrate [admin]
|
||
POST /db/migrate-cascade-user [admin]
|
||
```
|
||
|
||
### Служебные
|
||
|
||
```
|
||
GET /health
|
||
GET /version
|
||
```
|
||
|
||
---
|
||
|
||
## Перенос данных между серверами
|
||
|
||
Два сценария использования:
|
||
|
||
### Сценарий 1: Дополнение (transfer)
|
||
|
||
Ученики прошли тест на сервере A — нужно перенести их результаты на сервер B, не удаляя существующие данные на B.
|
||
|
||
```bash
|
||
# Сервер A — скачать архив (все таблицы + SVG-диаграммы)
|
||
curl -H "X-Admin-Key: <KEY_A>" \
|
||
http://server-a:8000/transfer/export -o export.zip
|
||
|
||
# Сервер B — на новой БД сначала создать схему (один раз)
|
||
curl -X POST -H "X-Admin-Key: <KEY_B>" http://server-b:8000/db/create-tables
|
||
curl -X POST -H "X-Admin-Key: <KEY_B>" http://server-b:8000/db/migrate
|
||
curl -X POST -H "X-Admin-Key: <KEY_B>" http://server-b:8000/db/migrate-cascade-user
|
||
|
||
# Сервер B — импортировать (добавляет только новое, дубли по UUID пропускаются)
|
||
curl -X POST -H "X-Admin-Key: <KEY_B>" \
|
||
-F "file=@export.zip" \
|
||
http://server-b:8000/transfer/import
|
||
```
|
||
|
||
**Ответ импорта:**
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"result": {
|
||
"inserted": {"organizations": 107, "users": 4, "responses": 34, "radar_results": 34, "svg_files": 23, ...},
|
||
"skipped": {"organizations": 0, "users": 0, "responses": 0, "radar_results": 0, ...}
|
||
}
|
||
}
|
||
```
|
||
|
||
При повторном импорте того же архива все записи попадают в `skipped` — **дублей не создаётся**.
|
||
|
||
### Сценарий 2: Резервное копирование и восстановление (backup)
|
||
|
||
Полный снимок БД с возможностью полного восстановления.
|
||
|
||
```bash
|
||
# Создать резервную копию (имя файла начинается с backup_)
|
||
curl -H "X-Admin-Key: <KEY>" \
|
||
http://localhost:8000/backup/export -o backup_20260410.zip
|
||
|
||
# Восстановить из резервной копии (УДАЛЯЕТ все текущие данные, вставляет из архива)
|
||
curl -X POST -H "X-Admin-Key: <KEY>" \
|
||
-F "file=@backup_20260410.zip" \
|
||
http://localhost:8000/backup/restore
|
||
```
|
||
|
||
> **Внимание:** `/backup/restore` полностью очищает БД (в обратном FK-порядке) перед вставкой данных. SVG-папка тоже очищается и восстанавливается. Используйте только для полного восстановления.
|
||
|
||
**Ответ восстановления:**
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"deleted": {"radar_result_items": 332, "users": 4, "organizations": 107, ...},
|
||
"inserted": {"organizations": 107, "users": 4, "radar_result_items": 332, "svg_files": 23, ...}
|
||
}
|
||
```
|
||
|
||
### Что входит в архив
|
||
|
||
Оба архива содержат одинаковый набор данных (12 таблиц в FK-порядке + SVG):
|
||
|
||
```
|
||
manifest.json ← версия формата, дата, количество записей
|
||
organizations.json
|
||
groups.json
|
||
polls.json
|
||
questions.json
|
||
choices.json
|
||
scale_dimensions.json
|
||
choice_scores.json
|
||
users.json
|
||
responses.json
|
||
answers.json
|
||
radar_results.json
|
||
radar_result_items.json
|
||
radar_svgs/ ← все SVG-файлы (структура папок сохраняется)
|
||
```
|
||
|
||
Все UUID сохраняются **без изменений** — FK-связи не рвутся.
|
||
|
||
---
|
||
|
||
## Паутинная диаграмма
|
||
|
||
### Как работает
|
||
|
||
При сдаче теста (`POST /polls/{id}/responses`) сервер автоматически:
|
||
|
||
1. Суммирует баллы по каждой оси (`ChoiceScore.score`) для выбранных вариантов
|
||
2. Сохраняет `radar_result` + `radar_result_items`
|
||
3. Генерирует SVG и сохраняет в `{APP_PATH}/data/radar/<тест>/<группа>/<user_id>.svg`
|
||
4. Записывает путь в `image_path` — всё в **одном** `session.commit()`
|
||
|
||
### SVG-файл
|
||
|
||
Генерируется чистым Python (`route/radar_svg_gen.py`):
|
||
- Фон `#2F184B`, полигон `rgba(155,114,207,0.55)`
|
||
- 5 концентрических сеток
|
||
- Подписи с именем типа и баллом
|
||
- Размер: 520×520 пикселей
|
||
|
||
### Скачивание из браузера
|
||
|
||
Фронтенд скачивает SVG-байты через внутренний API (httpx, Docker-сеть), кодирует в base64 data URI и открывает в браузере. Браузер не обращается к API напрямую — это важно, так как `api` не имеет публичного адреса.
|
||
|
||
---
|
||
|
||
## Redis и кэширование
|
||
|
||
### Настройка Redis
|
||
|
||
Используется connection pool (`max_connections=20`). При Redis с отключённой командой `CONFIG` (например, Debian `redis-server` по умолчанию) — настройки меняются только через `/etc/redis/redis.conf` + `systemctl restart redis`.
|
||
|
||
Рекомендуемые настройки для продакшна:
|
||
```
|
||
maxmemory 2048mb
|
||
maxmemory-policy volatile-lru
|
||
appendfsync everysec
|
||
timeout 300
|
||
tcp-keepalive 60
|
||
```
|
||
|
||
### Назначение ключей
|
||
|
||
| Префикс | Что хранит | TTL |
|
||
|---|---|---|
|
||
| `token_blacklist:<jti>` | Отозванные JWT-токены | До истечения токена |
|
||
| `cache:<md5>` | Кэш ответов `/stats/*` | 5 минут |
|
||
|
||
### Сброс кэша
|
||
|
||
Кэш статистики (`cache:*`) автоматически сбрасывается при удалении пользователя — чтобы счётчики обновились немедленно, а не через 5 минут.
|
||
|
||
---
|
||
|
||
## Тест Голланда
|
||
|
||
Модификация Г.В. Резапкиной теста профессиональной ориентации Дж. Голланда. 42 пары профессий, испытуемый выбирает предпочтительную в каждой паре.
|
||
|
||
| Тип | Название | Цвет |
|
||
|---|---|---|
|
||
| Р | Реалистичный | `#FF8C00` |
|
||
| И | Интеллектуальный | `#4169E1` |
|
||
| С | Социальный | `#32CD32` |
|
||
| К | Конвенциональный | `#9370DB` |
|
||
| П | Предприимчивый | `#DC143C` |
|
||
| А | Артистический | `#FF69B4` |
|
||
|
||
Каждый тип — 14 вопросов (14 максимальных баллов). `seed-scores` создаёт 6 осей (`ScaleDimension`) и 84 записи расшифровки (`ChoiceScore`).
|
||
|
||
---
|
||
|
||
## Деплой с внешним прокси
|
||
|
||
Минимальная конфигурация nginx:
|
||
|
||
```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.
|
||
|
||
В репозитории есть `nginx.conf` и `nginx.production.conf` с готовыми примерами.
|
||
|
||
---
|
||
|
||
## Известные ошибки и решения
|
||
|
||
### 1. `api` недоступен из браузера ✅
|
||
|
||
**Проблема:** Docker-имя `api` не резолвится у браузера пользователя. Любой URL вида `http://api:8000/...` в `page.launch_url()` давал ошибку DNS.
|
||
|
||
**Решение:** Фронтенд скачивает SVG-байты через httpx (внутренняя сеть), конвертирует в `data:image/svg+xml;base64,...` и передаёт готовый data URI в браузер. Прямых обращений к API из браузера нет.
|
||
|
||
---
|
||
|
||
### 2. SVG записывался без `image_path` при сбое ✅
|
||
|
||
**Проблема:** `image_path` обновлялся вторым `session.commit()` — при падении между первым и вторым записи оставались без пути к файлу.
|
||
|
||
**Решение:** SVG генерируется до коммита, `image_path` устанавливается до коммита. Один атомарный `commit()`.
|
||
|
||
---
|
||
|
||
### 3. `/docs` был открыт без авторизации ✅
|
||
|
||
**Решение:** `docs_url=None, openapi_url=None, redoc_url=None` — стандартные маршруты отключены. Вместо них собственные маршруты с HTTP Basic Auth (`secrets.compare_digest` против timing-атак).
|
||
|
||
---
|
||
|
||
### 4. Варианты ответов обрезались ✅
|
||
|
||
**Проблема:** `ft.Radio(label=...)` в Flet не переносит текст метки — длинные варианты обрезались.
|
||
|
||
**Решение:** Кастомные кликабельные контейнеры с `ft.Text(expand=True, no_wrap=False)`.
|
||
|
||
---
|
||
|
||
### 5. `image_path` требует миграции на старых БД ✅
|
||
|
||
**Решение:**
|
||
```sql
|
||
ALTER TABLE radar_results ADD COLUMN IF NOT EXISTS image_path VARCHAR(512);
|
||
```
|
||
Применяется через `POST /db/migrate`.
|
||
|
||
---
|
||
|
||
### 6. При удалении пользователя оставались его ответы ✅
|
||
|
||
**Проблема:** `responses.user_id` был простым UUID без FK-ограничения. При удалении пользователя его ответы, answers и radar_results оставались в БД (висящие строки).
|
||
|
||
**Решение:** Добавлен FK `responses.user_id → users(id) ON DELETE CASCADE`. Теперь удаление пользователя каскадно удаляет все его данные.
|
||
|
||
Применяется через `POST /db/migrate-cascade-user`.
|
||
|
||
---
|
||
|
||
### 7. Статистика не обновлялась после удаления пользователя ✅
|
||
|
||
**Проблема:** После удаления пользователя кэш `/stats/*` в Redis ещё 5 минут отдавал старые данные (включая удалённые ответы).
|
||
|
||
**Решение:** `delete_user` вызывает `cache_flush()` — удаляет все ключи `cache:*` через `scan_iter`. Следующий запрос к статистике сразу идёт в БД.
|
||
|
||
---
|
||
|
||
### 8. Redis на Debian отключает команду `CONFIG` ✅
|
||
|
||
**Проблема:** `/etc/redis/redis.conf` содержал `rename-command CONFIG ""` — команда `CONFIG SET` возвращала ошибку `ERR unknown command`.
|
||
|
||
**Решение:** Настройки меняются напрямую в `/etc/redis/redis.conf` + `systemctl restart redis`. Строку `rename-command CONFIG ""` при необходимости удалить или закомментировать.
|
||
|
||
---
|
||
|
||
### 9. `pg8000` вместо `psycopg` ✅
|
||
|
||
**Проблема:** `pyproject.toml` указывал `psycopg>=3.3.3`, реально использовался `pg8000`.
|
||
|
||
**Решение:** Зависимость заменена на `pg8000>=1.29.0`. Все запросы к БД синхронные.
|
||
|
||
---
|
||
|
||
### 10. Дублирование SVG-генератора
|
||
|
||
**Состояние:** `route/radar_svg_gen.py` (сервер) и `web/radar_svg.py` (клиент) — два разных генератора. Кнопка «Сохранить» скачивает серверный файл. `web/radar_svg.py` не используется кнопками, но файл остался в репозитории.
|