442 lines
31 KiB
Markdown
442 lines
31 KiB
Markdown
# Вечерний детектив
|
||
|
||
Онлайн-детективная игра: команды проходят дело из точек, дверей, ключей и улик.
|
||
|
||
## Структура 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) — детали бэкенда.
|