chore(release): 0.1.2 #23

Merged
codex-bot merged 6 commits from release/persist-tbank-data into main 2026-09-26 22:47:50 +03:00
25 changed files with 694 additions and 580 deletions
+10
View File
@@ -1,5 +1,15 @@
# Changelog # Changelog
## [0.1.2](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.1...v0.1.2) (2026-09-26)
### Documentation
* describe local frontend packages and ownership ([55610ea](https://git.personal.ksv741.ru/WealthLedger/docs/commit/55610ea2b4dc687f76ee9eb37c14d887b1b70100))
* document Nexus registry migration [#18](https://git.personal.ksv741.ru/WealthLedger/docs/issues/18) ([5de85c2](https://git.personal.ksv741.ru/WealthLedger/docs/commit/5de85c296f7681882f58476343d3da71f1dee28d))
* document versioned deployment bundle ([15d1d82](https://git.personal.ksv741.ru/WealthLedger/docs/commit/15d1d820f5401c25adda23f753b99e894bdcd4d3))
* explain persisted T-Bank reads and synchronization ([07de062](https://git.personal.ksv741.ru/WealthLedger/docs/commit/07de062ebbf043eff50bd7f53b48bff13bb7cb1b))
* use shared Nexus account [#18](https://git.personal.ksv741.ru/WealthLedger/docs/issues/18) ([6fd39cd](https://git.personal.ksv741.ru/WealthLedger/docs/commit/6fd39cd467566e23cca79a1235125dfb80fa99f7))
## [0.1.1](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.0...v0.1.1) (2026-09-16) ## [0.1.1](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.0...v0.1.1) (2026-09-16)
### Documentation ### Documentation
+72
View File
@@ -0,0 +1,72 @@
# 016. Версионируемый комплект развёртывания
**Status:** Accepted
Frontend и backend выпускаются независимо. Выбор их версий непосредственно
на сервере перекладывает подбор совместимой пары на оператора при каждом
обновлении. Принято решение выпускать комплект в репозитории `deploy`:
его релиз закрепляет проверенную пару конкретных версий обоих образов.
Независимые релизы компонентов сохраняются; выпуск компонента сам по себе
не означает готовность нового релиза развёртывания.
Оператор подключается к серверу по SSH и одной командой запускает обновление
до выбранного релиза развёртывания. При обновлении backend команда должна
скачать образы до его остановки, сохранить резервную копию остановленной базы,
выполнить миграции и проверить запуск. Ограничения
[ADR 015](015-single-server-sqlite-deployment.md)
сохраняются: один backend, краткий простой, неуспешная миграция блокирует запуск;
откат образа не восстанавливает базу автоматически.
## Доставка и подготовка сервера
На сервере хранится Git checkout `deploy`. Команда обновления получает
выбранный тег комплекта и загружает закреплённые образы из Nexus;
сборка приложения на сервере не требуется.
Docker с Compose, внешний HTTPS и доступ к registry считаются подготовленными.
README комплекта должен описывать последовательность этой разовой подготовки
и подключения существующей установки. Автоматическое provisioning сервера
в комплект не входит.
Рабочие данные, сертификат доверия T-Bank и секреты остаются вне обновляемого
checkout. При подключении существующей установки сохраняются её БД,
JWT secret и ключ шифрования. Сертификат `certs/tbank-ca.pem` монтируется
в контейнер только для чтения; путь внутри контейнера задаётся через
`TBANK_CA_CERT_PATH`. Этот сертификат не обслуживает входящий HTTPS.
## Резервные копии
Перед обновлением backend создаётся согласованная копия всего каталога данных
при остановленном backend. Копии хранятся в локальном `backups/` вне checkout,
без автоматического удаления; очистку выполняет оператор вручную.
Такие копии позволяют восстановиться после неудачного обновления, но сами
по себе не защищают от потери диска сервера.
## Обновление и сбои
Если релиз меняет только frontend, обновляется только его контейнер:
backend не останавливается, миграции и резервное копирование БД для этого
обновления не выполняются. Этот путь применим только при неизменных образе
и конфигурации backend.
Если миграции прошли, но новый backend не прошёл проверку запуска, обновление
завершается с ошибкой, неисправный backend останавливается. Оператор получает
диагностику, сведения о предыдущей версии и путь к резервной копии.
Автоматического отката нет: прежний образ может быть несовместим с новой
схемой. Откат образа и восстановление базы остаются отдельными явными действиями
по правилам ADR 015.
## Выпуск комплекта
После публикации образов компонентов отдельно готовится релиз `deploy`:
фиксируется совместимая пара версий, выполняется её проверка и выпускается
тег комплекта. Публикация компонента не создаёт релиз комплекта автоматически
и не запускает обновление сервера. Оператор выбирает выпущенный тег при
запуске команды развёртывания.
## Состояние реализации
Комплект и команда реализованы в отдельном репозитории `deploy`.
Публикация первого тега и обновление рабочего сервера — отдельные действия.
[Контейнерная поставка](../architecture/deployment.md) описывает границы,
пошаговая инструкция находится в README комплекта.
@@ -0,0 +1,140 @@
# 017. Локальное чтение данных T-Bank и фоновая синхронизация
**Status:** Accepted — архитектурное направление согласовано.
Реализация `persist-tbank-data` добавлена в рабочие ветки приложения;
performance/browser и canonical application gates пройдены. Это не отметка
развёртывания в production. Ниже сохранён контекст
принятия решения, а текущее устройство описывает
[модель свежести](../architecture/cross-cutting/data-freshness.md).
## Context
Инвестиционная история не сохраняется в БД. Серверные кэши находятся в памяти
процесса, а загрузка операций, аналитики дохода и рыночной истории зависит
от повторных обращений к T-Bank. Для журнала операций запрос доступных счетов
выполняется даже перед чтением кэшированной страницы. Сохранение только
ответов без изменения этого пути не устранит ожидание внешнего API.
Нужно ускорить все экраны, использующие T-Bank, и уменьшить повторную загрузку
данных. Текущие механизмы описаны в [политике свежести](../architecture/cross-cutting/data-freshness.md)
и [журнале операций](../domains/operations/overview.md).
## Decision
Для уже загруженных данных обычный запрос frontend обслуживается из постоянного
локального хранилища без ожидания ответа T-Bank. Обновление выполняется в фоне.
Пользователь видит время последнего успешного обновления и может запросить
обновление вручную. Сохранённый портфель может ещё не содержать новую сделку;
эта задержка принята ради быстрого открытия экрана.
Синхронизация запускается только запросами пользовательских экранов и ручным
действием «Обновить». Периодических запусков, ежедневного обновления неактивных
пользователей и плановой сверки истории нет. Фоновое выполнение означает,
что уже запрошенная работа не задерживает выдачу сохранённых данных;
самостоятельное расписание обновлений оно не подразумевает.
Обычный запрос запускает обновление, только если нужных данных нет либо
истёк согласованный срок их свежести:
| Данные | Срок свежести |
| --- | --- |
| Портфель и текущие цены | 1 минута |
| Операции текущего месяца | 5 минут |
| Список доступных счетов | 5 минут |
| Справочник инструментов и календари купонов и дивидендов | 1 сутки |
| Незавершённая дневная свеча | 1 минута |
Эти сроки проверяются при запросе и не задают расписание фоновых запусков.
Аналитика дохода зависит от полноты и свежести исходных операций.
Полностью загруженные предыдущие месяцы операций повторно читаются из T-Bank
только по кнопке «Обновить» для показанного периода. Обычный запрос догружает
отсутствующие интервалы и при необходимости обновляет текущий месяц.
Завершённые дневные свечи сохраняются; ручное обновление перечитывает
показанный диапазон. Автоматическое обнаружение поздних операций и исправлений
в уже полностью загруженных прошлых месяцах не обещается.
Кнопка «Обновить» обходит проверку давности и обновляет данные текущего
экрана: выбранного счёта, доступных счетов в режиме «Все счета» либо
инструмента и показанной личной истории на его странице. Нажатие не означает
повторную загрузку всей накопленной истории всех счетов и инструментов.
Решение охватывает все экраны с данными T-Bank. Исполненные операции и рыночная
история сохраняются; текущие позиции, остатки и цены представлены последним
успешно полученным состоянием. Будущие выплаты остаются изменяемым прогнозом,
а не исполненными операциями. Это не решение о накоплении исторических снимков
стоимости портфеля.
Рыночная история бумаг текущего портфеля загружается заранее в рамках
запрошенной пользователем работы. Для остальных бумаг первая загрузка
начинается при открытии инструмента, результат сохраняется для последующего
чтения. Предварительная загрузка истории всего каталога не требуется.
При первом подключении текущий портфель и недавние операции могут отображаться
до завершения загрузки более ранней истории. Частично загруженный период явно
отличается от полного: неполные итоги нельзя выдавать за окончательные,
а отсутствие загруженных записей — за подтверждённое отсутствие операций.
Смысл полноты определён в [глоссарии](../glossary.md#полнота-истории-счёта).
Первый запрос задаёт приоритет: сначала период, нужный экрану, включая
периоды сравнения дохода, затем вся доступная история счёта. Уже запущенная
догрузка может завершиться после ухода пользователя со страницы; нового
периодического запуска это не создаёт.
Полнота относится к успешно загруженным временным интервалам. Смена месяца
сама по себе не делает завершённым последний неполный интервал прошлого
месяца: его оставшаяся часть должна быть догружена. Ошибка промежуточной
страницы не подтверждает полноту и не удаляет ранее сохранённые данные.
Незавершённая свеча также не становится завершённой только из-за смены дня:
её окончательное состояние должен подтвердить источник.
Целевой ориентир — до 500 мс для обычного серверного запроса к заполненной БД.
Это согласованная цель измерения, а не подтверждённая характеристика системы.
Объём данных и набор запросов для проверки предстоит определить.
Для этого изменения принято допущение: ID брокерской операции не меняется
со временем. Обнаружение сменившихся ID и регулярная сверка старой истории
не входят в объём работы. Это допущение проекта, а не гарантия T-Bank:
используемый protobuf-контракт допускает изменение ID. Допущение относится
к идентичности операции; оно не требует игнорировать новые значения полей,
если операция с прежним ID снова получена от брокера.
Подключение T-Bank считается постоянным в рассматриваемом сценарии.
Архив без подключения, специальная политика хранения после отключения
и работа с закрытыми счетами пока не проектируются. Это ограничение объёма
не отменяет существующие проверки доступа и обработку ошибок интеграции.
## Alternatives
Ожидание свежего ответа T-Bank при каждом открытии экрана сохраняет зависимость
скорости и доступности интерфейса от брокера. Увеличение времени жизни кэша
в памяти уменьшает часть повторных запросов, но не сохраняет историю после
перезапуска и не обеспечивает локальное чтение всех нужных данных.
## Consequences
Интерфейсу нужны различимые состояния первичной загрузки, полноты истории,
свежести и ошибки фонового обновления. Успешная загрузка недавних операций
не подтверждает полноту более ранних периодов или периодов сравнения дохода.
Проверки локальной сессии и принадлежности личных данных сохраняются.
Способ подтверждения брокерского доступа должен позволять обслужить уже
загруженные данные без синхронного ожидания T-Bank.
Повторные запросы одного набора данных должны использовать уже выполняющуюся
загрузку. Завершённые интервалы и прогресс сохраняются в БД; после перезапуска
незавершённый интервал повторяется при следующем запросе или ручном обновлении.
Курсор брокера и максимальная дата полученной операции не доказывают полноту.
Ручное обновление периода согласует состав операций и их поля с полностью
полученным ответом источника. Только успешная загрузка всех страниц позволяет
зафиксировать полное покрытие, успешное обновление и удалить отсутствующие
в этом ответе записи периода. Ошибка не превращает частичный ответ в полный.
Личные операции и позиции сохраняют принадлежность пользователю и счёту.
Общие данные инструмента не должны содержать персональные позиции и расчёты;
поздний результат прежнего подключения не публикуется для нового подключения.
Существующие HTTP-контракты пока не поддерживают все необходимые состояния
загрузки и полноты; их изменение входит в будущую реализацию.
+2
View File
@@ -20,3 +20,5 @@
- [013 — НКР как первый источник кредитных рейтингов облигаций](013-nkr-bond-credit-ratings.md) - [013 — НКР как первый источник кредитных рейтингов облигаций](013-nkr-bond-credit-ratings.md)
- [014 — Актуальная OpenAPI-схема как вход frontend](014-trusted-openapi-input.md) - [014 — Актуальная OpenAPI-схема как вход frontend](014-trusted-openapi-input.md)
- [015 — SQLite в ограниченном single-server deployment](015-single-server-sqlite-deployment.md) - [015 — SQLite в ограниченном single-server deployment](015-single-server-sqlite-deployment.md)
- [016 — Версионируемый комплект развёртывания](016-versioned-deployment-bundle.md)
- [017 — Локальное чтение данных T-Bank и фоновая синхронизация](017-local-tbank-reads-and-background-sync.md)
+34 -23
View File
@@ -76,8 +76,8 @@ prerequisites: локальный smoke не подтверждает registry/s
| Часть | Ответственность | | Часть | Ответственность |
| --- | --- | | --- | --- |
| React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений | | React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений |
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и серверные кэши | | Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и синхронизация сохранённых источников |
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений и ручной банковской истории | | SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений, ручной банковской истории и инвестиционных источников с coverage/work |
| Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности | | Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности |
Frontend организован слоями `app → pages → features → entities → shared`: Frontend организован слоями `app → pages → features → entities → shared`:
@@ -86,6 +86,20 @@ Frontend организован слоями `app → pages → features → ent
где локальная аутентификация отделена от подключения брокера и инвестиционных где локальная аутентификация отделена от подключения брокера и инвестиционных
сценариев. Это внутренняя организация приложений, не набор микросервисов. сценариев. Это внутренняя организация приложений, не набор микросервисов.
Маршруты и AppShell принадлежат `app`. Page controller/VM управляет запросами,
формой и восстановлением; presentation получает данные и callbacks, поля
могут использовать контекст React Hook Form. В OperationForm controller
сохраняет intent операции, черновик и явное принятие актуальной версии после
конфликта. В account-income чистые преобразования периода принадлежат модели
страницы и не связывают форму с её запросами.
Локальные packages tokens/ui/icons образуют [визуальную основу](../design/overview.md#общая-основа-и-каталог)
внутри frontend и входят в сборку SPA. Это библиотеки, а не дополнительные
runtime-контейнеры. Провайдеры темы принадлежат приложению и Storybook;
предметные компоненты и API-клиент остаются в приложении. Отдельный API-пакет
и registry-поставка визуальных пакетов не реализованы. Существующие Docker
images приложения не означают независимого npm-выпуска этих библиотек.
Финансовые HTTP-запросы браузера идут через backend. Пользователь вводит token Финансовые HTTP-запросы браузера идут через backend. Пользователь вводит token
T-Bank в форме и отправляет его для сохранения, но API не возвращает token T-Bank в форме и отправляет его для сохранения, но API не возвращает token
в ответах; SPA не использует его для прямых брокерских запросов. Прямая в ответах; SPA не использует его для прямых брокерских запросов. Прямая
@@ -120,28 +134,24 @@ credentials не являются сохраняемым представлен
или перезапуска. Архив обратим и сохраняет историю. Подробности — или перезапуска. Архив обратим и сохраняет историю. Подробности —
[банковские карты](../domains/cards/overview.md). [банковские карты](../domains/cards/overview.md).
Сессии и подключение связаны с владельцем. Access token подтверждает Сессии и подключение связаны с владельцем. Backend локально проверяет
локального пользователя, но `accountId` из URL сам по себе не даёт права membership текущей revision перед каждым финансовым чтением. Unknown membership
читать брокерский счёт. Backend получает текущее подключение пользователя не открывает доступ к произвольному счёту; известный отказ токена блокирует
и его generation перед использованием финансового кэша. Проверка доступности личные данные. Ротация refresh token остаётся атомарной.
счёта у брокера зависит от сценария: журнал выполняет её до чтения страницы
кэша; свежий портфель одного счёта может быть возвращён без нового запроса
списка счетов. При обновлении портфеля доступ проверяется у брокера.
Ротация сессии атомарно заменяет hash текущего refresh token: два SQLite хранит полный последний portfolio, операции и их подтверждённые
конкурентных запроса не могут оба успешно выполнить одну ротацию. диапазоны, private прогнозы, общие instrument/market sources и intent/staging.
Вызов брокера и запись в локальную базу не образуют общую транзакцию: Нормализованные финансовые значения сохраняются decimal-строками. Полнота
при сохранении подключения сначала проверяется token, затем записывается отделена от свежести и пагинации. Ответ не ждёт внешних RPC; запрос при
шифротекст. Подробности находятся в необходимости запускает работу в том же backend process через существующий
[аутентификации](../domains/authentication/overview.md) и лимитер. На startup импорт не запускается; восстановление требует следующего
[интеграции T-Bank](../integrations/tbank.md). запроса. Подробности — [модель свежести](cross-cutting/data-freshness.md).
Брокерские счета, сводки, портфель, страницы журнала и будущие события PUT подключения сначала проверяет token у T-Bank, затем одной транзакцией
не являются долговременной историей в SQLite. Они получаются для текущего сохраняет encrypted token, новую revision и проверенный membership.
подключения и могут временно кэшироваться. Перезапуск backend не Сетевая операция не удерживает SQLite-транзакцию. Общие источники не содержат
восстанавливает прежние финансовые снимки и cursor continuations из базы; личных позиций, ожидаемых сумм или credentials. Произвольного архива
[модель свежести](cross-cutting/data-freshness.md) отделяет это от хранения отключённых счетов нет.
учётных данных.
## Сохранение и восстановление данных ## Сохранение и восстановление данных
@@ -196,7 +206,8 @@ Origin браузера, адрес API и cookie policy должны быть
режимах refresh-cookie имеет Secure flag и требует защищённого соединения. режимах refresh-cookie имеет Secure flag и требует защищённого соединения.
Single-server deployment не задаёт гарантий HA или zero-downtime. 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) — последний [Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — успешный
успешный ответ, сохранённый TanStack Query в SPA. При обычном фоновом обновлении ответ в TanStack Query. Он сохраняет показанное содержимое при временной ошибке
того же набора данных уже показанный результат остаётся на экране; ошибка запроса в той же границе доступа. SQLite сохраняет источники независимо от
обновления сообщает о проблеме, не заменяя известные значения пустотой. браузера: membership, последний полный portfolio, исполненные операции,
Клиентский кэш не является внешней системой или отдельным runtime-приложением. общие сведения об инструментах, котировки, свечи и календари.
[Portfolio Snapshot](../../glossary.md#portfolio-snapshot) — полный результат [Полнота периода](../../glossary.md#полнота-истории-счёта) и свежесть различаются.
загрузки портфеля на backend. В пределах разрешённого сценарием срока он может Успешно проверенный пустой интервал полон; последняя страница журнала сама
служить серверным fallback при временной ошибке upstream. Backend отмечает по себе полноту не доказывает. Ошибка сверки сохраняет прежние данные и время.
такой ответ `isStale`; frontend не выводит это поле из состояния собственного Старый полный портфель не удаляется по истечении TTL. `isStale` показывает
query-кэша. Серверное предупреждение и ошибка клиентского refetch могут возраст portfolio source, а не сам факт неудачного обновления.
возникать независимо.
Свежесть и допустимость fallback определяются сценарием. Портфельный fallback | Источник | Когда обычный запрос требует обновления |
не распространяется автоматически на журнал, прогноз событий или страницу | --- | --- |
инструмента. Точные правила находятся в | Portfolio, котировки, изменяемый хвост дневных свечей | Через 60 секунд |
[справочнике портфеля](../../domains/portfolio/data-reference.md), | Membership и изменяемый хвост операций | Через 5 минут |
[журнале операций](../../domains/operations/overview.md) и | Metadata, поиск каталога, company и календари | Через сутки |
[будущих событиях](../../domains/income/upcoming-events.md). | Полные прошлые интервалы операций и завершённые свечи | При явном обновлении показанного периода |
По [ADR 007](../../adr/007-react-query-fetch-policy.md) запрос связан с открытием TTL не создаёт таймер импорта. Запрошенная работа может закончиться после
экрана, а возврат фокуса окна не запускает refetch. Повторное открытие после ухода со страницы. После перезапуска нет автоматического обхода intent:
`staleTime` может обновить ответ в фоне. Эта политика не является таймером следующий подходящий запрос повторяет незавершённый интервал с первой страницы,
периодического обновления. Перезапуск backend уничтожает его in-memory переиспользуя полные интервалы. Cursor брокера не служит durable checkpoint.
снимки и continuations журнала; SQLite не восстанавливает их.
Ручное «Обновить» перечитывает зависимости текущего экрана и применённый
период независимо от 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 шифруется с новым Успешный PUT атомарно сохраняет token, новую случайную внутреннюю revision
AES-GCM nonce. Из сохранённого шифротекста backend вычисляет непрозрачную и membership из проверки token. Прежние личные datasets и work становятся
внутреннюю generation. Она не хранится отдельным полем базы и не входит недоступны. DELETE удаляет подключение и личные данные; общие неперсональные
в API, журналы или ошибки. Portfolio Snapshot и Ledger Page Cache допускают источники могут оставаться. Неуспешный PUT сохраняет действующую границу.
повторное использование только в пределах соответствующей generation.
Старый кэш или поздно завершившийся запрос прежней generation не обслуживает
новый запрос после замены подключения, включая сохранение того же plaintext
token. Удаление подключения не оставляет доступного контекста для чтения кэша.
После успешного изменения подключения frontend сначала записывает Публикация проверяет revision и attempt в той же SQLite-транзакции; поздняя
подтверждённый status, затем сбрасывает generated GET queries в поддеревьях работа старого подключения не меняет даже timestamp. Revision не попадает
`/api/v1/brokerage-accounts`, `/api/v1/stocks` и `/api/v1/bonds`. Активные в API, логи или cursor. Frontend очищает personal queries при смене подключения
выборки запрашиваются заново, неактивные — при следующем использовании. и известном отказе доступа; поздний ответ не восстанавливает скрытые данные.
Client Cached Snapshot прежнего подключения больше не служит результатом Logout очищает сессию и query cache. Подробнее —
новой границы. [интеграция T-Bank](../../integrations/tbank.md).
Неуспешная операция подключения не меняет статус, 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).
@@ -22,6 +22,18 @@
портфеля остаётся доступным после ошибки refetch; сообщение и повтор портфеля остаётся доступным после ошибки 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 остаются локальными: рядом Ошибки счетов, портфеля и подключения T-Bank остаются локальными: рядом
+97 -143
View File
@@ -1,30 +1,38 @@
# Контейнерная поставка # Контейнерная поставка
Runbook для одного Docker host, одного backend и краткого простоя. Поставка для одного Docker host, одного backend и краткого простоя.
[Топология](containers.md#контейнерная-поставка) и [Топология](containers.md#контейнерная-поставка) и
[ADR 015](../adr/015-single-server-sqlite-deployment.md) объясняют границы. [ADR 015](../adr/015-single-server-sqlite-deployment.md) объясняют границы.
Команды выполняются из checkout `WealthLedger/main`, содержащего `compose.yaml`. Production-комплект находится в отдельном [репозитории deploy](https://git.personal.ksv741.ru/WealthLedger/deploy).
Development дополнительно требует checkout `frontend/` и `backend/`. Его README — единственная пошаговая инструкция установки, обновления и recovery;
[ADR 016](../adr/016-versioned-deployment-bundle.md) объясняет выбор релиза комплекта.
Development-команды ниже выполняются из workspace с checkout `frontend/` и `backend/`.
## Предпосылки ## Предпосылки
- Docker Engine с Compose plugin **не ниже 2.24.4** (`!override` в development), - Docker Engine с Compose plugin **не ниже 2.24.4** (`!override` в development),
доступ к registry и достаточно свободного диска для non-root UID 1000. доступ к registry и достаточно свободного диска для non-root UID 1000.
Локально проверены Engine 29.8.0, Compose 5.5.1, Linux arm64. Production images `0.2.3` опубликованы для Linux amd64.
Другую архитектуру проверяют отдельно; multi-platform images не обещаны. Другую архитектуру проверяют отдельно; multi-platform images не обещаны.
- Gitea Container Registry; проверяемая версия сервера — 1.27.3. - Nexus Docker hosted repository `docker-hosted`, доступный через
`nexus.personal.ksv741.ru/docker-hosted/`. Docker Bearer Token Realm включён;
reverse proxy передаёт исходные host и HTTPS scheme. Адрес token realm
в `WWW-Authenticate` также должен начинаться с `https://`.
Deployment policy запрещает redeploy существующих release tags.
Runner label — `ubuntu-latest`, runner должен иметь Docker CLI, buildx и daemon. Runner label — `ubuntu-latest`, runner должен иметь Docker CLI, buildx и daemon.
Наличие label само по себе не доказывает Docker capability. Наличие label само по себе не доказывает Docker capability.
- В Actions каждого component repo нужны `REGISTRY_USERNAME` и - В Actions каждого component repo нужны `NEXUS_USERNAME` и
`REGISTRY_TOKEN` отдельной identity с package write permission. Server `NEXUS_PASSWORD` пользователя `wealthledger-ci`. Эта же учётная запись
использует другую identity только с package read permission. Built-in используется сервером для загрузки образов. Для публикации нужны
browse/read/add/edit на `docker-hosted`; фактически выданный набор прав
настраивается в Nexus. Built-in
workflow token не заменяет эту настройку. workflow token не заменяет эту настройку.
- На сервере Caddy уже завершает TLS и передаёт весь домен на - На сервере Caddy уже завершает TLS и передаёт весь домен на
`127.0.0.1:8080` (или выбранный frontend port). Отдельного `/api` upstream нет. `127.0.0.1:8080` (или выбранный frontend port). Отдельного `/api` upstream нет.
DNS, TLS, firewall и provisioning Caddy выполняются вне этого runbook. DNS, TLS, firewall и provisioning Caddy выполняются вне этого runbook.
Реальная публикация и server pull-only login требуют отдельной проверки Серверный login и доступ к images проверяются на целевом сервере.
на вашей инфраструктуре. Локальные images не доказывают registry permissions. Локальный smoke не доказывает серверные registry permissions.
## Development ## Development
@@ -63,7 +71,7 @@ Project `wealthledger-dev` хранит SQLite в `development-data`. Обычн
version, затем выполняет `npm ci`, `npm run verify`; backend дополнительно version, затем выполняет `npm ci`, `npm run verify`; backend дополнительно
выполняет `npm run test:e2e`. У каждого image OCI version/revision labels. выполняет `npm run test:e2e`. У каждого image OCI version/revision labels.
Images публикуются как `git.personal.ksv741.ru/wealthledger/frontend:X.Y.Z` Images публикуются как `nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:X.Y.Z`
и `.../backend:X.Y.Z`. `latest` не создаётся. и `.../backend:X.Y.Z`. `latest` не создаётся.
Workflow отказывается перезаписывать существующие release tags; Workflow отказывается перезаписывать существующие release tags;
защита release tags и запрет параллельных publishers обязательны. Если одна защита release tags и запрет параллельных publishers обязательны. Если одна
@@ -72,156 +80,102 @@ Workflow отказывается перезаписывать существу
Выберите опубликованные version tags, выполните pull и сравните OCI revision Выберите опубликованные version tags, выполните pull и сравните OCI revision
с release commit. Выполните smoke с disposable data/config до первого server с release commit. Выполните smoke с disposable data/config до первого server
rollout. В production env хранится пара полных image references, например rollout. В `release.env` комплекта хранится пара полных image references, например
`git.personal.ksv741.ru/wealthledger/frontend:0.1.0`. Сохраните versions/revisions `nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:0.2.3`. Сохраните versions/revisions
в deployment record. Release tags не должны переназначаться другим images. в deployment record. Release tags не должны переназначаться другим images.
## Перенос существующих images из Gitea
До переключения CI и server references проверьте HTTPS token realm и настройте
пользователя `wealthledger-ci`. В Actions добавьте `NEXUS_USERNAME`/`NEXUS_PASSWORD`; прежние
`REGISTRY_USERNAME`/`REGISTRY_TOKEN` сохраните для отката старого workflow.
Пароли вводятся интерактивно или через `--password-stdin`, не через аргументы.
Скопируйте каждый опубликованный release без пересборки. Например, для `0.2.0`
с установленным Skopeo и доступом чтения Gitea/записи Nexus:
```bash
skopeo login git.personal.ksv741.ru
skopeo login nexus.personal.ksv741.ru
for component in frontend backend; do
skopeo copy --all --preserve-digests \
"docker://git.personal.ksv741.ru/wealthledger/$component:0.2.0" \
"docker://nexus.personal.ksv741.ru/docker-hosted/wealthledger/$component:0.2.0" || exit 1
done
```
`--all` переносит весь image index, включая платформы и вложенные attestation
manifests; `--preserve-digests` прерывает перенос при изменении digest.
До копирования проверьте наличие destination tag: если он уже существует,
сравните исходный и целевой index digest и вложенные manifests. Совпадающий
artifact повторно не публикуйте; несовпадение требует расследования, не удаления
или перезаписи tag. Частичный перенос не считается завершённым.
После копирования сравните digests через `skopeo inspect --raw` и SHA-256,
выполните `docker pull` обоих images пользователем `wealthledger-ci` на сервере приложения,
проверьте OCI revision и container smoke на disposable data.
Только после этого меняйте production image references и запускайте обычную
последовательность обновления ниже. Старые Gitea packages и references сохраните:
откат registry возвращает прежние адреса тех же artifacts, не восстанавливает
SQLite и не меняет encryption keys. Не переносите Git release tags и не
запускайте заново старый tag workflow для копирования существующего image.
## Первая установка ## Первая установка
Создайте restricted environment file и data directory: На подготовленном сервере оператор клонирует `deploy`, настраивает внешний
HTTPS и registry login, размещает server env, сертификат T-Bank и data directory
вне checkout. Для новой пустой базы команда вызывается с `--init`.
Для действующей установки сохраняются прежние данные и ключи, а первый переход
выполняется как полное обновление с backup. Отсутствующая БД без `--init` — ошибка.
Точные команды и разрешения каталогов приведены в README комплекта.
```bash Git-тег `deploy` закрепляет пару images через `release.env`. Версия комплекта
sudo install -d -m 0700 /srv/wealthledger не обязана совпадать с версиями компонентов. Публикация image не выпускает
sudo install -d -m 0700 -o 1000 -g 1000 /srv/wealthledger/data комплект и не обновляет сервер автоматически. Команда получает выбранный тег,
sudo install -m 0600 .env.compose.example /srv/wealthledger/production.env загружает images до простоя и использует весь snapshot этого commit.
```
Дальнейшие команды выполняет оператор, имеющий доступ к environment file,
backup directory и Docker. Заполните env локальным защищённым редактором:
полные ссылки `FRONTEND_IMAGE` и `BACKEND_IMAGE` с опубликованными version tags,
публичный HTTPS origin и абсолютный data path. Версии в примере — образец
формата; перед запуском выберите реально опубликованные releases.
Пустые required значения блокируют config; некорректные secrets блокируют backend.
Не выводите полный rendered config с настоящими secrets в журналы.
Для новой базы создайте JWT secret длиной хотя бы 32 символа и отдельный
32-byte encryption key в base64 (`openssl rand -hex 32` и
`openssl rand -base64 32` в защищённом терминале). Сохраните ключ отдельно
от data backup. Для существующей установки перенесите прежние JWT secret
и encryption key, а согласованную копию **всего** data directory — при
остановленном прежнем backend. Файл должен называться `wealthledger.db`;
если переименовываете, согласуйте также WAL/SHM companion names до старта.
Не запускайте старый и новый backend на одной базе одновременно.
Войдите в registry server identity без помещения token в командную строку:
```bash
docker login git.personal.ksv741.ru --username SERVER_PULL_USER
export COMPOSE_FILE=compose.yaml
export COMPOSE_PROJECT_NAME=wealthledger
export COMPOSE_ENV_FILES=/srv/wealthledger/production.env
docker compose config --quiet
docker compose pull frontend backend migration
docker compose up -d --wait --wait-timeout 120
docker compose ps -a
```
Migration и backend используют один image и один data mount.
Migration должна завершиться с кодом 0; иначе backend не запускается.
Readiness принимает 401 от существующего `/api/v1/auth/me` без credentials.
Frontend становится healthy после backend. Backend host port отсутствует.
Runtime non-root; root filesystem read-only, nginx temp files находятся в tmpfs.
Проверьте SPA deep link, регистрацию/вход, refresh, существующие карты,
чтение старого зашифрованного подключения и один финансовый запрос.
Production refresh-cookie имеет Secure flag: проверяйте через HTTPS Caddy;
loopback HTTP curl без browser cookie jar не доказывает browser session.
При локальном smoke используйте disposable credentials и явную передачу cookie.
Направляйте Caddy на новый deployment только после успешной проверки.
Прежний runtime удаляйте только после проверки новой установки.
## Backup при остановленном backend ## Backup при остановленном backend
До backend update проверьте совместимость новой версии с текущим frontend Сначала останавливаются писатели, затем копируется весь каталог данных,
и схемой, а обратной версии — с новой схемой. При неизвестной совместимости включая возможные WAL/SHM. Только успешная копия допускает миграции. Локальные
не обещайте image rollback. В выбранном rollout отсутствуют параллельные API. backups находятся рядом с data directory, вне checkout, и автоматически
не удаляются. Незавершённая копия помечается `.partial-` и не подходит для restore.
Ниже data path соответствует примеру env; при другом path замените его. Ключ шифрования хранится отдельно. Локальная копия не защищает от потери диска.
Сохраните текущую пару image references/revisions и отдельно — ключи.
```bash
docker compose stop backend
docker compose ps -a backend
install -d -m 0700 /srv/wealthledger/backups
BACKUP_DIRECTORY="/srv/wealthledger/backups/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -m 0700 "$BACKUP_DIRECTORY"
cp -a /srv/wealthledger/data "$BACKUP_DIRECTORY/data"
```
Перед копированием убедитесь, что backend остановлен, migration не выполняется
и никто другой не пишет в каталог. Успех `cp` обязателен перед продолжением.
Не копируйте отдельный `.db` из работающего приложения. Проверяйте backup
на отдельной копии: migration status, `PRAGMA integrity_check`, auth, карты
и расшифровка тестового подключения исходным ключом. Backup без проверяемого
ключа недостаточен для восстановления подключения.
## Независимое обновление frontend ## Независимое обновление frontend
После проверки API compatibility замените только `FRONTEND_IMAGE` в env: Если изменились только image или конфигурация frontend, backend не
пересоздаётся, backup и migration не выполняются. Возможен короткий перерыв
```bash на внешней точке входа; старым browser tabs может потребоваться reload.
docker compose pull frontend При неизменных исправных контейнерах повтор команды не пересоздаёт сервисы.
docker compose up -d --no-deps --wait --wait-timeout 120 frontend
```
Backend container и SQLite не пересоздаются. Входная точка кратко недоступна.
Проверьте deep link, login/refresh и новую OCI revision. Старым browser tabs
может понадобиться reload: старые hashed chunks не сохраняются между images.
## Независимое обновление backend ## Независимое обновление backend
Выполните stopped-backend backup выше. Замените только `BACKEND_IMAGE` в env. Изменение image или конфигурации backend запускает stop → backup → migration →
Последовательность не продолжается после failed migration: readiness. Неизменный frontend не пересоздаётся. Во время простоя возможен 502.
Ошибка migration блокирует старт, ошибка готовности останавливает новый backend;
ошибка frontend не останавливает исправный backend. Успех фиксируется только
после проверок запуска затронутых компонентов.
```bash Одна file lock защищает установку от параллельных команд. Маркер незавершённого
docker compose pull backend migration обновления блокирует повтор до разбора оператором. Он содержит этап, версии
docker compose up --no-deps --force-recreate --abort-on-container-exit \ и путь к backup, но не секреты и не копию данных. После обновления нужен smoke
--exit-code-from migration migration && через HTTPS: healthcheck сам по себе не доказывает работу Secure refresh-cookie.
docker compose up -d --no-deps --wait --wait-timeout 120 backend
```
Frontend не пересоздаётся; nginx повторно разрешает backend DNS. Во время
перерыва возможен 502. При failure оставьте backend остановленным, сохраните
диагностику migration и выберите совместимый image либо отдельный restore.
Не используйте `migrate reset` или автоматический `resolve` как recovery.
Проверьте migration status, health, login/refresh, карты и расшифровку подключения.
После успешного smoke запишите фактически установленную пару image references.
## Откат image ## Откат image
Для frontend верните предыдущий совместимый version tag и повторите frontend update. Откат версии и восстановление базы — разные операции. Прежний backend можно
Для backend сначала остановите процесс и подтвердите совместимость старого запускать только после проверки совместимости с текущей схемой. Автоматического
image с **текущей** схемой. Верните предыдущий image reference и запустите только backend: rollback нет; migration service при ручном image rollback не запускается.
Порядок ручных Compose-команд и проверки recovery находится в README комплекта.
```bash
docker compose pull backend
docker compose up -d --no-deps --wait --wait-timeout 120 backend
```
Данные и encryption key не заменяются. Migration service здесь намеренно
не запускается: старые migrations не являются down-migrations. Прочитайте
новые записи, сделанные после backup, и проверьте auth/cards/connection.
При несовместимости оставьте приложение остановленным до решения о recovery.
## Explicit restore ## Explicit restore
Это отдельное решение оператора, **не часть image rollback**. Оператор выбирает Оператор выбирает проверенную копию и явно принимает потерю всех более новых
backup timestamp и явно принимает потерю всех изменений после него, включая записей. Сначала восстановление проверяется на isolated данных с прежним ключом
сессии, карты и подключения. Сначала воспроизведите restore на isolated project и совместимым image. На целевой установке останавливаются писатели, текущий
с отдельным data path/port и совместимым image; не используйте production data каталог сохраняется под новым именем, затем восстанавливается выбранная копия.
как fixture. Убедитесь в наличии соответствующего encryption key. Backend пересоздаётся, чтобы заново подключить заменённый bind-каталог:
простой stop/start может сохранить mount прежних данных.
После отдельного решения остановите backend и убедитесь, что migration не Проверяются целостность БД, auth/cards и расшифровка подключения. Только после
работает. Сохраните нынешний каталог под новым именем, ничего не удаляя: подтверждённого recovery можно снять маркер незавершённой операции.
```bash
docker compose stop backend
mv /srv/wealthledger/data /srv/wealthledger/data-before-restore-TIMESTAMP
cp -a /srv/wealthledger/backups/CHOSEN_TIMESTAMP/data /srv/wealthledger/data
```
Проверьте owner 1000:1000, permissions, выбранный compatible image и исходный
ключ. Запустите backend командой rollback с `--no-deps`, затем выполните smoke.
Сверьте наличие записей до backup и отсутствие известных записей потерянного
интервала. Сохраните прежний каталог для расследования. При необходимости
новой migration сначала выполните обычную backup/update последовательность.
+24
View File
@@ -20,6 +20,30 @@ artifact у себя и проверяет соответствие generated ty
API имеет prefix `/api/v1`; Swagger публикуется для локальной разработки по API имеет prefix `/api/v1`; Swagger публикуется для локальной разработки по
environment policy, а не используется frontend в runtime. 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 ## Validation и identity
Backend преобразует и проверяет входные данные на общей HTTP-границе. Backend преобразует и проверяет входные данные на общей HTTP-границе.
+30 -2
View File
@@ -4,6 +4,34 @@
типографику, интервалы, радиусы, тени и адаптивные размеры. типографику, интервалы, радиусы, тени и адаптивные размеры.
Поддерживается светлая схема; тёмная тема не заявлена. Поддерживается светлая схема; тёмная тема не заявлена.
## Общая основа и каталог
Внутри frontend используются три private npm workspace-пакета:
| Пакет | Ответственность |
| --- | --- |
| `@wealthledger/tokens` | JSON со значениями палитры, типографики, интервалов и других визуальных параметров |
| `@wealthledger/ui` | Mantine-тема, общие стили, StateView, LoadingSkeleton и EntryCard |
| `@wealthledger/icons` | React-иконки добавления, редактирования и удаления |
EntryCard отвечает за расположение переданных подписей, значения и действий.
Предметный компонент определяет смысл операции, форматирует данные и выбирает
цветовую роль. Денежные расчёты и запросы не входят в общий UI-пакет.
Иконки наследуют цвет через `currentColor`. По умолчанию они декоративные:
доступное имя принадлежит кнопке. Самостоятельная смысловая иконка получает
явные роль изображения и доступное имя. Предметные рисунки и анимации
остаются в приложении; поставки raw SVG и мобильного UI нет.
Storybook объединяет примеры приложения и пакетов. Он показывает синтетические
состояния без API, авторизации и query providers; формы получают локальные
callbacks. Каталог помогает проверять представление, но не заменяет проверки
запросов, денежных ограничений и восстановления после ошибок.
Пакеты используются локально и не опубликованы в registry. Независимый выпуск
не настроен. Потребители импортируют публичные exports пакетов напрямую; прежние
compatibility re-exports `shared/theme` и `shared/ui` удалены.
## Семантические роли ## Семантические роли
| Роль | Цветовая основа | Применение | | Роль | Цветовая основа | Применение |
@@ -16,8 +44,8 @@
| border | gray.3 | Граница поверхности | | border | gray.3 | Граница поверхности |
| primary | blue filled | Основное действие и активная навигация | | primary | blue filled | Основное действие и активная навигация |
| info | blue.6 | Информация | | info | blue.6 | Информация |
| positive | green.6 | Положительный результат с текстом или знаком | | positive | green.9 | Положительный результат с текстом или знаком |
| negative | red.6 | Ошибка или отрицательный результат с текстом или знаком | | negative | red.9 | Ошибка или отрицательный результат с текстом или знаком |
| warning | yellow.6 | Предупреждение; не мелкий текст на белом | | warning | yellow.6 | Предупреждение; не мелкий текст на белом |
Цвет не заменяет подпись или знак. Палитра не гарантирует доступный контраст Цвет не заменяет подпись или знак. Палитра не гарантирует доступный контраст
+16 -34
View File
@@ -7,30 +7,11 @@
только к backend. Счёт не хранится как долговременное локальное владение только к backend. Счёт не хранится как долговременное локальное владение
независимо от брокерского подключения. независимо от брокерского подключения.
```mermaid Экран читает локальный список и сохранённые сводки. Обычный запрос запускает
sequenceDiagram недостающие или устаревшие зависимости в фоне. Список и сводка каждого счёта
participant U as Пользователь имеют независимые timestamps и состояния. Неподтверждённый список показывает
participant F as Frontend загрузку доступа, а не отсутствие счетов. Поток и границы описаны в
participant B as Backend [модели свежести](../../architecture/cross-cutting/data-freshness.md).
participant D as SQLite
participant T as T-Bank gRPC
U->>F: Открывает /accounts
F->>B: GET /api/v1/brokerage-accounts (Bearer)
B->>D: Читает подключение T-Bank
alt Подключение отсутствует
B-->>F: 409 TBANK_NOT_CONNECTED
F-->>U: Переход к настройкам T-Bank
else Подключение сохранено
B->>T: GetAccounts(OPEN) over TLS
T-->>B: Счета или ошибка
par Для каждого поддерживаемого счета
B->>T: GetPortfolio + GetPositions over TLS
T-->>B: Позиции и деньги либо временная ошибка
end
B-->>F: Счета со сводками либо ошибка
F-->>U: Карточки счетов или объяснение ошибки
end
```
## Счёт и его сводка ## Счёт и его сводка
@@ -69,8 +50,8 @@ sequenceDiagram
сводка не подменяется нулями. сводка не подменяется нулями.
Временная недоступность сводки одного счёта остаётся локальной для него. Временная недоступность сводки одного счёта остаётся локальной для него.
Ошибка подключения, отклонение токена или невозможность получить исходный Известный отказ доступа скрывает личные данные. Transient failure сохраняет
список не превращаются в частичный успешный список. последний список и его время; отсутствие первого списка остаётся missing.
## Состояния ## Состояния
@@ -85,16 +66,17 @@ sequenceDiagram
- Неизвестная ошибка: экран предлагает только повторить загрузку. - Неизвестная ошибка: экран предлагает только повторить загрузку.
- Успешный пустой ответ: штатное состояние отсутствия инвестиционных счетов. - Успешный пустой ответ: штатное состояние отсутствия инвестиционных счетов.
Карточка ведёт на `/accounts/:accountId` по всей области и доступна с клавиатуры. На широком экране карточки расположены в две колонки, на узком — в одну. Карточка показывает название, локализованный тип, крупный валютный итог и вторую валюту при наличии, число остальных валют, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая сводка отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная сводка не подменяет их нулями. Наведение и видимый клавиатурный фокус подчёркивают переход; настройка уменьшения движения отключает анимацию. Карточка ведёт на `/accounts/:accountId` по всей области и доступна с клавиатуры. Карточки расположены в одну колонку при любой ширине. Карточка показывает название, локализованный тип, крупный валютный итог и вторую валюту при наличии, число остальных валют, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая сводка отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная сводка не подменяет их нулями. Наведение и видимый клавиатурный фокус подчёркивают переход; настройка уменьшения движения отключает анимацию.
## Свежесть и границы ## Свежесть и границы
Список запрашивается при открытии экрана без ручного обновления и без обновления при возврате фокуса. Frontend использует успешный кэш TanStack Query в течение 60 секунд, поэтому повторный вход в этот интервал не выполняет новый GET. Когда уже показанный список обновляется в фоне, он остаётся на экране: индикатор обновления и ошибка с повторной попыткой отображаются рядом с последним успешным ответом, включая пустой список. Это клиентский сохранённый ответ; серверный запасной список не выдаётся. Backend не хранит кэш и не передаёт метку свежести. Список не заменяет полный [портфель](../portfolio/overview.md), Обычное открытие использует query owner и локальный server snapshot.
[журнал операций](../operations/overview.md) или историю закрытых счетов. «Обновить» запрашивает membership и сводки доступных счетов; активная работа
Для совокупного результата доступен [режим «Все счета»](../portfolio/all-accounts.md). наблюдается без нового импорта. Истечение TTL само не запускает запрос.
Membership имеет пятиминутную свежесть, portfolio — минутную. Время списка
не подменяет время каждой сводки. Список не заменяет полный
[портфель](../portfolio/overview.md), [журнал](../operations/overview.md)
или историю закрытых счетов. Для общего результата доступен
[режим «Все счета»](../portfolio/all-accounts.md).
Согласованное поведение описывает [спецификация счетов](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-accounts/spec.md). Согласованное поведение описывает [спецификация счетов](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-accounts/spec.md).
Её раздел об экране пока требует одноколоночного списка и не описывает
счётчики акций и облигаций. Текущая реализация и её проверки используют
две колонки на широком экране и показывают эти счётчики. Расхождение требует
отдельного согласования требований; в этой документации отражён наблюдаемый экран.
+23 -14
View File
@@ -49,21 +49,19 @@
## Какие выплаты входят в результат ## Какие выплаты входят в результат
- Backend проверяет доступ к счёту, проходит все страницы исполненных операций и не возвращает частичный результат при ошибке страницы или повреждённой цепочке курсоров. - Backend проверяет доступ локально и читает сохранённые операции. Фоновая загрузка должна завершить все страницы; ошибка страницы или повреждённая цепочка курсоров не подтверждают полноту.
- Только точные типы купона и дивиденда источника формируют результат. Суммы остаются точными десятичными строками в исходной валюте; отрицательные корректировки сохраняют знак. Налоги, комиссии, сделки, переводы, погашения и переводы дивидендов не входят в доход. - Только точные типы купона и дивиденда источника формируют результат. Суммы остаются точными десятичными строками в исходной валюте; отрицательные корректировки сохраняют знак. Налоги, комиссии, сделки, переводы, погашения и переводы дивидендов не входят в доход.
- Итоги, сравнения, временные интервалы и разбиение по инструментам не смешивают валюты и не создают общий конвертированный итог. На странице показано только сравнение с годом ранее, когда календарная дата и история счёта позволяют. Предыдущий период остаётся в ответе API, но не отображается. - Итоги, сравнения, временные интервалы и разбиение по инструментам не смешивают валюты и не создают общий конвертированный итог. Страница показывает сравнения с предыдущим периодом и годом ранее, когда календарная дата и история счёта позволяют. Неполное сравнение имеет отдельное сообщение, без нулевой базы.
- Временные интервалы по-прежнему приходят с backend: дни для 1–45 дней, недели для 46–180 и месяцы для более длинного периода, включая пустые интервалы. Страница дохода не строит из них график или текстовую динамику; ответ API не меняется. - Временные интервалы по-прежнему приходят с backend: дни для 1–45 дней, недели для 46–180 и месяцы для более длинного периода, включая пустые интервалы. Страница дохода не строит из них график или текстовую динамику; ответ API не меняется.
- Раздел «Выплаты» показывает единый список карточек выплат в исходном порядке `entries`: одна карточка на каждую выплату. Внешних групп по инструментам или валютам, итогов и количества выплат инструмента нет. Описание, дата, доступные идентификаторы инструмента и сумма со знаком видны в каждой карточке; валюты по-прежнему разделены в итогах и сравнениях. Дополнительный запрос за составом не нужен. - Раздел «Выплаты» показывает единый список карточек выплат в исходном порядке `entries`: одна карточка на каждую выплату. Внешних групп по инструментам или валютам, итогов и количества выплат инструмента нет. Описание, дата, доступные идентификаторы инструмента и сумма со знаком видны в каждой карточке; валюты по-прежнему разделены в итогах и сравнениях. Дополнительный запрос за составом не нужен.
Сравнение с непосредственно предыдущим периодом сохраняется в API: Сравнение с непосредственно предыдущим периодом использует предыдущий полный
для полного месяца это предыдущий полный месяц, иначе — непосредственно месяц для полного выбранного месяца, иначе — непосредственно предшествующий
предшествующий интервал той же включительной длительности. Текущая страница интервал той же включительной длительности. Оно не подменяет сравнение годом
его не показывает. Это расходится с ранее. Незагруженное сравнение и ошибка его источника отличаются от
[основным требованием сравнений](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md#requirement-period-comparisons-preserve-calendar-and-currency-meaning), недоступности годового сравнения по календарю или дате открытия счёта.
которое описывает оба сравнения. Требуется отдельно согласовать этот разрыв;
здесь сохранено различие между ответом API и фактическим экраном.
Раздел «Годом ранее» показывает цветные панели без общей белой подложки и Каждый раздел сравнения показывает цветные панели без общей белой подложки и
внутренних отступов у внешнего блока. Точные периоды показаны в заголовках двух панелей с русским форматом диапазона внутренних отступов у внешнего блока. Точные периоды показаны в заголовках двух панелей с русским форматом диапазона
и UTC для календарных дат. и UTC для календарных дат.
Отдельного заголовка с датами и валютного бейджа нет; валюта остаётся у каждой суммы. Отдельного заголовка с датами и валютного бейджа нет; валюта остаётся у каждой суммы.
@@ -76,6 +74,16 @@
## Полнота и принадлежность выплат ## Полнота и принадлежность выплат
Доходы читают сохранённые операции и coverage в согласованной транзакции.
Selected, previous и yearAgo имеют независимую готовность. Неполный selected
не получает окончательных итогов; неполное сравнение не создаёт нулевую базу.
Подтверждённый пустой период отличается от ещё не загруженного. При смене
периода прежний snapshot не показывается под новым названием.
Ручная сверка полного периода сохраняет весь прежний результат до успешной
атомарной публикации всех необходимых интервалов и счетов. История счёта
может ещё догружаться после готовности выбранного месяца.
Каждая запись `income-analytics.entries` содержит обязательный `instrumentKey`, Каждая запись `income-analytics.entries` содержит обязательный `instrumentKey`,
равный `key` соответствующей строки `instruments`. Backend вычисляет этот ключ равный `key` соответствующей строки `instruments`. Backend вычисляет этот ключ
один раз при подготовке записей дохода и использует его же для группировки. один раз при подготовке записей дохода и использует его же для группировки.
@@ -90,9 +98,10 @@ Frontend проверяет принадлежность выплат по по
состоянию ошибки аналитики с повторной попыткой. Частичный список выплат не показывается как состоянию ошибки аналитики с повторной попыткой. Частичный список выплат не показывается как
полный; формула идентификации в клиенте не восстанавливается. Полностью пустой полный; формула идентификации в клиенте не восстанавливается. Полностью пустой
ответ допустим. Состав и исходный серверный порядок записей и знак сумм ответ допустим. Состав и исходный серверный порядок записей и знак сумм
сохраняются, frontend не пересчитывает денежные итоги. Повторяющиеся ID сохраняются, frontend не пересчитывает денежные итоги. Стабильный ID в пределах подключения и счёта определяет одну операцию:
операций остаются отдельными строками; повторные вхождения учитываются раздельно, а изменение источника логотипа повторная сверка обновляет её без двойного учёта. Одинаковые ID разных счетов
позволяет заново показать изображение после буквенной заглушки. остаются разными выплатами. Изменение источника логотипа позволяет заново
показать изображение после буквенной заглушки.
## Будущие события и ограничения ## Будущие события и ограничения
@@ -102,7 +111,7 @@ Frontend проверяет принадлежность выплат по по
выбранного счёта и сохраняет прежнее поведение. Будущие выплаты выбранного счёта и сохраняет прежнее поведение. Будущие выплаты
не включаются в итоги полученного дохода или сравнения. не включаются в итоги полученного дохода или сравнения.
Аналитика только читает данные, не хранит историю операций и не рассчитывает налоги, инвестиционную эффективность, доходность, валютную конвертацию или общий мультивалютный итог. Аналитика читает сохранённую историю операций и не рассчитывает налоги, инвестиционную эффективность, доходность, валютную конвертацию или общий мультивалютный итог.
Основное проверяемое поведение — в [спецификации дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md), Основное проверяемое поведение — в [спецификации дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md),
точная форма ответа — в [HTTP API](../../architecture/http-api.md). точная форма ответа — в [HTTP API](../../architecture/http-api.md).
+14 -30
View File
@@ -60,39 +60,23 @@ API принимает необязательные `from` и `to` в форма
`incomeTotalsByCurrency`, включающего только купоны и дивиденды. `incomeTotalsByCurrency`, включающего только купоны и дивиденды.
Итоги остаются в ответе API; полный экран показывает список без общего итога. Итоги остаются в ответе API; полный экран показывает список без общего итога.
Текущие controller, сервис и экран поддерживают выбор периода, тогда как
[основная спецификация](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-upcoming-events/spec.md#requirement-account-page-presents-an-upcoming-events-list)
описывает фиксированные 30 дней и экран без фильтров. Документация отражает
реализованный выбор периода, не изменяя это несогласованное требование.
## Получение и отказы ## Получение и отказы
```mermaid Запрос требует сессии, подключения и локально подтверждённого membership.
sequenceDiagram Прогноз строится из личного сохранённого portfolio и общих календарей: T-Bank для
participant F as Frontend дивидендов, MOEX для bond events. Отдельного синхронного получения portfolio
participant B as Backend нет. Личные оценки сохраняются отдельно от общих календарей.
participant T as T-Bank
participant M as MOEX ISS
F->>B: Счёт и выбранный период событий
B->>B: Проверяет сессию и текущее подключение
B->>T: Получает открытые счета и позиции доступного счёта
par События акций
B->>T: GetDividends за выбранный период
T-->>B: Известные дивиденды
and События облигаций
B->>M: Календарь bondization
M-->>B: Купоны, погашения и оферты
end
B->>B: Применяет период и текущее количество бумаг
B-->>F: Полный список известных событий либо ошибка
```
Запрос требует действующей сессии и подключения T-Bank. Получение Последний полный forecast привязан к scope, периоду и membership.
необходимых финансовых данных брокера или календаря MOEX завершается ошибкой Если новый portfolio требует ещё недоступного календаря, прежний полный
`503`, если источник недоступен: частичный список не выдаётся как полный. прогноз и его source times остаются, в том числе после рестарта. Новый
Доступные метаданные инструментов дополняют название и логотип; когда полный результат заменяет его атомарно. Без fallback показывается
исходного тикера достаточно для календаря, ошибка дополнительного поиска missing/partial, а не подтверждённое отсутствие выплат. Другой период или
метаданных не скрывает события. изменившийся membership не получают старый прогноз под новым названием.
Source failure сообщается в metadata и не изменяет успешное время.
Успешный ответ MOEX без запрошенного блока означает отсутствие поддерживаемых
событий этого источника; это отличается от transport failure и повреждённого
формата ответа.
При первом открытии показан каркас заголовка и строк, без интерактивных При первом открытии показан каркас заголовка и строк, без интерактивных
заглушек. Пустой ответ сохраняет возможность изменить период. заглушек. Пустой ответ сохраняет возможность изменить период.
+15 -14
View File
@@ -111,20 +111,21 @@ HTML-источник, общий кэш и поведение при измен
транспортного контракта задаёт [HTTP API](../../architecture/http-api.md). транспортного контракта задаёт [HTTP API](../../architecture/http-api.md).
`BondBy`, `FindInstrument` и `GetBondCoupons` используют способ `BondBy`, `FindInstrument` и `GetBondCoupons` используют способ
декодирования, сохраняющий отсутствие полей. Рыночные RPC проходят декодирования, сохраняющий отсутствие полей. Общие данные выпуска, quotes,
общие TLS, очередь, ограничение частоты, deadline и повторные попытки. Кэш успешных и совместных календари, MOEX metrics и необходимые результаты НКР сохраняются в SQLite.
незавершённых запросов ограничен LRU и сроком 30 секунд по пользователю, Личные позиции и оценки выплат используют текущий доступ и сохранённый
поколению подключения и параметрам. Ошибки не кэшируются; ошибки авторизации portfolio; они не попадают в общие источники.
не подавляются при дополнительной загрузке данных поиска.
Исходный ответ `BondBy` совместно используется поиском, карточкой, котировкой, BondBy переиспользуется зависимостями выпуска. Discovery принадлежит query,
графиком, купонами и сводкой показателей в пределах того же пользователя, поколения и TTL. свежесть — конкретному source/range. Missing/stale зависимости запрашиваются
Одновременная загрузка секций вызывает один `BondBy` на UID. Календарь в фоне через прежний limiter; обычный HTTP-ответ не ждёт брокера.
и сводка совместно используют один запрос купонов; котировка переиспользуется Покрытые завершённые свечи не перечитываются после restart. Открытый хвост
при расчёте купонной доходности. MOEX и НКР загружаются только для открытого остаётся изменяемым; ручной refresh охватывает показанный период.
выпуска, независимо от поиска. Для 30 новых
результатов поиск выполняет 32 RPC до повторных попыток: один `FindInstrument`, Последняя полная личная оценка сохраняется при ошибке новых обязательных
30 `BondBy` и один пакетный запрос цен. Общий ограничитель backend не заменяет зависимостей, вместе с исходным временем. Неполнота не означает отсутствие
квоты Т-Банка; при их исчерпании сохраняется `TBANK_UNAVAILABLE`. выплат. Наблюдение статуса не запускает повторный импорт. Политика источников
и границы — в [модели свежести](../../architecture/cross-cutting/data-freshness.md).
Разделы загружаются независимо, показывают каркас загрузки и локальную повторную попытку. Разделы загружаются независимо, показывают каркас загрузки и локальную повторную попытку.
Ошибка фонового обновления сохраняет последние данные. Смена подключения Ошибка фонового обновления сохраняет последние данные. Смена подключения
+20 -13
View File
@@ -73,7 +73,8 @@ Frontend не получает брокерский токен и не обра
Личная история использует существующий Личная история использует существующий
`GET /api/v1/brokerage-accounts/all/operations?instrumentUid=...`. `GET /api/v1/brokerage-accounts/all/operations?instrumentUid=...`.
Фильтр UID валидируется и передаётся upstream как `instrumentId`; ответ также Фильтр UID валидируется и применяется к локальной истории. При ручной
сверке бумаги он также передаётся upstream как `instrumentId`; ответ
проверяется по UID. Без `from` такой запрос начинается с самой ранней известной проверяется по UID. Без `from` такой запрос начинается с самой ранней известной
даты открытия доступных счетов; если даты неполны, нижняя граница — Unix epoch. даты открытия доступных счетов; если даты неполны, нижняя граница — Unix epoch.
Обычный журнал без фильтра инструмента сохраняет начало текущего года. Обычный журнал без фильтра инструмента сохраняет начало текущего года.
@@ -142,18 +143,24 @@ TTM означает последние 12 месяцев, MRQ — послед
## Получение данных и ограничения ## Получение данных и ограничения
Запросы T-Bank проходят общую серверную интеграцию с TLS, лимитом частоты, Общие metadata, discovery, company, quotes, свечи и календари сохраняются
очередью, предельным временем ожидания и повторными попытками. Проверка доступа в SQLite. Поиск хранит подтверждённый набор UID для конкретного query;
и расшифровка token выполняются только backend. Успешные ответы и совместные незавершённые запросы кэшируются успешный SBER не подтверждает пустой GAZP. Отсутствующее enrichment не
в ограниченном кэше LRU на 30 секунд по пользователю, поколению подключения и параметрам. скрывает найденную бумагу. Сохранённые общие источники переиспользуются
Для поиска `FindInstrument` дополняется сведениями `ShareBy` через тот же между пользователями, но личные позиции и выплаты всегда проверяют доступ.
кэш карточки инструмента и одним пакетным `GetLastPrices` для списка. Ошибка
дополнительной загрузки или исчезнувшая карточка не скрывает исходную запись Обычное чтение не ждёт RPC; missing/stale зависимости запрашиваются в фоне
каталога; недоступные логотипы и цены имеют заглушки. Ошибки авторизации через существующий T-Bank limiter. Минутная свежесть применяется к quotes
не подавляются. и открытому хвосту свечей, суточная — к metadata/company/календарям.
Ошибка целого запроса не кэшируется; отсутствие дополнительного источника Закрытые полные свечные интервалы, включая пустые, сохраняются после restart.
обозначается в ответе. Поддерево клиентских запросов акций сбрасывается вместе с финансовыми Новый день не делает прежнюю незавершённую свечу полной.
данными при изменении подключения; выход очищает кэш сессии.
«Обновить» передаёт текущий инструмент, выбранный график и personal-history
scope. Частичные строки видны с предупреждением; неполные личные оценки
не становятся нулями. Статус и `sync=observe` не создают новый импорт.
Смена подключения и известный access loss очищают личное представление;
поздние ответы не возвращают прежние данные. Детали —
[модель свежести](../../architecture/cross-cutting/data-freshness.md).
Полные даты в интерфейсе имеют формат ДД.ММ.ГГГГ; ISO-формат транспорта и Полные даты в интерфейсе имеют формат ДД.ММ.ГГГГ; ISO-формат транспорта и
временные зоны расчётов не меняются. Личная история использует общий с портфелем формат строки операции, временные зоны расчётов не меняются. Личная история использует общий с портфелем формат строки операции,
+25 -37
View File
@@ -46,8 +46,9 @@ ID операций разных счетов не означают дублик
На [страницах акций](../instruments/stocks.md) и На [страницах акций](../instruments/stocks.md) и
[облигаций](../instruments/bonds.md) этот же журнал становится личной историей [облигаций](../instruments/bonds.md) этот же журнал становится личной историей
конкретной бумаги. Фильтр `instrumentUid` передаётся брокеру и проверяется конкретной бумаги. Фильтр `instrumentUid` применяется к локальным операциям. Ручная сверка
в ответе. Без явного начала периода он охватывает историю с открытия счёта; личной истории может ограничить источниковый запрос этой бумагой, не присваивая
такому subset полноту всех операций счёта. Без явного начала периода он охватывает историю с открытия счёта;
в общем режиме — с самой ранней известной даты открытия доступных счетов. в общем режиме — с самой ранней известной даты открытия доступных счетов.
Если даты открытия неполны, нижняя граница — Unix epoch. Это позволяет Если даты открытия неполны, нижняя граница — Unix epoch. Это позволяет
видеть прошлые операции уже проданной бумаги, но не обещает историю видеть прошлые операции уже проданной бумаги, но не обещает историю
@@ -55,43 +56,29 @@ ID операций разных счетов не означают дублик
## Получение и полнота ## Получение и полнота
```mermaid `GET /api/v1/brokerage-accounts/:accountId/operations` читает сохранённые
sequenceDiagram операции и coverage после локальной проверки доступа. Период, категории,
participant F as Frontend UID, cursor и limit применяются к этой выборке. Транспорт описан в
participant B as Backend [HTTP API](../../architecture/http-api.md).
participant T as T-Bank
F->>B: Запрашивает применённый период и страницу операций
B->>B: Проверяет сессию и текущее подключение
B->>T: Получает открытые счета
T-->>B: Подтверждает доступность счёта
alt Подходящая страница есть в кэше
B-->>F: Сохранённая страница
else Нужна загрузка
B->>T: GetOperationsByCursor, только исполненные операции
T-->>B: Страница операций
B->>B: Нормализует записи и применяет фильтры
B-->>F: Записи и продолжение либо ошибка
end
```
`GET /api/v1/brokerage-accounts/:accountId/operations` требует Bearer token. Стабильность broker operation ID в пределах счёта и подключения принята как
Период, категории, необязательный UID, курсор и размер страницы задают запрос; допущение проекта. Повторный ID обновляет ту же запись без двойного учёта.
ответ содержит записи, время наблюдения и сведения о продолжении. Страницы источника сначала сохраняются в staging. Полный успешный обход
Точные поля и допустимые значения задаёт [HTTP API](../../architecture/http-api.md). разрешает upsert, удаление исчезнувших записей в перечитанном predicate
и публикацию coverage одной транзакцией. При повторной сверке полного
показанного периода транзакция охватывает все его окна и счета.
Backend хранит не более 100 страниц в памяти процесса. Свежая страница Полнота относится к UTC-интервалу `[from,to)`, включая подтверждённый пустой.
живёт до 30 секунд; при переполнении вытесняется давно не использовавшаяся. Категории, размер страницы и `hasNext=false` её не определяют. При первичном
Ключ разделяет пользователя, поколение подключения, счёт, нормализованный импорте подтверждённые строки видны как partial; итоги ждут полного периода.
период и категории, UID инструмента, курсор и размер страницы. Догрузка остальной истории обозначается отдельно. Без известной нижней
Отсутствующий конец периода приводится к текущему 30-секундному интервалу. границы нельзя заявить полную историю счёта.
Текущее подключение и доступ к счёту проверяются до выдачи страницы из кэша.
После успешного изменения подключения страницы предыдущего поколения, Cursor связан с пользователем, подключением, membership для `all`, фильтрами
в том числе поздно завершившиеся запросы, не выдаются журналу или аналитике и ревизией набора операций. Изменение строк даёт `INVALID_OPERATIONS_CURSOR`;
дохода. Они удаляются по обычным ограничениям времени и объёма. клиент возвращается на первую страницу, сохраняя применённые фильтры.
Локальной синхронизации и долговременного хранения операций нет. Coverage/status без изменения rowset cursor не ломают. Перезапуск сохраняет
Продолжения [общего журнала](../portfolio/all-accounts.md#общая-пагинация-операций) опубликованные интервалы; незавершённая работа возобновляется только запросом.
имеют собственные ограничения и восстановление после истечения.
## Представление и ошибки ## Представление и ошибки
@@ -110,7 +97,8 @@ Backend хранит не более 100 страниц в памяти проц
не заменяется каркасом при применении действий или фоновом обновлении. не заменяется каркасом при применении действий или фоновом обновлении.
Ошибки `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, недоступный счёт (`404`) Ошибки `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, недоступный счёт (`404`)
и `TBANK_UNAVAILABLE` сохраняют [общую семантику API](../../architecture/http-api.md). и `TBANK_UNAVAILABLE` сохраняют [общую семантику API](../../architecture/http-api.md).
Пустой успешный ответ означает отсутствие подходящих операций, а не сбой. Только полный пустой ответ означает отсутствие подходящих операций.
Partial empty сообщает о неполноте; source failure сохраняет прежний результат.
Согласованное поведение описано в [спецификации журнала](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md). Согласованное поведение описано в [спецификации журнала](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md).
Фильтр конкретной бумаги и общий режим расширяют исходное описание отдельного Фильтр конкретной бумаги и общий режим расширяют исходное описание отдельного
+29 -45
View File
@@ -51,9 +51,9 @@ Backend публикует статические маршруты под `/api/
используют прежнюю карточку. Доля относится к позициям той же валюты используют прежнюю карточку. Доля относится к позициям той же валюты
во всём показанном портфеле, без денежных остатков. во всём показанном портфеле, без денежных остатков.
- Журнал объединяет операции по убыванию времени, при равном времени использует - Журнал объединяет операции по убыванию времени, при равном времени использует
порядок идентификаторов счетов, сохраняя порядок внутри одного счёта. ID порядок идентификаторов счетов и операций. ID
операций разных счетов не считаются дублями. Переводы не схлопываются. операций разных счетов не считаются дублями. Переводы не схлопываются.
- Доходы собираются из всех страниц операций каждого счёта за общие периоды. - Доходы рассчитываются из согласованной локальной выборки операций за общие периоды.
Итоги, сравнения, временные интервалы и принадлежность выплат рассчитывает Итоги, сравнения, временные интервалы и принадлежность выплат рассчитывает
сервис дохода. сервис дохода.
Для сравнения с годом ранее используется текущий состав счетов; новый счёт Для сравнения с годом ранее используется текущий состав счетов; новый счёт
@@ -65,55 +65,39 @@ Backend публикует статические маршруты под `/api/
## Полнота, свежесть и доступ ## Полнота, свежесть и доступ
Для каждого общего запроса backend определяет состав доступных счетов Общий запрос читает membership и разрешённые snapshots из SQLite в одной
и единое поколение подключения. Получение данных использует эту же границу подключения; согласованной границе. Время источников может различаться: это не обещание
проверки сессии, доступа и существующий лимитер внешних запросов сохраняются. одновременного наблюдения счетов у брокера. Неполный счёт делает aggregate
Финансовые данные не сохраняются в БД; снимки и продолжения хранятся неполным; отсутствующие суммы не подменяются нулями. `lastSuccessfulAt`
в памяти процесса. показывает самое раннее время обязательных источников.
Ошибка источника не превращается в частичный успешный ответ раздела. Секции портфеля, операций, дохода и событий готовы независимо. Ошибка
Портфель может использовать разрешённый существующей политикой запасной снимок обновления сохраняет прежний полный результат того же scope и периода.
отдельного счёта. Тогда общий `isStale` равен `true`, а `lastSuccessfulAt` Известный отказ доступа и смена подключения очищают personal view.
показывает самое раннее время среди включённых снимков. Для пустого состава
возвращается корректный пустой результат. Общий запрос не является атомарным
наблюдением всех счетов в один момент времени.
Ошибка блока операций, доходов или будущих событий не скрывает основной
портфель. При фоновом обновлении страницы уже полученный результат сохраняется
с индикатором или предупреждением и повторной попыткой. Смена сессии или
подключения сбрасывает общий режим вместе с остальными финансовыми запросами.
## Общая пагинация операций ## Общая пагинация операций
Сервис объединяет потоки страниц каждого счёта и сохраняет непрочитанные строки Страница строится из локальных строк с порядком occurredAt, accountId и ID
между запросами. Фильтрованные пустые страницы не обрывают поток. Повреждённая операции. Cursor проверяет пользователя, подключение, membershipRevision,
цепочка курсоров и нарушенный порядок дат возвращают ошибку без частичной страницы. фильтры, limit и ревизии операций. Изменение rowset возвращает
`400 INVALID_OPERATIONS_CURSOR`; клиент сбрасывает стек страниц, сохраняя
применённые фильтры. Coverage/status без изменения строк не ломают cursor.
Продолжение не зависит от RAM-кэша или перезапуска.
Курсор общего журнала — случайный непрозрачный идентификатор продолжения, Повторная сверка полного показанного периода публикует все счета и интервалы
привязанного к пользователю, поколению подключения, составу счетов, фильтрам одной транзакцией; успешное первое окно не смешивается с прежними остальными.
и размеру страницы. Верхняя граница периода фиксируется первым запросом. Подробнее — [журнал](../operations/overview.md#получение-и-полнота).
Состояние ограничено 100 продолжениями на процесс со сроком жизни пять минут;
повторный запрос того же продолжения возвращает тот же успешный результат.
Неизвестный, вытесненный, истёкший либо несовместимый курсор возвращает
`400 INVALID_OPERATIONS_CURSOR`. Действие «Повторить» сбрасывает пагинацию,
обновляет первую страницу и сохраняет фильтры. Перезапуск backend также
инвалидирует продолжения. Раздел рассчитан на один экземпляр backend, как
существующий финансовый кэш.
## Совместимость и границы требований ## Совместимость и границы требований
Общий режим добавляет маршруты, сохраняя маршруты отдельных счетов. Общий режим использует те же persistent sources и envelope, что и отдельный
При совместном обновлении сначала нужен backend, затем frontend с счёт. Новый envelope несовместим с прежним bare response: backend и frontend
соответствующим сгенерированным контрактом; прежний клиент продолжает поставляются согласованной парой по [HTTP contract](../../architecture/http-api.md).
работать с отдельными счетами. При откате сначала возвращается клиент. Additive migration сохраняет прежние таблицы; rollback приложения не удаляет
Этот механизм не требует изменения схемы SQLite и миграции финансовых данных. новые данные и не заменяет рабочую базу старым backup. Возврат требует заранее
проверенной совместимой пары; произвольный прежний клиент не считается совместимым.
Процедура backup/restore описана в [runbook](../../architecture/deployment.md).
Основные спецификации [портфеля](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md), Основные спецификации областей сохраняют исходные понятия отдельного счёта.
[операций](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md), Утверждённый change `persist-tbank-data` распространяет полноту, локальный
[дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md) доступ, атомарную сверку и состояния синхронизации на режим `all`.
и [событий](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-upcoming-events/spec.md)
описывают отдельный счёт. Общий режим подтверждён действующими маршрутизаторами,
сервисами и [проверками агрегации](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/all-accounts.spec.ts),
но не выделен в самостоятельную основную спецификацию. Не следует считать
описание общего режима изменением требований к отдельному счёту.
+3 -3
View File
@@ -32,7 +32,7 @@
| Cash Balance | Суммы из `GetPositions.money`, объединённые по валюте | Заблокированные деньги не прибавляются; торгуемый инструмент `CURRENCY` остаётся позицией | | Cash Balance | Суммы из `GetPositions.money`, объединённые по валюте | Заблокированные деньги не прибавляются; торгуемый инструмент `CURRENCY` остаётся позицией |
| `totalsByCurrency` | Стоимости позиций + доступные деньги той же валюты | Не включает невычислимую стоимость; не обещает полную оценку портфеля | | `totalsByCurrency` | Стоимости позиций + доступные деньги той же валюты | Не включает невычислимую стоимость; не обещает полную оценку портфеля |
| `lastSuccessfulAt` | Время формирования успешного снимка, UTC | Не является временем каждой котировки; запасной снимок сохраняет исходное время | | `lastSuccessfulAt` | Время формирования успешного снимка, UTC | Не является временем каждой котировки; запасной снимок сохраняет исходное время |
| `isStale` | Backend вернул предыдущий полный снимок после временной ошибки | Не описывает состояние клиентского кэша или ошибку фонового запроса | | `isStale` | Истёк минутный срок свежести сохранённого portfolio source | Не описывает состояние клиентского кэша или ошибку фонового запроса |
Ненулевые денежные остатки одной валюты суммируются. Позиции одинаковых Ненулевые денежные остатки одной валюты суммируются. Позиции одинаковых
инструментов в ответе отдельного счёта не объединяются; визуальная группировка инструментов в ответе отдельного счёта не объединяются; визуальная группировка
@@ -56,8 +56,8 @@ UID определяет конкретный инструмент брокер
ISIN, тикер и `logoName` могут быть `null`. `logoName` — имя файла бренда, ISIN, тикер и `logoName` могут быть `null`. `logoName` — имя файла бренда,
а не URL: frontend строит ссылку на публичный CDN. Ошибка изображения а не URL: frontend строит ссылку на публичный CDN. Ошибка изображения
или метаданных не меняет количество, суммы и свежесть снимка и не скрывает или метаданных не меняет количество, суммы и свежесть снимка и не скрывает
финансовые данные. Имя ожидается перед завершением нового снимка; финансовые данные. Metadata обновляется независимо; отсутствие имени не задерживает выдачу
ошибка поиска оставляет доступный идентификатор. финансового снимка и оставляет доступный идентификатор.
## Структура и доли ## Структура и доли
+33 -145
View File
@@ -1,158 +1,46 @@
# Загрузка портфеля # Загрузка портфеля
Поток начинается при открытии `/accounts/:accountId` пользователем Экран `/accounts/:accountId` читает последний полный snapshot из SQLite
с действующей сессией. Его результат — полный нормализованный снимок после проверки сессии, подключения и локального membership. Если список
доступного счёта либо объяснимая ошибка. Браузер обращается к backend; счетов ещё неизвестен, сервер сообщает о загрузке доступа без выдачи личных
доступ к брокерскому токену и внешние запросы остаются на сервере. данных неизвестного ID. Известный недоступный счёт возвращает `404`.
## Основной обмен ## Основной обмен
Сначала frontend проверяет свой успешный кэш: в течение 30 секунд повторное Обычный GET возвращает локальный envelope. Missing/stale зависимости
открытие может показать ответ без нового HTTP-запроса. Следующая схема запрашиваются в фоне; чтение не ждёт T-Bank. Для нового полного snapshot
описывает запрос, который действительно дошёл до backend. нужны оба финансовых ответа `GetPortfolio` и `GetPositions`. Суммы и позиции
публикуются вместе; metadata дополняет представление независимо, с доступным
UID/FIGI вместо имени до её загрузки.
```mermaid Свежесть portfolio — 60 секунд. Истечение срока не удаляет snapshot и не
sequenceDiagram создаёт таймер. «Обновить» на обзоре перечитывает зависимости текущего экрана:
participant F as Frontend membership, portfolio, показанные операции, доход и будущие события.
participant B as Backend API Секции могут быть готовы независимо. Подробнее —
participant D as SQLite [свежесть](../../../architecture/cross-cutting/data-freshness.md).
participant T as T-Bank
F->>B: Запрос портфеля счёта с Bearer token
B->>B: Проверяет сессию
B->>D: Читает текущее подключение пользователя
D-->>B: Подключение либо отсутствие
alt Подключения нет
B-->>F: 409 TBANK_NOT_CONNECTED
else Подключение доступно
B->>B: Определяет поколение подключения
alt Снимок того же поколения не старше 30 секунд
B-->>F: Полный снимок, isStale=false
else Нужен новый снимок
B->>T: Получает открытые счета
T-->>B: Состав доступных счетов
alt Счёт недоступен
B-->>F: 404
else Счёт доступен
par Финансовые позиции
B->>T: GetPortfolio
T-->>B: Позиции и цены
and Доступные деньги
B->>T: GetPositions
T-->>B: Денежные остатки
end
B->>T: Метаданные отсутствующих инструментов по UID, затем FIGI
T-->>B: Метаданные, отсутствие или ошибка
B->>B: Нормализует состав и точно вычисляет суммы
B-->>F: Полный новый снимок, isStale=false
end
end
end
```
Свежий серверный снимок выдаётся после разрешения текущего сохранённого ## Границы доступа
подключения, но без нового `GetAccounts`. После истечения свежего кэша
проверка доступности счёта предшествует финансовым RPC. SQLite хранит
подключение, а не снимок или право владения счётом вне этого подключения.
Для нового снимка требуются оба финансовых ответа. Backend не выдаёт Публикация проверяет connection revision и attempt в той же транзакции.
позиции без денег или деньги без позиций как полный портфель. Замена или удаление подключения не допускает позднюю запись прежней работы.
Метаданные дополняют имя и идентификаторы, но их временная недоступность Frontend очищает недоступный personal view и отклоняет запоздалые ответы.
не срывает финансовый результат. Поиск выполняется партиями по пять позиций, В режиме [«Все счета»](../all-accounts.md#полнота-свежесть-и-доступ) состав
чтобы крупный портфель не переполнял очередь одновременными запросами. accounts и snapshots читается согласованно, но их source times могут различаться.
Порядок позиций сохраняется. Backend ищет сведения по UID, затем по FIGI,
если UID не дал пригодного названия или тикера.
Ожидание метаданных ограничено 30 секундами на снимок счёта. После этого
используется доступный кэш либо идентификатор, новые поиски для снимка
не запускаются. Уже начатые запросы могут закончиться позже и обновить общий
справочный кэш, но не меняют выданный финансовый снимок.
Общий серверный клиент обеспечивает TLS, очередь,
ограничение частоты, deadline RPC и разрешённые повторные попытки —
[граница интеграции](../../../integrations/tbank.md).
## Время и изменение подключения
| Слой | Что сохраняется | Условие повторного использования |
| --- | --- | --- |
| Frontend | Последний успешный HTTP-ответ для выбранного счёта и сессии | Свежим считается 30 секунд; при обычном обновлении уже показанный ответ остаётся видимым |
| Backend | Полный снимок пары пользователь–счёт в памяти процесса | До 30 секунд, только в том же поколении текущего подключения |
| Backend при временной ошибке | Тот же полный снимок | Не старше пяти минут с момента успешного сохранения, только в том же поколении |
| Метаданные инструмента | Успешный поиск или подтверждённое отсутствие | Свежими считаются 24 часа; общий кэш на 500 ключей UID/FIGI, ошибки не сохраняются |
Это параметры текущей реализации, а не самостоятельная политика этого
документа: [снимки](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/portfolio/application/portfolio.service.ts),
[метаданные](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/integrations/tbank/instrument-metadata.service.ts) и
[клиентский запрос](https://git.personal.ksv741.ru/WealthLedger/frontend/src/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3/src/entities/portfolio/api/usePortfolio.ts).
Пять минут — предельный возраст успешного снимка, а не дополнительные
пять минут после окончания свежих 30 секунд. Запасной ответ сохраняет
исходное `lastSuccessfulAt` и получает `isStale: true`.
После истечения 24 часов backend обновляет справочные данные. Если обновление
временно недоступно, ранее успешные сведения остаются запасным вариантом,
пока запись есть в ограниченном кэше процесса. Ошибка не продлевает срок
свежести: следующий поиск снова пытается обновить запись. Это относится только
к названию, тикеру, ISIN и логотипу, а не к ценам или денежным значениям.
Перезапуск backend и вытеснение записи из кэша удаляют этот запасной вариант.
Неполный ответ без названия и тикера также не затирает известные сведения
и не обновляет срок их свежести. Подтверждённое отсутствие инструмента
заменяет прежнюю запись отрицательным результатом поиска.
Время снимка фиксируется при получении финансовых ответов, до ожидания
названий. Сроки серверного кэша отсчитываются от этого времени, поэтому
долгая загрузка справочных данных не делает старые суммы новыми.
Успешные справочные данные общие для всех пользователей; выполняющиеся
запросы объединяются только внутри одного брокерского токена, чтобы ошибка
его авторизации не передавалась другому подключению.
Успешное создание, замена или удаление подключения меняет границу
повторного использования финансовых данных. Старое или поздно завершившееся
получение снимка не делает его доступным новым запросам другого поколения.
Явная очистка памяти при изменении подключения дополняет эту проверку.
Frontend сбрасывает всё поддерево финансовых запросов при изменении
подключения и общий кэш сессии при выходе; старый портфель не служит
заглушкой загрузки нового подключения.
В общем режиме сначала определяется состав доступных счетов одного
подключения, затем получаются их снимки. Это
[не атомарное наблюдение нескольких счетов](../all-accounts.md#полнота-свежесть-и-доступ).
## Ошибки и результат для пользователя ## Ошибки и результат для пользователя
| Причина | Ответ backend | Действие на экране | Нет подключения — `409 TBANK_NOT_CONNECTED`, известный отказ токена —
| --- | --- | --- | `400 INVALID_TBANK_TOKEN`, недоступный счёт — `404`. Они не маскируются
| Нет подключения | `409 TBANK_NOT_CONNECTED` | Открыть настройки T-Bank | запасным snapshot. Transient source failure оставляет прежний полный портфель
| Брокер отклонил токен | `400 INVALID_TBANK_TOKEN` | Открыть настройки; токен автоматически не удаляется | и его исходное время с безопасным предупреждением. При отсутствии snapshot
| Счёт недоступен через текущее подключение | `404` | Вернуться к списку счетов | показывается missing/failed, без нулевых итогов. `503` означает ошибку
| Временная ошибка, пригодный предыдущий снимок есть | `200`, `isStale: true` | Показать снимок и его исходное время с предупреждением | обслуживания HTTP-запроса, а не принятой фоновой работы.
| Временная ошибка, пригодного снимка нет | `503 TBANK_UNAVAILABLE` | Повторить загрузку |
| Неизвестная ошибка | Безопасная ошибка API | Повторить загрузку |
`INVALID_TBANK_TOKEN` и `404` не заменяются серверным запасным снимком. Observer запрашивает статус известной работы, затем `sync=observe` читает
Временная ошибка определения доступных счетов также может использовать изменившийся ресурс без запуска синхронизации. При уходе и terminal outcome
пригодный снимок того же поколения. При временном сбое метаданных сначала наблюдение прекращается. Повторное открытие может запросить недостающее;
используются ранее успешные сведения, а при их отсутствии — идентификатор после рестарта сохранённый snapshot доступен сразу.
инструмента; это не устанавливает `isStale`. Отказ авторизации при поиске
метаданных не маскируется запасным названием.
Для запроса самого портфеля на клиенте нет polling, автоматического повтора или Денежные правила находятся в [справочнике](../data-reference.md),
обновления при возврате фокуса. При обновлении того же уже показанного проверка расхождения — в [диагностике](../how-to/investigate-discrepancy.md),
результата клиент оставляет его на экране, добавляя индикатор загрузки RPC/лимиты — в [интеграции T-Bank](../../../integrations/tbank.md).
или предупреждение с доступной с клавиатуры повторной попыткой.
Это клиентский сохранённый ответ, независимо от серверного `isStale`.
Frontend отображает название и остальные сведения из ответа портфеля.
Повторные попытки поиска для акций, облигаций и других типов инструментов
принадлежат [общему клиенту T-Bank на backend](../../../integrations/tbank.md#конфигурация-и-ограничения).
Первое обращение к крупному портфелю может потребовать много метаданных.
Переполнение общей очереди RPC может произойти при конкурирующих запросах;
поиск метаданных в этом случае повторяется с ограниченными паузами. После
успешного поиска метаданные используются повторно до истечения
кэша. Значение `TBANK_RATE_LIMIT_PER_SECOND` следует соотносить с квотой
источника; его увеличение не является гарантированным исправлением
ограничения самого T-Bank.
Проверяемые правила: [свежесть и запасной снимок](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-свежесть-и-fallback-полного-portfolio-snapshot),
[клиентские состояния](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-состояния-и-ошибки-portfolio-на-клиенте).
Для расследования конкретного значения используйте
[порядок диагностики](../how-to/investigate-discrepancy.md).
@@ -17,7 +17,7 @@
каждой котировки, а общий портфель может соединять разные моменты. каждой котировки, а общий портфель может соединять разные моменты.
Если запрос завершился ошибкой, доступное действие «Повторить» должно Если запрос завершился ошибкой, доступное действие «Повторить» должно
обновить результат или оставить объяснимую ошибку. Перезагрузка backend обновить результат или оставить объяснимую ошибку. Перезагрузка backend
удаляет кэш и не восстанавливает историю стоимости. сохраняет последний полный снимок в SQLite, но не создаёт историю стоимости.
3. **Проверьте состав суммы.** Для одной валюты итог складывается из 3. **Проверьте состав суммы.** Для одной валюты итог складывается из
вычислимых стоимостей позиций и доступных денег. Не добавляйте деньги вычислимых стоимостей позиций и доступных денег. Не добавляйте деньги
второй раз, не складывайте валюты и не включайте заблокированные средства. второй раз, не складывайте валюты и не включайте заблокированные средства.
+8 -8
View File
@@ -17,11 +17,11 @@
[справочнике данных](data-reference.md). [справочнике данных](data-reference.md).
Доступность счёта определяется [текущим подключением и списком счетов](../accounts/overview.md). Доступность счёта определяется [текущим подключением и списком счетов](../accounts/overview.md).
SQLite не хранит сам портфель: backend получает данные через серверную Backend получает данные через серверную
[интеграцию T-Bank](../../integrations/tbank.md) и временно сохраняет успешный [интеграцию T-Bank](../../integrations/tbank.md) и постоянно сохраняет последний
снимок в памяти. Frontend получает нормализованный результат через HTTP API. полный снимок в SQLite. Frontend получает нормализованный результат через HTTP API.
[Поток загрузки](flows/portfolio-loading.md) объясняет границы доступа, [Поток загрузки](flows/portfolio-loading.md) объясняет границы доступа,
свежий кэш и восстановление после временной ошибки. свежесть снимка и восстановление после временной ошибки.
Обзор счёта также показывает пять последних [операций](../operations/overview.md), Обзор счёта также показывает пять последних [операций](../operations/overview.md),
[полученный доход](../income/overview.md) текущего месяца и до пяти [полученный доход](../income/overview.md) текущего месяца и до пяти
@@ -79,7 +79,7 @@ SQLite не хранит сам портфель: backend получает да
не содержат данных и не получают фокус. При обычном фоновом обновлении не содержат данных и не получают фокус. При обычном фоновом обновлении
сохраняются предыдущие суммы и время успешного получения в UTC. сохраняются предыдущие суммы и время успешного получения в UTC.
Серверный `isStale` обозначает разрешённый запасной снимок backend. Серверный `isStale` обозначает истечение минутной свежести снимка.
Клиентское предупреждение означает неудачную попытку обновить уже показанный Клиентское предупреждение означает неудачную попытку обновить уже показанный
ответ. Эти состояния независимы и могут появиться одновременно. ответ. Эти состояния независимы и могут появиться одновременно.
Успешный повторный запрос обновляет суммы и время, снимает клиентскую ошибку; Успешный повторный запрос обновляет суммы и время, снимает клиентскую ошибку;
@@ -88,9 +88,9 @@ SQLite не хранит сам портфель: backend получает да
В текущем решении нет долговременной истории снимков, графиков стоимости В текущем решении нет долговременной истории снимков, графиков стоимости
портфеля, валютной конвертации, учёта виртуальных позиций или заблокированных портфеля, валютной конвертации, учёта виртуальных позиций или заблокированных
средств. Кэш теряется при перезапуске и рассчитан на локальную работу одного средств. Последний полный снимок сохраняется после перезапуска; выполнение
экземпляра backend. Отдельной команды принудительного обновления на экране синхронизации рассчитано на один экземпляр backend. Действие «Обновить»
нет; повторная попытка доступна после ошибки. перечитывает зависимости открытого экрана независимо от их свежести.
Для проверки конкретной суммы используйте Для проверки конкретной суммы используйте
[руководство по расхождениям](how-to/investigate-discrepancy.md). [руководство по расхождениям](how-to/investigate-discrepancy.md).
+25 -2
View File
@@ -4,6 +4,14 @@
Связи между ними объясняет [обзор продукта](overview.md), подробные правила — Связи между ними объясняет [обзор продукта](overview.md), подробные правила —
[предметные области](README.md#предметные-области). [предметные области](README.md#предметные-области).
## Релиз развёртывания WealthLedger
Версия комплекта установки WealthLedger, закрепляющая проверенную совместимую
пару релизов frontend и backend. Понятие принято в
[ADR 016](adr/016-versioned-deployment-bundle.md).
_Не использовать_: релиз frontend или backend как синоним релиза всей установки.
## User ## User
Локальный владелец данных WealthLedger, идентифицируемый `id` и Локальный владелец данных WealthLedger, идентифицируемый `id` и
@@ -169,6 +177,19 @@ _Не использовать_: история Portfolio, баланс счёт
_Не использовать_: проводка, рассчитанная операция. _Не использовать_: проводка, рассчитанная операция.
## Полнота истории счёта
Подтверждённость того, что за указанный период загружены все доступные
исполненные операции счёта на момент успешной сверки с брокером. Полный период
может не содержать операций; полнота не гарантирует отсутствия последующих
исправлений брокера и отличается от свежести данных.
Понятие принято в [ADR 017](adr/017-local-tbank-reads-and-background-sync.md).
Текущее хранение coverage и независимая свежесть объяснены в
[модели данных](architecture/cross-cutting/data-freshness.md).
_Не использовать_: свежесть истории, отсутствие операций как признак полноты.
## Operation Category ## Operation Category
Нормализованная категория Ledger Entry для фильтрации и отображения: Нормализованная категория Ledger Entry для фильтрации и отображения:
@@ -178,13 +199,15 @@ _Не использовать_: upstream operation type как category.
## Ledger Page Cache ## Ledger Page Cache
Технический термин: кэш страницы Account Ledger, отделённый от кэша отдельных операций. Исторический технический термин: RAM-кэш страницы Account Ledger.
Текущее чтение использует сохранённые операции и coverage в SQLite;
страница не является единицей полноты.
_Не использовать_: operations cache, кэш отдельных операций. _Не использовать_: operations cache, кэш отдельных операций.
## Ledger Page Cache Key ## Ledger Page Cache Key
Ключ, разделяющий страницы журнала по пользователю, текущему подключению, Исторический ключ RAM-кэша, разделявший страницы по пользователю, подключению,
счёту и параметрам запроса, включая необязательный UID инструмента. счёту и параметрам запроса, включая необязательный UID инструмента.
Полный состав и правила нормализации описаны в Полный состав и правила нормализации описаны в
[журнале операций](domains/operations/overview.md#получение-и-полнота). [журнале операций](domains/operations/overview.md#получение-и-полнота).
+5 -4
View File
@@ -62,7 +62,7 @@ sequenceDiagram
B->>T: GetAccounts OPEN по gRPC over TLS B->>T: GetAccounts OPEN по gRPC over TLS
alt Проверка успешна, в том числе при пустом списке счетов alt Проверка успешна, в том числе при пустом списке счетов
T-->>B: Доступные открытые счета T-->>B: Доступные открытые счета
B->>D: Создать или заменить шифротекст AES-256-GCM B->>D: Атомарно сохранить token, новую revision и membership
B-->>F: Подключено и время сохранения B-->>F: Подключено и время сохранения
F->>F: Обновить статус и сбросить финансовые выборки F->>F: Обновить статус и сбросить финансовые выборки
F-->>U: Показать подтверждённое подключение F-->>U: Показать подтверждённое подключение
@@ -92,8 +92,9 @@ sequenceDiagram
`INVALID_TBANK_TOKEN` означает отказ внешней аутентификации или прав доступа. `INVALID_TBANK_TOKEN` означает отказ внешней аутентификации или прав доступа.
`TBANK_UNAVAILABLE` сообщает о временной или иной upstream-ошибке, включая `TBANK_UNAVAILABLE` сообщает о временной или иной upstream-ошибке, включая
исчерпание очереди. Ответы и ошибки не содержат token. Удаление подключения исчерпание очереди. Ответы и ошибки не содержат token. Удаление подключения
идемпотентно: удаляется локальный секрет и прекращается его использование идемпотентно: удаляются локальный секрет, личные datasets и work; поздняя
для последующих запросов. Оно не описывает отзыв token на стороне T-Bank. публикация прежней revision запрещена. Общие неперсональные источники могут
сохраняться. Оно не описывает отзыв token на стороне T-Bank.
Согласованное поведение сохранено в workspace-спецификации Согласованное поведение сохранено в workspace-спецификации
[T-Bank connection](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/tbank-connection/spec.md). [T-Bank connection](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/tbank-connection/spec.md).
@@ -106,7 +107,7 @@ sequenceDiagram
использования финансовых данных. Это относится и к сохранению того же самого использования финансовых данных. Это относится и к сохранению того же самого
token: предыдущее подключение не становится источником данных нового. token: предыдущее подключение не становится источником данных нового.
Неуспешная операция сохраняет прежнее подключение и его доступные данные. Неуспешная операция сохраняет прежнее подключение и его доступные данные.
Механизм generation, поздние ответы и сброс клиентских выборок объясняет Механизм внутренней revision, поздние ответы и сброс клиентских выборок объясняет
[свежесть данных](../architecture/cross-cutting/data-freshness.md#смена-подключения). [свежесть данных](../architecture/cross-cutting/data-freshness.md#смена-подключения).
## Конфигурация и ограничения ## Конфигурация и ограничения
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "wealth-ledger-docs", "name": "wealth-ledger-docs",
"version": "0.1.1", "version": "0.1.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wealth-ledger-docs", "name": "wealth-ledger-docs",
"version": "0.1.1", "version": "0.1.2",
"devDependencies": { "devDependencies": {
"@commitlint/cli": "21.2.2", "@commitlint/cli": "21.2.2",
"@commitlint/config-conventional": "21.2.2", "@commitlint/config-conventional": "21.2.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "wealth-ledger-docs", "name": "wealth-ledger-docs",
"version": "0.1.1", "version": "0.1.2",
"private": true, "private": true,
"engines": { "engines": {
"node": "24.21.0" "node": "24.21.0"