diff --git a/.env.example b/.env.example
index 50bda92..add725e 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,5 @@
# ═══════════════════════════════════════════════════════════════════════════
-# Forbidden Stars — единый .env (dev / test / prod)
+# Forbidden Stars — единый .env (dev / prod)
# Скопируйте в .env, заполните секреты. Реальный .env в git НЕ идёт.
# Окружение — строкой APP_ENV (ниже); публикация локалки наружу — LOCAL_PUBLIC.
# ═══════════════════════════════════════════════════════════════════════════
@@ -7,7 +7,6 @@
# ─── ГЛАВНЫЙ ПЕРЕКЛЮЧАТЕЛЬ ────────────────────────────────────────────────────
# Этот параметр читает ЛАУНЧЕР (run.ps1 / run.sh) и решает, что запускать:
# development — нативно: uvicorn --reload + vite, БД в ./data/dev/, вход TG+ник
-# test — прод-клон в Docker локально (порт 8080), вход только TG
# production — НЕ запускается лаунчером; деплой на Pi отдельно (docker compose up -d).
# Прод-контейнер ИГНОРИРУЕТ это значение и всегда production.
APP_ENV=development
@@ -15,21 +14,21 @@ APP_ENV=development
# ─── ПУБЛИКАЦИЯ ЧЕРЕЗ ДОМЕН (VPS-туннель) ─────────────────────────────────────
# LOCAL_PUBLIC — только для DEV на твоём ПК: local = приложение лишь на localhost;
# vps = лаунчер (run.ps1) дополнительно поднимает SSH-туннель → дев на forbidden-stars.ru.
-# TEST и PROD выставляют себя сами через туннель-КОНТЕЙНЕР (docker-compose*.yml) — им
-# LOCAL_PUBLIC не нужен, но VPS_TUNNEL_HOST/USER ниже они тоже читают.
+# PROD выставляет себя сам через туннель-КОНТЕЙНЕР (docker-compose*.yml) — ему
+# LOCAL_PUBLIC не нужен, но VPS_TUNNEL_HOST/USER ниже он тоже читает.
LOCAL_PUBLIC=local
-# Параметры VPS: их читают и dev-туннель (run.ps1), и туннель-контейнер test/prod.
+# Параметры VPS: их читают и dev-туннель (run.ps1), и туннель-контейнер прода.
# Ключ туннеля — в deploy/tunnel/id_tunnel (в git не идёт); pubkey → authorized_keys у tunnel@VPS.
VPS_TUNNEL_HOST=186.246.51.17
VPS_TUNNEL_USER=tunnel
# VPS_TUNNEL_PORT обычно НЕ задают — каждый контур берёт свой слот по умолчанию:
-# dev (run.ps1) → 9001, прод-контейнер → 9000, test-контейнер → 9001.
+# dev (run.ps1) → 9001, прод-контейнер → 9000.
# Раскомментируй и переопредели, только если нужен нестандартный слот.
#VPS_TUNNEL_PORT=9001
# Приватный ключ туннеля в base64 — чтобы на Pi хватило только docker-compose.yml + .env
-# (без файла deploy/tunnel/id_tunnel). Нужен ТОЛЬКО для прод-контейнера на Pi; для dev/test
-# на ПК ключ берётся из файла. Сгенерируй ключ на ПК и закодируй БЕЗ переносов строк:
+# (без файла deploy/tunnel/id_tunnel). Нужен ТОЛЬКО для прод-контейнера на Pi; временный прод
+# на ПК берёт ключ из файла. Сгенерируй ключ на ПК и закодируй БЕЗ переносов строк:
# Git Bash / Linux: base64 -w0 deploy/tunnel/id_tunnel
# PowerShell: [Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path "deploy/tunnel/id_tunnel")))
# Pubkey (deploy/tunnel/id_tunnel.pub) добавь в authorized_keys у tunnel@VPS.
@@ -45,10 +44,10 @@ ADMIN_BOOTSTRAP_ENABLED=true
# Методы входа задаёт APP_ENV: dev → Telegram + stub (вход по нику), prod → только
# Telegram. Для Telegram нужны токен и юзернейм бота (@BotFather). /setdomain у
# BotFather укажи на ОБА домена, где открывается виджет: forbiddenstars.ru (prod)
-# и forbidden-stars.ru (dev/test).
+# и forbidden-stars.ru (dev).
TELEGRAM_BOT_TOKEN=
TELEGRAM_BOT_USERNAME=
-# Внешний адрес (зарезервировано): prod https://forbiddenstars.ru; dev/test https://forbidden-stars.ru.
+# Внешний адрес (зарезервировано): prod https://forbiddenstars.ru; dev https://forbidden-stars.ru.
PUBLIC_BASE_URL=
# ─── БЕЗОПАСНОСТЬ / СЕССИИ ────────────────────────────────────────────────────
@@ -60,12 +59,12 @@ JWT_ALGORITHM=HS256
JWT_USER_TTL_MINUTES=10080
JWT_ADMIN_TTL_MINUTES=480
# COOKIE_SECURE задаётся АВТОМАТИЧЕСКИ по окружению (HTTPS-домен ⇒ Secure-cookie):
-# dev+localhost → false; dev через VPS, test, prod → true. Вручную задавать НЕ нужно.
+# dev+localhost → false; dev через VPS, prod → true. Вручную задавать НЕ нужно.
COOKIE_DOMAIN=
# ─── БАЗА ДАННЫХ (структура общая, файлы РАЗНЫЕ; выбор по APP_ENV) ────────────
-# dev → DEV_DATABASE_URL (файл в ./data/dev/); test и prod → PROD_DATABASE_URL
-# (том /data; у test и prod это РАЗНЫЕ тома контейнера, см. docker-compose*.yml).
+# dev → DEV_DATABASE_URL (файл в ./data/dev/); prod → PROD_DATABASE_URL
+# (том /data контейнера, см. docker-compose*.yml).
DEV_DATABASE_URL=sqlite:///./data/dev/forbidden_stars.db
PROD_DATABASE_URL=sqlite:////data/forbidden_stars.db
diff --git a/.gitattributes b/.gitattributes
index 741036e..f781db5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,9 +4,9 @@
*.sh text eol=lf
backend/entrypoint.sh text eol=lf
-# ── export-ignore: НЕ попадает в `git archive` (чистая выгрузка прод/тест) ─────
+# ── export-ignore: НЕ попадает в `git archive` (чистая выгрузка прода) ─────────
# В git эти файлы есть и доступны на всех ветках (нужны для разработки),
-# но в архив деплоя (scripts/export-*.sh) не идут. На Docker-сборку НЕ влияет —
+# но в архив деплоя (scripts/export-prod.sh) не идут. На Docker-сборку НЕ влияет —
# там чистоту образа обеспечивает .dockerignore.
backend/tests/ export-ignore
backend/app/auth/dev_stub.py export-ignore
diff --git a/.gitignore b/.gitignore
index a860c84..5620531 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,7 +15,7 @@ venv/
data/
backend/dev.db*
-# Env / секреты — коммитим ТОЛЬКО шаблон .env.example (единый .env для dev/test/prod)
+# Env / секреты — коммитим ТОЛЬКО шаблон .env.example (единый .env для dev/prod)
.env
!.env.example
diff --git a/README.md b/README.md
index ba30290..cb6d42a 100644
--- a/README.md
+++ b/README.md
@@ -16,21 +16,22 @@
backend/ FastAPI: ядро, REST API, БД, миграции Alembic, seed, тесты
frontend/ React + Vite SPA
deploy/ публикация и эксплуатация: vps/ (Caddy), pi/ (прод), tunnel/ и backup/ (образы)
-scripts/ build-push.* (сборка и пуш образов), fs-backup.* (бэкапы с ПК), export-*.sh
+scripts/ build-push.* (сборка и пуш образов), fs-backup.* (бэкапы с ПК), export-prod.sh
Dockerfile multi-stage сборка (фронт собирается node, отдаётся FastAPI)
docker-compose.yml прод на Pi: app + tunnel + backup
-docker-compose.test.yml тест-клон прода на ПК: app + tunnel + backup
docker-compose.temp.yml временный прод на ПК вместо Pi: app + tunnel
-run.ps1 / run.sh единый лаунчер dev/test
+run.ps1 / run.sh единый лаунчер dev
.env.example шаблон единого .env
```
## Локальная разработка
+Всё проверяется в `development` — отдельного тестового контейнера нет.
+
### Единый лаунчер (`run.ps1` / `run.sh`)
-После разовой настройки (ниже) dev и test запускаются **одной командой** — что именно,
-решает `APP_ENV` в корневом `.env`:
+После разовой настройки (ниже) dev запускается **одной командой** (лаунчер читает
+`APP_ENV` в корневом `.env`):
```powershell
.\run.ps1 # Windows (Linux / macOS / Git Bash: ./run.sh)
@@ -39,8 +40,8 @@ run.ps1 / run.sh единый лаунчер dev/test
| `APP_ENV` в `.env` | что делает лаунчер |
|---|---|
| `development` | сначала `alembic upgrade head`, затем `uvicorn --reload` (бэк) + `vite` (фронт) нативно: `run.ps1` — в отдельных окнах, `run.sh` — в текущем терминале (Ctrl+C останавливает оба). При `LOCAL_PUBLIC=vps` дополнительно поднимает SSH-туннель на `forbidden-stars.ru` |
-| `test` | `docker compose -f docker-compose.test.yml up --build -d` — прод-клон (app + tunnel + backup); портов на хост нет, открывается на `https://forbidden-stars.ru` |
| `production` | не запускает — прод деплоится отдельно (см. «Production» и «Git и деплой») |
+| другое значение | отказ: допустимы только `development` и `production` (бэкенд тоже не стартует) |
Разовая настройка перед первым запуском — поднять venv бэка и зависимости фронта
(после неё повседневный цикл — просто `.\run.ps1`). Vite проксирует `/api` на бэкенд.
@@ -128,46 +129,19 @@ docker compose up -d # pull_policy: always — тянет свежие
при первом создании админа, дальше его смена в `.env` ни на что не влияет (задача #73).
Пошагово — [`deploy/pi/README.md`](deploy/pi/README.md), бэкапы — [`deploy/backup/README.md`](deploy/backup/README.md).
-## Test — прод-клон в контейнере на ПК
-
-Тот же `Dockerfile` и поведение, что у прода (FastAPI отдаёт SPA, БД на томе, вход игроков
-по логину/паролю или через Telegram), но образ собирается локально — для проверки прод-сборки
-до выката на Pi. Портов на хост **нет**: тест-клон виден только на `https://forbidden-stars.ru`
-через свой туннель-контейнер (ключ — файл `deploy/tunnel/id_tunnel`).
-
-Проще всего — через лаунчер: поставить `APP_ENV=test` в `.env` и запустить `.\run.ps1`.
-Вручную (тот же эффект):
-```bash
-docker compose -f docker-compose.test.yml up -d --build
-# открыть https://forbidden-stars.ru (Swagger: /api/docs)
-docker compose -f docker-compose.test.yml logs -f app
-docker compose -f docker-compose.test.yml down -v # остановить и стереть тестовые данные
-```
-
-- Читает **тот же `.env`**, что dev/prod (отдельного `.env.test` больше нет); внутри
- контейнера `APP_ENV` форсится в `test` (см. `docker-compose.test.yml`). Cookie — Secure
- (снаружи HTTPS).
-- От прода test отличается тем, что OpenAPI/Swagger открыт и **нет** fail-fast по дефолтным
- секретам, хотя контур публичный (задача #69). Не держите в `.env` дефолтные `SECRET_KEY` /
- `ADMIN_PASSWORD`, когда поднимаете test, — особенно после `restore-test` с прод-данными.
-- Данные — на отдельных томах `db-data-test` / `uploads-data-test` / `achievements-data-test`
- (и `backup-data-test` у контейнера бэкапов, он работает без расписания и без VPS);
- с dev и Pi не пересекаются.
-- Слот VPS 9001 общий с dev-туннелем (`LOCAL_PUBLIC=vps`) — поднимайте что-то одно.
-- Вход: **админ-панель** (`/admin/login`) работает сразу по логину/паролю; **игроки** — по
- логину/паролю сразу, через Telegram — при настроенном боте (`/setdomain` → `forbidden-stars.ru`).
-- Учебное восстановление прод-бэкапа в тест-клон — `.\scripts\fs-backup.ps1 restore-test`
- ([`deploy/backup/README.md`, шаг 7](deploy/backup/README.md#шаг-7-учебное-восстановление-на-тест-клоне)).
- **Не используйте `$` в секретах.** Единый `.env` читают и pydantic (dev — `$` дословно),
- и docker compose (test/prod — `$` = подстановка переменной). Чтобы значение совпадало
+ и 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 | test / prod |
+| | dev | prod |
|---|---|---|
| Логин (= ник) и пароль | ✓ | ✓ (основной) |
| Telegram Login Widget | ✓ | ✓ |
@@ -189,7 +163,7 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
`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`).
- В test/prod аккаунт можно только отключить.
+ В проде аккаунт можно только отключить.
- **Telegram:** сервер проверяет подпись виджета (HMAC по `TELEGRAM_BOT_TOKEN`) и свежесть
данных (не старше суток). Первый вход регистрирует игрока под Telegram-тегом; если такой ник
занят или некорректен, фронт просит выбрать другой. `GET /api/auth/config` отдаёт доступные
@@ -197,43 +171,42 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
**Настройка Telegram (когда будете подключать реальный вход):**
1. Создать бота у [@BotFather](https://t.me/BotFather) → получить **токен** и **username**.
-2. `/setdomain` у BotFather → оба домена: `forbiddenstars.ru` (prod) и `forbidden-stars.ru` (dev/test).
+2. `/setdomain` у BotFather → оба домена: `forbiddenstars.ru` (prod) и `forbidden-stars.ru` (dev).
3. В `.env`: `TELEGRAM_BOT_TOKEN=...`, `TELEGRAM_BOT_USERNAME=...` (без `@`).
4. Виджету нужен HTTPS-домен (см. «Домен и публикация») — по голому HTTP/localhost он не работает.
Админ-вход (секретная панель, логин+пароль) — отдельный механизм, доступен во всех
окружениях. Страница — `/admin/login`; из интерфейса туда ведёт удержание кнопки «Меню» 10 секунд.
-## Окружения (dev / test / prod)
+## Окружения (dev / prod)
-Один и тот же код; контур задаёт `APP_ENV` в **едином** `.env` (его читает лаунчер):
+Один и тот же код; контур задаёт `APP_ENV` в **едином** `.env`. Допустимы только
+`development` и `production` — с любым другим значением бэкенд не стартует:
-| | dev | test (прод-клон локально) | prod (Pi) |
-|---|---|---|---|
-| Запуск | `.\run.ps1` → `uvicorn --reload` + `vite` | `.\run.ps1` → `docker compose -f docker-compose.test.yml` | `docker compose up -d` |
-| `APP_ENV` | `development` | `test` (форсится в compose) | `production` (форсится в compose) |
-| Env-файл | единый `.env` | единый `.env` | единый `.env` (на Pi) |
-| Раздача SPA | Vite (HMR), `:5173` | FastAPI, только через `https://forbidden-stars.ru` | FastAPI, только через `https://forbiddenstars.ru` |
-| База данных | `backend/data/dev/…` | том `db-data-test` (`/data`) | том `db-data` (`/data`) |
-| Вход игроков | пароль + Telegram + ник (stub) | пароль + Telegram | пароль + Telegram |
-| Swagger (`/api/docs`) | ✓ | ✓ | ✗ |
-| Fail-fast по дефолтным секретам | ✗ | ✗ | ✓ |
+| | 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` в нём решает, что
- запустит лаунчер (`development`/`test`); прод-контейнер это значение **игнорирует** и всегда
- `production`. Отдельного `.env.test` больше нет.
+- **Один `.env` на машину** в корне (рядом с `.env.example`). Прод-контейнер значение
+ `APP_ENV` из него **игнорирует** и всегда `production`.
- **Структура БД одна** (общие миграции Alembic), **файлы разные**: dev → `DEV_DATABASE_URL`
- (`backend/data/dev/`), test и prod → `PROD_DATABASE_URL` (том `/data`; у test и prod это
- РАЗНЫЕ тома). Так же раздельно лежат загрузки (`*_UPLOAD_DIR`) и ачивки (`*_ACHIEVEMENTS_DIR`).
+ (`backend/data/dev/`), prod → `PROD_DATABASE_URL` (том `/data`). Так же раздельно лежат
+ загрузки (`*_UPLOAD_DIR`) и ачивки (`*_ACHIEVEMENTS_DIR`).
- **В Docker идёт только прод-код:** dev-вход (stub), dev-удаление аккаунтов и тесты физически
- исключены из образа (`.dockerignore`); `test` собирается из того же `Dockerfile`, что и прод,
- просто локально и с `APP_ENV=test`.
+ исключены из образа (`.dockerignore`).
- **Внимание:** данные дева (`backend/data/dev/`) сейчас **попадают** в образ — правило
`data/` в `.dockerignore` исключает только корневую папку `data/` (задача #71).
## Git и деплой
-- Ветка **`dev`** — рабочая: весь код, лаунчер, тесты. Повседневная разработка и `test` здесь.
+- Ветка **`dev`** — рабочая: весь код, лаунчер, тесты. Повседневная разработка и проверка здесь.
- Ветка **`main`** — релиз прода: готовое промоутишь из `dev` через `merge dev→main`. Файлы во
всех ветках одинаковы (окружение задаёт `.env`/compose, а не ветка) → merge безболезненный;
чистоту прод-образа обеспечивает `.dockerignore`, а не разные наборы файлов.
@@ -241,10 +214,9 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
образы app + tunnel + backup под arm64), на Pi — `docker compose up -d`. На Windows нужна
именно PS-версия скрипта (`build-push.sh` из PowerShell уходит в WSL).
- **Чистая выгрузка в папку без git** (опц., к деплою на Pi не относится):
- `scripts/export-prod.sh
[ref]` / `scripts/export-test.sh [ref]` — через
- `git archive` + `export-ignore` из `.gitattributes` (без тестов, stub-входа, `pyproject.toml`,
- README-файлов; у прода ещё без лаунчера и тест-compose, у теста — без прод-compose). `dev_admin.py` в
- `export-ignore` пока не внесён (задача #70).
+ `scripts/export-prod.sh [ref]` — через `git archive` + `export-ignore` из
+ `.gitattributes` (без тестов, stub-входа, `pyproject.toml`, README-файлов и лаунчера).
+ `dev_admin.py` в `export-ignore` пока не внесён (задача #70).
Секреты (`.env`) и данные (`data/`, `*.db`) в git не идут — см. `.gitignore`.
@@ -252,21 +224,21 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
Публичные адреса отдаёт **VPS-привратник** (Caddy + HTTPS твоими сертификатами), а
приложение само открывает к нему SSH reverse-туннель (дома белого IP нет — CGNAT).
-У **test/prod** туннель — **отдельный контейнер** в их `docker-compose`, и портов на хост
-они не публикуют (доступны только через домен):
+У **прода** туннель — **отдельный контейнер** в `docker-compose.yml`, и портов на хост он
+не публикует (доступен только через домен):
| | домен | как выставляется | слот VPS |
|---|---|---|---|
| prod | `forbiddenstars.ru` | туннель-контейнер (постоянно) → `app:8000` | 9000 |
-| test | `forbidden-stars.ru` | туннель-контейнер → `app:8000` | 9001 |
| dev | `forbidden-stars.ru` | лаунчер (`run.ps1`/`run.sh`) при `LOCAL_PUBLIC=vps` → `localhost:5173` | 9001 |
-- Прод (9000) и dev/test (9001) на **разных слотах/доменах** → прод и (dev|test) работают
- одновременно. Dev и test делят слот 9001 → по очереди. Временный прод на ПК
- (`docker-compose.temp.yml`) занимает слот 9000 — одновременно с продом на Pi не запускать.
+- Прод (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 ⇒ нет).
-- Ключ туннеля: test и временный прод берут файл `deploy/tunnel/id_tunnel`, прод на Pi —
+- Ключ туннеля: временный прод берёт файл `deploy/tunnel/id_tunnel`, прод на Pi —
`TUNNEL_KEY_B64` (base64) в `.env`, dev-туннель — системный `ssh` с ключом по умолчанию
(`~/.ssh`). Публичные части — в `authorized_keys` пользователя `tunnel` на VPS.
- Пошаговая настройка — в [`deploy/`](deploy/README.md): `vps/` (Caddy, сертификаты, юзер `tunnel`),
@@ -276,7 +248,7 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
Контейнер `backup` (restic) в `docker-compose.yml` каждую ночь делает зашифрованный снимок
БД, `uploads` и `achievements` — на Pi (том `backup-data`) и на VPS по SFTP. С ПК снимки
-скачиваются и проверяются учебным восстановлением в тест-клон (`scripts/fs-backup.ps1`).
+скачиваются со сверкой sha256 (`scripts/fs-backup.ps1 pull`).
Настройка, восстановление и действия при гибели Pi — [`deploy/backup/README.md`](deploy/backup/README.md).
## Дополнения и фракции
diff --git a/backend/app/bootstrap.py b/backend/app/bootstrap.py
index 5a771a0..62bf9bd 100644
--- a/backend/app/bootstrap.py
+++ b/backend/app/bootstrap.py
@@ -38,7 +38,7 @@ def _ensure_admin(session: Session) -> None:
# Администратор уже существует.
if not settings.is_development:
- # В test/prod пароль НЕ перезаписываем (мог быть изменён через панель).
+ # В prod пароль НЕ перезаписываем (мог быть изменён через панель).
return
# DEV: подтягиваем логин/пароль из .env (env — источник истины в деве).
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
index 0085fbe..dfca3b6 100644
--- a/backend/app/core/config.py
+++ b/backend/app/core/config.py
@@ -4,7 +4,7 @@ from __future__ import annotations
from functools import lru_cache
from pathlib import Path
-from pydantic import model_validator
+from pydantic import field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
# Единый .env лежит в КОРНЕ репозитория (рядом с .env.example) — читается одинаково
@@ -18,6 +18,9 @@ _DEFAULT_SECRET_KEY = "change-me-dev-secret-not-for-production"
_DEFAULT_ADMIN_PASSWORD = "change-me-admin-password"
_MIN_SECRET_KEY_LENGTH = 32
+# Допустимые окружения. Отдельного test-контура нет: всё проверяется в development.
+_APP_ENVS = ("development", "production")
+
class Settings(BaseSettings):
model_config = SettingsConfigDict(
@@ -27,14 +30,13 @@ class Settings(BaseSettings):
case_sensitive=False,
)
- # ── Главный переключатель окружения: development | test | production ───────
+ # ── Главный переключатель окружения: development | production ─────────────
# development — нативный dev (uvicorn + vite), БД в ./data/dev/, вход Telegram+ник.
- # test — прод-клон в Docker локально (порт 8080), ведёт себя как прод.
# production — Docker на Pi; контейнер форсит это значение, игнорируя .env.
app_env: str = "development"
log_level: str = "INFO"
- # Публикация локального окружения (dev/test) наружу через VPS-туннель.
+ # Публикация локального dev-окружения наружу через VPS-туннель.
# Читает ЛАУНЧЕР (run.ps1/run.sh): local — только localhost; vps — плюс SSH-туннель
# на forbidden-stars.ru. Влияет на cookie_secure (vps ⇒ снаружи HTTPS ⇒ Secure-cookie).
local_public: str = "local"
@@ -91,17 +93,13 @@ class Settings(BaseSettings):
стартовый bootstrap в lifespan и синхронизацию админа из .env."""
return self.app_env.lower() == "development"
- @property
- def is_test(self) -> bool:
- return self.app_env.lower() == "test"
-
@property
def is_production(self) -> bool:
return self.app_env.lower() == "production"
@property
def database_url(self) -> str:
- """БД: dev — отдельный файл дева; test и prod — том контейнера (/data)."""
+ """БД: dev — отдельный файл дева; prod — том контейнера (/data)."""
return self.dev_database_url if self.is_development else self.prod_database_url
@property
@@ -122,13 +120,24 @@ class Settings(BaseSettings):
def cookie_domain_value(self) -> str | None:
return self.cookie_domain or None
+ @field_validator("app_env")
+ @classmethod
+ def _known_app_env(cls, value: str) -> str:
+ """Неизвестное окружение — ошибка старта, а не молчаливое «почти прод»: любое
+ значение, кроме development, выбирает прод-пути к данным и выключает dev-вход."""
+ if value.lower() not in _APP_ENVS:
+ raise ValueError(
+ f"APP_ENV={value!r} не поддерживается — допустимо: {', '.join(_APP_ENVS)}"
+ )
+ return value
+
@model_validator(mode="after")
def _forbid_default_secrets_in_prod(self) -> "Settings":
"""Fail-fast: в production не стартуем с дефолтными/слабыми секретами (#59).
Деплой, скопировавший .env.example дословно (или забывший поле), иначе поднялся бы
с общеизвестным ключом подписи JWT (подделка любого токена, включая админский) и
- известным паролем администратора. В dev/test проверка не мешает — там дефолты норма."""
+ известным паролем администратора. В dev проверка не мешает — там дефолты норма."""
if self.app_env.lower() != "production":
return self
problems: list[str] = []
diff --git a/backend/app/main.py b/backend/app/main.py
index b91cb9c..856034c 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -132,7 +132,7 @@ async def _lifespan(_app: FastAPI):
hub.bind_loop(asyncio.get_running_loop())
# В DEV приложение само подтягивает справочники и админа из .env при старте
- # (в test/prod это делает entrypoint.sh; в pytest отключено FS_STARTUP_BOOTSTRAP=0).
+ # (в prod это делает entrypoint.sh; в pytest отключено FS_STARTUP_BOOTSTRAP=0).
if settings.is_development and os.getenv("FS_STARTUP_BOOTSTRAP", "1") != "0":
try:
from app.bootstrap import bootstrap
@@ -151,7 +151,7 @@ async def _lifespan(_app: FastAPI):
def create_app() -> FastAPI:
- # Схему API (openapi.json + Swagger/ReDoc) отдаём только в dev/test: она нужна для
+ # Схему API (openapi.json + Swagger/ReDoc) отдаём только в dev: она нужна для
# `npm run gen:api` (генерация типов фронта) и удобной отладки. В production закрываем —
# незачем облегчать разведку поверхности API анонимам (#61).
docs_enabled = not settings.is_production
@@ -165,7 +165,7 @@ def create_app() -> FastAPI:
)
# CORS нужен только в dev (vite на :5173 и API на :8000 — разные origin).
- # В test/prod (и dev через VPS-туннель) всё single-origin → CORS не подключаем.
+ # В prod (и в dev через VPS-туннель) всё single-origin → CORS не подключаем.
if settings.is_development and settings.cors_origins_list:
app.add_middleware(
CORSMiddleware,
@@ -204,7 +204,7 @@ def create_app() -> FastAPI:
app.include_router(r, prefix="/api")
# DEV-роутеры (вход по нику, жёсткое удаление аккаунтов) — только в development
- # и только если код физически есть (в test/prod-образе dev_*-файлы исключены
+ # и только если код физически есть (в прод-образе dev_*-файлы исключены
# .dockerignore, импорт просто не выполнится).
if settings.is_development:
for mod_name in ("dev_auth", "dev_admin"):
diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py
index a3e5314..a07e8da 100644
--- a/backend/app/routers/admin.py
+++ b/backend/app/routers/admin.py
@@ -153,7 +153,7 @@ def set_user_password(
# Удаление аккаунта — намеренно НЕ здесь: это dev-only возможность, вынесена в
-# routers/dev_admin.py (исключён из прод/тест-образа). В проде аккаунт только
+# routers/dev_admin.py (исключён из прод-образа). В проде аккаунт только
# отключается (PATCH is_active), удалять нельзя.
diff --git a/backend/app/routers/dev_admin.py b/backend/app/routers/dev_admin.py
index 9e699f3..50e89af 100644
--- a/backend/app/routers/dev_admin.py
+++ b/backend/app/routers/dev_admin.py
@@ -1,9 +1,9 @@
"""DEV-ТОЛЬКО роутер: жёсткое удаление аккаунта игрока.
-Этот файл ФИЗИЧЕСКИ исключён из прод/тест-образа (.dockerignore), а роутер
+Этот файл ФИЗИЧЕСКИ исключён из прод-образа (.dockerignore), а роутер
подключается лишь когда APP_ENV == development (см. app/main.py). На фронте кнопка
удаления вырезается из прод-сборки тришейкингом (import.meta.env.DEV). Так
-возможность удаления не попадает ни в прод, ни в тест — там аккаунт можно только
+возможность удаления не попадает в прод — там аккаунт можно только
отключить (PATCH is_active).
Семантика («вычёркивание из партий»): аккаунт удаляется, а партии сохраняются —
diff --git a/backend/app/services/admin_service.py b/backend/app/services/admin_service.py
index d9b2334..c445df2 100644
--- a/backend/app/services/admin_service.py
+++ b/backend/app/services/admin_service.py
@@ -74,7 +74,7 @@ def set_player_password(session: Session, user_id: int, new_password: str) -> Us
# Жёсткое удаление пользователя — dev-only, в services/admin_service нет намеренно:
-# логика вынесена в routers/dev_admin.py (файл исключён из прод/тест-образа).
+# логика вынесена в routers/dev_admin.py (файл исключён из прод-образа).
# ─── Группы ──────────────────────────────────────────────────────────────────
diff --git a/backend/tests/test_api_hardening.py b/backend/tests/test_api_hardening.py
index 4de3185..d27bb3e 100644
--- a/backend/tests/test_api_hardening.py
+++ b/backend/tests/test_api_hardening.py
@@ -1,4 +1,4 @@
-"""Хардненинг API: раскрытие схемы закрыто в production, открыто в dev/test (#61, F6)."""
+"""Хардненинг API: раскрытие схемы закрыто в production, открыто в dev (#61, F6)."""
from __future__ import annotations
from fastapi.testclient import TestClient
diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py
index 7079c1b..af2e252 100644
--- a/backend/tests/test_auth.py
+++ b/backend/tests/test_auth.py
@@ -23,14 +23,12 @@ def test_enabled_methods_by_env(monkeypatch):
monkeypatch.setattr(settings, "app_env", "development")
assert set(enabled_methods()) == {"password", "telegram", "stub"}
- monkeypatch.setattr(settings, "app_env", "test")
- assert enabled_methods() == ["password", "telegram"] # test (прод-клон) → без stub
monkeypatch.setattr(settings, "app_env", "production")
assert enabled_methods() == ["password", "telegram"] # prod → без stub
def test_env_flags_and_db_path(monkeypatch):
- """dev → файл дева; test и prod → том /data (общая ветвь is_development)."""
+ """dev → файл дева; prod → том /data."""
from app.core.config import settings
monkeypatch.setattr(settings, "dev_database_url", "sqlite:///dev.db")
@@ -38,8 +36,6 @@ def test_env_flags_and_db_path(monkeypatch):
monkeypatch.setattr(settings, "app_env", "development")
assert settings.is_development and settings.database_url == "sqlite:///dev.db"
- monkeypatch.setattr(settings, "app_env", "test")
- assert settings.is_test and settings.database_url == "sqlite:////data/prod.db"
monkeypatch.setattr(settings, "app_env", "production")
assert settings.is_production and settings.database_url == "sqlite:////data/prod.db"
diff --git a/backend/tests/test_config_security.py b/backend/tests/test_config_security.py
index 8c73066..965d38e 100644
--- a/backend/tests/test_config_security.py
+++ b/backend/tests/test_config_security.py
@@ -65,3 +65,15 @@ def test_development_allows_defaults():
admin_password=config._DEFAULT_ADMIN_PASSWORD,
)
assert s.is_development
+
+
+@pytest.mark.parametrize("app_env", ["test", "staging", ""])
+def test_unknown_app_env_rejected(app_env):
+ # Отдельного test-контура больше нет: такое значение не должно молча включать прод-пути.
+ with pytest.raises(ValidationError):
+ config.Settings(app_env=app_env)
+
+
+def test_app_env_case_insensitive():
+ s = config.Settings(app_env="Development")
+ assert s.is_development
diff --git a/deploy/README.md b/deploy/README.md
index 3a74a4d..6388984 100644
--- a/deploy/README.md
+++ b/deploy/README.md
@@ -1,6 +1,6 @@
# Публикация: домены, VPS, туннели
-Приложение крутится дома (Pi — прод) и на твоём ПК (dev/test). Дома белого IP нет
+Приложение крутится дома (Pi — прод) и на твоём ПК (dev). Дома белого IP нет
(CGNAT), поэтому наружу выставляем через **VPS-привратник**: на нём Caddy терминирует
HTTPS твоими сертификатами и проксирует трафик в SSH reverse-туннели, которые
приложение само открывает к VPS.
@@ -11,10 +11,8 @@ HTTPS твоими сертификатами и проксирует трафи
│ ▲ туннель-КОНТЕЙНЕР │
│ └── Pi : app:8000 PROD │
forbidden-stars.ru ──►│ :443 (cert твой) → 127.0.0.1:9001 │
- │ ▲ контейнер (test) ИЛИ │
- │ ▲ ssh с ПК (dev) │
- │ ├── ПК test : app:8000 │
- │ └── ПК dev : vite:5173 │
+ │ ▲ ssh с ПК (по требованию) │
+ │ └── ПК dev : vite:5173 DEV │
└───────────────────────────────────────────────────┘
```
@@ -23,32 +21,26 @@ HTTPS твоими сертификатами и проксирует трафи
хост нет** — наружу его выставляет только туннель-контейнер (`ssh -R 9000:app:8000` к VPS).
Работает постоянно: при обрыве `ssh` завершается, и Docker перезапускает контейнер
(`restart: unless-stopped`). См. [`pi/`](pi/README.md).
-- **TEST** — ПК. То же самое: `docker compose -f docker-compose.test.yml up` собирает образы
- локально и поднимает `app` + `tunnel` (`ssh -R 9001:app:8000`) + `backup` (без расписания и
- без VPS). Портов на хост нет — тест виден только на `forbidden-stars.ru`. Обычно
- запускается лаунчером при `APP_ENV=test`.
- **DEV** — ПК, нативно (`uvicorn`+`vite`). По умолчанию только на localhost; при
`LOCAL_PUBLIC=vps` лаунчер (`run.ps1` / `run.sh`) дополнительно поднимает SSH-туннель с ПК
- (`ssh -R 9001:localhost:5173`) → дев виден на `forbidden-stars.ru`.
-- DEV и TEST делят слот **9001** (`forbidden-stars.ru`) → поднимай что-то **одно за раз**.
- PROD на отдельном слоте **9000** (`forbiddenstars.ru`) — работает независимо.
+ (`ssh -R 9001:localhost:5173`) → дев виден на `forbidden-stars.ru` (слот **9001**).
+- PROD на отдельном слоте **9000** (`forbiddenstars.ru`) — работает независимо от dev.
Временный прод на ПК (`docker-compose.temp.yml`) тоже занимает **9000** — одновременно с Pi нельзя.
Ключи туннеля (приватные, в git не идут; публичные части — в `authorized_keys` пользователя
`tunnel` на VPS):
- **Pi** — `TUNNEL_KEY_B64` (base64 приватного ключа) в `.env`; файла ключа на Pi нет.
-- **ПК, test и временный прод** — файл `deploy/tunnel/id_tunnel`, монтируется в туннель-контейнер.
+- **ПК, временный прод** — файл `deploy/tunnel/id_tunnel`, монтируется в туннель-контейнер.
- **ПК, dev** — `run.ps1`/`run.sh` зовут системный `ssh` без `-i`, то есть с ключом по
умолчанию из `~/.ssh`. Он должен быть в `authorized_keys` (можно тем же, что `id_tunnel`).
Настройка по шагам:
1. **VPS** — [`vps/README.md`](vps/README.md): Caddy, файрвол, пользователь `tunnel`, сертификаты, `Caddyfile`.
2. **Pi (прод)** — [`pi/README.md`](pi/README.md): ключ туннеля в `TUNNEL_KEY_B64`, `.env`, `docker compose up -d`.
-3. **ПК (dev/test)** — ключ в `deploy/tunnel/id_tunnel` (для test-контейнера) и/или ключ по
- умолчанию в `~/.ssh` (для dev-туннеля); pubkey — в `authorized_keys` у `tunnel@VPS`.
+3. **ПК (dev)** — ключ по умолчанию в `~/.ssh` (для dev-туннеля) и, если нужен временный прод,
+ файл `deploy/tunnel/id_tunnel`; pubkey — в `authorized_keys` у `tunnel@VPS`.
4. **Бэкапы** — [`backup/README.md`](backup/README.md): контейнер `backup` (restic) делает
- снимки на Pi и на VPS (`fsbackup@VPS`, только SFTP), скрипты ПК скачивают их и проверяют
- восстановление на тест-клоне.
+ снимки на Pi и на VPS (`fsbackup@VPS`, только SFTP), скрипты ПК скачивают их на ПК.
Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, `id_tunnel*`, `id_backup*`) живут на
VPS/Pi/ПК, в репозитории только `Caddyfile`, страница-заглушка, образы `deploy/tunnel/` и
diff --git a/deploy/backup/README.md b/deploy/backup/README.md
index 949a464..8048f06 100644
--- a/deploy/backup/README.md
+++ b/deploy/backup/README.md
@@ -16,7 +16,7 @@
4. [Сборка и публикация образов](#шаг-4-сборка-и-публикация-образов) — ПК
5. [Pi: включить бэкапы](#шаг-5-pi-включить-бэкапы) — Pi
6. [ПК: доступ к Pi и выгрузка бэкапов](#шаг-6-пк-доступ-к-pi-и-выгрузка-бэкапов) — ПК
-7. [Учебное восстановление на тест-клоне](#шаг-7-учебное-восстановление-на-тест-клоне) — ПК
+7. [Проверка скачанного архива](#шаг-7-проверка-скачанного-архива) — ПК
8. [Восстановление прода](#8-восстановление-прода) — Pi
9. [Катастрофа: Pi умер](#9-катастрофа-pi-умер) — новый Pi
10. [Повседневные действия](#10-повседневные-действия)
@@ -562,43 +562,42 @@
---
-## Шаг 7. Учебное восстановление на тест-клоне
+## Шаг 7. Проверка скачанного архива
-**Где:** ПК с Docker Desktop. **Зачем:** убедиться, что бэкап действительно
-восстанавливается, **до** того как это понадобится по-настоящему. Прод не затрагивается.
+**Где:** ПК. **Зачем:** убедиться, что БД в скачанном снимке целая и в ней те данные, что
+ожидаются, **до** того как это понадобится по-настоящему. Прод не затрагивается.
-> Данные тест-клона на ПК будут заменены данными из архива. Прежние данные тест-клона
-> сохраняются в его собственный снимок `pre-restore`.
+> Отдельного тестового контейнера для учебного восстановления больше нет. Сам механизм
+> `restore`/`import` отрабатывает только на Pi ([раздел 8](#8-восстановление-прода)); здесь
+> проверяется содержимое архива.
-1. Восстановите скачанный архив в тест-клон (подставьте имя своего файла):
+1. Распакуйте последний скачанный архив во временную папку:
```powershell
- .\scripts\fs-backup.ps1 restore-test -File backups\fs_20260914_0400_3f2a9c1d.tar
+ $f = (Get-ChildItem backups\fs_*.tar | Sort-Object LastWriteTime | Select-Object -Last 1).FullName
+ $d = Join-Path $env:TEMP "fs-check"; New-Item -ItemType Directory -Force $d | Out-Null
+ tar -xf $f -C $d
```
- В первый раз Docker соберёт образы тест-клона — это несколько минут.
+2. Откройте `%TEMP%\fs-check\forbidden_stars.db` в [DB Browser for SQLite](https://sqlitebrowser.org)
+ (вкладка «Выполнить SQL») и выполните:
-2. Проверьте, что приложение тест-клона поднялось:
+ ```sql
+ PRAGMA integrity_check;
+ SELECT (SELECT count(*) FROM users WHERE role = 'player') AS players,
+ (SELECT count(*) FROM matches) AS matches;
+ ```
+
+3. Закройте DB Browser и удалите временную папку — данные в ней не зашифрованы:
```powershell
- docker compose -f docker-compose.test.yml ps
- docker compose -f docker-compose.test.yml logs --tail 20 app
+ Remove-Item -Recurse -Force (Join-Path $env:TEMP "fs-check")
```
-3. Посмотрите на сайт: в `.env` на ПК временно поставьте `APP_ENV=test` и запустите
- `.\run.ps1`. Тест-клон откроется на `https://forbidden-stars.ru`: проверьте топ,
- профили, историю партий. Потом верните `APP_ENV=development`.
-
**Что должно получиться:**
-- в выводе `restore-test`:
- - `Развёрнутые данные в порядке: игроков N, партий M.` — те же числа, что в `list` на Pi;
- - `Данные восстановлены.`;
- - `Done. The test clone now runs on the restored data.`;
-- `docker compose ... ps` показывает `app` в состоянии `Up … (healthy)`;
-- на сайте тест-клона — данные прода на момент снимка.
-
-> Этим же способом можно восстановить в тест-клон старые архивы `fs_*.tar.gz` прежнего
-> `scripts/backup.sh`: `.\scripts\fs-backup.ps1 restore-test -File backups\fs_20260710_140914.tar.gz`.
+- `PRAGMA integrity_check` → `ok`;
+- `players` и `matches` совпадают со столбцами `Игроков` / `Партий` этого снимка в `list` на Pi;
+- в папке рядом с БД есть `uploads\…` (фото партий) и, если заводились, `achievements\…`.
---
@@ -651,8 +650,9 @@
Если локальный репозиторий повреждён или пуст, смотрите копию на VPS:
`docker compose exec backup fs-backup list vps`.
-2. **По желанию, но рекомендуется:** сначала отрепетируйте на ПК:
- `.\scripts\fs-backup.ps1 pull -Snapshot `, затем `restore-test` ([шаг 7](#шаг-7-учебное-восстановление-на-тест-клоне)).
+2. **По желанию, но рекомендуется:** перед восстановлением проверьте выбранный снимок на ПК:
+ `.\scripts\fs-backup.ps1 pull -Snapshot `, затем [шаг 7](#шаг-7-проверка-скачанного-архива).
+ Заодно у вас останется копия этого снимка вне Pi.
3. Остановите приложение. Сайт покажет страницу «Технические шоколадки»:
@@ -806,7 +806,7 @@
- **Перед каждым обновлением прода** — `now -Tag before-update` (метка — латиница, цифры, `.`, `_`, `-`).
- **Раз в месяц:**
- скачать снимок на ПК (`pull`);
- - раз в пару месяцев сделать учебное восстановление (`restore-test`);
+ - проверить скачанный архив ([шаг 7](#шаг-7-проверка-скачанного-архива));
- удалить с ПК старые архивы — они не зашифрованы.
- **Иногда:** посмотреть `docker compose ps`. Статус `unhealthy` у `backup` означает, что
бэкапы перестали проходить (причину покажет `fs-backup status`).
@@ -899,7 +899,7 @@ docker volume rm <имя тома>
| `WARNING: UNPROTECTED PRIVATE KEY FILE!` | у файла ключа слишком открытые права (ключ создан в Git Bash/WSL или скопирован) | `icacls <путь к ключу> /inheritance:r /grant:r "$($env:USERNAME):(R)"` |
| `Checksum mismatch … run pull again` | файл повредился при передаче | повторить `pull` (битый файл уже удалён) |
| `Already downloaded: …` | этот снимок уже скачан | ничего не делать; нужен новый — сначала `now`, потом `pull` |
-| `restore-test`: `Import failed … The test clone data was not changed` | архив повреждён или неполный | скачать заново (`pull`); текст ошибки выше в выводе |
+| шаг 7: `integrity_check` не `ok` или счётчики не совпадают с `list` | архив повреждён или скачан не тот снимок | удалить файл из `backups\` и скачать заново (`pull -Snapshot `); если повторяется — `fs-backup verify` на Pi |
---
@@ -935,11 +935,9 @@ docker volume rm <имя тома>
|---|---|
| `status`, `list [-Repo vps]`, `now [-Tag имя]`, `verify` | то же, что на Pi, но с ПК |
| `pull [-Snapshot ID] [-Repo vps]` | скачать снимок в `backups\` со сверкой sha256 |
-| `restore-test -File <архив>` | учебное восстановление в локальный тест-клон |
-| `-Target test` | выполнить `status`/`list`/`now`/`verify`/`pull` на локальном тест-клоне |
В bash-версии те же команды пишутся так: `list vps`, `now --tag имя`,
-`pull --repo vps`, `restore-test <архив>`, `--test` первым аргументом.
+`pull --repo vps`.
### Переменные `.env`
@@ -987,7 +985,7 @@ docker volume rm <имя тома>
- [ ] На Pi первый бэкап прошёл в `local` и `vps`, `status` без ошибок (шаг 5)
- [ ] `docker compose ps` показывает `backup` `(healthy)` (шаг 5)
- [ ] С ПК `status`, `list`, `pull` работают без пароля (шаг 6)
-- [ ] Учебное восстановление на тест-клоне прошло, данные на месте (шаг 7)
+- [ ] Скачанный архив проверен: БД целая, числа совпадают с `list` (шаг 7)
**Через сутки**
- [ ] В `list` появился снимок с меткой `scheduled` в 04:00
diff --git a/deploy/tunnel/tunnel.sh b/deploy/tunnel/tunnel.sh
index 78df777..7c99650 100644
--- a/deploy/tunnel/tunnel.sh
+++ b/deploy/tunnel/tunnel.sh
@@ -10,7 +10,7 @@ VPS_TUNNEL_USER="${VPS_TUNNEL_USER:-tunnel}"
UPSTREAM="${UPSTREAM:-app:8000}"
# Источник приватного ключа: либо TUNNEL_KEY_B64 (base64 в .env — прод: только compose+env),
-# либо смонтированный файл /key/id_tunnel (dev/test, где репозиторий есть на хосте).
+# либо смонтированный файл /key/id_tunnel (временный прод на ПК, где репозиторий есть на хосте).
mkdir -p /root/.ssh
KEY=/root/.ssh/id_tunnel
if [ -n "${TUNNEL_KEY_B64:-}" ]; then
diff --git a/deploy/vps/Caddyfile b/deploy/vps/Caddyfile
index c4f4f7f..08ad849 100644
--- a/deploy/vps/Caddyfile
+++ b/deploy/vps/Caddyfile
@@ -2,7 +2,7 @@
# Два домена, ОБА с твоими сертификатами; проксируют в SSH-туннели:
#
# forbiddenstars.ru → 127.0.0.1:9000 ← Pi (autossh, постоянно) PROD
-# forbidden-stars.ru → 127.0.0.1:9001 ← ПК (по требованию) DEV/TEST
+# forbidden-stars.ru → 127.0.0.1:9001 ← ПК (по требованию) DEV
#
# Caddy сам терминирует TLS (он и есть edge: видит реального клиента), а вниз к
# приложению передаёт X-Forwarded-Proto=https / X-Forwarded-For / Host —
@@ -38,10 +38,10 @@
-Server
}
- # Content-Security-Policy подготовлена, но ВЫКЛЮЧЕНА до проверки на test-клоне: строгая
- # политика легко ломает SPA (инлайновые стили Vite), Telegram-виджет входа (скрипт с
- # telegram.org + iframe oauth.telegram.org) и EventSource (/api/events). Раскомментировать
- # после проверки на forbidden-stars.ru, что вход и реал-тайм работают (#61).
+ # Content-Security-Policy подготовлена, но ВЫКЛЮЧЕНА до проверки: строгая политика легко
+ # ломает SPA (инлайновые стили Vite), Telegram-виджет входа (скрипт с telegram.org + iframe
+ # oauth.telegram.org) и EventSource (/api/events). Раскомментировать после проверки,
+ # что вход и реал-тайм работают (#61).
# header Content-Security-Policy "default-src 'self'; script-src 'self' https://telegram.org https://oauth.telegram.org; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-src https://oauth.telegram.org; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
# HTML-документ (навигации, Accept: text/html) НЕ кэшируем. Иначе браузер отдаёт старый
diff --git a/deploy/vps/README.md b/deploy/vps/README.md
index d2bb558..ecc4a94 100644
--- a/deploy/vps/README.md
+++ b/deploy/vps/README.md
@@ -1,11 +1,11 @@
# VPS (186.246.51.17) — реверс-прокси Caddy + точка входа SSH-туннелей
Единственная публичная точка. На VPS: Caddy терминирует HTTPS твоими сертификатами
-для двух доменов и проксирует трафик в SSH reverse-туннели от Pi (прод) и ПК (dev/test).
+для двух доменов и проксирует трафик в SSH reverse-туннели от Pi (прод) и ПК (dev).
```
-forbiddenstars.ru → 127.0.0.1:9000 ← Pi (туннель-контейнер, постоянно) PROD
-forbidden-stars.ru → 127.0.0.1:9001 ← ПК (контейнер test или ssh dev, по требованию) DEV/TEST
+forbiddenstars.ru → 127.0.0.1:9000 ← Pi (туннель-контейнер, постоянно) PROD
+forbidden-stars.ru → 127.0.0.1:9001 ← ПК (ssh из лаунчера, по требованию) DEV
```
> Туннель `ssh -R` по умолчанию слушает на loopback VPS (127.0.0.1) — ровно туда смотрит
@@ -106,8 +106,8 @@ systemctl reload caddy
> файл читается на каждый запрос).
## 7. Проверка
-1. Подними туннель прода на Pi (см. `deploy/pi/README.md`) и/или dev/test на ПК
- (лаунчер `run.ps1`: dev — при `LOCAL_PUBLIC=vps`, test — при `APP_ENV=test`).
+1. Подними туннель прода на Pi (см. `deploy/pi/README.md`) и/или dev на ПК
+ (лаунчер `run.ps1` при `LOCAL_PUBLIC=vps`).
2. Открой `https://forbiddenstars.ru` и `https://forbidden-stars.ru`.
3. Пока соответствующий туннель не поднят — Caddy отдаёт страницу-заглушку «Технические шоколадки»
(HTTP 503), это ожидаемо.
diff --git a/docker-compose.temp.yml b/docker-compose.temp.yml
index 3baaf72..f949d8e 100644
--- a/docker-compose.temp.yml
+++ b/docker-compose.temp.yml
@@ -5,7 +5,7 @@
#
# Отличия от docker-compose.yml (прод на Pi):
# • локальный образ (сборка x86 на ПК), НЕ из реестра и НЕ пушится;
-# • отдельный проект (name) и свои тома — не конфликтует с dev/test на этом ПК.
+# • отдельный проект (name) и свои тома — не конфликтует с dev на этом ПК.
# Всё остальное — как у прода (APP_ENV=production, туннель на 9000, лимиты, healthcheck).
#
# Запуск: docker compose -f docker-compose.temp.yml up -d --build
diff --git a/docker-compose.yml b/docker-compose.yml
index 23697e6..bda1dd3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -84,7 +84,7 @@ services:
restart: unless-stopped
environment:
BACKUP_PASSWORD: ${BACKUP_PASSWORD:-} # пароль шифрования; пусто = бэкапы отключены
- BACKUP_HOSTNAME: fs-prod # имя хоста в снимках (у тест-клона — fs-test)
+ BACKUP_HOSTNAME: fs-prod # имя хоста в снимках
BACKUP_SCHEDULE: ${BACKUP_SCHEDULE:-0 4 * * *}
BACKUP_VERIFY_SCHEDULE: ${BACKUP_VERIFY_SCHEDULE:-30 5 * * 0}
BACKUP_KEEP_DAILY: ${BACKUP_KEEP_DAILY:-14}
diff --git a/frontend/src/pages/admin/AdminAccountsPage.tsx b/frontend/src/pages/admin/AdminAccountsPage.tsx
index cc1b46f..29294e4 100644
--- a/frontend/src/pages/admin/AdminAccountsPage.tsx
+++ b/frontend/src/pages/admin/AdminAccountsPage.tsx
@@ -6,7 +6,7 @@ import { Spinner } from "../../components/Spinner";
import { useToast } from "../../context/ToastContext";
import { useAdminSetPassword, useAdminUpdateUser, useAdminUsers } from "../../hooks/admin";
// DEV-ТОЛЬКО: удаление аккаунтов. Импорт используется лишь под import.meta.env.DEV,
-// поэтому в прод/тест-сборке вырезается тришейкингом (как и dev-вход).
+// поэтому в прод-сборке вырезается тришейкингом (как и dev-вход).
import { DevDeleteAccountButton } from "./DevDeleteAccountButton";
export function AdminAccountsPage() {
diff --git a/frontend/src/pages/admin/DevDeleteAccountButton.tsx b/frontend/src/pages/admin/DevDeleteAccountButton.tsx
index 3b0b7fc..e3c75a4 100644
--- a/frontend/src/pages/admin/DevDeleteAccountButton.tsx
+++ b/frontend/src/pages/admin/DevDeleteAccountButton.tsx
@@ -10,9 +10,9 @@ import { useToast } from "../../context/ToastContext";
* DEV-ТОЛЬКО кнопка жёсткого удаления аккаунта.
*
* Эндпоинт `DELETE /api/admin/dev/users/{id}` существует только в dev-сборке бэкенда
- * (backend/app/routers/dev_admin.py, исключён из прод/тест-образа). Этот модуль
+ * (backend/app/routers/dev_admin.py, исключён из прод-образа). Этот модуль
* рендерится лишь под `import.meta.env.DEV` в AdminAccountsPage, поэтому в прод-сборке
- * он не используется и вырезается тришейкингом — в прод/тест удаление недоступно.
+ * он не используется и вырезается тришейкингом — в проде удаление недоступно.
*/
export function DevDeleteAccountButton({
userId,
@@ -39,7 +39,7 @@ export function DevDeleteAccountButton({