Files
specs/openspec/changes/quiet-user-feedback/design.md
T
ksv741 ed9025f1d7
Specs security / security (push) Failing after 9s
Specs verify / verify (push) Successful in 10s
spec: add quiet user feedback change and sync canonical specs
Part of the quiet-user-feedback change; frontend and docs land in their own repositories.
2026-10-06 07:03:14 +03:00

41 KiB
Raw Blame History

Context

Frontend уже разделяет получение данных, page VM и presentation. TanStack Query хранит успешные результаты; инвестиционные ресурсы дополнительно возвращают data и meta с полнотой, свежестью и состоянием источника. useResourceQuery сохраняет общую query identity обычного чтения и наблюдения, а useScreenSync наблюдает известную работу и предоставляет ручные действия. На счетах и портфеле используются InvestmentRefreshState и SourceSyncStatus; сходные элементы встроены в остальные finance views.

Сейчас query retries и focus refetch отключены, ошибка status GET прекращает наблюдение, а observe() также запрещает retry. В банковских формах отдельно существуют технические повторы загрузки справочников и перечитывание записи после конфликта версий. Это разные случаи: удаление всех обработчиков с именем retry или onReload без сохранения их ответственности недопустимо.

Предоставленный frontend OpenAPI уже содержит обычное чтение ресурсов, sync=observe, status GET и принудительный screen refresh. Дополнительная сверка реализации показала: обычное чтение запрашивает missing/stale источники, свежие полные источники переиспользуются, существующая running work объединяется. Принудительный refresh обходит freshness и охватывает зависимости экрана. Поэтому он не подходит для автоматического восстановления обычного чтения.

Основные specs текущего store отстают от части реализованной синхронизации. Есть пересечение с active change persist-tbank-data; его старые UI-требования не используются как новая норма поверх согласованных quiet Delta Specs. Это различие учитывается в порядке согласования требований ниже.

Goals / Non-Goals

Goals:

  • Сосредоточить transport recovery в существующем query layer, а восстановление инвестиционной работы — у существующего владельца useScreenSync.
  • Упростить presentation interface: view получает готовое содержимое, недоступность или необходимое пользовательское действие, а не набор технических статусов и callbacks повторной загрузки.
  • Сохранить один финансовый кэш, действующую отмену запросов, границы доступа и точные серверные финансовые результаты.
  • Встроить редкие уведомления в существующие Mantine theme и i18n.

Non-Goals:

  • Новый query client, offline-хранилище, event bus или универсальный scheduler.
  • Автоматическое повторение мутаций, изменение auth refresh или правил разрешения конфликтов пользовательских данных.
  • Новые endpoints, удаление существующего refresh endpoint, изменение DTO, серверных TTL, алгоритмов расчёта или схемы хранения.
  • Переработка композиции финансовых страниц и диаграмм.

Decisions

1. Два владельца восстановления вместо одного глобального retry

shared/api владеет классификацией transport errors, ограниченным retry и deadline application GET. Существующие authenticated query hooks применяют общую политику к чтению карт, справочников и статуса подключения. Auth bootstrap и пользовательские mutations сохраняют текущую отдельную политику.

useResourceQuery использует тот же transport recovery, но жизненный цикл инвестиционной работы остаётся в features/tbank-connection. useScreenSync становится автоматической orchestration текущего экрана: его callers больше не передают refresh/retry в UI. Page VM продолжает владеть смыслом готовности конкретного результата, выбранными параметрами и доступными действиями.

Альтернативы: глобальный retry всех запросов и mutations нарушает границы записи и доступа; независимые timers каждой страницы размножают политику. Собственный универсальный recovery service дублирует Query lifecycle.

2. Конечная transport policy

Для application GET принимаются следующие параметры:

