# #404 · implementation brief · Owner revision v2 **PROPOSED_NOT_OWNER_APPROVED / OWNER_REVIEW_REQUIRED.** Исходное приложение не менялось. Этот проход — дизайн и локальный HTML mock. Source SHA `8be7851a6958cc9e7cdae33fa015c8a95c0d8f70` указан доверенным заданием; Git отсутствует в переданном snapshot, независимая сверка commit не заявляется. ## Унаследованные основания v1 — без повторного исследования Таблица сохранена из предыдущего brief; это его source anchors, а не новые чтения v2. В этом проходе прочитаны только три переданных артефакта и два PNG. Исходники приложения и контракты повторно не исследовались. | Source anchor | Подтверждённая возможность / граница | |---|---| | `docs/product/SITEWIDE_UI_STYLE_GUIDE.md`, `DESKTOP_OVERLAY_STYLE_GUIDE.md` | Общая левая линия, бренд и существующий shell; overlays от 1024 с inset 40. В v2 обучение локально перекрывает слайдер, без fullscreen и route-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, ни отрицательное сообщение. Запрос хранится отдельно от пропуска. После подтверждённого результата показывается короткое «Запрос отправлен» и автоматически открывается следующий eligible кандидат; никакой отправки при смене карточки, 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; этот проход не загружал сеть и не менял зависимости. ## Изменение композиции и поведения по Owner feedback Одна большая поверхность профиля на desktop/tablet: cover + аватар внутри cover + тёмное имя/уровень/место + краткая биография + действия. Подробности раскрываются в этой же карточке. Убраны отдельный правый inspector и постоянная инструкция жестов. Рабочая область растёт по viewport; browser Fullscreen API не используется. Mobile сохраняет прежний визуальный характер и закреплённые действия. Боковые шевроны и Left/Right на сфокусированном слайдере — только просмотр соседних карточек с восстановлением известного relationship state. Они никогда не вызывают request, skip, accept или decline. Свайпы и явные кнопки остаются командами: влево skip, вправо request. Подтверждённый request автоматически переходит к следующему eligible кандидату и показывает короткое подтверждение; UNKNOWN/busy не дают повторно отправить или объявить дружбу. На последнем кандидате открывается конец подборки; previous позволяет увидеть сохранённый исходящий статус. Интеграционная пагинация и scope-ограничения выше сохраняются. Первое обучение находится на слайдере, закрывается «Понятно» / X / Escape, без автотаймера; «?» открывает его повторно. В автономной галерее sessionStorage хранит только версионированный boolean-маркер `gb-player-gallery-tutorial-v2=1`. Без storage — память страницы. Это не хранилище операций или пользовательских данных; существующая приватная защита незавершённых операций приложения не меняется. Во время обучения содержимое карточки inert, навигация приложения доступна; фокус возвращается слайдеру. Pointer handler переиспользован, сохранены pan-y/pinch-zoom, порог, pointercancel/lostcapture и защита вертикального чтения; добавлена проверка конечных координат отпускания. Детали и интерактивные элементы не начинают drag. Без нового framework, API, зависимостей или тестового harness. ## Проверка и следующий шаг V2: inline JS прошёл Node syntax check; packaged asset paths проверены. Два native PNG v1 просмотрены. **Browser/layout/реальные жесты v2, screen reader и UAT — NOT_TESTED.** Прежний PMA Chromium PASS относится к v1, не к этой редакции. Старый DOM harness не запускался и не расширялся. PMA выполняет адресную проверку v2 существующим Chromium: крупная сцена на 1440×900 / 1920×1080 и tablet, mobile swipe + cancel, request → auto-advance → previous/outgoing, обучение один раз + help, UNKNOWN без resend. Затем публикует immutable v2 URL для Owner review. Авторский проход не публикует, не меняет приложение/runtime и не заявляет продуктовую готовность. **PROPOSED_NOT_OWNER_APPROVED.** Owner рассматривает композицию. CI, независимая QA и DEV release относятся к будущей отдельно допущенной реализации. ### Проверка PMA после завершения автора Реальный Chromium проверил большую карточку при1440×900/1920×1080, mobile390×844, стрелки без запросов, автопереход после запроса, сохранение pending при возврате, свайпы, одноразовое обучение после reload и повторное открытие через «?», запрет отправки при UNKNOWN. JS exceptions/реальные API mutations — 0. Это проверка макета, не приложения. Интегратор добавил только явный ink-цвет текста стандартных кнопок: после переноса в тёмную поверхность белый цвет наследовался некорректно. JS/компоновка не менялись; исходник автора сохранён. Авторское Chromium NOT_TESTED выше относится к среде Astra, отдельная проверка PMA проведена.