docs: explain persisted T-Bank reads and synchronization
Conventional commits / commits (pull_request) Successful in 14s
Docs security / security (pull_request) Successful in 8s
Docs verify / verify (pull_request) Successful in 13s
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 12s

Document local data ownership, coverage and freshness, refresh behavior, failure
recovery and additive migration rollback. Record ADR 017.
This commit was merged in pull request #22.
This commit is contained in:
2026-09-26 21:22:50 +03:00
parent 55610ea2b4
commit 07de062ebb
19 changed files with 459 additions and 432 deletions
+20 -23
View File
@@ -76,8 +76,8 @@ prerequisites: локальный smoke не подтверждает registry/s
| Часть | Ответственность |
| --- | --- |
| React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений |
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и серверные кэши |
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений и ручной банковской истории |
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и синхронизация сохранённых источников |
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений, ручной банковской истории и инвестиционных источников с coverage/work |
| Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности |
Frontend организован слоями `app → pages → features → entities → shared`:
@@ -134,28 +134,24 @@ credentials не являются сохраняемым представлен
или перезапуска. Архив обратим и сохраняет историю. Подробности —
[банковские карты](../domains/cards/overview.md).
Сессии и подключение связаны с владельцем. Access token подтверждает
локального пользователя, но `accountId` из URL сам по себе не даёт права
читать брокерский счёт. Backend получает текущее подключение пользователя
и его generation перед использованием финансового кэша. Проверка доступности
счёта у брокера зависит от сценария: журнал выполняет её до чтения страницы
кэша; свежий портфель одного счёта может быть возвращён без нового запроса
списка счетов. При обновлении портфеля доступ проверяется у брокера.
Сессии и подключение связаны с владельцем. Backend локально проверяет
membership текущей revision перед каждым финансовым чтением. Unknown membership
не открывает доступ к произвольному счёту; известный отказ токена блокирует
личные данные. Ротация refresh token остаётся атомарной.
Ротация сессии атомарно заменяет hash текущего refresh token: два
конкурентных запроса не могут оба успешно выполнить одну ротацию.
Вызов брокера и запись в локальную базу не образуют общую транзакцию:
при сохранении подключения сначала проверяется token, затем записывается
шифротекст. Подробности находятся в
[аутентификации](../domains/authentication/overview.md) и
[интеграции T-Bank](../integrations/tbank.md).
SQLite хранит полный последний portfolio, операции и их подтверждённые
диапазоны, private прогнозы, общие instrument/market sources и intent/staging.
Нормализованные финансовые значения сохраняются decimal-строками. Полнота
отделена от свежести и пагинации. Ответ не ждёт внешних RPC; запрос при
необходимости запускает работу в том же backend process через существующий
лимитер. На startup импорт не запускается; восстановление требует следующего
запроса. Подробности — [модель свежести](cross-cutting/data-freshness.md).
Брокерские счета, сводки, портфель, страницы журнала и будущие события
не являются долговременной историей в SQLite. Они получаются для текущего
подключения и могут временно кэшироваться. Перезапуск backend не
восстанавливает прежние финансовые снимки и cursor continuations из базы;
[модель свежести](cross-cutting/data-freshness.md) отделяет это от хранения
учётных данных.
PUT подключения сначала проверяет token у T-Bank, затем одной транзакцией
сохраняет encrypted token, новую revision и проверенный membership.
Сетевая операция не удерживает SQLite-транзакцию. Общие источники не содержат
личных позиций, ожидаемых сумм или credentials. Произвольного архива
отключённых счетов нет.
## Сохранение и восстановление данных
@@ -210,7 +206,8 @@ Origin браузера, адрес API и cookie policy должны быть
режимах refresh-cookie имеет Secure flag и требует защищённого соединения.
Single-server deployment не задаёт гарантий HA или zero-downtime.
Кэши и лимитер T-Bank действуют на экземпляр backend. Ротация ключа шифрования
Coordinator и лимитер T-Bank действуют на один экземпляр backend. Сохранённые
данные переживают перезапуск; processRunId не является distributed lease. Ротация ключа шифрования
подключений и согласование изменений подключения между распределёнными
экземплярами не входят в реализованный механизм. Эти ограничения не
позволяют переносить локальную схему на несколько экземпляров без отдельного
+53 -59
View File
@@ -1,72 +1,66 @@
# Свежесть данных и повторное использование
WealthLedger показывает финансовое наблюдение на момент получения данных.
Временный кэш уменьшает повторные обращения к брокеру, но не превращает
снимки в историю портфеля и не обещает рыночные данные в реальном времени.
WealthLedger читает инвестиционные данные из SQLite. Обычный GET возвращает
сохранённый результат и запрашивает фоновые зависимости, если они отсутствуют
или устарели. HTTP-ответ не ждёт T-Bank. Модель принята в
[ADR 017](../../adr/017-local-tbank-reads-and-background-sync.md).
## Клиентский и серверный снимки
[Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — последний
успешный ответ, сохранённый TanStack Query в SPA. При обычном фоновом обновлении
того же набора данных уже показанный результат остаётся на экране; ошибка
обновления сообщает о проблеме, не заменяя известные значения пустотой.
Клиентский кэш не является внешней системой или отдельным runtime-приложением.
[Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — успешный
ответ в TanStack Query. Он сохраняет показанное содержимое при временной ошибке
запроса в той же границе доступа. SQLite сохраняет источники независимо от
браузера: membership, последний полный portfolio, исполненные операции,
общие сведения об инструментах, котировки, свечи и календари.
[Portfolio Snapshot](../../glossary.md#portfolio-snapshot) — полный результат
загрузки портфеля на backend. В пределах разрешённого сценарием срока он может
служить серверным fallback при временной ошибке upstream. Backend отмечает
такой ответ `isStale`; frontend не выводит это поле из состояния собственного
query-кэша. Серверное предупреждение и ошибка клиентского refetch могут
возникать независимо.
[Полнота периода](../../glossary.md#полнота-истории-счёта) и свежесть различаются.
Успешно проверенный пустой интервал полон; последняя страница журнала сама
по себе полноту не доказывает. Ошибка сверки сохраняет прежние данные и время.
Старый полный портфель не удаляется по истечении TTL. `isStale` показывает
возраст portfolio source, а не сам факт неудачного обновления.
Свежесть и допустимость fallback определяются сценарием. Портфельный fallback
не распространяется автоматически на журнал, прогноз событий или страницу
инструмента. Точные правила находятся в
[справочнике портфеля](../../domains/portfolio/data-reference.md),
[журнале операций](../../domains/operations/overview.md) и
[будущих событиях](../../domains/income/upcoming-events.md).
| Источник | Когда обычный запрос требует обновления |
| --- | --- |
| Portfolio, котировки, изменяемый хвост дневных свечей | Через 60 секунд |
| Membership и изменяемый хвост операций | Через 5 минут |
| Metadata, поиск каталога, company и календари | Через сутки |
| Полные прошлые интервалы операций и завершённые свечи | При явном обновлении показанного периода |
По [ADR 007](../../adr/007-react-query-fetch-policy.md) запрос связан с открытием
экрана, а возврат фокуса окна не запускает refetch. Повторное открытие после
`staleTime` может обновить ответ в фоне. Эта политика не является таймером
периодического обновления. Перезапуск backend уничтожает его in-memory
снимки и continuations журнала; SQLite не восстанавливает их.
TTL не создаёт таймер импорта. Запрошенная работа может закончиться после
ухода со страницы. После перезапуска нет автоматического обхода intent:
следующий подходящий запрос повторяет незавершённый интервал с первой страницы,
переиспользуя полные интервалы. Cursor брокера не служит durable checkpoint.
Ручное «Обновить» перечитывает зависимости текущего экрана и применённый
период независимо от TTL. Повторная сверка полного периода публикуется одной
транзакцией после всех страниц, интервалов и счетов, включая `all`.
Сбой сохраняет весь прежний результат. Первая загрузка может публиковать
полные малые интервалы и показывать частичный период без окончательных сумм.
Клиент наблюдает только известную активную работу открытого экрана.
Status GET и data GET с `sync=observe` не запускают работу. После изменения
состояния ответ попадает в ту же query identity; после ухода или завершения
наблюдение прекращается. Поздний ответ не заменяет уже принятый новый.
[HTTP API](../http-api.md) объясняет envelope и экранный refresh.
## Смена подключения
Кэш финансовых данных относится к пользователю, счёту и текущему
подключению T-Bank. Backend сначала читает подключение и его generation, затем
проверяет возможность повторного использования ответа. Свежий портфель
одного счёта не требует нового запроса списка счетов у брокера; при обновлении
портфеля доступ проверяется заново. Журнал операций проверяет доступ до чтения
кэшированной страницы. Идентификатор счёта и совпадение параметров запроса
сами по себе недостаточны для выдачи кэша.
Каждое чтение проверяет локальную сессию, текущее подключение, известный отказ
токена и сохранённый состав доступных счетов. Расшифровка token и GetAccounts
для локальной выдачи не нужны. Старый membership допускает выдачу и фоновую
сверку; это задержка обнаружения отзыва доступа, а не live-проверка брокера.
Unknown membership не даёт доступа к произвольному accountId: ответ сообщает
о загрузке доступа. Успешный новый список немедленно ограничивает последующие
чтения. Известный invalid token блокирует личные данные.
При каждом успешном сохранении подключения token шифруется с новым
AES-GCM nonce. Из сохранённого шифротекста backend вычисляет непрозрачную
внутреннюю generation. Она не хранится отдельным полем базы и не входит
в API, журналы или ошибки. Portfolio Snapshot и Ledger Page Cache допускают
повторное использование только в пределах соответствующей generation.
Старый кэш или поздно завершившийся запрос прежней generation не обслуживает
новый запрос после замены подключения, включая сохранение того же plaintext
token. Удаление подключения не оставляет доступного контекста для чтения кэша.
Успешный PUT атомарно сохраняет token, новую случайную внутреннюю revision
и membership из проверки token. Прежние личные datasets и work становятся
недоступны. DELETE удаляет подключение и личные данные; общие неперсональные
источники могут оставаться. Неуспешный PUT сохраняет действующую границу.
После успешного изменения подключения frontend сначала записывает
подтверждённый status, затем сбрасывает generated GET queries в поддеревьях
`/api/v1/brokerage-accounts`, `/api/v1/stocks` и `/api/v1/bonds`. Активные
выборки запрашиваются заново, неактивные — при следующем использовании.
Client Cached Snapshot прежнего подключения больше не служит результатом
новой границы.
Неуспешная операция подключения не меняет статус, generation и доступные
финансовые данные. Обычная ошибка refetch также сохраняет клиентский снимок
текущей границы. Logout — отдельное событие: оно очищает состояние
пользовательской сессии, status подключения и query cache.
Пользовательский сценарий описан в [интеграции T-Bank](../../integrations/tbank.md).
Согласованные правила frontend находятся в workspace-спецификациях
[query coherence](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/frontend-query-coherence/spec.md)
и [data resilience](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/frontend-data-resilience/spec.md);
серверные правила — в
[portfolio](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md) и
[ledger](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md).
Публикация проверяет revision и attempt в той же SQLite-транзакции; поздняя
работа старого подключения не меняет даже timestamp. Revision не попадает
в API, логи или cursor. Frontend очищает personal queries при смене подключения
и известном отказе доступа; поздний ответ не восстанавливает скрытые данные.
Logout очищает сессию и query cache. Подробнее —
[интеграция T-Bank](../../integrations/tbank.md).
@@ -22,6 +22,18 @@
портфеля остаётся доступным после ошибки refetch; сообщение и повтор
относятся к обновлению, а не к отсутствию инвестиций.
Серверная полнота не выводится из query lifecycle. Missing показывает
skeleton только отсутствующих секций, partial — подтверждённые строки и
сообщение о неполноте. Totals ждут обязательных зависимостей, comparisons
готовятся независимо. Только complete empty означает отсутствие операций.
Время источника не заменяется временем получения HTTP-ответа.
Ручное обновление сохраняет показанный результат и applied filters; кнопка
недоступна во время уже запрошенной работы. Ошибка transport отличается от
ошибки source work. Status observation прекращается после terminal outcome
или ухода; само истечение TTL не запускает импорт. Известный access loss
очищает personal view, включая защиту от позднего ответа.
## Известная ошибка сценария
Ошибки счетов, портфеля и подключения T-Bank остаются локальными: рядом
+24
View File
@@ -20,6 +20,30 @@ artifact у себя и проверяет соответствие generated ty
API имеет prefix `/api/v1`; Swagger публикуется для локальной разработки по
environment policy, а не используется frontend в runtime.
## Инвестиционные данные и синхронизация
T-Bank data GET возвращают envelope `{ data, meta }`. `data` может быть `null`
при missing или неполном финансовом агрегате; partial ledger/candles могут
содержать подтверждённые строки. `meta` разделяет availability, freshness,
lastSuccessfulAt, sync, coverage и состояния зависимостей. `dataRevision`
описывает набор данных, `stateVersion` — наблюдаемое состояние, включая
пустое завершённое покрытие. Ни одно поле не раскрывает connection revision.
Обычный GET планирует missing/stale зависимости после проверки доступа.
GET с `sync=observe` только читает. `POST /api/v1/tbank-sync` принимает
валидированный screen descriptor и возвращает `202` с handle и нормализованным
scope. `GET /api/v1/tbank-sync/:id` наблюдает разрешённую работу; чужой или
прежний handle возвращает `404`. Приём работы не означает её успех.
Source failure принятой работы передаётся безопасным `meta.sync.errorCode`;
HTTP transport/access/input errors сохраняют Problem Details. Старые данные
при transient failure сохраняют исходное успешное время. Детали поведения —
[свежесть и полнота](cross-cutting/data-freshness.md).
Envelope несовместим с прежними bare-array ответами: backend/frontend
поставляются согласованной парой. Финальный artifact передаётся явно;
локальная генерация и CI frontend читают только собственный snapshot.
## Validation и identity
Backend преобразует и проверяет входные данные на общей HTTP-границе.