19 KiB
module-02. Проектная спецификация тестового frontend-сайта
Статус: целевая спецификация реализации MVP; это проектирование, не код.
Канонические источники:README.md,arch-00-glossary.md,arch-01-system-architecture.md,arch-02-api-contracts.md,arch-03-docker-compose-blueprint.md,arch-04-settings-and-content.md,arch-05-agent-development-process.md,module-01-api-backend.md.
1. Назначение и границы
Сайт нужен для ручной, интеграционной и E2E-проверки всех пользовательских сценариев HAN Chat через реальные публичные API. Он остаётся простым по визуальному дизайну, но функционально покрывает guest, OTP/PKCE, bootstrap, UX-сессию, чат, файлы, профиль, realtime и деградации.
Сайт не реализует бизнес-решения backend, не обращается к PostgreSQL, Redis, Bitrix24 или S3 постоянными credentials и не подменяет Message Safety. Публичный вопрос отправляется обычным текстовым сообщением.
2. Зафиксированный стек
- Expo SDK + React Native + TypeScript, web target через Expo Router.
- React Query для server state; локальный reducer/state machine для auth и отложенной отправки.
expo-auth-session/OIDC Authorization Code Flow with PKCE; парольный flow запрещён.- SecureStore на native; для test web — защищённая browser storage adapter с явным предупреждением о риске XSS. Access token предпочтительно держать в памяти, refresh token — в доступном платформе secure storage.
- React Hook Form + schema validation (Zod либо эквивалент).
- WebSocket API браузера; REST polling как обязательный fallback.
- Playwright для web E2E, Vitest/Jest + Testing Library для unit/component.
- Никакого отдельного frontend nginx: production-статику отдаёт единственный корневой nginx.
3. Предлагаемая структура
frontend-test-site/
app/
_layout.tsx
index.tsx
auth/callback.tsx
dialogs/index.tsx
dialogs/[dialogId].tsx
profile.tsx
diagnostics.tsx
src/
api/{client,errors,public,auth,dialogs,attachments,profile}.ts
auth/{oidc,pkce,token-store,refresh-single-flight}.ts
session/{ux-session,activity}.ts
realtime/{socket,polling,reconcile}.ts
flows/{deferred-send,bootstrap}.ts
components/
config/
accessibility/
tests/{unit,component,contract,e2e}/
app.config.ts
package.json
4. Runtime state
| Состояние | Хранение | Правило |
|---|---|---|
| access token | память | не логировать, не показывать полностью |
| refresh token | secure adapter | очищать при logout/invalid_grant |
| PKCE verifier/state/nonce | session storage, короткий TTL | одноразовые, проверяются callback |
ux_session_id, last_activity_at |
только память | не localStorage |
guest_session_id |
локально, опционально | не auth, не посылается как право доступа |
| pending message/file intent | память | восстанавливает отправку после OTP |
| REST cursors | память по dialog | opaque, не парсить |
Auth state machine: guest → authorizing → bootstrapping → authenticated; при refresh failure — обратно guest. UX-сессия независима от Keycloak-сессии.
5. Экраны
5.1. Главная
- загрузка
GET /api/v1/public/app-configи/content; - приветствие, популярные вопросы, textarea, attach button, send;
- индикаторы загрузки/ошибки и повтор;
- ссылка на историю и профиль (при guest запускают auth только по явному действию);
- диагностический badge режима: guest/authenticated, WS/polling, без раскрытия token.
Выбор популярного вопроса сразу запускает тот же send flow, что ручной текст.
5.2. Согласия и OTP
Modal согласий отображает актуальные URL/версии из config. personal_data и user_agreement обязательны, marketing необязателен. После подтверждения intent остаётся в памяти, начинается OIDC PKCE redirect.
OTP вводится на странице/теме Keycloak. В MVP Keycloak сверяет mock-код из env; frontend не хранит и не проверяет код. Для тестовой среды UI может показывать только текст «используется тестовый OTP», но не получать secret из API.
5.3. Диалоги и чат
- история:
GET /dialogs, cursor pagination; - карточка: статус, сообщения, composer, attachment;
- сообщения сортируются по
created_at asc, дубли объединяются поmessage_id; waiting_for_company,waiting_for_client,closedотображаются русскими подписями;- экран истории содержит CTA «Новый диалог» через
POST /dialogs; - closed dialog readonly и содержит CTA «Начать новый диалог»;
- промежуточный safety
203клиенту не показывается: send request остаётся в progress до финального ответа.
5.4. Профиль
Readonly блок «Личные данные» из GET /me; блок «Документы» из /me/documents, допускается пустой. Редактирование отсутствует. Для изменения данных — CTA в чат. Download URL запрашивается только после клика и не сохраняется.
5.5. Diagnostics (только non-production)
Последние безопасные request id, HTTP status, WS state, cursor, время token expiry и UX-session id. Tokens, OTP, PII, тела сообщений и presigned URL не выводятся.
6. Startup, auth и bootstrap
- Немедленно показать guest UI и параллельно загрузить public config/content.
- Проверить refresh token. При наличии — выполнить silent Refresh Token Grant через single-flight.
- При успехе определить новую UX-сессию (
cold_startпри новом page lifecycle), вызватьsession-start, затем загрузить profile/dialogs. - При отсутствии/истечении refresh token оставаться guest до protected action.
- После OTP callback проверить
state/nonce, обменять code с PKCE, вызватьPOST /auth/bootstrapс согласиями и device metadata. - Создать UX-сессию, если её нет; затем продолжить pending intent.
Bootstrap повторяем безопасно после неопределённого сетевого результата. Телефон в body никогда не передаётся.
7. UX-сессия
ux_session_idи activity timestamp живут только в памяти вкладки.- После JWT
session-startвызывается сfirst_launch,cold_startлибоidle_timeout. - Idle timeout берётся из app-config; default UI не подменяет server config.
- Visibility/focus/user input обновляют activity; возврат после превышения timeout создаёт новую сессию.
- Refresh token grant не создаёт новую UX-сессию.
X-Ux-Session-Idдобавляется ко всем JWT REST-запросам, когда id уже получен.
8. HTTP client и заголовки
Каждый API-запрос получает X-Request-ID (UUID), traceparent при активной трассировке и Accept: application/json. Protected request получает Bearer token и X-Ux-Session-Id.
Idempotency-Key обязателен для POST /dialogs и POST .../messages; ключ создаётся один раз на пользовательское действие и сохраняется на retry этого действия. Новый intent получает новый key. Для attachment init/complete повтор соблюдает state/idempotency контракта backend.
Единый error envelope маппится по error.code, а request_id показывается в деталях поддержки. Тело/headers с credentials не логируются.
9. Token refresh single-flight
- планировать refresh за 60 секунд до
exp; - один Promise/mutex на refresh; все параллельные запросы ждут его;
- на первом
401 unauthorized/token_expired— один refresh и один replay исходного запроса; - mutating replay использует исходный
Idempotency-Key; - второй
401не запускает цикл; invalid_grantочищает tokens, закрывает WS, переводит в guest;- WS auth failure использует тот же single-flight, затем reconnect;
- logout отзывает/завершает OIDC-сессию best effort и всегда очищает local secrets.
10. Отправка текста
- Валидировать непустой нормализованный текст и клиентский max length из контракта.
- Если guest — сохранить intent, consent → OTP → bootstrap → session-start.
POST /dialogsс idempotency key, сохранитьdialog_id.POST /dialogs/{id}/messagesс отдельным key.- Блокировать повторный click только для того же intent; другие действия не замораживать.
- На
201mergeMessageResponse; на422 message_blockedпоказать безопасный текст без повтора; на503/504предложить retry с тем же key.
11. Файловый flow
MVP допускает ровно один файл, только allow-list extension+MIME, до 5 МБ или значений app-config.
- Локальная prevalidation.
- Создать/reuse dialog.
POST .../attachments/initс filename, MIME, size.- Выполнить прямой
PUT upload_urlс точно выданнымиupload_headers; API domain при этом не используется. - Вычислить SHA-256, вызвать
complete. - Отправить file message с
attachment_idиsha256:<hex>.
Presigned URL не сохраняется и редактируется из диагностик. Abort позволяет отменить PUT; orphan очищает backend. При expiry init выполняется повторно в рамках согласованного состояния. CORS S3 должен разрешать origin сайта, PUT и необходимые headers.
12. Realtime и polling
Предпочтение — WSS /api/v1/realtime, token через согласованный subprotocol; query token допускается только для совместимости и не логируется.
- connected → subscribe актуальных dialog ids;
- события
message.new,message.status,dialog.statusmerge идемпотентно; - отвечать
pongнаping; - reconnect 1, 2, 4…30 секунд с jitter;
- после каждого reconnect делать REST gap reconciliation по последнему cursor;
- если WS недоступен более 30 секунд — polling
GET .../messages?after=...; - polling прекращается после устойчивого WS, но только после reconciliation;
- hidden tab снижает polling frequency без нарушения восстановления;
- неизвестные event types игнорируются с безопасной метрикой.
13. Ошибки и UX
| Ситуация | Поведение |
|---|---|
| offline/network | banner, сохранение intent в памяти, ручной retry |
| 400 validation | подсветить поле; не retry автоматически |
| 401 | single-flight refresh; при провале guest |
| 403 consents | обновить config, повторить consent flow |
| 404 | безопасное «ресурс недоступен», обновить список |
| 409 idempotency | остановить retry, показать request id |
| 422 blocked | нейтральное сообщение, контент не отправлен |
| 429 | countdown по Retry-After |
| 503/504 | зависимость недоступна; retry с тем же key |
| S3 PUT error | оставить attachment intent, предложить повтор |
| WS failure | polling badge, чат остаётся usable |
Skeleton/empty/error states обязательны для каждого data screen. Никаких optimistic «delivered» до 201.
14. Конфигурация и сборка
Public build-time env содержит только URL/realm/client id:
EXPO_PUBLIC_API_BASE_URL=https://tohin.ru
EXPO_PUBLIC_AUTH_BASE_URL=https://tohin.ru/auth
EXPO_PUBLIC_KEYCLOAK_REALM=han-chat
EXPO_PUBLIC_KEYCLOAK_CLIENT_ID=han-chat-frontend
EXPO_PUBLIC_APP_ENV=production-like
Redirect URI и allowed origins фиксируются в Keycloak/nginx. Service tokens, S3 keys и mock OTP code во frontend env запрещены. Бизнес-конфиг приходит через /public/app-config, тексты — /public/content.
Production: статический export монтируется в корневой nginx, try_files $uri /index.html; hashed assets immutable, index.html no-cache/revalidate. Local dev: Expo dev server, опциональный proxy корневого nginx через FRONTEND_DEV_PROXY_ENABLED=true.
15. Доступность
- WCAG 2.1 AA как цель; полная keyboard navigation и видимый focus.
- Семантические headings/landmarks, labels и error descriptions.
- Modal: focus trap, возврат focus, Escape только если не нарушает обязательный flow.
- Live region для новых сообщений и статусов без повторного озвучивания всей ленты.
- Контраст, zoom 200%, reduced motion, touch targets не менее 44×44 CSS px.
- Статусы не кодируются одним цветом; файлы имеют доступные имена и progress.
- OTP поля поддерживают paste/autocomplete, но не логируют значение.
16. Тестовая матрица
Unit/component
- auth state machine, PKCE callback state/nonce;
- refresh scheduler/single-flight/concurrent 401;
- UX idle boundary;
- idempotency key reuse;
- error mapping/redaction;
- text/file union и file validation;
- WS merge, duplicate, reconnect и poll switch;
- accessibility scans ключевых экранов.
Contract/integration
- DTO соответствует
api-backend/openapi.yaml; - public cache/ETag;
- bootstrap без phone body;
- dialog 200/201;
- message 201/422/429/503/504;
- presigned PUT headers/checksum/expiry;
- profile/documents readonly;
- WS события и REST reconciliation.
E2E
| Сценарий | Варианты |
|---|---|
| guest | просмотр public content; write закрыт |
| first send | manual/popular → consents → mock OTP → delivered |
| return | valid refresh без OTP; expired refresh с OTP |
| text safety | allow, block, pending-to-final, timeout |
| file | PDF/image allow, deny, wrong MIME/size/checksum, expired URL |
| realtime | message, status, close, reconnect, polling fallback |
| concurrency | два send click, несколько 401, две вкладки |
| profile | filled/null fields, empty documents, download failure |
| security | XSS text, token absence in logs/storage diagnostics |
Browsers: Chromium, Firefox, WebKit; viewport desktop/mobile. Реальный Keycloak mock realm и API stub/compose используются в CI.
17. Definition of Done
- все экраны и flows выше реализованы на русском;
- guest не вызывает protected write;
- PKCE/OTP, silent refresh, bootstrap и pending intent проверены E2E;
- UX-session создаётся и передаётся строго по правилам;
- idempotency и refresh single-flight выдерживают concurrency;
- text/file lifecycle работает через presigned PUT;
- WS и polling не оставляют gap;
- profile readonly и documents empty state реализованы;
- CSP/CORS совместимы без unsafe token practices;
- отсутствуют secrets, PII, message body и URLs в логах;
- accessibility checks и keyboard сценарии проходят;
- unit/component/contract/E2E matrix зелёная;
- production static и dev proxy режимы проверены через единственный nginx.
18. Решения, допущения и TBD
Решения: Expo/TypeScript; server state через React Query; auth state machine; WS best effort + обязательная REST reconciliation; никаких optimistic delivered.
Допущения: test site использует те же API и Keycloak realm contracts, что мобильные клиенты; locale MVP — ru; browser secure storage не эквивалентен OS Keychain, поэтому CSP и отсутствие сторонних scripts обязательны.
TBD:
- F1: окончательный Keycloak browser adapter и token rotation policy;
- F2: точные max lengths и WS limits после OpenAPI;
- F3: web storage policy refresh token перед production security review;
- F4: окончательный DTO app-config (G10);
- F5: WS
event_id/protocol version (TBD module-01/G11); - F6: продуктовые тексты всех error states по мнемоникам.