Параметр Решение
HTTP deadline 15 секунд на попытку, включая чтение тела ответа
Автоматические повторы Не более двух после исходного запроса
Паузы 1 и 2 секунды; серия не перезапускается от render
Временные ошибки Подтверждённая transport/network failure, timeout, HTTP 408, 500, 502, 503, 504, TBANK_UNAVAILABLE
Не повторять Отмена, ошибки ввода, 401/403/404, известные access/domain failures, 409, неизвестный постоянный исход
Rate limiting 429 не повторяется короткой серией без доступного корректного Retry-After; наличие серверной отсрочки исключает более ранний повтор
Новая серия Новый актуальный запрос, возвращение сети или возвращение пользователя на экран

Классификация использует существующие ApiErrorCode и структурированный Problem Details status, с приоритетом отказа доступа над общим HTTP status. Отсутствие известного code само по себе не означает временную ошибку. Transport failure маркируется там, где выполняется GET: ошибка render, select или преобразования данных не превращается в network failure. Если Retry-After используется, его transport metadata сохраняется локально в ошибке клиента, без нового поля публичного DTO.

Deadline применяется только к application GET, не к auth endpoints и не к mutations. Сигнал Query объединяется с сигналом timeout; исходная отмена сохраняет причину и не повторяется. Существующий единственный auth refresh не дублируется query retry. Восстановление GET после исчерпанного auth refresh не допускается до восстановления сессии.

Для обычных authenticated reads применяются штатные Query focus/reconnect events и текущие entity staleTime. На инвестиционных ресурсах обработка этих событий координируется useScreenSync: параллельный автоматический refetch тех же ресурсов из второго владельца выключен. Lifecycle события берутся из Query focus/online managers, без второго набора browser listeners.

Уход последнего consumer отменяет запрос и его retries через существующий AbortSignal. Imperative чтение, начатое screen owner, также прекращается при его cleanup, если у запроса не осталось других активных consumers.

3. Восстановление источника — обычным GET, наблюдение — только observe

Для инвестиционного ресурса разделяются три ситуации:

Ситуация Действие владельца
Не получен HTTP-ответ Ограниченный transport retry того же режима чтения
Известна running work Status GET и чтение проекции с sync=observe
Необходимые данные missing/stale, а work failed/cancelled либо отсутствует Ограниченный повтор обычного GET ресурса без sync=observe

Успешный HTTP 200 с data: null или sync.state: failed не считается успешным восстановлением источника. Источники определяются из meta и meta.dependencies; unavailableReason и предметные ограничения не считаются временным сбоем. Неудачная прежняя принудительная сверка уже полного свежего результата не запускается заново только ради удаления её внутреннего статуса.

После terminal source failure допускаются две повторные обычные загрузки нуждающегося в данных ресурса: через 5 и 15 секунд после соответствующего неуспеха. Бюджет относится к активации текущей выборки, а не к ID очередной серверной попытки. Новый ID работы, успешное чтение failed metadata и повторный render не обнуляют бюджет. После его исчерпания следующий запуск возможен при новом lifecycle событии восстановления или выборе новой выборки.

Transport retry и source recovery не вкладываются бесконтрольно: следующая source attempt начинается только после завершения предыдущего GET и его transport retries. Для одной серии это не более трёх обычных загрузок ресурса, каждая не более чем с тремя HTTP-попытками. Наблюдение running work не является новой source attempt и никогда не вызывает принудительный POST refresh.

Status observation сохраняет существующий интервал 1 секунда при активном экране и известной running work. Повторный tick не отменяет выполняющееся чтение или его retry. После terminal status выполняется заключительное observe-чтение; если другое чтение ещё идёт, заключительное выполняется после него, а не теряется. После ухода, потери сессии или terminal status периодический опрос соответствующей работы прекращается. В скрытой вкладке и offline новые циклы приостанавливаются; возвращение возобновляет актуальную работу.

404 status ID не означает автоматически потерю доступа ко всем счетам: ресурс перечитывается по действующему адресу, где проверяется текущий доступ. Подтверждённые access errors ресурса либо статуса подключения сохраняют существующую invalidation личных данных. Коды отказа из metadata также не должны маскироваться успешным HTTP-ответом.

