From dd0c29ccad1a1c993de6b1b7bcb0f1ecb7543fab Mon Sep 17 00:00:00 2001 From: jze9 Date: Thu, 16 Jul 2026 11:55:56 +0500 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20README=20=D0=BF=D0=BE=D0=B4=20=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20web-react=20=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D0=B4=D1=91=D0=BD=D0=BD=D1=83?= =?UTF-8?q?=D1=8E=20=D1=83=D0=B1=D0=BE=D1=80=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Документация всё ещё описывала 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 --- README.md | 84 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 5f14e92..ad364c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # api-copp -Платформа ЦОПП Челябинской области для проведения профориентационных тестирований. REST API на FastAPI + веб-интерфейс на Flet. Поддерживает тесты Голланда, Климова и Гломштока с паутинными диаграммами результатов. +Платформа ЦОПП Челябинской области для проведения профориентационных тестирований. REST API на FastAPI + веб-интерфейс на React (SPA). Поддерживает тесты Голланда, Климова и Гломштока с паутинными диаграммами результатов. Тест можно пройти анонимно — достаточно выбрать школу и класс, аккаунт не нужен. --- @@ -25,14 +25,14 @@ ``` Браузер пользователя │ :80 - [ web — Flet ASGI ] ← единственный публичный сервис + [ web-react — nginx (SPA + reverse-proxy /api) ] ← единственный публичный сервис │ http://api:8000 (только Docker-сеть) [ api — FastAPI :8000 ] │ │ [ 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 могут запускаться как: - отдельным 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/redoc` | ReDoc | | `http://localhost:8081` | Дашборд статистики | +> Реальные значения переменных окружения (пароли, ключи) держите в `.env` в корне репозитория — `docker compose` подставляет их автоматически. Шаблон без секретов — в `.env.example`. Файл `.env` в `.gitignore`, в репозиторий не попадает. + ### 3. Инициализация БД (первый запуск) ```bash @@ -99,11 +101,12 @@ curl -X POST http://localhost:8000/polls/glomshtok//seed-scores -H "X-A ## Переменные окружения -> Файл `docker-compose.yml` содержит **демо-значения**. Перед продакшн-деплоем обязательно смените все пароли и ключи. +> `docker-compose.yml` читает значения из `.env` (не коммитится, см. `.env.example` за шаблоном). Перед продакшн-деплоем обязательно смените все пароли и ключи в своём `.env`. | Переменная | Описание | Пример | |---|---|---| | `APP_PATH` | Путь к директории приложения внутри контейнера | `/app` | +| `DEBUG` | Режим отладки API | `False` | | `DB_HOST` | Хост PostgreSQL | `postgres` | | `DB_PORT` | Порт PostgreSQL | `5432` | | `DB_USER` | Пользователь БД | `postgres` | @@ -126,12 +129,13 @@ curl -X POST http://localhost:8000/polls/glomshtok//seed-scores -H "X-A ``` 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 +├── pyproject.toml / uv.lock # зависимости API (единственный источник — читает Dockerfile.api через uv) +├── .dockerignore # что не попадает в build context образов +├── .env.example # шаблон переменных окружения без секретов +├── Dockerfile.api # образ API +├── Dockerfile.stats # образ дашборда статистики +├── docker-compose.yml # API + web-react + Stats +├── docker-compose_db.yml # PostgreSQL + Redis │ ├── bd/ │ ├── __init__.py # Settings (pydantic), make_engine() @@ -168,13 +172,13 @@ api-copp/ │ ├── 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/ +├── web-react/ # React SPA-фронтенд (порт 80, свой Dockerfile+nginx) +│ ├── src/api/client.ts # единая точка HTTP-запросов к API +│ ├── src/auth/AuthContext.tsx # токен-логин + анонимная сессия (школа+класс без аккаунта) +│ ├── src/pages/ # страницы (Login, AnonymousEntry, Poll, MyTests, ...) +│ └── nginx.conf # раздаёт статику SPA, проксирует /api → api:8000 +│ +├── stats/ # отдельный дашборд статистики (Flet, порт 8081) │ └── data/ # том Docker (./data:/app/data) ├── postgres/ # данные PostgreSQL @@ -259,6 +263,8 @@ GET /polls/users/{user_id}/responses — история пользовате 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: ``, 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 location / { @@ -505,24 +511,20 @@ location / { 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` с готовыми примерами. +> **⚠ `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`, поэтому его правка не входит в эту уборку без отдельного подтверждения (ошибка здесь ломает публичный домен). Если используете внешний прокси в проде — сверьте его с примером выше перед следующим деплоем. --- ## Известные ошибки и решения -### 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.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`. Добавлен в зависимости.