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:
78
README.md
78
README.md
@@ -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`. Добавлен в зависимости.
|
||||||
|
|||||||
Reference in New Issue
Block a user