Обычное чтение и observe остаются в одной generated query identity. Внутри владельца режим запроса задаётся явно; observe не оставляет постоянный observe-only queryFn для последующего обычного восстановления. При чтении проверяется фактическое in-flight состояние Query, а не только снимок props. Повторный event не создаёт второй запрос того же key. Переход на новую выборку отменяет старые timers и не позволяет late response менять новую область.

4. UI readiness отдельно от query status

Page VM выбирает presentation в следующем порядке:

  1. Подтверждённый access failure: скрытые личные данные и необходимое действие.
  2. Доступный результат текущей выборки: loaded, partial или подтверждённый empty.
  3. Отсутствие результата: первоначальный skeleton либо unavailable.

Loaded view не получает isFetching, время получения и callbacks технического повтора. Технические metadata остаются у data owner для восстановления и определения полноты. Не вводится второй кэш успешных финансовых payload.

Первичное ожидание области ограничивается 10 секундами. Ошибка завершившегося получения или истечение этого срока переводит её в unavailable. Это UI budget, а не отмена полезной source work: поздний успешный результат заменяет состояние автоматически. Одна общая небольшая presentation-функция/hook в shared/lib владеет временем ожидания и latch unavailable для identity области. Она не выполняет запросы, не знает T-Bank и не копирует финансовые данные. Page VM передаёт readiness и identity; смена выборки сбрасывает ожидание, очередная попытка той же выборки не вызывает мигание skeleton.

Доступность проверяется явно, а не через truthiness суммы. data: [] и полный пустой Portfolio — успешные результаты; data: null не равен пустому результату. Для неполных totals/comparisons выводится — с доступной подписью «Итог пока недоступен»/«Сравнение пока недоступно». Для неполного списка допустима подпись «Показана часть операций», без статуса синхронизации.

Ключи loaded containers не зависят от isFetching, revision или времени ответа. Фильтры, локальные раскрытия, формы и focus сохраняют существующих владельцев. В банковской пагинации прежняя страница не используется как placeholder новой: текущий placeholderData, допускающий иной page, убирается для этого перехода. Сохранение данных при refetch той же страницы обеспечивается самим Query.

InvestmentRefreshState и SourceSyncStatus удаляются вместе с их callers и ненужными presentation props. Общие StateView и page skeleton переиспользуются; нейтральная недоступность не требует нового компонента-обёртки. Ошибки форм, валидация и сообщения, объясняющие финансовый смысл, не удаляются поиском строк.

5. Формы и конфликты сохраняют самостоятельное подтверждение

Запросы категорий, исходной покупки возврата и текущей версии записи получают обычную политику чтения. Временный refetch не очищает ранее доступные варианты и ввод. Когда необходимые данные отсутствуют, соответствующее сохранение недоступно; отмена и выход из формы остаются доступны.

После BANK_VERSION_CONFLICT или аналогичного предметного конфликта существующая invalidation автоматически получает актуальную запись. VM использует этот результат вместо отдельной кнопки перечитывания. Пользователь по-прежнему видит конфликт, сохраняет ввод и явно принимает новую версию перед повторным сохранением или удалением. Фоновый ответ не принимает версию за него. Пока новая версия неизвестна, повтор опасной записи заблокирован, но форма не находится в бесконечном submit и допускает закрытие.

Создание и правка карт/операций, удаление, категории и изменение подключения сохраняют retry: false, version и существующие intent IDs. Общая read policy не означает автоматического повторения неуспешной записи.

6. Mantine Notifications с явным владельцем сообщения

Используется @mantine/notifications 9.6.1, согласованный с установленными @mantine/core и @mantine/hooks 9.6.1. Registry подтверждает эту версию и peer requirements React/React DOM ^19.2.0, совместимые с manifest проекта. Стили пакета подключаются после Mantine core; один Notifications host находится в application composition под MantineProvider.

