# Мои игры — usecase contract ## Основание и аудит Owner screenshot проверен визуально. Он показывает три раздельных переключателя, два крупных постигровых блока с названием «44», пустое участие и два sidebar-блока. Приглашение перенаправляет в сообщения. Это занимает первый экран без контекста конкретной встречи. Read-only источники изучены в переданной рабочей копии. | Источник | Наблюдение | |---|---| | MyGamesPage.tsx | relation определяет взаимоисключающую загрузку managed или participation. По умолчанию загружаются только участия. История — отдельный view. | | PersonalGameCard | При owned выводится статус игры вместо статуса участия. Это теряет вторую роль при будущем объединении. | | messaging-api.ts / InvitationDetail.tsx | Уже существуют listInvitations, getInvitation, invitationPath и accept/decline через useSocialMutation. | | InvitationsService.listMine | Возвращает приглашения только текущему invitee, с пагинацией. DTO содержит invitation IDs/status/version и gameId, но не название, расписание или game.version. | | ManagedGame.management | Сервер предоставляет role и createdByMe. Назначенный manager поддерживается отдельно от создателя. | | GameRecommendationsSection / #182 | Received/sent, capability, privacy lifecycle, hide/withdraw, version и UNKNOWN уже реализованы. | | GAME_RECOMMENDATIONS_V001 | Received game projection содержит только id/title/startsAt/endsAt/timezone. Рекомендация может касаться полной игры или закрытого приёма заявок. | | PostgameReminders / #372 | DTO даёт gameTitle, endsAt, allowedActions, resultsPath. Текущий вывод ограничен первыми пятью записями. | | MeGameResultsPage | Даёт собственную attendance, notice, gameTitle/endsAt. Содержит nextCursor. Отдельная permission-safe проекция работает при недоступной публичной карточке. | | formal-invitation-privacy-authority-boundary.test.ts | Зафиксированы private 404, отсутствие roster/contact из invitation, конкуренция за последнее место и actor-scoped replay. Тесты только прочитаны. | ## Информационная иерархия 1. Существующий ConsumerShell и H1 «Мои игры». Справа «Создать игру». 2. Группа обычных filter buttons с aria-pressed. «Все», «Создаю и управляю», «Заявки и участие», «Приглашения». 3. Только при реальных pending postgame actions — компактная строка «После игры ждут ответа» с переходом к первой соответствующей исторической карточке. Счётчик отражает загруженные уникальные игры, при наличии курсора получает «+». В прототипе показаны две полностью известные игры. 4. «Предстоящие». Один вертикальный список карточек в порядке времени. Текущая игра получает «Идёт сейчас». Записи с неизвестным временем располагаются после датированных и сообщают об отсутствии времени. Черновики управляемых игр доступны в коротком раскрытии «Черновики» после активного списка. 5. При фильтре «Все» и существующих рекомендациях — «Друзья советуют» в том же потоке. Полученный совет по gameId уже в основном списке находится внутри основной карточки. Повторная game-card для него не создаётся. 6. «История» ниже. Сначала прошедшие в обратном порядке endsAt. Если есть завершённые отношения к ещё будущим играм, отдельная компактная подгруппа «Завершённые заявки и отмены». Собственные доступные результаты, включая orphan, входят сюда один раз на игру. При unknown endsAt — подгруппа «Дата недоступна» без объявления игры прошедшей. 7. «Мои советы» и «Отзывы» — компактные раскрытия по существующим источникам. Прямой вход rec=sent сразу открывает отправленные советы. Review history сохраняет received/sent, публичность и действия существующих компонентов. Порядок основных групп сохраняется при фильтрации. Ссылка «К истории» прокручивает текущую страницу и переводит focus на заголовок. Фильтрация применяется к обеим временным группам. Нуждающееся во внимании действие по итогам не исчезает из верхнего указателя при узком фильтре, его ссылка включает «Все» и приводит к соответствующей карточке. ## Минимальные фильтры | Фильтр | Предикат | Важная деталь | |---|---|---| | Все | union всех собственных источников | Общий default после открытия /my-games. | | Создаю и управляю | Есть ManagedGame для gameId | createdByMe → «Создана мной», manager → «Управляю», owner без createdByMe → «Владелец». | | Заявки и участие | Есть Participation для gameId | pending остаётся «Заявка на рассмотрении». История сохраняет rejected/withdrawn/removed/expired/cancelled_by_game. | | Приглашения | Есть входящий GameInvitation | Доступны pending и прошлые состояния в своих группах. Счётчик рядом с фильтром считает только требующие ответа pending. | Создатель без Participation виден по серверному management. Совпадение displayName никогда не определяет роль. Если профиль и организатор имеют одно имя, это обычные две независимые сущности. ## Одна карточка на gameId UI-join объединяет независимые доказанные отношения в одну display row. Поля relationships остаются отдельными. Наличие management не стирает Participation, invitation или собственную attendance. При принятии invitation уже существующая карточка обновляет состояние участия в том же месте. У карточки есть дата, время площадки, название, доступное место, собственная роль и статус участия. У приглашения явно указан факт приглашения. Действия следуют роли и состоянию. - Management + confirmed показывают «Создана мной · Участие подтверждено» и вход в управление. - Management + pending сохраняют обе метки. Созданная игра остаётся активной, пока сама игра активна. - Invitation pending + Participation pending показывают «Приглашение в игру» и текущую заявку в одной карточке. Accept пользуется существующей серверной операцией. - Invitation accepted без актуальной confirmed participation показывает «Приглашение принято · Проверяем участие» или текущий фактический статус. Повторно утверждать подтверждение по accepted нельзя. - Received recommendations на ту же игру показывают имена доступных отправителей и индивидуальное «Скрыть совет». При нескольких советах раскрытие содержит отдельные строки с их versions и id. Общее скрытие всех советов не добавляется. - Orphan own result дополняет карточку этой игры. Несколько собственных contexts сохраняются внутри неё и на results route. Они не превращаются в roster или отдельные повторяющиеся gamecards. Идентификаторы находятся в data/URL. Заголовок использует предоставленное название. Если title отсутствует, текст «Игра» дополняется доступной датой. Если title равен «44», сохраняется «44», рядом обязательно доступное время/дата или «Дата недоступна». ## Временная классификация | Доказанные данные | Размещение | |---|---| | published, startsAt в будущем, endsAt в будущем, активное отношение | Предстоящие по startsAt ASC. | | published, startsAt ≤ now < endsAt | В начале предстоящих с «Идёт сейчас». | | endsAt ≤ now | История с датой. Факт проведения определяется outcome, attendance определяется своей проекцией. | | completed | История, даже если часы клиента отстают. «Завершена» допускается по status. | | cancelled | История или её подгруппа отмен будущих игр. «Игра отменена». | | Только terminal participation или terminal invitation, игра ещё в будущем | Подгруппа завершённых отношений в истории. Не показывать «Игра прошла». | | Есть активный management/participation/pending invitation и одно старое terminal отношение | Одна активная карточка, старое отношение остаётся вторичным. | | draft | Раскрытие черновиков в активной области для владельца/менеджера. Старое startsAt не делает черновик проведённой игрой. | | Публичный Game недоступен, собственный endsAt доказывает прошлое | История с собственной безопасной проекцией. | | Game и endsAt недоступны | Дата недоступна. Сохранить доступный статус отношения, не делать вывод о времени. | Для дат используют timezone игры. Если narrow own-result содержит только endsAt без timezone, можно использовать уже существующий formatter с явной подписью локального времени пользователя. Не маркировать его временем площадки. Refresh при возврате во вкладку и границах времени обновляет группировку. Финальные условия мутаций проверяет сервер. ## Основные сценарии | Сценарий | Путь и результат | |---|---| | Узнать ближайшую игру | Открыть /my-games → сразу все предстоящие → первая датированная карточка. | | Найти игру, которую создал | «Создаю и управляю» → карточка с серверной меткой → существующий /my-games/:id. | | Получил роль manager | Та же группа, метка «Управляю», действия по текущему доступу. | | Проверить заявку | «Заявки и участие» → точный статус → обычная страница игры. Pending не получает подтверждённого места. | | Принять приглашение | Карточка pending → «Принять приглашение» → busy → validated receipt и reread → confirmed только по Participation.status. | | Отклонить приглашение | Карточка pending → «Отклонить» → receipt → перенос в завершённые отношения, если отсутствует другая активная связь. | | Последнее место занято | GAME_FULL → краткая ошибка внутри карточки, перечитать игру и приглашение, возможность отклонить сохраняется при доступности. | | Условия изменились | 409 GAME_VERSION_CHANGED/VERSION_CONFLICT → перечитать, показать актуальные условия, новый intent после ознакомления. | | Отправка неизвестна | Network/5xx → устойчивый UNKNOWN в карточке, existing same-key retry. Confirmed до доказательства отсутствует. | | Друг посоветовал игру | Короткая строка совета с автором → «Открыть игру» → текущая карточка игры и обычная заявка при доступности. | | Уже участвую в рекомендованной игре | Совет прикрепляется к той же карточке. Подтверждённое участие показано по Participation. | | Скрыть совет | Скрыть конкретную полученную recommendation с expectedVersion, reread. Участие сохраняется по своему источнику. | | Отозвать отправленный совет | «Мои советы» → existing sent → withdraw. Знакомый не получает ложного отказа в приглашении. | | Подвести итоги | Указатель наверху → историческая карточка → «Подвести итоги» → existing resultsPath. | | Отложить | «Позже» на той же карточке → later endpoint → server returned reminders. | | Проверить посещение | Историческая карточка показывает собственную отметку и «Проверить отметку» → existing results route. Просмотр My Games не ACK. | | Есть возражение | Собственное open dispute, если оно присутствует в разрешённой attendance projection → «Уточнение открыто» → results?section=clarification. | | Написать/прочитать отзыв | Историческая карточка → результаты → существующая feedback section. Общая история отзывов доступна в нижнем раскрытии. | ## Постигровые действия Компактная нижняя полоса внутри конкретной исторической карточки. Слева ясный факт, справа действие. Если у одной игры одновременно reminder и notice, обе строки остаются внутри одной карточки. В верхнем указателе игра считается один раз. «Позже» появляется по allowedActions.canLater. Существующая политика содержит максимум три ordinal, интервалы 1h/24h/72h, минимум 24h между активациями, snooze 24h и сохранение общего cap. Эти значения не превращаются в клиентский таймер и не пересчитываются при обновлении страницы. PostgameReminders DTO использует limit 20. Если ответ содержит больше пяти, render не теряет хвост, «Ещё действия» раскрывает оставшиеся загруженные строки. Источник не даёт общего счётчика, поэтому число в UI ограничено доступными данными. Notice не закрывается фильтрацией, скроллом или переходом к истории. Возражение не закрывается ответом manager. Новая отметка и открытое возражение могут сосуществовать. Без данных о dispute показывается только доступная notice/attendance. Полная загрузка roster всех игр ради цифры «споры» не нужна. ## Empty, partial и privacy | Состояние | UI | |---|---| | Все источники завершены, своих игр нет | Одна компактная empty surface «Пока нет ваших игр» + «Найти игру». Create остаётся в header. | | Предстоящих нет, история есть | Одна строка «Предстоящих игр пока нет» + перейти к поиску. История сразу ниже. | | Выбранный фильтр пуст | «В этом фильтре пока нет игр» + «Показать все». Вся страница не объявляется пустой. | | Рекомендаций нет | В default нет пустой панели. Прямой rec=received может открыть компактное empty состояние. | | Recommendation capability false | Private list не запрашивается. Прямой вход показывает краткую причину доступности по existing gate. | | Первичная загрузка | Общая skeleton-композиция, aria-busy и status. Нулевые счётчики отсутствуют. | | Managed загружен, invitations fail | Managed показан, локальное сообщение «Приглашения не удалось загрузить» с Retry. Invitation filter без ложного нуля. | | Pagination ещё продолжается | Count не заявляет total. Пустая страница recommendation с nextCursor продолжает загрузку или даёт «Ещё». | | Auth expired/session switch | Очищение всех собственных DTO/derived rows и generation guards. AuthEntry с текущим returnPath. | | Invitation есть, Game private 404 | «Детали игры недоступны». Принятие недоступно без game.version. Отклонение использует invitation.version и текущие серверные права. | | Public Game network fail | Сохранить допустимый relation-state, дать retry. Error не переименовывать в privacy claim. | | Own results 403/404 | Локальное состояние недоступности с понятным текстом. Не выдавать его за отсутствие истории. | | Отсутствует имя inviter | «Приглашение в игру». Не подставлять organizer как inviter, так как приглашать мог manager. | | Длинные названия / локализация | Перенос без line-clamp важного статуса, min-width 0, CTA на следующей строке. | ## Responsive и доступность Desktop 1280/1920 использует существующий rail min(1380px, viewport − 64px). Колонка списка занимает ширину rail. У карточки компактный date badge, main content и action area. Карточки располагаются вертикально. Sidebar отсутствует. На 820 используется тот же один список. Карточка переносит действия под content при нехватке места. На 390 внешний inset 20, date badge 48, название и статусы переносятся, действия занимают отдельную полосу. Четыре фильтра переносятся в две строки. Bottom nav и резерв 76 + safe-area сохраняются. Все кнопки и ссылки ≥44px по высоте. Список фильтров использует buttons и aria-pressed с обычным Tab. Enter/Space активируют. Порядок DOM соответствует чтению. Для history jump focus переводится на heading с tabindex=-1. При удалении/переносе строки focus переходит к ближайшему устойчивому heading/результату действия. aria-live сообщает итог действия один раз. Outline видим на белом и lime. Цвет дублируется словами. Демонстрационный selector сценариев и служебная отметка «Прототип» присутствуют только в evidence-артефакте.