[F8-04] Lobby: стиль site + ссылка на профиль #47
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Подзадача полировки UI (этап F8), родительский #43.
Цель: Привести Lobby к стилю главной страницы site (шрифт, левая навигация) и заменить шапку на «Player-xxxx» со ссылкой на редактируемый профиль.
Пункты из #43:
Заметка (бэкенд готов, работа чисто фронтовая). Команды
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— неизменяемый handleplayer-<suffix>(pkg/model/user/user.go:158). Подтверждено владельцем: username =display_name.Порядок обработки: F8-04 из 12. Создано в рамках #43; зонтичный issue остаётся открытым до закрытия всех подзадач.
@owner Привет! Взялась за F8-04. Перед тем как трогать код — три развилки, из-за которых не хочу угадывать (потом дороже переделывать):
1. Что значит «навигация слева, как у site»?
У site (VitePress) слева — список страниц: «Galaxy / Overview». Это шаблон документации с многими страницами. У Lobby сейчас одна страница, четыре секции в одной колонке (мои игры, приглашения, заявки, публичные игры). Варианты, как это перенести:
Я склоняюсь к (а) как буквальной интерпретации просьбы. Но если ты задумывала рост экосистемы под лобби — лучше (б), тогда якоря не нужны.
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. Таймзону — отдельной задачей.Склоняюсь к минимуму (меньше диф, чище ревью). Но если предпочитаешь сразу всё — скажи.
Если коротко: жду решение по (1), а (2) и (3) подтверди или поправь.
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 byCloses #47.Merge gate decision:
development, body hasCloses #47.a679d9c— fresh.a679d9cgreen via per-run conclusions (combined commit-status readfailure— 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.Post-merge CI on
98d1fe6c(development):deploy(push) — success.test(push, ui-test) — success.go-unitandintegrationdid not fire (merge diff is ui/** only — path filters correctly excluded them).Outcome:
claude/doneset; issue closed.Привет. :) Отличные вопросы, отвечу по порядку.
@owner Готово, заходи смотреть → PR #60.
Что собрано (как договорились):
lib/screens/lobby-shell.svelte): слева список страниц (Overview / Profile), сверху справа «Player-xxxxxxxx» (имя как ссылка, открывает форму профиля) + logout. Шрифт--font-mono— как у site (VitePress). На ширине ≤ 640 px сайдбар схлопывается в горизонтальную полоску над контентом.device session idи «hello, X!». Идентичность теперь — компактная строка-ссылка.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 переключается сразу.src/api/account.ts(getMyAccount,updateMyProfile,updateMySettings) — отдельный модуль, лобби и профиль её используют.lobby.nav.*иprofile.*(en + ru), убраны теперь-неиспользуемыеlobby.title/lobby.device_session_id_label/lobby.greeting.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).
Тебе осталось два штриха, которые я локально проверить не могу:
user.profile.update/user.settings.updateдействительно идут до бэка (мне-то — мок отвечал).Если что не так — отлуплюсь и переделаю; если всё ок — апрув на PR, и закрою issue через merge gate.
Сначала по твоим вопросам.
Теперь мои пожелания.
Сейчас при навигации между "overview" / "profile" заметно скачет заголовок окна, там где display_name и кнопка logout. Видимо, обращения идут каждый раз к бэкенду за полной инфой, потому что я вижу как мелькает надпись, заканчивающаяся на "account..." - исправить бы поведение. С учётом изложенного ниже, возможно, сократится количество запросов на каждый переход, хотя не уверен с учётом скрытия пунктов меню. Попробуй оптимизировать.
Страницу "overview" давай уберём, как самостоятельную единицу. На её место поставим "games" ("партии") c подменю:
"Create new game" (создание приватной игры) выпадает из общей навигационной структуры, надо встроить в навигационную сетку.
Synthetic test reports давай сделаем как отдельную единицу меню, доступную только в dev.
@owner Поняла, спасибо за развёрнутый ответ. Прежде чем кодить — два важных момента, которые хочу с тобой согласовать, и одна развилка по таймзонам.
1. Объём этого PR — предлагаю разделить
Ты сейчас дал мне 7 разноразмерных штук. Я могу засунуть всё в #60, но это превратит его в большой не-обзоримый PR (нынешний — уже не маленький, ~+1500/-500). Предлагаю разбить так:
Остаётся в #60 (мелкие правки, ~полдня):
getMyAccountв общий store на уровне сессии — оба экрана будут читать оттуда, бэкенд дернётся один раз на вход в lobby).integration/user_profile_update_test.goиintegration/user_settings_update_test.go(round-trip с PostgreSQL через testcontainers, проверяют и persistence, и невалидные входы). Их я просто перепрогоню в этом PR.В новый issue F8-04b (или как ты захочешь обозвать, всё что про навигацию):
Причины разделить:
MaxRegisteredRaceNamesper tier. Нужно решить: гейт только в UI (легко обходится прямым вызовомlobby.game.create) или ещё и на бэке (тогда это меняетlobby.game.create-контракт). И задокументировать вdocs/FUNCTIONAL.md.lobby.public.games.list+lobby.my.applications.list), либо новое полеmy_application_statusвGameSummary. Если клиентский join — ок, но это отдельное решение.Это не про лень, а про размер 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.<input list>+<datalist>. Combobox с типизирующим фильтром (нативно). Минус: вид как у обычного текст-инпута, неочевидно, что это список; на iOS Safari поведение датасета слегка отличается.Я бы взяла (а) — нативно, без библиотек, мобильный picker не страдает, плюс пред-выбор по
Intl.DateTimeFormat().resolvedOptions().timeZoneпри пустом значении. (б) ставит UX-приоритет на клавиатурный поиск, что хорошо для тех, кто знает имя зоны; (в) — самый понятный визуально, но самый громоздкий по кликам.Какой берём?
3. И пара уточнений к твоему ответу про навигацию (на потом, для F8-04b)
Когда будем обсуждать большой issue — мне понадобится твоё мнение по двум развилкам, можешь сразу подумать:
lobby.game.createдля бесплатного тарифа)?Если по таймзонам и разделению PR согласна — продолжаю. Если нет — поправь, пожалуйста.
Ок, соглашусь. Оставляем предложенные тобой правки в #60, по вопросу timezone dropdown давай сделаем по варианту (a), как ты предлагаешь.
По остальным доработкам (для F8-04b), сначала отвечаю на твои уточняющие вопросы:
Исходя из этого, напиши, пожалуйста, отдельным сообщением в md-формате под "спойлером" полноценную постановку задачи по отдельным доработкам для F8-04b, как ты её для себя видишь - от общего к частному, если остаются незакрытые вопросы - меня не спрашивай, а отметь их отдельным блоком в тексте. Я в последствии этот текст использую для новой issue.
@owner Готово, второй раунд по PR #60. Сделала ровно то, что договорились — без расширения скоупа в сторону F8-04b.
Что в этом раунде:
<select>с<optgroup>по континентам, источник —Intl.supportedValuesOf("timeZone")(заиграется во всех наших целевых браузерах, бандлить ничего не пришлось). Континенты отсортированы, города внутри — тоже. Если стандарт у браузера не поддерживается (теоретически — старый ходовой WebView) — фолбэк на тот же текстовый input, чтобы человек хотя бы мог сохраниться. Если в аккаунте лежит зона, которой нет в текущем IANA-списке — она добавляется отдельным пунктом в группуOther, чтобы round-trip-save её не потерял. На пустом значении форма открывается на поясе, который определил браузер (Intl.DateTimeFormat().resolvedOptions().timeZone).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кеш чистится, чтобы при логине под другим аккаунтом не подмигнуло чужое имя.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снова не трогала — бэк-семантика не изменилась.Дальше — две вещи, которые мне нужны от тебя:
Постановку для F8-04b оформлю отдельным сообщением ниже, под спойлером (как ты просила) — целиком на копипасту в новую issue.
@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-pastlobby.my.games.listrecruitmentlobby.public.games.list+lobby.my.applications.listinvitationslobby.my.invites.list(толькоpending)private gameslobby.my.games.list(фильтр: owner=я, тип=private)Раздел
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. Тир-гейт приватных игр
Два уровня гейта, оба обязательны:
private gamesи кнопкаcreate new gameскрыты, покаaccount.entitlement.is_paid !== true. В dev-сборке игнорируется (см. §1 — паттернVITE_GALAXY_DEV_AFFORDANCES), чтобы тестовому пользователю было удобно.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сегодня присутствует во FBSEntitlementSnapshot, но НЕ декодируется в TS-интерфейсAccount(ui/frontend/src/api/account.ts) — придётся расширитьdecodeAccountViewи типAccount.5. Мобильная развёртка
Десктоп (≥ 768 px) — подменю
gamesживёт прямо в левом сайдбаре, всегда раскрыто, когдаappScreen.screenуказывает на любой из подпунктовgames.Мобильник (< 768 px) — сайдбар уже схлопывается в горизонтальную полоску (сделано в F8-04). При активной странице
games:Скрытые подпункты (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, кнопка в правом верхнем углу.private gamesи synthetic reports видны всегда независимо от тарифа (черезVITE_GALAXY_DEV_AFFORDANCES), чтобы owner мог щупать обе ветки до prod.Тестовое покрытие, обязательное
private gamesи кнопкаcreate new gameотсутствуют в DOM.create new gameуводит наlobby-create.active-pastпри пустом списке и появление при ненулевом.recruitmentдля каждого статуса (processing / accepted / rejected).lobby.game.createдля free-tier пользователя возвращаетforbidden; для paid — ok. Существующих интеграционных тестов наcreateобновить недостаточно — нужен новый сценарий с явным free-tier пользователем.is_paidвAccountчерезgetMyAccountmock'у.Открытые вопросы (на закрытие в обсуждении новой issue)
Эти точки я не вижу как решить однозначно из кода, и не хочу решать за owner-а — лучше обсудить отдельно в новой issue, чем зафиксировать наугад.
recruitment— оставить технические термины (processing/accepted/rejected) или причесать (на рассмотрении/принята/отклонена)? Сейчас в i18n уже естьlobby.application.status.*с человеческими переводами — переиспользовать их или ввести более короткие чипы?gamesна десктопе при переходе с другого top-level пункта. Авто-раскрытие при первом заходе наgames, ручной toggle (как accordion), всегда раскрыто? Я бы взяла «всегда раскрыто, пока active page — внутри games» (без отдельного «свернуть»), но это субъективно.gamesпри клике на сам заголовок «games» (без выбора подпункта). Варианты: первый видимый, последний выбранный (через sessionStorage), всегдаrecruitmentкак «всегда видимый»?lobby.game.createна free tier — что показывать в UI как fallback на случай гонки (UI обновлён, тариф уже free, кнопка ещё не пересчитана)? Tост, инлайн-ошибка на экранеlobby-create, редирект наlobbyс тостом?private games— что показывать на месте, когда подраздел пуст? Сейчас по тексту: «приватных партий не создано — нажми кнопку в правом верхнем углу». Альтернатива: большая CTA-кнопка в центре empty-state. Какой UX-приоритет?После того как эти точки закрыты, по объёму это один умеренный PR (UI-сторона) + один отдельный PR на бэк-гейт
lobby.game.create— их можно сшить под одной issue, но рассматривать отдельными мерджами.