Параметры: limit=2, autoClose=4000, desktop bottom-right, narrow viewport bottom-center; видимая кнопка закрытия имеет русское доступное имя. Сообщение не получает отдельный заголовок «Успех» и не переносит focus. Текст принадлежит существующему i18n; имя пользователя, token и необработанный error в него не входят. Очередь очищается при завершении или смене пользовательской сессии.

Уведомление вызывается из одного обработчика успешного действия в его VM, не из useEffect по isSuccess и не из глобального query/mutation interceptor. ID относится к выполнению действия, а не к названию операции вообще. Повторное завершение того же выполнения подавляется у владельца и получает тот же ID; следующее отдельное действие имеет новый ID. Не создаётся постоянный журнал показанных сообщений.

Решение о необходимости подтверждения принадлежит VM, знающей оставшийся экран. Критерий проверяется по фактически видимой выборке, а не по одному наличию успешного HTTP-ответа:

Действие и оставшийся контекст Обратная связь
Созданная/изменённая карта или операция видна в обновлённом списке Содержимое подтверждает результат, toast отсутствует
Успешная операция не видна из-за периода, страницы, фильтра или другого счёта Одно «Операция сохранена» после закрытия формы
Удалённая запись исчезла из видимого списка Toast отсутствует
Запись удалена, но оставшийся экран не отражает её исчезновение Одно «Операция удалена»
Создание/удаление подключения меняет видимый статус Toast отсутствует
Замена token при прежнем статусе «Подключён» Одно «Подключение обновлено»
Категория появилась или изменена в открытом списке/поле Toast отсутствует
Сохранение успешно, но связанное чтение не подтвердило результат на экране Краткое подтверждение сохранения без сообщения о сбое чтения

VM получает outcome связанного обновления от существующего владельца invalidation; её решение о подтверждении выполняется один раз для action ID. Успех записи отделён от успеха последующего GET: ошибка чтения не становится ошибкой сохранения и не предлагает повторить уже выполненную запись. Получение видимого подтверждения ограничено тем же 10-секундным UI budget; после него при отсутствии подтверждения используется одно уведомление. Отдельный глобальный наблюдатель mutation results для этого не вводится.

Альтернатива react-toastify добавляет вторую визуальную систему без нужной дополнительной функции. Собственный toast из Mantine Notification потребовал бы обслуживать очередь, timers и доступность; готовый пакет дешевле.

UI/UX Applicability and Package

Applicability and scope: new/changed — меняются feedback и состояния существующих экранов, а не их финансовая композиция.

Package revision: quiet-user-feedback-ui-v1.

Пакет полностью inline. Отдельные PNG/SVG не нужны: новая геометрия страниц, иллюстрации и визуализация финансовых данных не вводятся. Следующие описания и схемы входят в единое Design approval.

Screens, states and interactions:

Область Loaded / background failure Нет результата Действия
Счета, портфель, режим всех счетов Заголовок сразу переходит в карточки и содержимое; строки синхронизации отсутствуют Page-owned skeleton → нейтральная недоступность Открыть счёт, перейти в раздел, настроить отсутствующее подключение
Операции, доход, события Фильтры и доступный результат сохраняются; даты событий остаются Недоступность только текущей выборки или расчёта Применить/сбросить фильтры, сменить страницу, навигация
Поиск и детали инструментов Поиск, график и доступные секции без технических controls Незагруженная секция сохраняет свой заголовок и локальное состояние Поиск, выбор инструмента и диапазона
Карты, сводки и история Данные и пользовательские controls сохраняются Локальная недоступность без ложного нулевого остатка Добавить/изменить, фильтры, страницы
Настройки подключения Статус и введённый token сохраняются при временном сбое чтения Нет формы до первого подтверждённого статуса Подключить/заменить/отключить после получения статуса
Формы и подтверждения удаления Локальное ожидание явного действия; ошибки у владельца Недоступность нужного справочника или актуальной версии Сохранить/отменить; явное принятие новой версии при конфликте

Схема loaded портфеля:

Счета / Основной счёт
Основной счёт

