docs: обновить README под актуальный web-react и проведённую уборку

Документация всё ещё описывала Flet как текущий фронтенд, хотя прод
уже на web-react с eac6192. Обновлены: архитектурная схема, структура
проекта, описание SVG-доставки через same-origin прокси, переменные
окружения (.env вместо демо-значений в compose), деплой с внешним
прокси (WS-заголовки Flet больше не нужны — предупреждение, что
nginx.conf/nginx.production.conf в репозитории написаны под старый
Flet и не обновлялись, править боевой конфиг test.cloud-copp74.ru не
стал без отдельного подтверждения). Добавлена документация анонимного
входа и записи по итогам этой уборки в «Известные ошибки».

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-16 11:55:56 +05:00
parent 31ee79bd97
commit dd0c29ccad

View File

@@ -1,6 +1,6 @@
# api-copp # api-copp
Платформа ЦОПП Челябинской области для проведения профориентационных тестирований. REST API на FastAPI + веб-интерфейс на Flet. Поддерживает тесты Голланда, Климова и Гломштока с паутинными диаграммами результатов. Платформа ЦОПП Челябинской области для проведения профориентационных тестирований. REST API на FastAPI + веб-интерфейс на React (SPA). Поддерживает тесты Голланда, Климова и Гломштока с паутинными диаграммами результатов. Тест можно пройти анонимно — достаточно выбрать школу и класс, аккаунт не нужен.
--- ---
@@ -25,14 +25,14 @@
``` ```
Браузер пользователя Браузер пользователя
│ :80 │ :80
[ web — Flet ASGI ] ← единственный публичный сервис [ web-react — nginx (SPA + reverse-proxy /api) ] ← единственный публичный сервис
│ http://api:8000 (только Docker-сеть) │ http://api:8000 (только Docker-сеть)
[ api — FastAPI :8000 ] [ api — FastAPI :8000 ]
│ │ │ │
[ PostgreSQL :5432 ] [ Redis :6379 ] [ PostgreSQL :5432 ] [ Redis :6379 ]
``` ```
Сервис `api` **не публикует порты наружу** — доступен только контейнеру `web` внутри Docker-сети. Пользователь никогда не обращается к API напрямую. Сервис `api` **не публикует порты наружу** — доступен только контейнеру `web-react` внутри Docker-сети. Браузер обращается к `/api/*`, nginx внутри `web-react` перенаписывает и проксирует запрос на `api:8000` (см. `web-react/nginx.conf`). Пользователь никогда не обращается к API напрямую.
PostgreSQL и Redis могут запускаться как: PostgreSQL и Redis могут запускаться как:
- отдельным compose-файлом `docker-compose_db.yml` (локально в Docker) - отдельным compose-файлом `docker-compose_db.yml` (локально в Docker)
@@ -56,11 +56,13 @@ docker compose up -d --build
| Адрес | Что | | Адрес | Что |
|---|---| |---|---|
| `http://localhost` | Веб-интерфейс (Flet) | | `http://localhost` | Веб-интерфейс (React SPA) |
| `http://localhost:8000/docs` | Swagger UI (HTTP Basic Auth) | | `http://localhost:8000/docs` | Swagger UI (HTTP Basic Auth) |
| `http://localhost:8000/redoc` | ReDoc | | `http://localhost:8000/redoc` | ReDoc |
| `http://localhost:8081` | Дашборд статистики | | `http://localhost:8081` | Дашборд статистики |
> Реальные значения переменных окружения (пароли, ключи) держите в `.env` в корне репозитория — `docker compose` подставляет их автоматически. Шаблон без секретов — в `.env.example`. Файл `.env` в `.gitignore`, в репозиторий не попадает.
### 3. Инициализация БД (первый запуск) ### 3. Инициализация БД (первый запуск)
```bash ```bash
@@ -99,11 +101,12 @@ curl -X POST http://localhost:8000/polls/glomshtok/<poll_id>/seed-scores -H "X-A
## Переменные окружения ## Переменные окружения
> Файл `docker-compose.yml` содержит **демо-значения**. Перед продакшн-деплоем обязательно смените все пароли и ключи. > `docker-compose.yml` читает значения из `.env` (не коммитится, см. `.env.example` за шаблоном). Перед продакшн-деплоем обязательно смените все пароли и ключи в своём `.env`.
| Переменная | Описание | Пример | | Переменная | Описание | Пример |
|---|---|---| |---|---|---|
| `APP_PATH` | Путь к директории приложения внутри контейнера | `/app` | | `APP_PATH` | Путь к директории приложения внутри контейнера | `/app` |
| `DEBUG` | Режим отладки API | `False` |
| `DB_HOST` | Хост PostgreSQL | `postgres` | | `DB_HOST` | Хост PostgreSQL | `postgres` |
| `DB_PORT` | Порт PostgreSQL | `5432` | | `DB_PORT` | Порт PostgreSQL | `5432` |
| `DB_USER` | Пользователь БД | `postgres` | | `DB_USER` | Пользователь БД | `postgres` |
@@ -126,11 +129,12 @@ curl -X POST http://localhost:8000/polls/glomshtok/<poll_id>/seed-scores -H "X-A
``` ```
api-copp/ api-copp/
├── main.py # точка входа FastAPI, регистрация роутеров ├── main.py # точка входа FastAPI, регистрация роутеров
├── requirements.txt # зависимости API ├── pyproject.toml / uv.lock # зависимости API (единственный источник — читает Dockerfile.api через uv)
├── .dockerignore # что не попадает в build context образов
├── .env.example # шаблон переменных окружения без секретов
├── Dockerfile.api # образ API ├── Dockerfile.api # образ API
├── Dockerfile.web # образ фронтенда (Flet)
├── Dockerfile.stats # образ дашборда статистики ├── Dockerfile.stats # образ дашборда статистики
├── docker-compose.yml # API + Web + Stats ├── docker-compose.yml # API + web-react + Stats
├── docker-compose_db.yml # PostgreSQL + Redis ├── docker-compose_db.yml # PostgreSQL + Redis
├── bd/ ├── bd/
@@ -168,13 +172,13 @@ api-copp/
│ ├── stats_crud.py # статистика (кэшируется в Redis, TTL 5 мин) │ ├── stats_crud.py # статистика (кэшируется в Redis, TTL 5 мин)
│ └── transfer_crud.py # экспорт/импорт/бэкап/восстановление БД │ └── transfer_crud.py # экспорт/импорт/бэкап/восстановление БД
├── web/ # Flet-фронтенд (порт 80) ├── web-react/ # React SPA-фронтенд (порт 80, свой Dockerfile+nginx)
│ ├── main.py │ ├── src/api/client.ts # единая точка HTTP-запросов к API
│ ├── router.py │ ├── src/auth/AuthContext.tsx # токен-логин + анонимная сессия (школа+класс без аккаунта)
│ ├── api_client.py │ ├── src/pages/ # страницы (Login, AnonymousEntry, Poll, MyTests, ...)
── designer.py # дизайн-система ── nginx.conf # раздаёт статику SPA, проксирует /api → api:8000
├── radar_chart.py # виджет RadarChart
│ └── views/content_users/ ├── stats/ # отдельный дашборд статистики (Flet, порт 8081)
└── data/ # том Docker (./data:/app/data) └── data/ # том Docker (./data:/app/data)
├── postgres/ # данные PostgreSQL ├── postgres/ # данные PostgreSQL
@@ -259,6 +263,8 @@ GET /polls/users/{user_id}/responses — история пользовате
GET /responses/{id} — конкретный результат с ответами GET /responses/{id} — конкретный результат с ответами
``` ```
`POST /polls/{poll_id}/responses` не требует авторизации: `user_id` необязателен. `web-react` использует это для анонимного прохождения — школьник на `/anonymous` выбирает только школу и класс (без пароля и аккаунта), выбор кладётся в `group_id`/`organization_id` тела запроса вместо `user_id`. Результат сохраняется в базе как снимок школы/класса без привязки к пользователю; истории результатов после закрытия вкладки у анонимной сессии нет — только у настоящих аккаунтов (`/my-tests`).
### Результаты (паутинная диаграмма) ### Результаты (паутинная диаграмма)
``` ```
@@ -446,7 +452,7 @@ radar_svgs/ ← все SVG-файлы (структура папок
### Скачивание из браузера ### Скачивание из браузера
Фронтенд скачивает SVG-байты через внутренний API (httpx, Docker-сеть), кодирует в base64 data URI и открывает в браузере. Браузер не обращается к API напрямую — это важно, так как `api` не имеет публичного адреса. `web-react` отдаёт SVG через тот же same-origin прокси, что и остальной API: `<img src="/api/responses/{id}/radar/image">`, nginx внутри контейнера `web-react` перенаправляет запрос на `api:8000` (см. `web-react/nginx.conf`). Браузер по-прежнему не обращается к `api` напрямую — `api` не публикует порты.
--- ---
@@ -497,7 +503,7 @@ tcp-keepalive 60
## Деплой с внешним прокси ## Деплой с внешним прокси
Минимальная конфигурация nginx: `web-react` — статический SPA + reverse-proxy внутри своего же контейнера (`web-react/nginx.conf`), долгоживущих WebSocket-соединений (как было у Flet) больше нет. Минимальная конфигурация внешнего nginx перед контейнером:
```nginx ```nginx
location / { location / {
@@ -505,24 +511,20 @@ location / {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1; 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` в репозитории всё ещё написаны под старый Flet-фронтенд** (`upstream flet_app`, отдельный `location /ws` для WebSocket, `location /radar-image/`). После перехода на `web-react` эти файлы не актуальны и не обновлялись — `nginx.production.conf`, судя по всему, конфиг боевого внешнего nginx для `test.cloud-copp74.ru`, поэтому его правка не входит в эту уборку без отдельного подтверждения (ошибка здесь ломает публичный домен). Если используете внешний прокси в проде — сверьте его с примером выше перед следующим деплоем.
В репозитории есть `nginx.conf` и `nginx.production.conf` с готовыми примерами.
--- ---
## Известные ошибки и решения ## Известные ошибки и решения
### 1. `api` недоступен из браузера ✅ ### 1. `api` недоступен из браузера ✅ (относилось к старому Flet-фронтенду)
**Проблема:** Docker-имя `api` не резолвится у браузера пользователя. Любой URL вида `http://api:8000/...` в `page.launch_url()` давал ошибку DNS. **Проблема:** Docker-имя `api` не резолвится у браузера пользователя. Любой URL вида `http://api:8000/...` в `page.launch_url()` (Flet) давал ошибку DNS.
**Решение:** Фронтенд скачивает SVG-байты через httpx (внутренняя сеть), конвертирует в `data:image/svg+xml;base64,...` и передаёт готовый data URI в браузер. Прямых обращений к API из браузера нет. **Решение в `web-react`:** same-origin прокси `/api/*` в `web-react/nginx.conf` — браузер всегда обращается к тому же origin, что и отдал SPA, прямых обращений к `api` из браузера нет в принципе (см. «Скачивание из браузера» выше).
--- ---
@@ -540,11 +542,11 @@ location / {
--- ---
### 4. Варианты ответов обрезались ✅ ### 4. Варианты ответов обрезались ✅ (относилось к старому Flet-фронтенду)
**Проблема:** `ft.Radio(label=...)` в Flet не переносит текст метки — длинные варианты обрезались. **Проблема:** `ft.Radio(label=...)` в Flet не переносит текст метки — длинные варианты обрезались.
**Решение:** Кастомные кликабельные контейнеры с `ft.Text(expand=True, no_wrap=False)`. **Решение:** актуально не для этого проекта — в `web-react` варианты ответа рендерятся обычной HTML-разметкой (`PollPage.tsx`), перенос текста работает из коробки.
--- ---
@@ -592,6 +594,24 @@ ALTER TABLE radar_results ADD COLUMN IF NOT EXISTS image_path VARCHAR(512);
--- ---
### 10. Дублирование SVG-генератора ### 10. Дублирование SVG-генератора
**Состояние:** `route/radar_svg_gen.py` (сервер) и `web/radar_svg.py` (клиент) — два разных генератора. Кнопка «Сохранить» скачивает серверный файл. `web/radar_svg.py` не используется кнопками, но файл остался в репозитории. **Было:** `route/radar_svg_gen.py` (сервер) и `web/radar_svg.py` (клиент, Flet) — два разных генератора, второй нигде не вызывался.
**Решение:** старый Flet-фронтенд `web/` целиком удалён вместе с `Dockerfile.web` (заменён на `web-react` в коммите `eac6192`). Единственный генератор SVG теперь — `route/radar_svg_gen.py`.
---
### 11. Реальные секреты в `docker-compose.yml` ✅
**Проблема:** пароли БД/Redis, `SECRET_KEY`, `ADMIN_KEY` были прописаны в открытом виде прямо в `docker-compose.yml` и `docker-compose_db.yml`, закоммичены в git.
**Решение:** значения вынесены в `.env``.gitignore`, не коммитится), compose-файлы читают их через `${VAR}`. Шаблон без секретов — `.env.example`. **Важно:** старые значения уже есть в истории git (коммит `71887b0`) — сама по себе эта правка их не отзывает, реальные пароли на серверах БД/Redis нужно ротировать отдельно.
---
### 12. Зависимости API расходились между `pyproject.toml` и `requirements.txt` ✅
**Проблема:** `Dockerfile.api` ставил зависимости из `requirements.txt`, а не из `pyproject.toml`/`uv.lock` — версии успели разойтись (`redis==5.0.0` vs `>=7.1.0`, `sqlalchemy==2.0.32` vs `>=2.0.47`).
**Решение:** `Dockerfile.api` теперь ставит зависимости через `uv sync --frozen` напрямую из `pyproject.toml`/`uv.lock`. `requirements.txt` удалён — единственный источник правды. При переходе обнаружилась ещё одна расхождение: `python-multipart` (нужен FastAPI для `UploadFile`/`Form`, используется в `organizations_crud.py` при импорте .docx) был в `requirements.txt`, но отсутствовал в `pyproject.toml` — без него API падал при старте с `RuntimeError`. Добавлен в зависимости.