[F8-04] Lobby: стиль site + ссылка на профиль #47

Closed
opened 2026-05-25 21:40:19 +00:00 by owner · 9 comments
Collaborator

Подзадача полировки UI (этап F8), родительский #43.

Цель: Привести Lobby к стилю главной страницы site (шрифт, левая навигация) и заменить шапку на «Player-xxxx» со ссылкой на редактируемый профиль.

Пункты из #43:

4. Lobby. Было бы не лишним сделать в одном стиле в главной страницей site - тот же шрифт, та же навигация слева.
5. Lobby. Заголовки "you are logged in", "device session id" убираем, вместо "hello, Player-xxxxxxxx" давай напишем просто "Player-xxxxxxxx" со ссылкой на редактируемый профиль пользователя (проверь, должны уже быть готовы соответствующие API - как минимум для редактирования language и username).

Заметка (бэкенд готов, работа чисто фронтовая). Команды user.account.get (отдаёт user_name, display_name, preferred_language, time_zone), user.profile.update (правит только display_name) и user.settings.update (preferred_language, time_zone) заведены в gateway/internal/backendclient/ и уже вызываются из ui/frontend/src/lib/screens/lobby-screen.svelte. Редактируемое «имя» = display_name; user_name — неизменяемый handle player-<suffix> (pkg/model/user/user.go:158). Подтверждено владельцем: username = display_name.


Порядок обработки: F8-04 из 12. Создано в рамках #43; зонтичный issue остаётся открытым до закрытия всех подзадач.

Подзадача полировки UI (этап **F8**), родительский #43. **Цель:** Привести Lobby к стилю главной страницы site (шрифт, левая навигация) и заменить шапку на «Player-xxxx» со ссылкой на редактируемый профиль. **Пункты из #43:** > **4.** Lobby. Было бы не лишним сделать в одном стиле в главной страницей site - тот же шрифт, та же навигация слева. > **5.** Lobby. Заголовки "you are logged in", "device session id" убираем, вместо "hello, Player-xxxxxxxx" давай напишем просто "Player-xxxxxxxx" со ссылкой на редактируемый профиль пользователя (проверь, должны уже быть готовы соответствующие API - как минимум для редактирования language и username). _Заметка (бэкенд готов, работа чисто фронтовая)._ Команды `user.account.get` (отдаёт `user_name`, `display_name`, `preferred_language`, `time_zone`), `user.profile.update` (правит **только `display_name`**) и `user.settings.update` (`preferred_language`, `time_zone`) заведены в `gateway/internal/backendclient/` и уже вызываются из `ui/frontend/src/lib/screens/lobby-screen.svelte`. Редактируемое «имя» = `display_name`; `user_name` — неизменяемый handle `player-<suffix>` (`pkg/model/user/user.go:158`). Подтверждено владельцем: username = `display_name`. --- _Порядок обработки: **F8-04** из 12. Создано в рамках #43; зонтичный issue остаётся открытым до закрытия всех подзадач._
owner added the
Priority
Medium
3
Kind/Enhancementready
labels 2026-05-25 21:40:19 +00:00
developer was assigned by owner 2026-05-25 21:40:19 +00:00
developer added
claude
working
and removed ready labels 2026-05-26 06:40:58 +00:00
Owner

@owner Привет! Взялась за F8-04. Перед тем как трогать код — три развилки, из-за которых не хочу угадывать (потом дороже переделывать):

1. Что значит «навигация слева, как у site»?

У site (VitePress) слева — список страниц: «Galaxy / Overview». Это шаблон документации с многими страницами. У Lobby сейчас одна страница, четыре секции в одной колонке (мои игры, приглашения, заявки, публичные игры). Варианты, как это перенести:

  • (а) Сайдбар-якоря: слева список секций (My games / Invitations / Applications / Public games), клик скроллит к секции. Двухколоночная вёрстка, контент справа, на мобильнике сайдбар схлопывается под «бургер». Самое визуально близкое к site сейчас.
  • (б) Сайдбар-страницы: слева список «подэкранов» лобби (Overview / Profile / [позже: Sessions / Stats]). Структурно тождественно site, но пока «страница» по сути одна — выглядит малонаполненно, оживёт по мере добавления подэкранов.
  • (в) Без сайдбара, только шрифт: моноширинный шрифт + минимальная чистка, а сайдбар отложить до момента, когда будет ≥2 подэкранов лобби (Profile станет первым). Минимальный диф.

Я склоняюсь к (а) как буквальной интерпретации просьбы. Но если ты задумывала рост экосистемы под лобби — лучше (б), тогда якоря не нужны.

2. Куда вести по клику на Player-xxxx?

Есть рабочий паттерн — appScreen.go("lobby-create") (отдельный top-level экран в app-shell, Back возвращает в лобби, F5 восстанавливает). Логично добавить "profile" тем же способом. Подойдёт, или хочешь модалку/слайд-овер поверх лобби?

3. Состав формы профиля.

Бэкенд позволяет править: display_name (= «username» по твоему уточнению), preferred_language, time_zone. Issue требует «минимум язык и username». Включаю в форму этого PR:

  • (минимум) только display_name + preferred_language. Таймзону — отдельной задачей.
  • (полный) все три поля сразу, раз API уже умеет. PR чуть больше, но логично иметь «настройки аккаунта» одним блоком.

Склоняюсь к минимуму (меньше диф, чище ревью). Но если предпочитаешь сразу всё — скажи.


Если коротко: жду решение по (1), а (2) и (3) подтверди или поправь.

@owner Привет! Взялась за F8-04. Перед тем как трогать код — три развилки, из-за которых не хочу угадывать (потом дороже переделывать): ### 1. Что значит «навигация слева, как у site»? У site (VitePress) слева — список **страниц**: «Galaxy / Overview». Это шаблон документации с многими страницами. У Lobby сейчас одна страница, четыре секции в одной колонке (мои игры, приглашения, заявки, публичные игры). Варианты, как это перенести: - **(а)** **Сайдбар-якоря**: слева список секций (My games / Invitations / Applications / Public games), клик скроллит к секции. Двухколоночная вёрстка, контент справа, на мобильнике сайдбар схлопывается под «бургер». Самое визуально близкое к site сейчас. - **(б)** **Сайдбар-страницы**: слева список «подэкранов» лобби (Overview / Profile / [позже: Sessions / Stats]). Структурно тождественно site, но пока «страница» по сути одна — выглядит малонаполненно, оживёт по мере добавления подэкранов. - **(в)** **Без сайдбара, только шрифт**: моноширинный шрифт + минимальная чистка, а сайдбар отложить до момента, когда будет ≥2 подэкранов лобби (Profile станет первым). Минимальный диф. Я склоняюсь к **(а)** как буквальной интерпретации просьбы. Но если ты задумывала рост экосистемы под лобби — лучше **(б)**, тогда якоря не нужны. ### 2. Куда вести по клику на `Player-xxxx`? Есть рабочий паттерн — `appScreen.go("lobby-create")` (отдельный top-level экран в app-shell, Back возвращает в лобби, F5 восстанавливает). Логично добавить `"profile"` тем же способом. Подойдёт, или хочешь модалку/слайд-овер поверх лобби? ### 3. Состав формы профиля. Бэкенд позволяет править: `display_name` (= «username» по твоему уточнению), `preferred_language`, `time_zone`. Issue требует «минимум язык и username». Включаю в форму этого PR: - **(минимум)** только `display_name` + `preferred_language`. Таймзону — отдельной задачей. - **(полный)** все три поля сразу, раз API уже умеет. PR чуть больше, но логично иметь «настройки аккаунта» одним блоком. Склоняюсь к **минимуму** (меньше диф, чище ревью). Но если предпочитаешь сразу всё — скажи. --- Если коротко: жду решение по (1), а (2) и (3) подтверди или поправь.
Owner
Working log (issue-agent) — click to expand

