# #404 · краткий brief после дизайн-решения **PROPOSED_NOT_OWNER_APPROVED / OWNER_REVIEW_REQUIRED.** Исходное приложение не менялось. Этот проход — дизайн и локальный HTML mock. Source SHA `8be7851a6958cc9e7cdae33fa015c8a95c0d8f70` указан доверенным заданием; Git отсутствует в переданном snapshot, независимая сверка commit не заявляется. ## Что действительно прочитано и можно использовать Все пути ниже относительно `INPUT/source`. | Source anchor | Подтверждённая возможность / граница | |---|---| | `docs/product/SITEWIDE_UI_STYLE_GUIDE.md`, `DESKTOP_OVERLAY_STYLE_GUIDE.md` | Общая левая линия, бренд и существующий shell; overlays от 1024 с inset 40. В предлагаемом деке overlay не нужен | | `apps/web/src/components/consumer/players/PlayerProfileContent.tsx` → `PlayerProfileHero` | Обложка + Avatar внутри неё, имя, самооценка уровня, bio, area, статусы; optional distance документирован как Radar-only | | `players/ProfileComposition.tsx`; `styles/players.css:65` и `styles/players-profiles.css` | Avatar/Badge/ProfileState; имеющаяся компактная композиция профиля. Не менять глобальный hero: локальный вариант/класс для дека | | `players/PublicPlayerProfile.tsx:144` | Общий публичный профиль, независимые contact/friendship, ordinary upcoming — unavailable stub; safety/report остаются в полном профиле | | `pages/PlayersDirectoryPage.tsx` | `listPlayers`, filters, cursor scope, дедупликация publicPlayerId, ordinary каталог независимо от Radar; новый вид «По одному» рядом со списком | | `lib/consumer-types.ts:297` → `PublicPlayer`, `PublicPlayerPage` | cover/avatar/name/cityId/areaKey/level/bio/trainer/lookingForGame; cursor. Нет distances, ratings, reviews, публичных игр или расписания тренировок | | `lib/friendships.ts` → `friendCommand` | Только server capabilities разрешают request/accept/decline/cancel/remove; версии — decimal strings | | `friends/ProfileFriendship.tsx`, `friends/FriendActions.tsx` | Проверка social-capabilities/state, foreground/auth expiry, существующие кнопки доменных действий | | `messages/useSocialMutation.ts` | Busy lock, pending identity `(path, body, key, method)` в sessionStorage, 0/5xx остаются pending. `retry()` повторяет POST с прежним ключом: **не** является read-only проверкой статуса | | `apps/api/src/friendships/friendships.controller.ts`, `.policy.ts`, выбранные `.service.ts:create/charge/states` | Cookie mutation auth, idempotency-key, eligibility/blocks, TTL, expectedVersion; DEV feature fences, 429/Retry-After; states batch до 50 ID | | `packages/contracts/src/friendship.generated.ts` | none / outgoing_pending / incoming_pending / friends / unavailable, capabilities, request/version, receipt outcome | | `lib/radar-types.ts`, `radar-presentation.ts`, `unified-directory.ts`, `radar/UnifiedPlayerPreview.tsx`, `packages/contracts/src/unified.generated.ts` | Legacy proximity bands не километры; unified приблизительная distance и upcomingGames существуют только за действующим разрешённым preview/TTL fence | | `styles/consumer-tokens.css`, `foundation.css`; `app/layout.tsx` | Текущие consumer цвета, shell rail, размеры, Onest через next/font/google | Контракт существует в source; текущая DEV-конфигурация и фактическая runtime-доступность **не проверялись** и этим заданием не разрешались. Наличие методов не означает READY/ACTIVE или разрешение включить feature flag. ## Минимальный будущий scope После Owner-дизайн-решения: локальный `PlayerBrowseDeck` в каталоге + scoped CSS, переиспользование представления профиля и текущего friendship command builder. Сохранить прежний список/фильтры, shell и принятый feed. Не добавлять route/provider/engine/scheduler/migration. Не переносить gallery JS, TEST-данные или панель сценариев в приложение. 1. Ordinary `listPlayers` даёт список и cover; `listCities` разрешает cityId в название. Дочитывать `getPlayer` только когда это нужно для профиля. Не заполнять отсутствующие поля. Отдельно проверить global social capability, авторизацию и текущие friendship states. Пока права не загружены, request disabled; гость может читать, вход — отдельное существующее действие, не автоматическая мутация после логина. 2. Основной дек предлагает только не-self `relationship=none && canRequest`. Outgoing/friends/incoming/unavailable исключаются из новых кандидатов. Внезапно изменившееся состояние открытой карточки показывается явно. Входящий запрос не превращает правый свайп в accept: кнопки accept/decline разрешены исключительно `canAccept/canDecline`. Причина unavailable не выводится как «вас заблокировали». Собственная блокировка и жалобы остаются в полном профиле. 3. Request: `POST /v1/friend-requests`, `{targetPublicPlayerId}`; сервер требует session/CSRF через текущий клиент и `Idempotency-Key`. Решения: `POST /v1/friend-requests/:requestId/{accept,decline,cancel}`, `{expectedVersion}`. Отмена доступна лишь с текущими `canCancel`, request.id, version. Никакого generic undo для отправленного запроса. Remove вне нового дека. 4. До POST сохранить идентичность одной операции и заблокировать повторное нажатие/жест. Успешный receipt показывает отправку; не показывать «Вы друзья» до accepted. Конфликт/stale/входящий ответ обновляет права и состояние; не подменять исходное намерение новым действием. 429 сохраняет реальный code/Retry-After, блокирует новые отправки до разрешённого времени, не блокирует обычное чтение. Нельзя придумывать countdown, если данных нет. 5. Transport 0/5xx после POST: сохранить сырые status/problem и исходные key/body/peer, показать UNKNOWN. «Проверить статус» читает существующий `GET /v1/me/friendship-states?publicPlayerId=…` (или известный friendship id). GET — не скрытый POST replay. Уверенно известный текущий pending/friends можно показать; `none`, ошибка чтения или исчезновение записи **не доказывают** неотправку именно предыдущей операции. Latch не снимать по таймеру/перелистыванию/возврату. Existing `retry()` нельзя напрямую повесить на эту кнопку. Если нужна полная сверка operation receipt, отдельно подтвердить существующие server semantics; нового status endpoint в этом дизайне не выдумано. ## Пропуск, страницы, возвращение Пропуск — только память текущего browsing scope: ID, позиция, стек локального undo. Ни block, ни decline, ни отрицательное сообщение. Запрос хранится отдельно от пропуска. После отправки показывается итог и явный «Следующий игрок»; никакой отправки при смене карточки, focus, restore или пагинации. Переиспользовать `scopedCatalogCursor` и набор текущих фильтров; смена scope сбрасывает cursor/page cache/локальные пропуски, но не стирает незавершённые операции. ID дедуплицируются в scope, включая пагинацию. Начинать с небольшой уже загруженной страницы, без бесконечного eager scanning страниц ради поиска подходящего кандидата; при исчерпании видимых кандидатов и наличии nextCursor — явное «Загрузить ещё». Не утверждать «все просмотрены», пока nextCursor существует. По возврату из публичного профиля восстановить позицию и scroll тем же существующим способом, затем проверить visibility и права. Локальный undo возвращает кандидата после перепроверки; исчезнувший профиль не воскрешает. Полный reload может начать ordinary подборку заново: серверный resume не требуется V1. Unresolved mutations остаются в существующей приватной scope-защите и не становятся новым запросом после reload/auth change. Кросс-устройственный skip/resume не обещан. ## Данные, которых не нужно придумывать - **Расстояние:** ordinary не имеет поля; V1 без него. Уже разрешённая unified-проекция может добавить `distanceText` на срок текущего результата. Не переносить geo/resultRef/private payload в публичный DOM, URL или storage. Включение Radar и согласие не связаны с деком. - **Игры:** ordinary PublicPlayerProfile сейчас показывает unavailable. Сохраняем этот честный fallback + полный профиль. Уже разрешённый UnifiedPreview даёт лишь публичные будущие игры в пределах TTL, максимум первые три как в текущем preview; это не календарь доступности. В макете список игр не симулирован. - **Отзывы/оценки:** в выбранном публичном DTO нет источника. Не включать в V1; приватный feedback не публиковать. Самостоятельный уровень не рейтинг. - **Дефицит локального шрифта:** нужен имеющийся продуктовый Onest при реализации. Для точной автономной галереи trusted PMA может добавить разрешённый файл Onest; этот проход не загружал сеть и не менял зависимости. ## Что проверено в этом проходе `checks/check-prototype.cjs` исполняет настоящие inline handlers в минимальном DOM harness: 15 адресных проверок переходов, pointercancel/half-drag/вертикального жеста, keyboard scope, UNKNOWN, rate, incoming, local undo, связности файлов. Синтаксис JS проверен Node. Это **самопроверка макета**, не независимая QA и не browser E2E. Браузерный rendering, touch hardware, screen reader, 1440/1024/768/390 screenshots — **NOT_TESTED**: установленного браузера/Playwright в доступной среде не найдено; установки и сеть не выполнялись. Product CI/DEV smoke/UAT не выполнялись, потому что приложение не менялось и выпуска нет. Единственное Owner-решение: принять или скорректировать предлагаемую композицию «По одному». Жесты и смысл дружбы уже заданы Owner и повторного разрешения не требуют. Следующие CI/независимая QA/DEV release выполняются только будущим допущенным процессом реализации. **Сейчас приложение остаётся неизменным.** ## Проверка интегратора после завершения Astra PMA выполнил фактическую проверку макета в Chromium: desktop1440/mobile390, кнопка запроса, реальные перетаскивания влево/вправо, раскрытие и UNKNOWN без повторной отправки. JavaScript exceptions и мутаций API — 0, горизонтального overflow нет. Это проверка автономного макета, не QA приложения и не реальная дружба. Единственная правка интегратора: тёмный цвет текста на светлом status-badge для контраста; исходный Astra HTML сохранён, JS и компоновка неизменны. Указанное выше авторское browser NOT_TESTED относится к среде Astra; браузерная проверка PMA выполнена отдельно. Продуктовый Onest при реализации сохраняется; у автономного макета остаётся указанное ограничение резервного шрифта.