scoring.py получает движок из docs/rating/rating-system.md: ожидание пары, K 64 → 16 за 20 партий, вес стола G(N), множитель отрыва (темп, цели, миры, clamp [0.5, 2]) и близость по типу победы, включая last_standing. rate_match и replay — чистые функции без БД; replay отдаёт рейтинги без округления, ΔR и результат относительно ожидания по каждой партии. Тесты: 15 примеров раздела 6 с числами документа, совпадение констант и всех ΔR сезона с эталоном simulate.py (полные партии и история без деталей), монотонность и сумма-ноль. League Points пока остаётся в модуле — витрины переводятся отдельным коммитом. #23 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
Forbidden Stars — учёт партий
Мобильное веб-приложение для учёта партий настольной игры Forbidden Stars: профили игроков (вход по логину и паролю или через Telegram), группы с приглашениями, создание партий с рандомом фракций и фильтром по дополнениям группы, фото партий, уведомления, статистика и общий топ, админ-панель.
- Бэкенд / ядро + API: Python · FastAPI · SQLModel · SQLite
- Фронтенд: React · Vite · TypeScript (SPA; с ядром общается по REST API, живые
обновления приходят SSE-потоком
/api/events) - Хостинг: Raspberry Pi 4 (ARM64) в Docker; наружу — через VPS-привратник (см. «Домен и публикация»)
Структура
backend/ FastAPI: ядро, REST API, БД, миграции Alembic, seed, тесты
frontend/ React + Vite SPA
deploy/ публикация и эксплуатация: vps/ (Caddy), pi/ (прод), tunnel/ и backup/ (образы)
scripts/ build-push.* (сборка и пуш образов), fs-backup.* (бэкапы с ПК), export-prod.sh
Dockerfile multi-stage сборка (фронт собирается node, отдаётся FastAPI)
docker-compose.yml прод на Pi: app + tunnel + backup
docker-compose.temp.yml временный прод на ПК вместо Pi: app + tunnel
run.ps1 / run.sh единый лаунчер dev
.env.example шаблон единого .env
Локальная разработка
Всё проверяется в development — отдельного тестового контейнера нет.
Единый лаунчер (run.ps1 / run.sh)
После разовой настройки (ниже) dev запускается одной командой (лаунчер читает
APP_ENV в корневом .env):
.\run.ps1 # Windows (Linux / macOS / Git Bash: ./run.sh)
APP_ENV в .env |
что делает лаунчер |
|---|---|
development |
сначала alembic upgrade head, затем uvicorn --reload (бэк) + vite (фронт) нативно: run.ps1 — в отдельных окнах, run.sh — в текущем терминале (Ctrl+C останавливает оба). При LOCAL_PUBLIC=vps дополнительно поднимает SSH-туннель на forbidden-stars.ru |
production |
не запускает — прод деплоится отдельно (см. «Production» и «Git и деплой») |
| другое значение | отказ: допустимы только development и production (бэкенд тоже не стартует) |
Разовая настройка перед первым запуском — поднять venv бэка и зависимости фронта
(после неё повседневный цикл — просто .\run.ps1). Vite проксирует /api на бэкенд.
1) Бэкенд
Windows PowerShell (команды по одной — в PowerShell 5.1 нет &&):
cd backend
python -m venv .venv
.\.venv\Scripts\Activate.ps1 # если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
pip install -e ".[dev]" # .[dev] — один аргумент (пакет + dev-зависимости)
Copy-Item ..\.env.example ..\.env # ЕДИНЫЙ .env лежит в КОРНЕ репозитория
alembic upgrade head # применит миграции и сидинг справочников
python -m app.bootstrap # справочники + создаст/синхронизирует администратора из .env (опционально)
uvicorn app.main:app --reload --timeout-graceful-shutdown 2 # http://localhost:8000 (Swagger: /api/docs)
Windows cmd.exe (здесь && поддерживается):
cd backend
python -m venv .venv
.venv\Scripts\activate.bat
pip install -e ".[dev]"
copy ..\.env.example ..\.env
alembic upgrade head
python -m app.bootstrap
uvicorn app.main:app --reload --timeout-graceful-shutdown 2
Linux / macOS / Git Bash:
cd backend
python -m venv .venv && . .venv/Scripts/activate # на *nix: . .venv/bin/activate
pip install -e ".[dev]"
cp ../.env.example ../.env
alembic upgrade head
python -m app.bootstrap
uvicorn app.main:app --reload --timeout-graceful-shutdown 2
В dev тот же bootstrap (справочники + админ из
.env) выполняется сам при старте uvicorn, поэтомуpython -m app.bootstrapвручную обычно не нужен. Dev-БД, загрузки и ачивки лежат вbackend/data/dev/(пути в.envотносительные — запускайте изbackend/).
--timeout-graceful-shutdownобязателен. Открытая вкладка держит SSE-поток/api/events, и без лимита--reloadждёт его закрытия вечно — сайт висит на загрузке, а в логе толькоReloading....
Единый
.env— в корне репозитория (ForbidenStarsApp/.env), рядом с.env.example. Его читают и бэкенд (через абсолютный путь, независимо от рабочей папки), иdocker compose. Файл — локальный, на каждой машине свой (dev/prod различаются строкойAPP_ENV).
2) Фронт (в отдельном терминале)
cd frontend
npm install
npm run gen:api # сгенерирует типы из живого OpenAPI (бэкенд должен быть запущен)
npm run dev # http://127.0.0.1:5173 или http://localhost:5173 (оба стека)
Вход в dev-режиме — экран /login: логин/пароль, Telegram и вход по нику без пароля (stub);
в проде stub нет (см. раздел «Аутентификация»).
Production (Docker на Pi)
На Pi нужны только два файла — docker-compose.yml и .env: образы app, tunnel и
backup собираются на ПК под arm64 и пушатся в Gitea-реестр, Pi тянет их сам.
# ПК (обычно с ветки main): собрать и опубликовать образы
docker login gitea.arseniev.info
.\scripts\build-push.ps1
# Pi, папка с docker-compose.yml и .env
docker compose up -d # pull_policy: always — тянет свежие образы, без сборки
Портов на хост нет — прод доступен только на https://forbiddenstars.ru через
туннель-контейнер. FastAPI отдаёт собранный SPA и API с одного origin. Миграции, сидинг
справочников и создание админа выполняются автоматически при старте (entrypoint.sh).
В production закрыты OpenAPI/Swagger, а с дефолтным или коротким SECRET_KEY либо
дефолтным ADMIN_PASSWORD приложение не стартует. Учтите: ADMIN_PASSWORD применяется только
при первом создании админа, дальше его смена в .env ни на что не влияет (задача #73).
Пошагово — deploy/pi/README.md, бэкапы — deploy/backup/README.md.
- Не используйте
$в секретах. Единый.envчитают и pydantic (dev —$дословно), и docker compose (prod —$= подстановка переменной). Чтобы значение совпадало везде, вSECRET_KEY/ADMIN_PASSWORDне должно быть$. Удобно генерировать так:python -c "import secrets;print(secrets.token_urlsafe(48))"(даёт только[A-Za-z0-9_-]). - Временный прод на ПК (вместо Pi):
docker compose -f docker-compose.temp.yml up -d --build— поведение production, локальная сборка x86, ключ туннеля из файлаdeploy/tunnel/id_tunnel, тот же слот VPS 9000, что у Pi (одновременно не запускать).
Аутентификация
Методы входа зависят от окружения (APP_ENV):
| dev | prod | |
|---|---|---|
| Логин (= ник) и пароль | ✓ | ✓ (основной) |
| Telegram Login Widget | ✓ | ✓ |
| Вход по нику без пароля (stub) | ✓ | ✗ (физически отсутствует) |
- Логин и пароль (
app/auth/password.py) — основной вход. Логин — это ник игрока (смена ника меняет логин). Пароль: от 8 символов, не длиннее 72 байт, хранится bcrypt. От перебора — окно 15 минут в памяти процесса: 5 неудач на пару «IP + логин», 20 на IP и 50 на аккаунт (независимо от IP), дальше429 TOO_MANY_ATTEMPTS. Регистраций — не больше 10 с одного IP за окно. Игрок без пароля (из Telegram или созданный до паролей) после входа видит обязательное окно «Задайте пароль»: закрыть его нельзя, только задать пароль или выйти; в dev-сборке есть кнопка «Позже (dev)». В профиле пароль меняется (нужен текущий) и привязывается Telegram (ник не меняется). Забытый пароль задаёт админ на вкладке аккаунтов — почту приложение не хранит. Смена или сброс пароля завершает все прежние сессии игрока (при смене в профиле текущее устройство остаётся в системе); «Выйти» отзывает токен этого устройства. - Stub-вход (по нику) и жёсткое удаление аккаунтов в админке — только для разработки.
Их код физически не попадает в прод: файлы
backend/app/auth/dev_stub.py,backend/app/routers/dev_auth.pyиbackend/app/routers/dev_admin.pyисключены из Docker-образа (.dockerignore), роутеры подключаются лишь приAPP_ENV=development(app/main.py), а на фронте dev-блоки вырезаются из прод-сборки (import.meta.env.DEV). В проде аккаунт можно только отключить. - Telegram: сервер проверяет подпись виджета (HMAC по
TELEGRAM_BOT_TOKEN) и свежесть данных (не старше суток). Первый вход регистрирует игрока под Telegram-тегом; если такой ник занят или некорректен, фронт просит выбрать другой.GET /api/auth/configотдаёт доступные методы иtelegram_bot_usernameдля виджета.
Настройка Telegram (когда будете подключать реальный вход):
- Создать бота у @BotFather → получить токен и username.
/setdomainу BotFather → оба домена:forbiddenstars.ru(prod) иforbidden-stars.ru(dev).- В
.env:TELEGRAM_BOT_TOKEN=...,TELEGRAM_BOT_USERNAME=...(без@). - Виджету нужен HTTPS-домен (см. «Домен и публикация») — по голому HTTP/localhost он не работает.
Админ-вход (секретная панель, логин+пароль) — отдельный механизм, доступен во всех
окружениях. Страница — /admin/login; из интерфейса туда ведёт удержание кнопки «Меню» 10 секунд.
Окружения (dev / prod)
Один и тот же код; контур задаёт APP_ENV в едином .env. Допустимы только
development и production — с любым другим значением бэкенд не стартует:
| dev | prod (Pi) | |
|---|---|---|
| Запуск | .\run.ps1 → uvicorn --reload + vite |
docker compose up -d |
APP_ENV |
development |
production (форсится в compose) |
| Env-файл | единый .env |
единый .env (на Pi) |
| Раздача SPA | Vite (HMR), :5173 |
FastAPI, только через https://forbiddenstars.ru |
| База данных | backend/data/dev/… |
том db-data (/data) |
| Вход игроков | пароль + Telegram + ник (stub) | пароль + Telegram |
Swagger (/api/docs) |
✓ | ✗ |
| Fail-fast по дефолтным секретам | ✗ | ✓ |
- Один
.envна машину в корне (рядом с.env.example). Прод-контейнер значениеAPP_ENVиз него игнорирует и всегдаproduction. - Структура БД одна (общие миграции Alembic), файлы разные: dev →
DEV_DATABASE_URL(backend/data/dev/), prod →PROD_DATABASE_URL(том/data). Так же раздельно лежат загрузки (*_UPLOAD_DIR) и ачивки (*_ACHIEVEMENTS_DIR). - В Docker идёт только прод-код: dev-вход (stub), dev-удаление аккаунтов и тесты физически
исключены из образа (
.dockerignore). - Внимание: данные дева (
backend/data/dev/) сейчас попадают в образ — правилоdata/в.dockerignoreисключает только корневую папкуdata/(задача #71).
Git и деплой
- Ветка
dev— рабочая: весь код, лаунчер, тесты. Повседневная разработка и проверка здесь. - Ветка
main— релиз прода: готовое промоутишь изdevчерезmerge dev→main. Файлы во всех ветках одинаковы (окружение задаёт.env/compose, а не ветка) → merge безболезненный; чистоту прод-образа обеспечивает.dockerignore, а не разные наборы файлов. - Деплой на Pi: на ПК с ветки
main—.\scripts\build-push.ps1(собирает и пушит образы app + tunnel + backup под arm64), на Pi —docker compose up -d. На Windows нужна именно PS-версия скрипта (build-push.shиз PowerShell уходит в WSL). - Чистая выгрузка в папку без git (опц., к деплою на Pi не относится):
scripts/export-prod.sh <dir> [ref]— черезgit archive+export-ignoreиз.gitattributes(без тестов, stub-входа,pyproject.toml, README-файлов и лаунчера).dev_admin.pyвexport-ignoreпока не внесён (задача #70).
Секреты (.env) и данные (data/, *.db) в git не идут — см. .gitignore.
Домен и публикация
Публичные адреса отдаёт VPS-привратник (Caddy + HTTPS твоими сертификатами), а
приложение само открывает к нему SSH reverse-туннель (дома белого IP нет — CGNAT).
У прода туннель — отдельный контейнер в docker-compose.yml, и портов на хост он
не публикует (доступен только через домен):
| домен | как выставляется | слот VPS | |
|---|---|---|---|
| prod | forbiddenstars.ru |
туннель-контейнер (постоянно) → app:8000 |
9000 |
| dev | forbidden-stars.ru |
лаунчер (run.ps1/run.sh) при LOCAL_PUBLIC=vps → localhost:5173 |
9001 |
- Прод (9000) и dev (9001) на разных слотах/доменах → работают одновременно. Временный
прод на ПК (
docker-compose.temp.yml) занимает слот 9000 — одновременно с продом на Pi не запускать. - Dev по умолчанию только на localhost;
LOCAL_PUBLIC=vps+ лаунчер выставляет его на домен. В dev при этом открыты stub-вход по нику и Swagger — держите туннель поднятым только на время проверки (задача #69). COOKIE_SECUREвыводится автоматически (HTTPS-домен ⇒ Secure-cookie; dev+localhost ⇒ нет).- Ключ туннеля: временный прод берёт файл
deploy/tunnel/id_tunnel, прод на Pi —TUNNEL_KEY_B64(base64) в.env, dev-туннель — системныйsshс ключом по умолчанию (~/.ssh). Публичные части — вauthorized_keysпользователяtunnelна VPS. - Пошаговая настройка — в
deploy/:vps/(Caddy, сертификаты, юзерtunnel),pi/(app + туннель + бэкапы в Docker), образ туннеля —deploy/tunnel/.
Бэкапы
Контейнер backup (restic) в docker-compose.yml каждую ночь делает зашифрованный снимок
БД, uploads и achievements — на Pi (том backup-data) и на VPS по SFTP. С ПК снимки
скачиваются со сверкой sha256 (scripts/fs-backup.ps1 pull).
Настройка, восстановление и действия при гибели Pi — deploy/backup/README.md.
Дополнения и фракции
| Дополнение | Фракции |
|---|---|
| База (всем) | Орки, Ультрамарины, Эльдары, Хаоситы |
| Forgotten Worlds | Имперская гвардия, Тау, Некроны, Тираниды |
| Forsaken Voids | Инквизиция, Сёстры битвы, Друкхари, Адептус Механикус |
Группа отмечает имеющиеся дополнения; при создании партии доступны только фракции
этих дополнений (база — всегда). Справочник сидится из backend/app/seed/reference_data.py.
Админ может переименовать фракцию в панели, но сейчас переименование откатывается при
каждом перезапуске приложения (задача #72).