342 lines
34 KiB
Markdown
342 lines
34 KiB
Markdown
# DECISIONS.md — журнал решений (ADR)
|
||
|
||
Каждое решение — короткая запись, чтобы потом не гадать, почему всё устроено так.
|
||
Формат: дата, статус (принято/рассмотрение/заменено), суть, аргументы за/против, результат.
|
||
|
||
---
|
||
|
||
## ADR-001 — Используем свой токен на pump.fun (мемкойн Binary Rocket)
|
||
- **Дата:** 2026-09-09
|
||
- **Статус:** принято
|
||
- **Суть:** выпускаем СВОЙ токен на pump.fun, а не пользуемся чужим.
|
||
- **За:** бренд/маркетинг, контроль экономики, стимул игрокам, единая валюта ставок.
|
||
- **Против:** надо эмитировать и следить за курсом; чужой токен дал бы меньше работы.
|
||
- **Решение:** свой токен. decimals = 6, Token-2022, создание через `create_v2` (по доке pump.fun).
|
||
|
||
## ADR-002 — Курс живой рыночный (Способ 1)
|
||
- **Дата:** 2026-09-09
|
||
- **Статус:** принято
|
||
- **Суть:** вне игры баланс — в фантиках, курс берётся с рынка (bonding curve/AMM pump.fun).
|
||
Ввод = swap SOL→фантики, вывод = swap фантики→SOL. Никакой фикс-привязки/стейблока.
|
||
- **За:** нативно pump.fun, привычно игроку-трейдеру, технически проще, маркетинг (рост/падение токена).
|
||
- **Против:** баланс «гуляет» с рынком; определённым аудиториям неудобно.
|
||
- **Решение:** Способ 1. (Это и диктует, что ставки идут фантиками через контракт.)
|
||
|
||
## ADR-003 — Ставки идут фантиками через контракт
|
||
- **Дата:** 2026-09-09
|
||
- **Статус:** принято (вытекает из ADR-002)
|
||
- **Суть:** игрок ставит токен, контракт держит фантики в своём ATA-ваулте и платит фантиками.
|
||
- **Следствие:** контракт переводится с нативного SOL (lamports) на SPL-токен — это ядро Фазы 1.
|
||
- **Против:** трогаем хорошо работающий контракт; но без этого сценарий ввода-игры-вывода не работает.
|
||
|
||
## ADR-004 — Ввод/вывод через pump.fun swap (`POST /agents/swap`)
|
||
- **Дата:** 2026-09-09
|
||
- **Статус:** принято
|
||
- **Суть:** пополнение = `inputMint: NATIVE_MINT(SOL), outputMint: наш_mint`; вывод = наоборот.
|
||
Клиент на бэкенде (Ktor `PumpApiClient`) уже готов в `smart-updown-server`.
|
||
- **За:** возвращает готовую подписанную swap-транзакцию, бэкенд не держит ликвидность.
|
||
- **Против:** весь ввод/вывод завязан на доступность pump.fun API.
|
||
|
||
## ADR-05 (идея) — мета-репозиторий для документации и трекинга
|
||
- **Дата:** 2026-09-09
|
||
- **Статус:** принято
|
||
- **Суть:** создать `pump-game-ops`, где живут карта репозиториев, решения, экономика и список задач.
|
||
- **За:** единая точка оркестрации, не плаваем по чату. **Против:** риск превратиться во второй трекер.
|
||
|
||
---
|
||
|
||
## ADR-005 — Флоу входа/вывода подтверждён (как описал юзер)
|
||
- **Дата:** 2026-09-09
|
||
- **Статус:** принято
|
||
- **Суть:** человек заходит в игру → видит баланс 0 → кнопка «Пополнить» (одна, без выбора) →
|
||
swap SOL→фантики → баланс появляется → ставит фантики через контракт (честное списание/начисление
|
||
по исходу) → при выходе нажимает «Вывести» → swap фантики→SOL, баланс уменьшается.
|
||
- **За:** точный пользовательский сценарий, подтверждённый владельцем; простая UX-модель.
|
||
- **Решение:** именно так и работаем — это источник требований для Фазы 1 (контракт на токене) и Фазы 5 (UI).
|
||
|
||
---
|
||
|
||
## ADR-006 — Предмет ставки: цена SOL (валюта — фантики)
|
||
- **Дата:** 2026-09-09
|
||
- **Статус:** принято
|
||
- **Суть:** валютой ставки являются наши фантики (токен Binary Rocket), но ставка делается на
|
||
**направление цены SOL** (вверх/вниз) — как и в текущей версии игры. Контракт по-прежнему
|
||
сравнивает entry/exit_price цены SOL.
|
||
- **Цена SOL сейчас** — случайная заглушка (`RandomPriceSource`). Позже заменяется на реальную
|
||
(провайдер/оракул). Это отдельная задача, не блокирует Фазу 1 (контракт от способа получения цены не зависит).
|
||
**→ Обновление 2026-09-11:** реальный источник выбран — см. ADR-007.
|
||
- **Следствие:** фантики = касса/валюта ввода-вывода; контракт оперирует токеном, но исход считает
|
||
по цене SOL. Экономика токена (ADR-002 живой курс) не влияет на логику «вверх/вниз».
|
||
|
||
---
|
||
|
||
## ADR-007 — Источник цены SOL = upstream-сервис `binance-market`
|
||
- **Дата:** 2026-09-11
|
||
- **Статус:** принято
|
||
- **Суть:** цена SOL для `PriceSource` берётся из уже работающего в инвест-кластере сервиса
|
||
`binance-market` (репо `subochev/binance-market`), а не из своей оракуль-логики и не из прямого
|
||
опроса Binance. Контракт между игрой и сервисом — публичный:
|
||
- **NATS-подписка** на топик `market.price.solusdt` (предпочтительно, в кластере); либо
|
||
- **WebSocket** `ws://<binance-market>:8080/ws/price?symbol=solusdt` (fallback / внешние сети).
|
||
Источник цены внутри `binance-market` — last-trade (`aggTrade.p` с биржи Binance), тики идут
|
||
на каждой сделке → фактически real-time.
|
||
- **За:**
|
||
- Уже работает и развёрнут в кластере `kube` (ns `invest`), chart `binom/binance-market`,
|
||
последний релиз v10. Не надо поднимать ещё один процесс ради цены.
|
||
- Есть и NATS, и WS-контракт — обе формы потребления готовы.
|
||
- Покрытие `solusdt` уже в списке символов; дополнительных подписок не требуется.
|
||
- Цена приходит в тиках сделок (не сэмплинг раз в N секунд) — лучшее разрешение для
|
||
сравнения entry/exit в round-based игре.
|
||
- Общий сервис инвест-кластера, используется не только этой игрой; его судьба — не наша забота.
|
||
- **Против:**
|
||
- Один источник (Binance). Если Binance недоступен — игра без цены. Заглушка-fallback
|
||
на `RandomPriceSource` остаётся как dev-only.
|
||
- Зависимость по латентности от стороннего сервиса. В кластере это несколько мс,
|
||
приемлемо.
|
||
- **Результат:** задачи «TODO Цена SOL: реальный источник» (Фаза 0) и «PriceSource: реальная цена
|
||
(вместо RandomPriceSource)» (Фаза 2) переводятся в `IN_PROGRESS` со ссылкой на это решение.
|
||
Никаких новых подписок/оракулов не делаем — только интеграция в релейере (`smart-updown-token`/
|
||
`relayer` или `smart-updown-server`). Внешний статус сервиса см. README → «Внешние зависимости».
|
||
**→ Обновление 2026-09-13: реализовано, см. ADR-008** (NATS-консьюмер в шлюзе, цена ×1e8,
|
||
раздача фронту по WS).
|
||
---
|
||
|
||
## ADR-008 — Реализация: цена SOL из NATS в шлюзе + раздача фронту по WS
|
||
- **Дата:** 2026-09-13
|
||
- **Статус:** принято, реализовано
|
||
- **Контекст:** ADR-006 зафиксировал предмет ставки (цена SOL), ADR-007 выбрал источник
|
||
(`binance-market` через NATS). Оставалось реализовать: сама цена была случайной
|
||
заглушкой `RandomPriceSource` / `simulated = 100_000_000 + Math.random()`. Исход раунда был
|
||
случаен, а не рыночен. При этом в контуре уже работает `binance-market` — сборщик сделок с
|
||
Binance, который публикует каждую цену в NATS (`market.price.<symbol>`).
|
||
- **Решение:** игровой шлюз (`smart-updown-token/relayer`) подписывается на NATS
|
||
(`market.price.solusdt`) и берёт цену оттуда. Заглушка удалена.
|
||
- **Транспорт:** core NATS `nats://192.168.88.93:4222`, subject `market.price.solusdt`.
|
||
Режим — **только чтение**; `binance-market` не трогаем.
|
||
- **Шкала:** строка из NATS переводится в целое ×**1e8** строковым парсером (без float):
|
||
`"101.89000000"` → `10189000000`. То же юнит-пространство, что у оракульных цен и у
|
||
прежней заглушки (`100_000_000`), поэтому контракт сравнивает цены корректно.
|
||
- **Раздача фронту:** шлюз транслирует каждый тик по WebSocket (`/ws`, снаружи
|
||
`wss://testfront.binom.pw/api/ws`), сообщение — только цена:
|
||
`{"type":"price","symbol":"solusdt","price":"102.30...","ts":...}`.
|
||
- **Отсутствие цены:** считается аварийным состоянием и обрабатывается мягко — шлюз
|
||
стартует без NATS, запрос цены ждёт таймаут, фронт показывает последнее значение и
|
||
переподключается. Отдельный сценарий деградации — тема для будущего решения.
|
||
- **За:** исход раунда становится рыночным (entry/exit — реальные цены Binance);
|
||
переиспользуется уже работающий сборщик, торговый контур не дублируется; шина отделяет
|
||
источник цены от её потребителей (игра, аналитика, будущие сервисы).
|
||
- **Против:** появляется зависимость от NATS и от `binance-market`; core NATS не хранит
|
||
историю (нет JetStream) — при разрыве подписки цена не «догоняется», нужен живой поток.
|
||
- **Альтернативы:** (а) прямая подписка шлюза на Binance WS — дублирование сборщика, лишние
|
||
внешние коннекты; (б) оракул on-chain (Pyth/Switchboard) — правильнее для боя, но
|
||
дороже и требует фидов; (в) заглушка — отклонено как единственный вариант, оставлена
|
||
возможность (`setPriceOverride`) для тестов.
|
||
- **Детали и диаграммы:** `docs/PRICE-NATS.md`, `docs/diagrams/price-flow.puml`.
|
||
|
||
## ADR-009 — Один бэк на оба домена: прод-домен переведён на TS-шлюз (2026-09-15)
|
||
- **Дата:** 2026-09-15
|
||
- **Статус:** принято, реализовано
|
||
- **Контекст:** на `smart-updown.binom.pw` (Unity-игра) стоял **канонический Kotlin-бэк**
|
||
`smart-updown-server` с programId `3NWEK…`, а на `testfront.binom.pw` — **TS-шлюз**
|
||
`updown-relayer` (:8895), кастодиальный, на контракте `9ALs…`. Два бэка = два контракта =
|
||
разные цепочки состояния: у Kotlin-бэка вообще НЕТ WebSocket, а его программа в цепи
|
||
отсутствовала (`getAccountInfo` → `null`), поэтому `/api/state` отдавал
|
||
`global state is not initialized`. Игра при этом читала баланс напрямую из цепи через свой
|
||
nginx-прокси `/solana-rpc/getBalance` — то есть жила в третьей реальности.
|
||
- **Решение:** **не деплоить `3NWEK` под Kotlin**, а повесить игровой домен на **тот же** TS-шлюз,
|
||
что и тестовый: `/api` и `/ws` → `testfront-gateway-smart-updown-relayer:8895`. Формулировка
|
||
владельца: «тестовый фронт мы для этого и делали»; игру переделают под API шлюза — расхождение
|
||
контрактов API не блокер.
|
||
- Kotlin-бэк выключен **обратимо**: `replicas=0` + его ingress удалён (иначе два ingress'а на
|
||
один хост+path = недетерминированный роутинг Traefik).
|
||
- Бэкап состояния до правки — `/root/bak-smart-updown-15.09/` на 192.168.76.120.
|
||
- **За:** один бэк = одно состояние на оба стенда (счётчик 55=55, касса 29320); у игры
|
||
появляется живой WS; тестовый фронт больше не «отдельный мир»; откат — две команды.
|
||
- **Против:** API шлюза и ожидания игры расходятся (`minAmount`/`maxAmount` строкой против
|
||
`minAmountLamports`/`entryPrice`; тело ставки `{address, side, amountUnits}` против
|
||
`{side, amount, entryPrice, betId, signedTx}`; `GET /api/bet/{id}` у шлюза нет) — игру надо
|
||
править. Принято осознанно.
|
||
- **Что осталось не сделано:** программа `3NWEK` в цепь так и не задеплоена (ключ
|
||
`/root/game_program_kp.json`, `.so` 217496 б лежат в LXC 151; на адрес аирдропнуто 5 SOL).
|
||
Это задел на будущее, не потеря.
|
||
|
||
## ADR-010 — `/api`,`/ws` вынесены в ingress вне helm; образ фронта поднимается явным `--set image.tag` (2026-09-15)
|
||
- **Дата:** 2026-09-15
|
||
- **Статус:** принято, реализовано
|
||
- **Контекст — две независимые ловушки, обе пойманы живьём:**
|
||
1. Чарт `fun-game-front2` (`helm/templates/ingress.yaml`) рендерит **ровно один** path `/`
|
||
(жёстко, host из `values.yaml`) — значений для `/api`,`/ws` в чарте НЕТ. Значит **любой**
|
||
`helm upgrade` игры перерисовывает ingress и **сносит** `/api`,`/ws`: домен начинает отдавать
|
||
HTML игры на `/api/*` (nginx 405 на `POST /wallet`, `/api/state` = HTML Unity вместо JSON).
|
||
2. Шаблон деплоя объявляет образ как
|
||
`image: "{{ Values.image.name }}:{{ default Chart.AppVersion Values.image.tag }}"`, а в
|
||
`helm/values.yaml` **захардкожен `image.tag`** (был `26`). Непустой `Values.image.tag`
|
||
побеждает `appVersion`, поэтому `helm upgrade` на чарт `28` отрендерил под со **старым**
|
||
тегом `26`: спека не изменилась, новый ReplicaSet **не создался**, старый под (от 8 сентября)
|
||
продолжал жить. Внешне — «на домене открывается старый билд» при зелёном CI и
|
||
`Upgrade complete` в истории helm.
|
||
- **Решение:**
|
||
1. `/api` и `/ws` живут в **отдельном ingress `smart-updown-api`** (манифест
|
||
`k8s/smart-updown-api-ingress.yaml`), front2-ingress остаётся `paths: [/]`. Релиз игры
|
||
больше не может их снести.
|
||
2. Фронт игры поднимать **всегда** явным тегом и с сохранением values:
|
||
`helm upgrade fun-game-front2 binom/fun-game-front2 -n game --reuse-values --set image.tag=<N> --wait`.
|
||
Без `--reuse-values` затираются `imagePullSecrets: regcred` и `ingress.host`.
|
||
- **Проверка результата — по КУБУ и домену, а не по helm** (helm отрапортует «успех» и в случае 2):
|
||
```bash
|
||
kubectl get pod -n game -l app.kubernetes.io/name=fun-game-front2 \
|
||
-o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,CREATED:.metadata.creationTimestamp'
|
||
kubectl exec -n game <pod> -- ls -la /usr/share/nginx/html/Build/ # дата файлов = дата билда
|
||
curl -sI https://smart-updown.binom.pw/Build/WebGL.data | grep -i last-modified
|
||
kubectl -n game get ingress \
|
||
-o custom-columns='NAME:.metadata.name,HOST:.spec.rules[0].host,PATHS:.spec.rules[0].http.paths[*].path'
|
||
```
|
||
- **Против / остаточный риск (не забыть):** в `nginx.conf` все Unity-ассеты объявлены
|
||
`Cache-Control: public, max-age=604800, immutable`, а имена файлов (`WebGL.data`, `WebGL.wasm`,
|
||
`WebGL.framework.js`) между сборками **НЕ меняются**. Браузер, уже игравший, будет до 7 суток
|
||
отдавать старые ассеты из кэша, игнорируя новый билд (`index.html` спасает `no-store`, ассеты — нет).
|
||
Лечится сбросом кэша/инкогнито у игрока. **Правильно на будущее** — хэш содержимого в имени
|
||
файла на шаге сборки (CI), тогда `immutable` становится корректным.
|
||
- **Альтернативы:** (а) патчить чарт фронта (добавить в него `/api`,`/ws`) — чужой репозиторий и
|
||
чужой релизный цикл; (б) держать образ на `latest` — нет воспроизводимости; (в) убрать `image.tag`
|
||
из `values.yaml` и опираться только на `Chart.AppVersion` — правильнее, но это правка чужого репо
|
||
(отдать фронтендеру, см. `TASKS.md`).
|
||
|
||
---
|
||
|
||
## ADR-011 — Статика игры вынесена из куба на nginx (LXC 152), версии — каталогами (2026-09-15)
|
||
|
||
**Контекст.** До этого и корень, и `/api`,`/ws` жили в кубе: внешний Traefik (VDS) вёл весь домен
|
||
`smart-updown.binom.pw` на k3s `192.168.76.120`, статику отдавал под `fun-game-front2`
|
||
(nginx внутри образа), API — отдельный ingress `smart-updown-api` → TS-шлюз `:8895`.
|
||
Владелец решил **разделить плоскости**: бэк честно деплоится в куб, а сама игра (статика)
|
||
раздаётся с nginx, развёрнутого на Proxmox — со **версионированием по каталогам** (`/<номер>/`).
|
||
|
||
**Решение:**
|
||
1. **nginx на Proxmox** — новый LXC **152** (`game-web`, 192.168.76.152, Ubuntu 24.04, 2 ядра,
|
||
1 ГБ RAM, диск 8 GB на `local-lvm`, `onboot=1`, unprivileged). Отдельный LXC потому, что
|
||
статика игры — «10 строк конфига и каталог», держать её в кубе рядом с бэком смысла нет.
|
||
2. **Версионирование каталогами:**
|
||
- `/opt/game/releases/<N>/` — файлы сборки N (`Build/`, `TemplateData/`, `index.html`);
|
||
- `/opt/game/current` — симлинк на активную сборку;
|
||
- `https://smart-updown.binom.pw/` → активная сборка, `/28/…` → конкретная сборка.
|
||
3. **Маршрутизация на внешнем Traefik (VDS)** — два router'а на один host, вместо одного:
|
||
- `smart-updown-api-router`: `Host(...) && (PathPrefix('/api') || PathPrefix('/ws'))` → `192.168.76.120` (куб);
|
||
- `smart-updown-router`: `Host(...)` → `192.168.76.152` (nginx на проксмоксе).
|
||
PathPrefix-роутер специфичнее Host-роутера, поэтому `/api`,`/ws` **не перехватываются статикой**.
|
||
IP LXC 152 достижим с VDS через wg-туннель (192.168.76.0/22 dev wg0).
|
||
4. **В кубе ничего не менялось**: `ingress smart-updown-api` (`/api`,`/ws` → шлюз) как был, вне helm.
|
||
Под `fun-game-front2` остался жив, но домен он больше не обслуживает (статика ушла на nginx) —
|
||
в куб его релизить больше не обязательно; релизы — только доставка статики на nginx.
|
||
5. **Инструменты** (`tools/proxmox-nginx/`): `unpack-release.sh` (распаковка dir-дампа образа в
|
||
`releases/<N>`), `publish-release.sh` (тянет образ из реестра, раскладывает, по `--activate`
|
||
переключает `current`), `game.nginx.conf`, `smart-updown.traefik.yaml`.
|
||
|
||
**Питфоллы, пойманные живьём:**
|
||
- `skopeo copy docker://… dir:` кладёт слои по **sha256-digest**, а `manifest.json` — это **OCI-манифест**
|
||
(словарь `layers[].digest`, не список строк как в docker-archiv). Парсить `json['layers'][i]['digest']`,
|
||
а не печатать элемент целиком (иначе `tar: Cannot connect to {'mediaType': ...}`).
|
||
- Голый шаблон `/28` (без слэша) отдаёт **301** — это `try_files ... /$build$subpath/`, т.е. редирект
|
||
на `/28/`. Для относительных путей Unity (`src="Build/WebGL.loader.js"`) важно, чтобы **браузерный
|
||
URL заканчивался слэшем**; иначе `Build/...` уедет в `/Build/...` и вернётся 404. Ссылаться на
|
||
версии только со слэшем (`/28/`), либо ставить редирект в nginx (`return 301 /$build/`).
|
||
- `/solana-rpc/` **не** перехватывается PathPrefix `/api` — это отдельный path, и на VDS он уходит
|
||
в корневой router (nginx). Поэтому страховочный `location /solana-rpc/` в nginx нужен: старые сборки
|
||
игры стучатся именно туда (проверено: `getHealth` → `ok` через домен).
|
||
- Имена ассетов Unity внутри сборки не меняются (`WebGL.data`/`.wasm`/`.framework.js`), но теперь
|
||
URL содержит номер сборки → `immutable`-кэш больше не «прилипает» между релизами (закрыт остаточный
|
||
риск из ADR-010). Внутри одной версии ассеты неизменяемы, т.к. каталог версии не перезаписывается.
|
||
|
||
**Приёмка (живой домен, 2026-09-15):** корень `/` → 200 от `nginx/1.24.0 (Ubuntu)` (LXC 152);
|
||
`/27/`,`/28/` → 200, ассеты версий различаются (md5 `WebGL.data`: `76b6f3…` vs `47453d…`);
|
||
`/api/state` → 200 JSON (куб), `/api/ws` → `[open]` + `{"type":"price"}`; сквозной
|
||
`tools/autoclose-check.cjs` на `BASE=https://smart-updown.binom.pw/api` — **9/9**; `testfront` не задет.
|
||
Откат: вернуть `/opt/traefik/static/smart-updown.yaml` из бэкапа VDS
|
||
(`/root/bak-smart-updown-nginx-2026-09-15/`) — домен снова целиком уйдёт в куб.
|
||
|
||
**Альтернативы:** (а) оставить статику в кубе и версионировать каталогами внутри образа — тянет
|
||
пересборку образа на каждый билд игры и не решает вопрос кэша; (б) поднять в LXC не nginx, а
|
||
podman-контейнер с nginx — лишний слой на 10 строках конфига; (в) отдавать статику прямо с k3s-Traefik
|
||
по PVC — не то, о чём просил владелец (он хотел nginx на проксмоксе).
|
||
|
||
## ADR-011-доп — корень редиректит на активную сборку (закрыта кэш-мина ADR-010) (2026-09-15)
|
||
|
||
**Что нашли на сборке 29 (проверка живьём, не по документам).** `index.html` Unity ссылается на
|
||
ассеты **относительным** путём (`var buildUrl = "Build"`). Пока корень отдавал файлы напрямую из
|
||
`/opt/game/current`, игрок получал ассеты по URL **без номера сборки** — `/Build/WebGL.wasm`,
|
||
`/Build/WebGL.data` и т.д. Эти URL между релизами **не меняются**, а в конфиге они попадали под
|
||
маску `immutable` (7 суток). Итог: у игравшего игрока браузер неделю держал бы ассеты старой сборки
|
||
и молча игнорировал новый билд. То есть моя собственная маска кэша воспроизводила ровно ту мину,
|
||
которую ADR-011 объявлял закрытой. Версионирование каталогами **само по себе** её не лечило —
|
||
лечило только то, чтобы URL ассетов реально содержали номер сборки.
|
||
|
||
**Решение.** Корень больше не раздаёт файлы — он **302-редиректит на активную сборку**:
|
||
- `publish-release.sh --activate` пишет `/etc/nginx/game-active.inc`:
|
||
`location = / { return 302 /<N>/; }` + то же для `/index.html`;
|
||
- `include /etc/nginx/game-active.inc;` подключён в server-блок;
|
||
- после редиректа `location.pathname` = `/29/`, относительный `buildUrl = "Build"` резолвится
|
||
в `/29/Build/…` — версионированный URL, старый URL перестаёт использоваться вовсе;
|
||
- маска `immutable` сужена до путей **с номером сборки** (`~^/[0-9]+/…`); всё остальное,
|
||
включая неверсионированные ассеты, теперь `no-store` (страховка от отравления кэша).
|
||
|
||
**Проверено живьём (сборка 29):** `/` → **302 → `/29/`**; браузер приземляется на `/29/` и тянет
|
||
`/29/Build/WebGL.wasm`, `/29/Build/WebGL.data`, `/29/Build/WebGL.framework.js` — все 200, ни одной
|
||
ошибки в консоли; `/28/`,`/27/` живы; неверсионированный `/Build/WebGL.wasm` → 200 с `cache-control:
|
||
no-store`; `/api/state`,`/api/price`,`/solana-rpc/` → 200; сквозная ставка `autoclose-check` — **9/9**;
|
||
`testfront` не задет (200).
|
||
|
||
**Почему не «отдавать корень без редиректа».** Это работало бы (неверсионированные ассеты получили бы
|
||
`no-store`), но теряется весь смысл `immutable`-кэширования: ассеты в 52 МБ + 18 МБ перекачивались бы
|
||
при каждом заходе. Редирект даёт и корректность, и кэш.
|
||
|
||
**Остаточный риск.** Игрок, у которого в кэше уже лежат ассеты по **старому** неверсионированному URL
|
||
(`/Build/WebGL.wasm`, полученные до этого фикса), один раз может получить старый билд — URL теперь
|
||
`no-store`, так что после первой перезагрузки браузер сходит на сервер и получит свежее. Новые
|
||
версии этой проблемы не имеют вовсе.
|
||
|
||
## ADR-012 — Именованные сборки (zip от фронтендера) рядом с номерными: `/sol/` (2026-09-15)
|
||
|
||
**Контекст.** Фронтендер прислал готовый Unity-WebGL-билд **zip-архивом** — вне релизного цикла Gitea
|
||
(не через CI, без образа в реестре, без номера сборки). Владелец: «создай там сам папку и выложи».
|
||
|
||
**Решение.** Каталоги на nginx теперь двух видов, живут рядом:
|
||
- `/opt/game/releases/<N>/` — номерные релизы из CI/реестра (`1`, `2`, … `29`);
|
||
- `/opt/game/releases/<имя>/` — именованные сборки из zip (`sol`).
|
||
|
||
**Именованные сборки АКТИВНУЮ не трогают**: `current` остаётся на номере, корень по-прежнему
|
||
редиректит на `/29/`. Именованная доступна только явной ссылкой `/<имя>/`.
|
||
|
||
**Изменения в nginx** (`game.nginx.conf`):
|
||
- regex-локейшн сборок расширен с чисел до имён: `^/(?<build>[a-zA-Z0-9][a-zA-Z0-9_-]*)(?<subpath>/.*)?$`;
|
||
- ⚠️ служебные пути получили префикс **`^~`** (`location ^~ /api`, `^~ /ws`, `^~ /solana-rpc/`).
|
||
Без этого расширенный regex перехватывал бы `/api/...` как «сборку с именем api» — regex-локейшн
|
||
в nginx приоритетнее обычного префиксного. `^~` подавляет проверку regex для этого префикса.
|
||
**Это была реальная регрессия на первом применении: после расширения шаблона `/api/state` начал
|
||
уходить в файловый резолвер.**
|
||
- маска `immutable` осталась **только для номерных** каталогов (`~^/[0-9]+/…`): именованную папку
|
||
могут перезалить тем же именем, поэтому кэшировать её ассеты на 7 суток нельзя. Добавлено
|
||
расширение `unityweb` (сборки фронтендера используют `.unityweb`-ассеты).
|
||
- новый скрипт `tools/proxmox-nginx/publish-zip.sh <имя> <путь.zip>` — распаковка, подъём
|
||
единственной вложенной папки, раскладка, reload, проверка.
|
||
|
||
**Приёмка (живой домен):** `/sol/` и все его ассеты (`Build.loader.js`, `Build.wasm.unityweb`,
|
||
`Build.data.unityweb`, `Build.framework.js.unityweb`, `TemplateData/*`) → **200**; `/` → 302 на `/29/`;
|
||
`/27/`,`/28/`,`/29/` живы; `/api/state`,`/api/price`,`/solana-rpc/` → 200; `testfront` цел.
|
||
|
||
**⚠️ Что нашли в присланной сборке (отдать фронтендеру, не наш блокер):**
|
||
1. **Двойной префикс API.** Игра зовёт `GET https://smart-updown.binom.pw/api/api/state` → **404**.
|
||
Причина в исходниках: `SmartUpdownConfig.ApiBase()` для WebGL возвращает `Origin() + "/api"`,
|
||
а `BackendClient.HttpGetAsync("/api/state")` добавляет путь целиком → `…/api` + `/api/state`.
|
||
В сборке 29 тот же код, но она до этого места не доходила (в headless падала раньше).
|
||
2. **`ReferenceError: getProvider is not defined`** — падение на `_SolanaBridgeConnect` при подключении
|
||
кошелька. В собранном `framework.js` есть **вызовы** `getProvider()`, `errMsg()`, `loadWeb3()`,
|
||
но **нет их определений** (в `SolanaBridge.jslib` они объявлены как функции внутри IIFE, а в
|
||
библиотеку через `mergeInto(LibraryManager.library, {...})` экспортированы только
|
||
`SolanaBridge*`-методы). Ошибка воспроизводится и в сборке 29 — то есть это **не регресс от
|
||
переезда на nginx**, а дефект jslib-обвязки, который просто не был виден раньше.
|
||
Файл — `Assets/Plugins/WebGL/SolanaBridge.jslib` (коммит `090ebf1`).
|
||
|
||
⚠️ Что **подтверждено** этим билдом: игра в браузере **стартует и доходит до игрового цикла**
|
||
(`[BinaryRocket] Phase -> Idle` → `Phase -> Approach`), чего в headless-окружении не делала ни одна
|
||
предыдущая сборка. То есть статика с nginx отдаётся корректно.
|