[ ₽ 125 000,50 ]  [ $ 320,00 ]
Структура портфеля
Позиции / Деньги / Последние операции / Доход

Между заголовком и содержимым нет зарезервированного места под удалённые кнопки, даты и баннеры. Temporary refetch failure имеет тот же вид, что loaded.

Схема локальной недоступности:

Получено купонами и дивидендами
Пока недоступно

Открыть пассивный доход →

Ссылка присутствует только там, где у блока уже есть предметная навигация. Нет дополнительного «Попробуйте позже», countdown или обещания завершения. Для основной страницы сохраняются shell и существующие пути навигации. Нейтральное состояние — читаемый текст, не warning/error Alert. Символ — для отсутствующей суммы всегда имеет доступное объяснение.

Схема редкого подтверждения:

                         Операция сохранена   ×

Нотификация объявляется ненавязчиво, не закрывает modal, не переводит фокус и не является единственным носителем ошибки или необходимого действия. При отображении над формой учитывается её focus trap; закрытие уведомления не отбирает доступ к кнопкам формы. Повторные фоновые попытки не создают live-announcements. Skeleton busy остаётся только у реально ожидающей области.

Responsive behavior: существующие layout и semantic tokens сохраняются. При ширине 320–767 px нейтральные состояния занимают ширину области; toast располагается снизу по центру с отступами и учётом safe area, без горизонтального скролла. От 768 px — справа снизу. Сообщения переносятся, кнопка закрытия остаётся доступной. Контейнер уведомлений не блокирует pointer events вне самих сообщений и не перекрывает единственный путь к основным controls. Появление учитывает reduced motion.

Reused source and differences: формального reuse чужого SDD UI package нет. Применяются текущие Mantine theme, StateView и page-owned skeleton; их ответственность сохраняется. Отдельная новая theme или макет неизменяемых финансовых секций не создаются.

Delivery Map

Area Status Architectural outcome or N/A reason
frontend affected Query recovery, автоматическое наблюдение ресурсов, упрощённые VM/views, сохранение form conflict flow, Mantine Notifications, согласованные UI/data conventions и каталог состояний
backend N/A Существующие обычные GET, observe и server-side work deduplication достаточны; DTO, force endpoint, TTL, хранение и серверные повторы не меняются
specs affected Согласованные Delta Specs заменяют старые UI/retry obligations; новое правило feedback входит в canonical specs и индекс; пересечение с persist lifecycle разрешается без переписывания чужого active change
docs affected As-built объяснение тихой загрузки, восстановления и уведомлений заменяет описание ручного refresh в UI resilience, data freshness и затронутых предметных страницах; принятое сквозное решение фиксируется в журнале ADR

Solution Assessment

Обязательный trigger — изменение общей политики caching/recovery и координации concurrent reads; дополнительно выбирается общий механизм уведомлений.

Вариант Fit и зрелость Security и ограничения Operating / migration cost Решение
Существующий TanStack Query + текущие resource/screen owners Уже владеет cache, retry, cancellation, focus/online events; domain metadata остаётся отдельной Сохраняет session cancellation; требует отличать read failure от failed work и запретить retries mutations Без новой runtime-инфраструктуры; изменения сосредоточены в существующих owners Выбран
Новый scheduler или state-machine runtime Потребовал бы повторить текущий query lifecycle Добавляет второе состояние и риск late-response/cache рассогласования Новая dependency и миграция всех consumers без текущей необходимости Отклонён
Периодический force refresh Не соответствует ограниченному recovery текущих missing/stale данных Повторяет внешнюю работу и игнорирует freshness Лишние RPC и нагрузка; усложняет rate limiting Отклонён
Mantine Notifications 9.6.1 Зрелый companion текущей UI-системы; очередь, lifecycle, responsive position Только доверенные локализованные тексты, без HTML/error payload Один согласованный пакет; нет нового сервиса или отдельной темы Выбран
React-Toastify Зрелый самостоятельный механизм, но дополнительная функция не нужна Потребует отдельно согласовывать styling и accessibility Вторая UI-интеграция и её поддержка Отклонён
Собственная очередь над Mantine Notification Встроенный primitive есть, готовой очереди в core нет Собственные timers, focus/live-region и cleanup повышают риск Собственный код дороже готового companion Отклонён

