287 lines
19 KiB
Markdown
287 lines
19 KiB
Markdown
# module-02. Проектная спецификация тестового frontend-сайта
|
||
|
||
> Статус: целевая спецификация реализации MVP; это проектирование, не код.
|
||
> Канонические источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.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. Предлагаемая структура
|
||
|
||
```text
|
||
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` отображаются русскими подписями;
|
||
- closed dialog readonly; создание нового — только через контракт backend;
|
||
- промежуточный 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
|
||
|
||
1. Немедленно показать guest UI и параллельно загрузить public config/content.
|
||
2. Проверить refresh token. При наличии — выполнить silent Refresh Token Grant через single-flight.
|
||
3. При успехе определить новую UX-сессию (`cold_start` при новом page lifecycle), вызвать `session-start`, затем загрузить profile/dialogs.
|
||
4. При отсутствии/истечении refresh token оставаться guest до protected action.
|
||
5. После OTP callback проверить `state`/`nonce`, обменять code с PKCE, вызвать `POST /auth/bootstrap` с согласиями и device metadata.
|
||
6. Создать 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. Отправка текста
|
||
|
||
1. Валидировать непустой нормализованный текст и клиентский max length из контракта.
|
||
2. Если guest — сохранить intent, consent → OTP → bootstrap → session-start.
|
||
3. `POST /dialogs` с idempotency key, сохранить `dialog_id`.
|
||
4. `POST /dialogs/{id}/messages` с отдельным key.
|
||
5. Блокировать повторный click только для того же intent; другие действия не замораживать.
|
||
6. На `201` merge `MessageResponse`; на `422 message_blocked` показать безопасный текст без повтора; на `503/504` предложить retry с тем же key.
|
||
|
||
## 11. Файловый flow
|
||
|
||
MVP допускает ровно один файл, только allow-list extension+MIME, до 5 МБ или значений app-config.
|
||
|
||
1. Локальная prevalidation.
|
||
2. Создать/reuse dialog.
|
||
3. `POST .../attachments/init` с filename, MIME, size.
|
||
4. Выполнить прямой `PUT upload_url` с точно выданными `upload_headers`; API domain при этом не используется.
|
||
5. Вычислить SHA-256, вызвать `complete`.
|
||
6. Отправить 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.status` merge идемпотентно;
|
||
- отвечать `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:
|
||
|
||
```text
|
||
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 по мнемоникам.
|