Files
2026-10-17 16:37:32 +07:00

442 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Вечерний детектив
Онлайн-детективная игра: команды проходят дело из точек, дверей, ключей и улик.
## Структура workspace
Корневой репозиторий `agents` хранит рабочие инструкции и документацию. Код живёт
в двух отдельных репозиториях:
- [`evening_detective_server/`](evening_detective_server/) — Go-бэкенд;
- [`evening_detective_frontend/`](evening_detective_frontend/) — Vue/TypeScript-фронтенд;
- [`email_server/`](email_server/) — старый собственный почтовый сервер, не нужен для локального запуска.
У каждого из двух основных проектов своя история Git и свой remote. Не удаляйте
вложенные `.git` и не смешивайте коммиты между репозиториями.
## Локальный запуск
Требования: Go, Docker Compose, Node.js 20.19+ или 22.12+, npm.
Короткие команды из корня проекта:
```sh
make dev # разработка: локальные БД/RustFS, Go-бэкенд и Vite-фронтенд
make game # локальная игра: запуск venue с HTTPS из готовых артефактов
make stop # остановка venue, данные сохраняются
make venue-clean # полное удаление данных venue без подтверждения
```
`make dev` запускает приложения в текущем терминале; `Ctrl+C` останавливает оба
приложения, БД и RustFS остаются запущены. Перед первым запуском подготовьте
зависимости и `.env` по инструкции ниже; схема БД должна быть подготовлена через
`make -C evening_detective_server migrate-up` (требуется Goose).
`make game` — алиас `make venue`: перед первым запуском нужны сборка
`make prod-build`, сертификат и локальный DNS, как описано в разделе площадки.
`make stop` останавливает только площадку; процессы разработки не затрагивает.
### 1. Зависимости сервера
```sh
cd evening_detective_server
docker compose -f docker-compose-db.yml up -d
cp .env.example .env
make run
```
Сервис запускает:
- gRPC на `http://localhost:8080`;
- REST gateway и Swagger на `http://localhost:8090`.
Для локального фронтенда в `evening_detective_frontend/.env` укажите:
```dotenv
VITE_API_URL=http://localhost:8090
```
### 2. Фронтенд
В другом терминале:
```sh
cd evening_detective_frontend
npm install
cp .env.example .env
npm run dev
```
Фронтенд будет доступен на `http://localhost:5173`.
### 3. Локальный пользователь
```sh
cd evening_detective_server
make dev-user
```
По умолчанию создаётся пользователь `dev@example.com` с паролем `dev123456`.
Для изменения используйте `DEV_USER_EMAIL` и `DEV_USER_PASSWORD`.
## Автономная площадка (ED-029)
`venue/` — окружение для проведения игры на площадке без интернета.
Локальная разработка описана выше и запускается отдельно.
После подготовки сертификата и локального DNS (инструкция ниже) из корня workspace запустите:
```sh
make prod-build
make venue
```
`make prod-build` заранее готовит артефакты для `prod/` и `venue/`.
Деплой не компилирует Go/Vue и не устанавливает зависимости исходного кода.
Если артефактов нет, запуск завершится с предложением выполнить `make prod-build`.
Первый запуск с интернетом сам создаёт настройки и случайные секреты в
`venue/.env`, готовит Docker-образы из готовых файлов, запускает миграции и создаёт организатора.
Каждый `make venue` пересобирает Docker-образ из готовых файлов и принудительно
пересоздаёт контейнеры с HTTPS. При наличии runtime-образов и Alpine запуск
работает без интернета; отсутствующие образы скачиваются при первой подготовке.
Email и пароль для входа указаны в `VENUE_USER_EMAIL` и `VENUE_USER_PASSWORD`
в `venue/.env`; существующий файл настроек не перезаписывается.
Аккаунт организатора при запуске синхронизируется с этими значениями.
После изменения кода обновите площадку, пока интернет доступен:
```sh
make prod-build
make venue
```
Сайт доступен по `https://evening-detective.crabs-games.art`; API и материалы
обслуживаются тем же прокси. Интерфейс и API также можно открыть без локального
DNS по `http://<VENUE_BIND_IP>:<VENUE_PORT>`. Ссылки на материалы, QR-коды и
билеты при этом остаются на прежнем HTTPS-домене. Для подключения по LAN задайте
постоянный IP ноутбука в `VENUE_BIND_IP` в `venue/.env`.
Приложение, база и RustFS подключены только к внутренней Docker-сети.
Прокси дополнительно подключён к сети публикации порта.
База и RustFS сохраняются в именованных томах.
Миграции Goose выполняются до старта приложения. Для остановки без потери данных:
```sh
make stop
```
Для полного сброса используйте `make venue-clean`. Команда без подтверждения
останавливает площадку и необратимо удаляет аккаунты, игры, команды, результаты
и материалы. Настройки `venue/.env`, сертификаты, настройки Cudy, сборка и
Docker-образы сохраняются; окружение разработки не затрагивается. Повторный
вызов допустим. Следующий `make venue` создаст пустую БД и организатора из `.env`.
Внешние тома БД и материалов обычный `down -v` не удаляет.
После изменений кода или миграций
выполните `make venue`, пока интернет доступен. Секреты, сборка,
сертификаты и пакеты игры игнорируются Git в `venue/`.
`APP_MODE=venue` отключает SMTP, фоновые письма участия/отзывов и внешний webhook
регистрации. Аккаунты готовятся через bootstrap; почтовая регистрация аккаунта
и сброс пароля возвращают явную ошибку без изменения данных. Команды создаёт
организатор на странице игры; участие подтверждается по существующим правилам:
при создании в последние 24 часа до игры или при первом игровом действии.
Для дополнительных операторов используйте отдельные локальные email
и пароли в `.env` и снова выполните `make venue` (старые аккаунты сохраняются).
На основном сервере в настройках игры, в блоке «Передача игры на площадку»,
кнопка «Сбросить локальный режим» снимает офлайн-отметку после подтверждения.
Данные игры сохраняются; скачанный пакет продолжает работать на площадке,
его результаты можно загрузить позже. «Подготовить новый пакет» доступна при
любом статусе игры и предыдущего экспорта: новый снимок содержит актуальные
материалы, команды и настройки для игры с нуля, без ходов, ответов и баллов.
Статус и прогресс исходной игры не меняются. Новая подготовка отменяет предыдущую
незавершённую попытку; готовые архивы сохраняются для возврата результатов.
Импорт нового пакета не заменяет уже импортированную игру автоматически.
Проверки для разработчика находятся в `tests/venue/` и не нужны для запуска
площадки. `deploy.py` проверяет сценарии запуска без Docker; `smoke.py` проверяет
работающее окружение и создаёт тестовый сценарий, игру, команду и материал через API:
```sh
python3 tests/venue/deploy.py
python3 tests/venue/smoke.py
make venue
python3 tests/venue/smoke.py --verify
```
Последняя команда проверяет ранее созданные записи и файл после перезапуска.
Тестовые данные остаются в локальной базе; повторное создание запускайте только
когда нужен новый набор. Дождитесь ответа `/api/test/ping` перед проверкой.
### Учёт устройств (этап 12)
Venue автоматически учитывает браузеры игроков, организаторов и экранов:
`POST /api/devices/heartbeat` публично регистрирует подключение, а
`GET /api/devices?gameID=<ID>` возвращает сгруппированные устройства организатору,
администратору или разработчику. Фильтр необязателен; при его использовании
список содержит только подключения выбранной игры, без непривязанных браузеров.
Вкладка «Состояние» (`/games/:id?tab=state`, этап 13) доступна в venue
организатору, администратору и разработчику. Она показывает число доступных
устройств, устройств без связи и подключений, UUID профиля и последнее
обращение; раскрытие устройства показывает тип подключения, ID команды/экрана,
статус, IP и User-Agent. Устройства без связи идут первыми, далее сортировка
по UUID. Статусы и время обновления берутся из серверного снимка.
Снимок запрашивается сразу и раз в 5 секунд только на активной вкладке,
без параллельных запросов; поздние ответы после ухода игнорируются.
При ошибке предыдущий снимок и время остаются видимыми, доступен повтор.
«Нет связи» означает отсутствие heartbeat сайта, а не диагностику Wi-Fi.
Блок «Wi-Fi площадки» (этапы 14–15) показывает текущие Wi-Fi-клиенты
всего Cudy: MAC, IP, диапазон и доступный сигнал в dBm. Он не связывает MAC
с UUID браузера или выбранной игрой, не показывает имена клиентов и трафик.
Сборщик читает роутер каждые 10 секунд; при ошибке оставляет последний успешный
снимок и его время, в том числе после перезапуска сборщика. Снимок старше 30 секунд помечается устаревшим; отсутствие
сигнала отображается как «Нет данных». История не сохраняется.
Поддержан и проверен TR3000 V1 с прошивкой 2.5.27-20260804-183625, штатная
английская HTML-страница клиентов. Неизвестная разметка возвращает ошибку,
а не пустой список. После обновления прошивки нужно проверить адаптер снова.
Настройки Cudy сборщик не меняет; использует только вход и чтение списка.
Он работает отдельным процессом в сети `entrance`, без доступа к БД/RustFS;
backend читает только общий том снимка, подключённый к нему read-only.
Сбор Cudy необязателен. Для включения создайте `venue/cudy/` с правами 0700,
сохраните пароль администратора в `venue/cudy/password` с правами 0600 и добавьте
в локальный `venue/.env`:
```dotenv
CUDY_PASSWORD_FILE=/run/secrets/cudy/password
CUDY_URL=http://192.168.10.1
```
Пароль не записывается в `.env`, API, логи или Git; `venue/cudy/` исключён из Git.
URL допускает только числовой локальный IP без credentials и пути. После
обновления артефактов запустите штатный `make venue`; отдельно сборщик backend
бинарника запускается с `--collect-cudy`. Без `CUDY_PASSWORD_FILE` обращения
к роутеру отсутствуют и панель сообщает, что сбор не настроен. Работающая
площадка при реализации этапов 14–15 не обновлялась.
Разработчик без роли организатора/администратора/автора в venue получает
список игр и только вкладку состояния: сведения игры ограничены ID, названием,
описанием, форматом, местом, плановым началом и статусом. Команды, пароли,
сценарий, материалы и игровой прогресс не выдаются и не загружаются интерфейсом.
Создание игр и результаты скрыты; существующий импорт сохраняется.
Heartbeat разработчика остаётся браузерным без контекста организатора.
В prod этот доступ и вкладка состояния не открываются.
UUID браузерного профиля хранится отдельно от авторизации в localStorage;
каждая вкладка и перезагрузка страницы создают новый UUID подключения.
Без localStorage устройство получает временный UUID. Контекст страницы
полностью заменяется при heartbeat и очищается при навигации. Организатор
проверяется по JWT и правам игры, команда — по паролю и неотменённому участию,
экран — по существующей сессии; игру команды и экрана определяет сервер.
Credentials используются только для проверки и не сохраняются в реестре.
Heartbeat отправляется сразу и каждые 20 секунд без параллельных запросов,
а также при возвращении вкладки и восстановлении сети. Ошибки не блокируют
игру и не показывают toast; повтор выполняется по интервалу. В prod ответ
содержит `enabled=false`, после чего браузер прекращает heartbeat;
список устройств в prod недоступен.
Реестр хранится только в памяти backend: UUID, серверные времена,
последние IP/User-Agent и проверенные ID игры/команды/экрана. IP не участвует
в идентификации и авторизации. Связь актуальна строго менее 60 секунд;
устройство доступно, если доступно хотя бы одно его подключение в списке.
Подключения удаляются через 15 минут отсутствия при heartbeat или чтении.
Предел — 1024 подключения; новые отклоняются с `ResourceExhausted`, если
после очистки нет места. После перезапуска открытые браузеры регистрируются
заново. Времена устройства вычисляются по сохранённым подключениям.
«Нет связи» означает отсутствие heartbeat сайта, а не отключение от Wi-Fi.
UUID обозначает браузерный профиль, а не физический телефон; очистка
localStorage создаёт новое устройство. История посещений, полный URL,
имена игроков, email и credentials в список не попадают.
### Прежние домены и HTTPS (этап 3)
Подготовка в репозитории готова; проверка реального Cudy и Android/iPhone без WAN
ещё требуется. `make venue` всегда запускает HTTPS с пересборкой Docker-образа
и перезапуском контейнеров. Для первого запуска:
1. Пока интернет доступен, подготовьте артефакты через `make prod-build`.
Запуск `make venue` выполните после подготовки сертификата и локального DNS.
2. Выпустите отдельный доверенный сертификат с новым ключом для двух имён:
`evening-detective.crabs-games.art` и `evening-detective-api.crabs-games.art`.
Используйте DNS-01: проверяются TXT-записи `_acme-challenge` в публичном DNS;
публичные A/AAAA-записи production менять не нужно. Не копируйте ключ production.
Например, на машине с установленным Certbot:
```sh
certbot certonly --manual --preferred-challenges dns \
--cert-name evening-detective-venue \
-d evening-detective.crabs-games.art \
-d evening-detective-api.crabs-games.art
```
Следуйте подсказкам Certbot для TXT-записей. Выпуск сертификата выполняется
отдельно от приложения; Certbot не входит в runtime площадки.
3. Скопируйте полную цепочку (сертификат сервера и промежуточные сертификаты) в
`venue/certificates/fullchain.pem`, незашифрованный закрытый ключ — в
`venue/certificates/privkey.pem`. Ограничьте доступ к каталогу и ключу владельцем
(режимы 700 и 600); процесс Nginx в контейнере должен иметь доступ на чтение.
Каталог игнорируется Git. Перед игрой проверьте дату окончания сертификата:
```sh
openssl x509 -in venue/certificates/fullchain.pem -noout -dates
```
4. На Cudy вручную закрепите IP ноутбука в **Advanced Settings → IP/MAC Binding**.
На TR3000 с прошивкой 2.5.27 DE в прочитанных разделах Custom DNS и DHCP нет
формы статических записей. Поэтому venue запускает отдельный CoreDNS на ноутбуке.
В **Advanced Settings → DHCP Server** задайте **Preferred DNS = IP ноутбука**,
**Alternate DNS** оставьте пустым. Переподключите телефоны к Wi-Fi, чтобы они
получили новые параметры DHCP. Custom DNS роутера менять не требуется.
Проверьте клиентскую изоляцию и настройки Private DNS/DoH устройств: запросы
двух доменов должны приходить на локальный DNS, а клиенты — достигать ноутбука.
Скрипты проекта не меняют роутер и не прошивают его.
5. В `venue/.env` задайте `VENUE_BIND_IP=<постоянный-IP-ноутбука>` и разрешите
входящие TCP 80/443, TCP `VENUE_PORT` и TCP/UDP 53 из LAN в межсетевом экране
ноутбука.
`VENUE_BIND_IP` должен быть конкретным IPv4-адресом ноутбука, не `0.0.0.0`:
он используется и для публикации портов, и для DNS-ответов. Порты должны быть свободны.
Для HTTPS менять `APP_BASE_URL` в `.env` не нужно: Compose-дополнение задаёт
прежний HTTPS-домен интерфейса для QR-ссылок и материалов.
6. Запустите:
```sh
make venue
```
Команда проверяет доверие цепочке через системное хранилище OpenSSL, оба домена,
срок действия и соответствие ключа до обращения к Docker. Ошибка не переключает
работающее окружение на HTTP. Docker-образ приложения пересобирается при каждом
запуске из готовых артефактов, без компиляции Go/Vue. При наличии всех базовых
образов интернет не нужен. Доверие на реальных телефонах проверяется отдельно.
Интерфейс открывается по `https://evening-detective.crabs-games.art`, API — по
`https://evening-detective-api.crabs-games.art`. Интерфейс использует свой `/api/`;
прежний API-домен также обслуживается локально. На `VENUE_PORT` интерфейс и API
доступны по IP через HTTP без редиректа; запросы по доменным именам на порту 80
перенаправляются на HTTPS с сохранением пути и параметров.
CoreDNS публикуется только на `VENUE_BIND_IP:53` по UDP/TCP. Оба домена получают
этот IPv4 локально; AAAA-запросы не получают внешние адреса. Для них интернет
не требуется. Остальные домены пересылаются на 1.1.1.1/8.8.8.8 и работают при
доступном интернете; их недоступность без WAN не мешает локальным доменам.
DNS подключён к сети публикации портов и не имеет доступа к внутренней сети
backend/БД. Образ CoreDNS скачивается при первой подготовке и используется
повторно без скачивания. При изменении IP ноутбука обновите `VENUE_BIND_IP`,
настройки DHCP Cudy и повторите `make venue`.
Проверка DNS до настройки клиентов:
```sh
python3 tests/venue/dns.py --server 192.168.10.155
```
После продления сертификата с доступным интернетом замените оба PEM-файла и
повторите `make venue`: Nginx проверяет конфигурацию и перечитывает сертификат.
Продление не выполняется автоматически. После обновления кода выполните
`make prod-build`, затем `make venue`; контейнеры всегда перезапускаются с HTTPS.
Для остановки HTTPS-окружения с сохранением данных:
```sh
docker compose -f venue/docker-compose.yml -f venue/docker-compose.https.yml \
--env-file venue/.env down
```
Проверка после настройки DNS и запуска (первый smoke создаёт тестовые данные):
```sh
python3 tests/venue/smoke.py
python3 tests/venue/smoke.py --verify
```
TLS-проверка в smoke включена; самоподписанный сертификат и обход предупреждений
не подходят для приёмки. На Android и iPhone отключите WAN и мобильные данные,
подключитесь к Wi-Fi площадки и проверьте DNS обоих доменов, отсутствие TLS-ошибок,
вход организатора, материалы и QR-ссылки тестовой игры. Повторите после перезапуска.
Проверьте запрос доступа к камере отдельно: распознавание QR в текущем интерфейсе
также требует поддержки браузером `BarcodeDetector`.
Этап 3 сохраняет адреса, но не переносит данные production. Production-QR может
вести на игру или билет, которых в локальной базе ещё нет; перенос и возврат
данных относятся к этапам 4–11. До их завершения используйте только тестовые игры.
## Проверки перед изменениями
Сервер:
```sh
cd evening_detective_server
make test
```
Фронтенд:
```sh
cd evening_detective_frontend
npm run type-check
npm run lint
npm run lint:css
```
При изменении protobuf сначала обновляйте сгенерированный код существующими
командами `make generate` на сервере и `npm run proto:gen` на фронтенде.
Сгенерированные файлы руками не редактируйте.
## Сборка и публикация
Из корня workspace можно собрать и опубликовать root, backend и frontend одной
командой:
```sh
make publish MSG="Исправить подсчёт ходов в игре"
```
Команда вызывает локальные build-команды репозиториев, создаёт коммит с
указанным сообщением и выполняет `git push`.
Общая команда собирает бэкенд один раз и сохраняет артефакты в обе папки:
`prod/` и `venue/`. Обе используют `backend/evening_detective_server`,
`frontend/dist/` и `migrations/`; Compose называется `docker-compose.yml`,
образ бэкенда описан в `Dockerfile.backend`. Фронтенд собирается отдельно для
каждого окружения: production сохраняет текущие Vite-настройки, venue использует
API через свой прокси (`VITE_API_URL=/.`). Локальные Goose и SQL подготовки
аккаунта лежат в `venue/backend/`.
Для сборки артефактов обоих окружений используйте:
```sh
make build
```
`make prod-build` выполняет ту же общую сборку. `make venue` пересобирает
Docker-образ из готовых артефактов и перезапускает площадку с HTTPS.
`make venue-update` и `make venue-https` сохранены как алиасы `make venue`. Команды
обновляют только генерируемые артефакты в `prod`; конфигурация `prod` не меняется.
## Окружение и секреты
Файлы `.env` предназначены только для локальных значений и не коммитятся.
Начинайте настройку с `.env.example`. Секреты SMTP, JWT, S3, webhook и внешних
API храните только в окружении или секрет-хранилище.
Публичный адрес приложения задаётся через `APP_BASE_URL`; он используется в
ссылках из писем и QR-кодах команд.
## Рабочий процесс
1. Перед изменением прочитайте корневой [`AGENTS.md`](AGENTS.md) и инструкции
целевого проекта.
2. Делайте минимальное изменение в одном дочернем репозитории.
3. Запускайте релевантные проверки.
4. Коммиты и push выполняет только пользователь; агент никогда не коммитит и не отправляет изменения.
Текущие подробные инструкции:
- [`AGENTS.md`](AGENTS.md) — правила workspace;
- [`evening_detective_frontend/AGENTS.md`](evening_detective_frontend/AGENTS.md) — правила фронтенда;
- [`evening_detective_server/README.md`](evening_detective_server/README.md) — детали бэкенда.