Выбор опирается на текущие исходники и публичную документацию Query retries, Query cancellation, focus refetch и Mantine Notifications. Документация объясняет возможности библиотек; domain recovery и UI-пакет полностью определены здесь и не зависят от доступа к этим ссылкам.

Risks / Trade-offs

  • Тишина не означает актуальность в реальном времени. TTL сам не создаёт расписание; после исчерпания recovery budget новая серия ждёт lifecycle event. Это ограничивает нагрузку ценой возможного ожидания следующего возвращения на экран при долгой недоступности источника.
  • Короткий UI budget и долгая история. Через 10 секунд может появиться «Пока недоступно», хотя работа продолжается. Готовый результат заменяет это состояние; уже доступные области не ждут чужой истории.
  • Умножение повторов. Transport, status polling и source recovery имеют разных владельцев и конечные бюджеты. Новые source IDs и failed metadata не запускают неограниченную серию.
  • Access loss важнее cached content. Ошибки доступа проверяются до loaded presentation. Status ID not found не смешивается с account not found.
  • Конфликт версии не является ошибкой чтения. Автоматическое получение новой версии не подтверждает её принятие и не повторяет запись.
  • Уведомления могут дублировать содержимое. Решение остаётся у action VM, где известна видимая выборка; глобальный toast на каждый success исключён.
  • Исходные ветки не равны новой canonical норме. Существующие local изменения переноса PortfolioStructure сохраняются; design не требует их отката. Удалённая актуальность specs ранее не была подтверждена из-за DNS; фактическая база поставки требует сверки перед её объединением.

Migration Plan

  1. Сохранить текущую canonical базу quiet change и порядок требований: утверждённое quiet-поведение заменяет старые обязательства показывать статусы и ручной retry. Технические возможности уже предоставленного API используются без автоматического переноса всех artifacts persist-tbank-data. Его последующий sync/archive не должен восстанавливать отменённые UI-правила: пересекающийся change требует отдельного rebase и approval до своего archive. Если он попадает в canonical specs раньше quiet, quiet deltas адаптируются к новой базе с сохранением согласованного поведения и повторным approval затронутых artifacts. Чужие active artifacts сейчас не переписываются.
  2. Ввести recovery у существующих владельцев чтения и отделить presentation readiness от transport status. Сохранить текущие auth/access/invalidation semantics и различие обычного чтения и observe.
  3. Перевести экраны и формы на тихую presentation, убрать ручные технические controls и сохранить предметные conflict actions. Подключить единый host уведомлений и явные action outcomes. Эти части выпускаются одним frontend изменением: удаление recovery-кнопок не поставляется отдельно от замены.
  4. Согласовать новые conventions и presentation states, затем обновить as-built docs и canonical specs по принятому lifecycle. Серверный rollout и миграция данных для этого решения не требуются.

Rollback Contract

Field Decision
Status required
Reason Recovery касается сохранения личных снимков, access invalidation и взаимодействия с финансовыми mutations
Trigger Новая политика показывает данные прежней границы, создаёт неконтролируемые запросы, повторяет запись или ломает восстановление без ручных controls
Actions Вернуть предыдущую frontend-поставку целиком: старые query policies вместе с прежними UI recovery controls; не откатывать только автоматическое восстановление под уже тихим UI
Data safety Не менять БД, серверные данные, connection revision, intent IDs и versions; не повторять пользовательские mutations для компенсации; при подозрении на неправильный показ завершить локальную сессию и очистить её кэш
Verification После отката данные разделены по сессии и подключению, временный сбой имеет прежний доступный recovery path, чтение ограничено прежней политикой, сохранение и удаление требуют явного действия