State: done
Updated: 2026-05-26 (UTC, after post-merge CI)
Branch / PR: feature/issue-47-lobby-site-style → PR #60 (merged as 98d1fe6cae); issue auto-closed by Closes #47.

Merge gate decision:

  • PR mergeable, base development, body has Closes #47.
  • Author approval (review 14, owner, APPROVED) on head a679d9c — fresh.
  • No PR review threads to resolve.
  • CI on a679d9c green via per-run conclusions (combined commit-status read failure — phantom-pending; trusted run conclusions per the gitea-actions reference memory). Integration was filtered by paths (frontend-only diff), as the agent flagged in comment 523.
  • Merged with a normal merge commit.

Post-merge CI on 98d1fe6c (development):

  • Run 386 deploy (push) — success.
  • Run 387 test (push, ui-test) — success.
  • go-unit and integration did not fire (merge diff is ui/** only — path filters correctly excluded them).

Outcome: claude/done set; issue closed.

<!-- issue-agent:worklog --> <details> <summary>Working log (issue-agent) — click to expand</summary> **State:** done **Updated:** 2026-05-26 (UTC, after post-merge CI) **Branch / PR:** feature/issue-47-lobby-site-style → PR #60 (merged as 98d1fe6cae31db4ffcc2a2638f703c1bc724b3a1); issue auto-closed by `Closes #47`. **Merge gate decision:** - PR mergeable, base `development`, body has `Closes #47`. - Author approval (review 14, owner, APPROVED) on head a679d9c — fresh. - No PR review threads to resolve. - CI on a679d9c green via per-run conclusions (combined commit-status read `failure` — phantom-pending; trusted run conclusions per the gitea-actions reference memory). Integration was filtered by paths (frontend-only diff), as the agent flagged in comment 523. - Merged with a normal merge commit. **Post-merge CI on 98d1fe6c (development):** - Run 386 `deploy` (push) — success. - Run 387 `test` (push, ui-test) — success. - `go-unit` and `integration` did not fire (merge diff is ui/** only — path filters correctly excluded them). **Outcome:** `claude/done` set; issue closed. </details>
developer added
claude
blocked
and removed
claude
working
labels 2026-05-26 06:46:19 +00:00
Author
Collaborator

Привет. :) Отличные вопросы, отвечу по порядку.

  1. (б) - сайдбар-страницы, то что нужно.
  2. На форму профиля.
  3. Все три поля, конечно. Спасибо за подсказку.
Привет. :) Отличные вопросы, отвечу по порядку. 1. (б) - сайдбар-страницы, то что нужно. 2. На форму профиля. 3. Все три поля, конечно. Спасибо за подсказку.
developer added
claude
working
and removed
claude
blocked
labels 2026-05-26 10:25:09 +00:00
developer added ready and removed
claude
working
labels 2026-05-26 10:27:09 +00:00
developer added
claude
blocked
and removed ready labels 2026-05-26 11:11:57 +00:00
developer added
claude
working
and removed
claude
blocked
labels 2026-05-26 11:14:54 +00:00
developer added
claude
in-review
and removed
claude
working
labels 2026-05-26 11:47:50 +00:00
Owner

@owner Готово, заходи смотреть → PR #60.

Что собрано (как договорились):

  • Лобби + профиль теперь делят одну «сайтовую» оболочку (lib/screens/lobby-shell.svelte): слева список страниц (Overview / Profile), сверху справа «Player-xxxxxxxx» (имя как ссылка, открывает форму профиля) + logout. Шрифт --font-mono — как у site (VitePress). На ширине ≤ 640 px сайдбар схлопывается в горизонтальную полоску над контентом.
  • Из шапки лобби убраны: заголовок «you are logged in», блок device session id и «hello, X!». Идентичность теперь — компактная строка-ссылка.
  • Новый top-level экран profile (lib/screens/profile-screen.svelte). Поведение Back/Forward как у lobby-create. Форма с тремя полями: display_name (необязательное, пустое значение очищает имя — PATCH-семантика бэка), preferred_language (select по SUPPORTED_LOCALES, неподдерживаемое сохранённое значение оставляем как есть), time_zone (текстовое поле, placeholder показывает текущий пояс браузера; бэк валидирует через time.LoadLocation). На «сохранить» — PATCH только тех полей, что реально изменились; если новый preferred_language входит в SUPPORTED_LOCALES, активная локаль i18n переключается сразу.
  • API-обёртка src/api/account.ts (getMyAccount, updateMyProfile, updateMySettings) — отдельный модуль, лобби и профиль её используют.
  • i18n: добавлены lobby.nav.* и profile.* (en + ru), убраны теперь-неиспользуемые lobby.title / lobby.device_session_id_label / lobby.greeting.
  • Тесты: старые e2e перенесены с device-session-id / account-greeting на новый lobby-account-name; в одном Webkit-тесте про «отзыв сессии» пришлось добавить ожидание загруженного имени, иначе SubscribeEvents не успевал стартовать. Новый tests/e2e/profile-screen.spec.ts — 4 кейса (открытие, сохранение имени, смена языка с переключением локали, отмена). Фикстура lobby-fbs.ts расширена так, чтобы можно было задавать preferredLanguage / timeZone / declaredCountry.
  • Документация: ui/docs/lobby.md и ui/docs/navigation.md обновлены под новую раскладку. FUNCTIONAL.md не трогала — бэк-семантика та же.

Что прогнала локально: pnpm test (777/777), pnpm exec svelte-check (0/0), pnpm build (чисто), pnpm exec playwright test (245 pass), pnpm test:pwa (3/3).

CI зелёная на обоих контекстах (push 2m34s, pull_request 2m30s).

Тебе осталось два штриха, которые я локально проверить не могу:

  1. Глазами посмотреть, как сайдбар + идентичность садятся на разные ширины (особенно ≤ 640 px и переходный диапазон).
  2. На dev-deploy убедиться, что user.profile.update / user.settings.update действительно идут до бэка (мне-то — мок отвечал).

Если что не так — отлуплюсь и переделаю; если всё ок — апрув на PR, и закрою issue через merge gate.

@owner Готово, заходи смотреть → PR #60. **Что собрано (как договорились):** - **Лобби + профиль теперь делят одну «сайтовую» оболочку** (`lib/screens/lobby-shell.svelte`): слева список страниц (Overview / Profile), сверху справа «Player-xxxxxxxx» (имя как ссылка, открывает форму профиля) + logout. Шрифт `--font-mono` — как у site (VitePress). На ширине ≤ 640 px сайдбар схлопывается в горизонтальную полоску над контентом. - **Из шапки лобби убраны:** заголовок «you are logged in», блок `device session id` и «hello, X!». Идентичность теперь — компактная строка-ссылка. - **Новый top-level экран `profile`** (`lib/screens/profile-screen.svelte`). Поведение Back/Forward как у `lobby-create`. Форма с тремя полями: `display_name` (необязательное, пустое значение очищает имя — PATCH-семантика бэка), `preferred_language` (select по `SUPPORTED_LOCALES`, неподдерживаемое сохранённое значение оставляем как есть), `time_zone` (текстовое поле, placeholder показывает текущий пояс браузера; бэк валидирует через `time.LoadLocation`). На «сохранить» — PATCH только тех полей, что реально изменились; если новый `preferred_language` входит в `SUPPORTED_LOCALES`, активная локаль i18n переключается сразу. - **API-обёртка** `src/api/account.ts` (`getMyAccount`, `updateMyProfile`, `updateMySettings`) — отдельный модуль, лобби и профиль её используют. - **i18n**: добавлены `lobby.nav.*` и `profile.*` (en + ru), убраны теперь-неиспользуемые `lobby.title` / `lobby.device_session_id_label` / `lobby.greeting`. - **Тесты**: старые e2e перенесены с `device-session-id` / `account-greeting` на новый `lobby-account-name`; в одном Webkit-тесте про «отзыв сессии» пришлось добавить ожидание загруженного имени, иначе `SubscribeEvents` не успевал стартовать. Новый `tests/e2e/profile-screen.spec.ts` — 4 кейса (открытие, сохранение имени, смена языка с переключением локали, отмена). Фикстура `lobby-fbs.ts` расширена так, чтобы можно было задавать `preferredLanguage` / `timeZone` / `declaredCountry`. - **Документация**: `ui/docs/lobby.md` и `ui/docs/navigation.md` обновлены под новую раскладку. `FUNCTIONAL.md` не трогала — бэк-семантика та же. **Что прогнала локально:** `pnpm test` (777/777), `pnpm exec svelte-check` (0/0), `pnpm build` (чисто), `pnpm exec playwright test` (245 pass), `pnpm test:pwa` (3/3). **CI зелёная** на обоих контекстах (push 2m34s, pull_request 2m30s). Тебе осталось два штриха, которые я локально проверить не могу: 1. Глазами посмотреть, как сайдбар + идентичность садятся на разные ширины (особенно ≤ 640 px и переходный диапазон). 2. На dev-deploy убедиться, что `user.profile.update` / `user.settings.update` действительно идут до бэка (мне-то — мок отвечал). Если что не так — отлуплюсь и переделаю; если всё ок — апрув на PR, и закрою issue через merge gate.
Author
Collaborator

Сначала по твоим вопросам.

  1. По ширине разных экранов проверил, выглядит неплохо - от десктопа до мобилки.
  2. Данные профиля сохраняются (тут лучше тебе проверить, что интеграционные тесты это подтверждают - или дописать такие тесты). Только вот с таймзоной в профиле UX-проблема: сейчас у меня в поле ввода стоит "UTC" а формат ввода требуется вообще другой, неочевидный для пользователя. Давай лучше сделаем выпадающий список с таймзонами в нужном формате, отсортированный по America/* Europe/* и т.д., внутри континента - уже по названию городов. Не уверен, как лучше этот список организовать, чтобы он не был в километр длиной, предложи удобные варианты?
  3. Сюда же, как доработка - успешное сохранение данных профиля не должно уводить на "главную" страницу лобби, надо оставлять пользователя там же.

Теперь мои пожелания.

  1. Сейчас при навигации между "overview" / "profile" заметно скачет заголовок окна, там где display_name и кнопка logout. Видимо, обращения идут каждый раз к бэкенду за полной инфой, потому что я вижу как мелькает надпись, заканчивающаяся на "account..." - исправить бы поведение. С учётом изложенного ниже, возможно, сократится количество запросов на каждый переход, хотя не уверен с учётом скрытия пунктов меню. Попробуй оптимизировать.

  2. Страницу "overview" давай уберём, как самостоятельную единицу. На её место поставим "games" ("партии") c подменю:

  • "active/past" (сейчас это "my games") - сюда попадут все активные/завершенные игры с участием пользователя. Не отображать подраздел, если у игрока нет ничего в настоящем или прошлом.
  • "recruitment" (сейчас это наверное "public games") - партии, в которые открыт набор. Отображаем всегда, если пусто - пишем "набор в партии ещё не открыт". Там же, чтобы не плодить раздел "applications", отмечать партии, в которые игрок направил заявку с результатом (обрабатывается/принято/отклонено).
  • "invitations" - раздел с поступившими приглашениями в приватные партии.
  • "private games" - обособленный раздел с приватными играми, которые создаёт пользователь. Для dev-пользователя доступен, но в проде - доступен при подписочном тарифе (должно быть в документации).
  • Все подменю в мобильной версии можно сделать выпадающим списком из "games". То есть, в десктопе это в левой колонке будет раскрывающийся список сразу при проваливании в games, а в мобильной - выпадающий список по нажатию на кнопку верхнего меню. Если есть более изящный вариант, готов обсудить.
  • Полагаюсь на твои тесты с отображением/скрытием элементов меню, т.к. до прода мне это сложно будет визуально проверить.
  1. "Create new game" (создание приватной игры) выпадает из общей навигационной структуры, надо встроить в навигационную сетку.

  2. Synthetic test reports давай сделаем как отдельную единицу меню, доступную только в dev.

Сначала по твоим вопросам. 1. По ширине разных экранов проверил, выглядит неплохо - от десктопа до мобилки. 2. Данные профиля сохраняются (тут лучше тебе проверить, что интеграционные тесты это подтверждают - или дописать такие тесты). Только вот с таймзоной в профиле UX-проблема: сейчас у меня в поле ввода стоит "UTC" а формат ввода требуется вообще другой, неочевидный для пользователя. Давай лучше сделаем выпадающий список с таймзонами в нужном формате, отсортированный по America/* Europe/* и т.д., внутри континента - уже по названию городов. Не уверен, как лучше этот список организовать, чтобы он не был в километр длиной, предложи удобные варианты? 3. Сюда же, как доработка - успешное сохранение данных профиля не должно уводить на "главную" страницу лобби, надо оставлять пользователя там же. Теперь мои пожелания. 1. Сейчас при навигации между "overview" / "profile" заметно скачет заголовок окна, там где display_name и кнопка logout. Видимо, обращения идут каждый раз к бэкенду за полной инфой, потому что я вижу как мелькает надпись, заканчивающаяся на "account..." - исправить бы поведение. С учётом изложенного ниже, возможно, сократится количество запросов на каждый переход, хотя не уверен с учётом скрытия пунктов меню. Попробуй оптимизировать. 2. Страницу "overview" давай уберём, как самостоятельную единицу. На её место поставим "games" ("партии") c подменю: - "active/past" (сейчас это "my games") - сюда попадут все активные/завершенные игры с участием пользователя. Не отображать подраздел, если у игрока нет ничего в настоящем или прошлом. - "recruitment" (сейчас это наверное "public games") - партии, в которые открыт набор. Отображаем всегда, если пусто - пишем "набор в партии ещё не открыт". Там же, чтобы не плодить раздел "applications", отмечать партии, в которые игрок направил заявку с результатом (обрабатывается/принято/отклонено). - "invitations" - раздел с поступившими приглашениями в приватные партии. - "private games" - обособленный раздел с приватными играми, которые создаёт пользователь. Для dev-пользователя доступен, но в проде - доступен при подписочном тарифе (должно быть в документации). - Все подменю в мобильной версии можно сделать выпадающим списком из "games". То есть, в десктопе это в левой колонке будет раскрывающийся список сразу при проваливании в games, а в мобильной - выпадающий список по нажатию на кнопку верхнего меню. Если есть более изящный вариант, готов обсудить. - Полагаюсь на твои тесты с отображением/скрытием элементов меню, т.к. до прода мне это сложно будет визуально проверить. 3. "Create new game" (создание приватной игры) выпадает из общей навигационной структуры, надо встроить в навигационную сетку. 4. Synthetic test reports давай сделаем как отдельную единицу меню, доступную только в dev.
developer added
claude
working
and removed
claude
in-review
labels 2026-05-26 17:31:31 +00:00
Owner

@owner Поняла, спасибо за развёрнутый ответ. Прежде чем кодить — два важных момента, которые хочу с тобой согласовать, и одна развилка по таймзонам.

1. Объём этого PR — предлагаю разделить

Ты сейчас дал мне 7 разноразмерных штук. Я могу засунуть всё в #60, но это превратит его в большой не-обзоримый PR (нынешний — уже не маленький, ~+1500/-500). Предлагаю разбить так:

Остаётся в #60 (мелкие правки, ~полдня):

  • Таймзона как dropdown (см. пункт 3 ниже).
  • Не уводить с профиля при save (просто показать «сохранено», остаться на экране).
  • Поправить мигание имени при навигации overview ⇄ profile (хойстну getMyAccount в общий store на уровне сессии — оба экрана будут читать оттуда, бэкенд дернётся один раз на вход в lobby).
  • Подтвердить, что save профиля действительно ходит до бэка — у нас уже есть integration/user_profile_update_test.go и integration/user_settings_update_test.go (round-trip с PostgreSQL через testcontainers, проверяют и persistence, и невалидные входы). Их я просто перепрогоню в этом PR.

В новый issue F8-04b (или как ты захочешь обозвать, всё что про навигацию):

  • Заменить «overview» на «games» с подразделами active-past / recruitment / invitations / private games.
  • Объединить applications-with-status в recruitment.
  • Условное скрытие active-past, тир-гейт private games.
  • Мобильная развёртка подменю (dropdown vs. expanding tree).
  • Перенести «create new game» под navigation grid.
  • Synthetic test reports вынести отдельной dev-only единицей меню.

Причины разделить:

  • Тир-гейт private games — это не чисто UI, а новое продуктовое правило. Сейчас в коде нет понятия «приватные игры — только платным». На бэке есть только MaxRegisteredRaceNames per tier. Нужно решить: гейт только в UI (легко обходится прямым вызовом lobby.game.create) или ещё и на бэке (тогда это меняет lobby.game.create-контракт). И задокументировать в docs/FUNCTIONAL.md.
  • Объединение applications в recruitment — это бэкенд-сторону тоже трогает: чтобы помечать публичную игру статусом моей заявки, нужен либо отдельный join на клиенте (склеить lobby.public.games.list + lobby.my.applications.list), либо новое поле my_application_status в GameSummary. Если клиентский join — ок, но это отдельное решение.
  • Свёртка sidebar в выпадушку на мобиле — реальный дизайн-фрагмент, заслуживающий отдельной развёртки и e2e-покрытия.

Это не про лень, а про размер review-цикла. Если ты против — скажи, сделаю всё одним PR, но тогда review будет тяжёлым и мерж скорее всего разъедется на пару раундов.

2. Таймзона — варианты организации

Что у нас есть из коробки: Intl.supportedValuesOf("timeZone") — браузерный API, возвращает полный IANA-список (~500 зон). Поддержка: Chrome 99+ / Firefox 93+ / Safari 15.4+, то есть все наши целевые браузеры. Это значит: список не надо ни бандлить, ни поддерживать.

Варианты подачи:

  • (а) Нативный <select> с <optgroup> по континенту. 9 групп (Africa / America / Antarctica / Asia / Atlantic / Australia / Europe / Indian / Pacific / Etc), внутри каждой — отсортированные по городу опции. Без JS-зависимостей, нативная клавиатурная навигация (буква-jump), на мобиле — нативный picker. Минус: 500 опций — это всё ещё 500 опций, никакого filter-as-you-type.
  • (б) Тот же IANA-список, но через <input list> + <datalist>. Combobox с типизирующим фильтром (нативно). Минус: вид как у обычного текст-инпута, неочевидно, что это список; на iOS Safari поведение датасета слегка отличается.
  • (в) Каскад: сначала select континента, потом select города в нём. По 30-100 опций в каждом select. Минус: два клика, текстового поиска нет.

Я бы взяла (а) — нативно, без библиотек, мобильный picker не страдает, плюс пред-выбор по Intl.DateTimeFormat().resolvedOptions().timeZone при пустом значении. (б) ставит UX-приоритет на клавиатурный поиск, что хорошо для тех, кто знает имя зоны; (в) — самый понятный визуально, но самый громоздкий по кликам.

Какой берём?

3. И пара уточнений к твоему ответу про навигацию (на потом, для F8-04b)

Когда будем обсуждать большой issue — мне понадобится твоё мнение по двум развилкам, можешь сразу подумать:

  • Private games в проде: гейт только в UI (скрываем подменю) или ещё и на бэке (отклоняем lobby.game.create для бесплатного тарифа)?
  • Create new game: куда вкорнить? «games > private games > [+ новая]» (раскрытием подраздела), отдельный пункт sidebar «create», или fab-кнопка в углу private-games?

Если по таймзонам и разделению PR согласна — продолжаю. Если нет — поправь, пожалуйста.

@owner Поняла, спасибо за развёрнутый ответ. Прежде чем кодить — два важных момента, которые хочу с тобой согласовать, и одна развилка по таймзонам. ## 1. Объём этого PR — предлагаю разделить Ты сейчас дал мне 7 разноразмерных штук. Я могу засунуть всё в #60, но это превратит его в большой не-обзоримый PR (нынешний — уже не маленький, ~+1500/-500). Предлагаю разбить так: **Остаётся в #60 (мелкие правки, ~полдня):** - Таймзона как dropdown (см. пункт 3 ниже). - Не уводить с профиля при save (просто показать «сохранено», остаться на экране). - Поправить мигание имени при навигации overview ⇄ profile (хойстну `getMyAccount` в общий store на уровне сессии — оба экрана будут читать оттуда, бэкенд дернётся один раз на вход в lobby). - Подтвердить, что save профиля действительно ходит до бэка — у нас уже есть `integration/user_profile_update_test.go` и `integration/user_settings_update_test.go` (round-trip с PostgreSQL через testcontainers, проверяют и persistence, и невалидные входы). Их я просто перепрогоню в этом PR. **В новый issue F8-04b (или как ты захочешь обозвать, всё что про навигацию):** - Заменить «overview» на «games» с подразделами active-past / recruitment / invitations / private games. - Объединить applications-with-status в recruitment. - Условное скрытие active-past, тир-гейт private games. - Мобильная развёртка подменю (dropdown vs. expanding tree). - Перенести «create new game» под navigation grid. - Synthetic test reports вынести отдельной dev-only единицей меню. Причины разделить: - **Тир-гейт private games** — это не чисто UI, а новое продуктовое правило. Сейчас в коде нет понятия «приватные игры — только платным». На бэке есть только `MaxRegisteredRaceNames` per tier. Нужно решить: гейт только в UI (легко обходится прямым вызовом `lobby.game.create`) или ещё и на бэке (тогда это меняет `lobby.game.create`-контракт). И задокументировать в `docs/FUNCTIONAL.md`. - **Объединение applications в recruitment** — это бэкенд-сторону тоже трогает: чтобы помечать публичную игру статусом моей заявки, нужен либо отдельный join на клиенте (склеить `lobby.public.games.list` + `lobby.my.applications.list`), либо новое поле `my_application_status` в `GameSummary`. Если клиентский join — ок, но это отдельное решение. - **Свёртка sidebar в выпадушку на мобиле** — реальный дизайн-фрагмент, заслуживающий отдельной развёртки и e2e-покрытия. Это не про лень, а про размер review-цикла. Если ты против — скажи, сделаю всё одним PR, но тогда review будет тяжёлым и мерж скорее всего разъедется на пару раундов. ## 2. Таймзона — варианты организации Что у нас есть из коробки: `Intl.supportedValuesOf("timeZone")` — браузерный API, возвращает полный IANA-список (~500 зон). Поддержка: Chrome 99+ / Firefox 93+ / Safari 15.4+, то есть все наши целевые браузеры. Это значит: список не надо ни бандлить, ни поддерживать. Варианты подачи: - **(а)** Нативный `<select>` с `<optgroup>` по континенту. 9 групп (Africa / America / Antarctica / Asia / Atlantic / Australia / Europe / Indian / Pacific / Etc), внутри каждой — отсортированные по городу опции. Без JS-зависимостей, нативная клавиатурная навигация (буква-jump), на мобиле — нативный picker. Минус: 500 опций — это всё ещё 500 опций, никакого filter-as-you-type. - **(б)** Тот же IANA-список, но через `<input list>` + `<datalist>`. Combobox с типизирующим фильтром (нативно). Минус: вид как у обычного текст-инпута, неочевидно, что это список; на iOS Safari поведение датасета слегка отличается. - **(в)** Каскад: сначала select континента, потом select города в нём. По 30-100 опций в каждом select. Минус: два клика, текстового поиска нет. Я бы взяла **(а)** — нативно, без библиотек, мобильный picker не страдает, плюс пред-выбор по `Intl.DateTimeFormat().resolvedOptions().timeZone` при пустом значении. (б) ставит UX-приоритет на клавиатурный поиск, что хорошо для тех, кто знает имя зоны; (в) — самый понятный визуально, но самый громоздкий по кликам. Какой берём? ## 3. И пара уточнений к твоему ответу про навигацию (на потом, для F8-04b) Когда будем обсуждать большой issue — мне понадобится твоё мнение по двум развилкам, можешь сразу подумать: - **Private games в проде**: гейт только в UI (скрываем подменю) или ещё и на бэке (отклоняем `lobby.game.create` для бесплатного тарифа)? - **Create new game**: куда вкорнить? «games > private games > [+ новая]» (раскрытием подраздела), отдельный пункт sidebar «create», или fab-кнопка в углу private-games? Если по таймзонам и разделению PR согласна — продолжаю. Если нет — поправь, пожалуйста.
developer added
claude
blocked
and removed
claude
working
labels 2026-05-26 17:36:34 +00:00
Author
Collaborator

Ок, соглашусь. Оставляем предложенные тобой правки в #60, по вопросу timezone dropdown давай сделаем по варианту (a), как ты предлагаешь.


По остальным доработкам (для F8-04b), сначала отвечаю на твои уточняющие вопросы:

  • Да, для бесплатного тарифа надо скрывать и подменю на UI, и отклонять на бэкенде.
  • "Create new game" должно идти в "Private games" > кнопка в углу.

Исходя из этого, напиши, пожалуйста, отдельным сообщением в md-формате под "спойлером" полноценную постановку задачи по отдельным доработкам для F8-04b, как ты её для себя видишь - от общего к частному, если остаются незакрытые вопросы - меня не спрашивай, а отметь их отдельным блоком в тексте. Я в последствии этот текст использую для новой issue.

Ок, соглашусь. Оставляем предложенные тобой правки в #60, по вопросу timezone dropdown давай сделаем по варианту (a), как ты предлагаешь. --- По остальным доработкам (для F8-04b), сначала отвечаю на твои уточняющие вопросы: - Да, для бесплатного тарифа надо скрывать и подменю на UI, и отклонять на бэкенде. - "Create new game" должно идти в "Private games" > кнопка в углу. Исходя из этого, напиши, пожалуйста, отдельным сообщением в md-формате под "спойлером" полноценную постановку задачи по отдельным доработкам для F8-04b, как ты её для себя видишь - от общего к частному, если остаются незакрытые вопросы - меня не спрашивай, а отметь их отдельным блоком в тексте. Я в последствии этот текст использую для новой issue.
developer added
claude
working
and removed
claude
blocked
labels 2026-05-26 20:24:19 +00:00
Owner

@owner Готово, второй раунд по PR #60. Сделала ровно то, что договорились — без расширения скоупа в сторону F8-04b.

Что в этом раунде:

  • Часовой пояс — вариант (а), как условились. Поле теперь <select> с <optgroup> по континентам, источник — Intl.supportedValuesOf("timeZone") (заиграется во всех наших целевых браузерах, бандлить ничего не пришлось). Континенты отсортированы, города внутри — тоже. Если стандарт у браузера не поддерживается (теоретически — старый ходовой WebView) — фолбэк на тот же текстовый input, чтобы человек хотя бы мог сохраниться. Если в аккаунте лежит зона, которой нет в текущем IANA-списке — она добавляется отдельным пунктом в группу Other, чтобы round-trip-save её не потерял. На пустом значении форма открывается на поясе, который определил браузер (Intl.DateTimeFormat().resolvedOptions().timeZone).
  • Save-stay. После «сохранить» пользователь остаётся на странице профиля; снизу появляется компактная строка «saved». Любая правка её гасит. Только cancel теперь уводит обратно в лобби.
  • Скачущая шапка вылечена. Завела общий account-store на уровне сессии (lib/account-store.svelte.ts). И lobby, и profile дёргают account.ensure(client) — первый зашедший фетчит, остальные читают из руны. Шапка lobby-shell тоже читает оттуда же — переход overview ⇄ profile больше не мигает «loading account…» и не плодит лишних user.account.get. Сохранение профиля делает write-through: новое имя в шапке появляется без обращения к бэку. На signOut кеш чистится, чтобы при логине под другим аккаунтом не подмигнуло чужое имя.
  • Интеграционные тесты для save round-trip уже есть (integration/user_profile_update_test.go, integration/user_settings_update_test.go), отдельно ничего не добавляла — они уже проверяют и persistence, и невалидные входы через testcontainers + PostgreSQL. По договорённости проверяла, что они проходят на бэке, дописывать туда нечего.

Тесты:

  • pnpm check — 0/0.
  • pnpm test — 802/802 (16 в skip — wasm-сценарии, не относящиеся к этому диффу).
  • pnpm exec playwright test --project=chromium-desktop — 85/89 (4 skipped, ожидаемо).
  • pnpm exec playwright test --project=webkit-desktop — 72/89 (17 skipped).
  • pnpm test:pwa — 3/3.

Точечно в e2e profile-screen.spec.ts: старые 4 кейса перевела на save-stay, добавила два новых — на dropdown часового пояса (проверяет <optgroup>-структуру, defaultValue из стора, save-round-trip) и на стабильность шапки при переключении overview ⇄ profile (счётчик user.account.get остаётся на 1).

CI: ui-test (PR) и go-unit (push) — оба зелёные на a679d9c. integration.yaml не запускалась — паттерн её путей фильтрует только Go-изменения, диф здесь чисто-фронтовый. Это поведение workflow, не пропуск с моей стороны.

Документация: обновила ui/docs/lobby.md — описала общий кеш аккаунта, новую форму выбора часового пояса и поведение save-stay. FUNCTIONAL.md снова не трогала — бэк-семантика не изменилась.

Дальше — две вещи, которые мне нужны от тебя:

  1. Глазами проверь шапку лобби и форму профиля на разных ширинах (особенно мобильник — там нативный picker таймзоны должен выглядеть пристойно). Я в playwright это вижу как DOM, но не как визуальный UX.
  2. PR-апрув, если всё ок — закрою через Merge gate.

Постановку для F8-04b оформлю отдельным сообщением ниже, под спойлером (как ты просила) — целиком на копипасту в новую issue.

@owner Готово, второй раунд по PR #60. Сделала ровно то, что договорились — без расширения скоупа в сторону F8-04b. **Что в этом раунде:** - **Часовой пояс — вариант (а), как условились.** Поле теперь `<select>` с `<optgroup>` по континентам, источник — `Intl.supportedValuesOf("timeZone")` (заиграется во всех наших целевых браузерах, бандлить ничего не пришлось). Континенты отсортированы, города внутри — тоже. Если стандарт у браузера не поддерживается (теоретически — старый ходовой WebView) — фолбэк на тот же текстовый input, чтобы человек хотя бы мог сохраниться. Если в аккаунте лежит зона, которой нет в текущем IANA-списке — она добавляется отдельным пунктом в группу `Other`, чтобы round-trip-save её не потерял. На пустом значении форма открывается на поясе, который определил браузер (`Intl.DateTimeFormat().resolvedOptions().timeZone`). - **Save-stay.** После «сохранить» пользователь остаётся на странице профиля; снизу появляется компактная строка «saved». Любая правка её гасит. Только `cancel` теперь уводит обратно в лобби. - **Скачущая шапка вылечена.** Завела общий `account`-store на уровне сессии (`lib/account-store.svelte.ts`). И lobby, и profile дёргают `account.ensure(client)` — первый зашедший фетчит, остальные читают из руны. Шапка `lobby-shell` тоже читает оттуда же — переход overview ⇄ profile больше не мигает «loading account…» и не плодит лишних `user.account.get`. Сохранение профиля делает write-through: новое имя в шапке появляется без обращения к бэку. На `signOut` кеш чистится, чтобы при логине под другим аккаунтом не подмигнуло чужое имя. - **Интеграционные тесты для save round-trip уже есть** (`integration/user_profile_update_test.go`, `integration/user_settings_update_test.go`), отдельно ничего не добавляла — они уже проверяют и persistence, и невалидные входы через testcontainers + PostgreSQL. По договорённости проверяла, что они проходят на бэке, дописывать туда нечего. **Тесты:** - `pnpm check` — 0/0. - `pnpm test` — 802/802 (16 в skip — wasm-сценарии, не относящиеся к этому диффу). - `pnpm exec playwright test --project=chromium-desktop` — 85/89 (4 skipped, ожидаемо). - `pnpm exec playwright test --project=webkit-desktop` — 72/89 (17 skipped). - `pnpm test:pwa` — 3/3. **Точечно в e2e `profile-screen.spec.ts`:** старые 4 кейса перевела на save-stay, добавила два новых — на dropdown часового пояса (проверяет `<optgroup>`-структуру, defaultValue из стора, save-round-trip) и на стабильность шапки при переключении overview ⇄ profile (счётчик `user.account.get` остаётся на 1). **CI:** `ui-test` (PR) и `go-unit` (push) — оба зелёные на `a679d9c`. `integration.yaml` не запускалась — паттерн её путей фильтрует только Go-изменения, диф здесь чисто-фронтовый. Это поведение workflow, не пропуск с моей стороны. **Документация:** обновила `ui/docs/lobby.md` — описала общий кеш аккаунта, новую форму выбора часового пояса и поведение save-stay. `FUNCTIONAL.md` снова не трогала — бэк-семантика не изменилась. Дальше — две вещи, которые мне нужны от тебя: 1. **Глазами** проверь шапку лобби и форму профиля на разных ширинах (особенно мобильник — там нативный picker таймзоны должен выглядеть пристойно). Я в playwright это вижу как DOM, но не как визуальный UX. 2. **PR-апрув**, если всё ок — закрою через Merge gate. Постановку для **F8-04b** оформлю отдельным сообщением ниже, под спойлером (как ты просила) — целиком на копипасту в новую issue.
Owner

@owner Готово. Постановка для F8-04b ниже под спойлером — целиком на копипасту в новую issue.

F8-04b — навигационная сетка лобби (постановка задачи)

Зачем

Сейчас в лобби две страницы — Overview и Profile. Overview — это длинная одна-колонка из четырёх секций (мои игры, приглашения, заявки, публичные игры) плюс DEV-секция synthetic reports и кнопка «create new game», болтающаяся сверху. Из обсуждения F8-04: эта одна-колонка стала тесной, навигация перестала «читаться слева», а тир-режим для приватных игр должен учитываться и на UI, и на бэке. F8-04b разворачивает лобби в полноценную сетку из двух top-level пунктов навигации — games и profile — с подразделами и условной видимостью, и закрывает ту часть тир-гейта, которая сегодня вообще никак не учтена в коде.

Что меняется (от общего к частному)

1. Левый сайдбар лобби-шелла

  • Overview как самостоятельный пункт убирается. На его место — games (новое имя — «партии») с подменю.
  • Profile остаётся как top-level пункт.
  • Добавляется Synthetic test reports как top-level пункт, виден только при VITE_GALAXY_DEV_AFFORDANCES === "true" (тот же гейт, что и сейчас на секции внутри Overview). В prod-бандле пункт не появляется.

Итоговый верхнеуровневый список (paid tier): games · profile. В dev сборке к этому добавляется synthetic test reports. Active page подсвечивается так же, как сейчас.

2. Подменю games

Четыре подпункта в строго таком порядке:

Подпункт Источник данных Видимость Пустое состояние
active-past lobby.my.games.list Скрыт, если у игрока нет ни активных, ни завершённых партий (пункт не показан вовсе)
recruitment lobby.public.games.list + lobby.my.applications.list Всегда «набор в партии ещё не открыт»
invitations lobby.my.invites.list (только pending) Всегда «приглашений нет»
private games lobby.my.games.list (фильтр: owner=я, тип=private) Виден только paid (см. §4). В dev для нелогического тестирования виден всегда «приватных партий не создано — нажми кнопку в правом верхнем углу»

Раздел applications как самостоятельный исчезает. Его данные мерджатся в recruitment — у карточки публичной игры, в которую игрок подал заявку, появляется бейдж со статусом заявки (processing / accepted / rejected). Источник истины — lobby.my.applications.list; client-side join по gameId. Бейдж-чип ставится в правый верх карточки (тем же местом, что сегодня status у карточки application).

recruitment остаётся точкой входа для подачи заявки — текущая inline-форма «race name» сохраняется.

3. Кнопка «Create new game»

  • Из верхней секции лобби уходит.
  • Появляется как кнопка в правом верхнем углу панели private games. Открывает существующий top-level экран lobby-create (без изменений в самом lobby-create-screen).
  • Невидима/задизейблена, когда раздел private games скрыт (т.е. на free tier — см. §4).

4. Тир-гейт приватных игр

Два уровня гейта, оба обязательны:

  • UI: подпункт private games и кнопка create new game скрыты, пока account.entitlement.is_paid !== true. В dev-сборке игнорируется (см. §1 — паттерн VITE_GALAXY_DEV_AFFORDANCES), чтобы тестовому пользователю было удобно.
  • Backend: lobby.game.create отвергается с forbidden для free tier. Сейчас тариф определяется через Entitlement.IsPaid, видимым у account.entitlement (pkg/model/user/...). Конкретная точка проверки — gateway/internal/lobby/... перед прокидыванием на backend/.... Документируется в docs/FUNCTIONAL.md (§3.x «Lobby — create game») и в galaxy/<service>/openapi.yaml (новый код результата forbidden / уточнённый message — точную форму смотреть по месту, чтобы не сломать существующие классы).

is_paid сегодня присутствует во FBS EntitlementSnapshot, но НЕ декодируется в TS-интерфейс Account (ui/frontend/src/api/account.ts) — придётся расширить decodeAccountView и тип Account.

5. Мобильная развёртка

Десктоп (≥ 768 px) — подменю games живёт прямо в левом сайдбаре, всегда раскрыто, когда appScreen.screen указывает на любой из подпунктов games.

Мобильник (< 768 px) — сайдбар уже схлопывается в горизонтальную полоску (сделано в F8-04). При активной странице games:

  • Верхний пункт «games» становится кнопкой-дропдауном с активным подпунктом в качестве лейбла (например, «games · recruitment ▾»).
  • Тап раскрывает выпадающий список подпунктов под кнопкой.
  • Выбор подпункта — стандартная навигация.

Скрытые подпункты (active-past при пустоте, private-games на free) не показываются в выпадушке так же, как на десктопе.

6. Активная страница (appScreen)

Сейчас AppScreen"login" | "lobby" | "lobby-create" | "profile" | "game". Нужно либо расширить enum (lobby-games-active-past, lobby-games-recruitment, …, lobby-synthetic-reports), либо ввести второй параметр (lobbyPage: "overview" | "profile" | "games/active-past" | …) — это решение внутри реализации, не пользовательский вопрос. По текущему стилю кода (один screen rune, явные литералы) рекомендую расширить enum строковыми ключами в формате games-active-past, games-recruitment, games-invitations, games-private-games, synthetic-reports. Это даст явный switch в диспатчере и не сломает persistent navigation snapshot (sanitize-fallback на lobby).

7. Документация и контракты

Патчем должны обновиться:

  • ui/docs/lobby.md — раздел «Shell» (новый список pages + видимость), «Overview sections» (исчезает), «Games panels» (новый), «Synthetic reports» (отдельным разделом, не внутри Overview), «Profile» (без изменений).
  • ui/docs/navigation.md — список AppScreen (новые ключи), раздел про подменю на десктопе/мобиле.
  • docs/FUNCTIONAL.md + docs/FUNCTIONAL_ru.md — пункты про создание приватной игры (тир-гейт, бэкенд-отказ), про то что Player-xxxx видит recruitment с бейджем заявки.
  • docs/ARCHITECTURE.md — упоминание тир-гейта на lobby.game.create (одна строка, чтобы не плодить дубли).
  • gateway/<service>/openapi.yaml и/или .protoforbidden для lobby.game.create при free tier с описанием.

Принятые решения (на повторное согласование не нужно)

  • Тир-гейт private gamesUI + backend, оба обязательны.
  • create new gameвнутри private games, кнопка в правом верхнем углу.
  • В dev-сборке private games и synthetic reports видны всегда независимо от тарифа (через VITE_GALAXY_DEV_AFFORDANCES), чтобы owner мог щупать обе ветки до prod.
  • Тестовое покрытие должно покрыть видимость/скрытие пунктов по тарифу, потому что до prod это глазами не проверить (owner-confirmed).

Тестовое покрытие, обязательное

  • e2e:
    • На free tier: пункты private games и кнопка create new game отсутствуют в DOM.
    • На paid tier: пункты видны; нажатие create new game уводит на lobby-create.
    • В dev-режиме (VITE_GALAXY_DEV_AFFORDANCES=true): пункты видны независимо от тарифа.
    • Скрытие active-past при пустом списке и появление при ненулевом.
    • Бейдж заявки на карточке recruitment для каждого статуса (processing / accepted / rejected).
    • Мобильный дропдаун подменю: тап раскрывает, выбор перемещает страницу, повторный тап на активный — no-op.
  • backend integration: lobby.game.create для free-tier пользователя возвращает forbidden; для paid — ok. Существующих интеграционных тестов на create обновить недостаточно — нужен новый сценарий с явным free-tier пользователем.
  • vitest: декодирование is_paid в Account через getMyAccount mock'у.

Открытые вопросы (на закрытие в обсуждении новой issue)

Эти точки я не вижу как решить однозначно из кода, и не хочу решать за owner-а — лучше обсудить отдельно в новой issue, чем зафиксировать наугад.

  1. Текст бейджа заявки в recruitment — оставить технические термины (processing / accepted / rejected) или причесать (на рассмотрении / принята / отклонена)? Сейчас в i18n уже есть lobby.application.status.* с человеческими переводами — переиспользовать их или ввести более короткие чипы?
  2. Поведение подменю games на десктопе при переходе с другого top-level пункта. Авто-раскрытие при первом заходе на games, ручной toggle (как accordion), всегда раскрыто? Я бы взяла «всегда раскрыто, пока active page — внутри games» (без отдельного «свернуть»), но это субъективно.
  3. Подпункт по умолчанию у games при клике на сам заголовок «games» (без выбора подпункта). Варианты: первый видимый, последний выбранный (через sessionStorage), всегда recruitment как «всегда видимый»?
  4. Forbidden message от бэка при lobby.game.create на free tier — что показывать в UI как fallback на случай гонки (UI обновлён, тариф уже free, кнопка ещё не пересчитана)? Tост, инлайн-ошибка на экране lobby-create, редирект на lobby с тостом?
  5. Identity strip на мобиле — кнопка «Player-xxxx» уже служит «открыть профиль». При мобильной выпадушке подменю стоит ли как-то отделить эти два жеста (профиль ↔ навигация), или это уже разнесено достаточно хорошо?
  6. Кнопка «Create new game» в углу private games — что показывать на месте, когда подраздел пуст? Сейчас по тексту: «приватных партий не создано — нажми кнопку в правом верхнем углу». Альтернатива: большая CTA-кнопка в центре empty-state. Какой UX-приоритет?

После того как эти точки закрыты, по объёму это один умеренный PR (UI-сторона) + один отдельный PR на бэк-гейт lobby.game.create — их можно сшить под одной issue, но рассматривать отдельными мерджами.

@owner Готово. Постановка для **F8-04b** ниже под спойлером — целиком на копипасту в новую issue. <details> <summary>F8-04b — навигационная сетка лобби (постановка задачи)</summary> ## Зачем Сейчас в лобби две страницы — `Overview` и `Profile`. Overview — это длинная одна-колонка из четырёх секций (мои игры, приглашения, заявки, публичные игры) плюс DEV-секция synthetic reports и кнопка «create new game», болтающаяся сверху. Из обсуждения F8-04: эта одна-колонка стала тесной, навигация перестала «читаться слева», а тир-режим для приватных игр должен учитываться и на UI, и на бэке. F8-04b разворачивает лобби в полноценную сетку из двух top-level пунктов навигации — `games` и `profile` — с подразделами и условной видимостью, и закрывает ту часть тир-гейта, которая сегодня вообще никак не учтена в коде. ## Что меняется (от общего к частному) ### 1. Левый сайдбар лобби-шелла - `Overview` как самостоятельный пункт убирается. На его место — **`games`** (новое имя — «партии») с подменю. - `Profile` остаётся как top-level пункт. - Добавляется **`Synthetic test reports`** как top-level пункт, виден **только при `VITE_GALAXY_DEV_AFFORDANCES === "true"`** (тот же гейт, что и сейчас на секции внутри Overview). В prod-бандле пункт не появляется. Итоговый верхнеуровневый список (paid tier): `games` · `profile`. В dev сборке к этому добавляется `synthetic test reports`. Active page подсвечивается так же, как сейчас. ### 2. Подменю `games` Четыре подпункта в строго таком порядке: | Подпункт | Источник данных | Видимость | Пустое состояние | |-------------------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------| | `active-past` | `lobby.my.games.list` | Скрыт, если у игрока **нет** ни активных, ни завершённых партий | (пункт не показан вовсе) | | `recruitment` | `lobby.public.games.list` + `lobby.my.applications.list` | Всегда | «набор в партии ещё не открыт» | | `invitations` | `lobby.my.invites.list` (только `pending`) | Всегда | «приглашений нет» | | `private games` | `lobby.my.games.list` (фильтр: owner=я, тип=private) | Виден **только paid** (см. §4). В dev для нелогического тестирования виден всегда | «приватных партий не создано — нажми кнопку в правом верхнем углу» | Раздел `applications` как самостоятельный исчезает. Его данные мерджатся в `recruitment` — у карточки публичной игры, в которую игрок подал заявку, появляется бейдж со статусом заявки (`processing` / `accepted` / `rejected`). Источник истины — `lobby.my.applications.list`; client-side join по `gameId`. Бейдж-чип ставится в правый верх карточки (тем же местом, что сегодня `status` у карточки `application`). `recruitment` остаётся точкой входа для подачи заявки — текущая inline-форма «race name» сохраняется. ### 3. Кнопка «Create new game» - Из верхней секции лобби уходит. - Появляется как **кнопка в правом верхнем углу панели `private games`**. Открывает существующий top-level экран `lobby-create` (без изменений в самом lobby-create-screen). - Невидима/задизейблена, когда раздел `private games` скрыт (т.е. на free tier — см. §4). ### 4. Тир-гейт приватных игр Два уровня гейта, оба обязательны: - **UI**: подпункт `private games` и кнопка `create new game` скрыты, пока `account.entitlement.is_paid !== true`. В dev-сборке игнорируется (см. §1 — паттерн `VITE_GALAXY_DEV_AFFORDANCES`), чтобы тестовому пользователю было удобно. - **Backend**: `lobby.game.create` отвергается с `forbidden` для free tier. Сейчас тариф определяется через `Entitlement.IsPaid`, видимым у `account.entitlement` (`pkg/model/user/...`). Конкретная точка проверки — `gateway/internal/lobby/...` перед прокидыванием на `backend/...`. Документируется в `docs/FUNCTIONAL.md` (§3.x «Lobby — create game») и в `galaxy/<service>/openapi.yaml` (новый код результата `forbidden` / уточнённый message — точную форму смотреть по месту, чтобы не сломать существующие классы). `is_paid` сегодня **присутствует во FBS `EntitlementSnapshot`, но НЕ декодируется** в TS-интерфейс `Account` (`ui/frontend/src/api/account.ts`) — придётся расширить `decodeAccountView` и тип `Account`. ### 5. Мобильная развёртка Десктоп (≥ 768 px) — подменю `games` живёт прямо в левом сайдбаре, всегда раскрыто, когда `appScreen.screen` указывает на любой из подпунктов `games`. Мобильник (< 768 px) — сайдбар уже схлопывается в горизонтальную полоску (сделано в F8-04). При активной странице `games`: - Верхний пункт «games» становится кнопкой-дропдауном с активным подпунктом в качестве лейбла (например, «games · recruitment ▾»). - Тап раскрывает выпадающий список подпунктов под кнопкой. - Выбор подпункта — стандартная навигация. Скрытые подпункты (active-past при пустоте, private-games на free) не показываются в выпадушке так же, как на десктопе. ### 6. Активная страница (`appScreen`) Сейчас `AppScreen` — `"login" | "lobby" | "lobby-create" | "profile" | "game"`. Нужно либо расширить enum (`lobby-games-active-past`, `lobby-games-recruitment`, …, `lobby-synthetic-reports`), либо ввести второй параметр (`lobbyPage: "overview" | "profile" | "games/active-past" | …`) — это решение **внутри реализации**, не пользовательский вопрос. По текущему стилю кода (один screen rune, явные литералы) рекомендую расширить enum строковыми ключами в формате `games-active-past`, `games-recruitment`, `games-invitations`, `games-private-games`, `synthetic-reports`. Это даст явный switch в диспатчере и не сломает persistent navigation snapshot (sanitize-fallback на `lobby`). ### 7. Документация и контракты Патчем должны обновиться: - `ui/docs/lobby.md` — раздел «Shell» (новый список pages + видимость), «Overview sections» (исчезает), «Games panels» (новый), «Synthetic reports» (отдельным разделом, не внутри Overview), «Profile» (без изменений). - `ui/docs/navigation.md` — список `AppScreen` (новые ключи), раздел про подменю на десктопе/мобиле. - `docs/FUNCTIONAL.md` + `docs/FUNCTIONAL_ru.md` — пункты про создание приватной игры (тир-гейт, бэкенд-отказ), про то что Player-xxxx видит recruitment с бейджем заявки. - `docs/ARCHITECTURE.md` — упоминание тир-гейта на `lobby.game.create` (одна строка, чтобы не плодить дубли). - `gateway/<service>/openapi.yaml` и/или `.proto` — `forbidden` для `lobby.game.create` при free tier с описанием. ## Принятые решения (на повторное согласование не нужно) - Тир-гейт `private games` — **UI + backend**, оба обязательны. - `create new game` — **внутри `private games`, кнопка в правом верхнем углу**. - В dev-сборке `private games` и synthetic reports видны всегда независимо от тарифа (через `VITE_GALAXY_DEV_AFFORDANCES`), чтобы owner мог щупать обе ветки до prod. - Тестовое покрытие должно покрыть видимость/скрытие пунктов по тарифу, потому что до prod это глазами не проверить (owner-confirmed). ## Тестовое покрытие, обязательное - **e2e**: - На free tier: пункты `private games` и кнопка `create new game` отсутствуют в DOM. - На paid tier: пункты видны; нажатие `create new game` уводит на `lobby-create`. - В dev-режиме (VITE_GALAXY_DEV_AFFORDANCES=true): пункты видны независимо от тарифа. - Скрытие `active-past` при пустом списке и появление при ненулевом. - Бейдж заявки на карточке `recruitment` для каждого статуса (processing / accepted / rejected). - Мобильный дропдаун подменю: тап раскрывает, выбор перемещает страницу, повторный тап на активный — no-op. - **backend integration**: `lobby.game.create` для free-tier пользователя возвращает `forbidden`; для paid — ok. Существующих интеграционных тестов на `create` обновить недостаточно — нужен новый сценарий с явным free-tier пользователем. - **vitest**: декодирование `is_paid` в `Account` через `getMyAccount` mock'у. ## Открытые вопросы (на закрытие в обсуждении новой issue) Эти точки я не вижу как решить однозначно из кода, и не хочу решать за owner-а — лучше обсудить отдельно в новой issue, чем зафиксировать наугад. 1. **Текст бейджа заявки** в `recruitment` — оставить технические термины (`processing` / `accepted` / `rejected`) или причесать (`на рассмотрении` / `принята` / `отклонена`)? Сейчас в i18n уже есть `lobby.application.status.*` с человеческими переводами — переиспользовать их или ввести более короткие чипы? 2. **Поведение подменю `games` на десктопе при переходе с другого top-level пункта.** Авто-раскрытие при первом заходе на `games`, ручной toggle (как accordion), всегда раскрыто? Я бы взяла «всегда раскрыто, пока active page — внутри games» (без отдельного «свернуть»), но это субъективно. 3. **Подпункт по умолчанию у `games`** при клике на сам заголовок «games» (без выбора подпункта). Варианты: первый видимый, последний выбранный (через sessionStorage), всегда `recruitment` как «всегда видимый»? 4. **Forbidden message от бэка** при `lobby.game.create` на free tier — что показывать в UI как fallback на случай гонки (UI обновлён, тариф уже free, кнопка ещё не пересчитана)? Tост, инлайн-ошибка на экране `lobby-create`, редирект на `lobby` с тостом? 5. **Identity strip на мобиле** — кнопка «Player-xxxx» уже служит «открыть профиль». При мобильной выпадушке подменю стоит ли как-то отделить эти два жеста (профиль ↔ навигация), или это уже разнесено достаточно хорошо? 6. **Кнопка «Create new game» в углу `private games`** — что показывать на месте, когда подраздел пуст? Сейчас по тексту: «приватных партий не создано — нажми кнопку в правом верхнем углу». Альтернатива: большая CTA-кнопка в центре empty-state. Какой UX-приоритет? После того как эти точки закрыты, по объёму это один умеренный PR (UI-сторона) + один отдельный PR на бэк-гейт `lobby.game.create` — их можно сшить под одной issue, но рассматривать отдельными мерджами. </details>
developer added
claude
in-review
and removed
claude
working
labels 2026-05-26 20:43:41 +00:00
developer added
claude
working
and removed
claude
in-review
labels 2026-05-26 20:53:07 +00:00
developer added
claude
done
and removed
claude
working
labels 2026-05-26 20:56:45 +00:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/galaxy-game#47