From f0cab445d7e7076bf92a61f1023b8bf51e094d33 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Fri, 9 Oct 2026 19:56:40 +0300 Subject: [PATCH] docs: document hourly investment reads --- ...7-local-tbank-reads-and-background-sync.md | 4 +- ...y-reads-and-online-instrument-reference.md | 30 +++++-- architecture/cross-cutting/data-freshness.md | 84 +++++++++++-------- architecture/http-api.md | 41 ++++----- domains/income/upcoming-events.md | 53 +++++++----- domains/portfolio/flows/portfolio-loading.md | 63 +++++++------- 6 files changed, 154 insertions(+), 121 deletions(-) diff --git a/adr/017-local-tbank-reads-and-background-sync.md b/adr/017-local-tbank-reads-and-background-sync.md index 15eef69..e0478ba 100644 --- a/adr/017-local-tbank-reads-and-background-sync.md +++ b/adr/017-local-tbank-reads-and-background-sync.md @@ -1,6 +1,8 @@ # 017. Локальное чтение данных T-Bank и фоновая синхронизация -**Status:** Accepted — архитектурное направление согласовано. +**Status:** Accepted — историческое архитектурное направление. Решения ниже о +request-driven запуске, ручном обновлении и отсутствии периодического обхода +заменены [ADR 019](019-hourly-reads-and-online-instrument-reference.md). Prefetch policy ниже заменена [ADR 018](018-minimal-runtime-coordination.md): свечи загружаются по запрошенному диапазону, без предварительной загрузки diff --git a/adr/019-hourly-reads-and-online-instrument-reference.md b/adr/019-hourly-reads-and-online-instrument-reference.md index 37a0d7b..7ec7f9e 100644 --- a/adr/019-hourly-reads-and-online-instrument-reference.md +++ b/adr/019-hourly-reads-and-online-instrument-reference.md @@ -1,25 +1,37 @@ # 019. Часовой обход и онлайн-справочник инструментов -**Status:** Accepted. Дополняет [ADR 017](017-local-tbank-reads-and-background-sync.md) -и [ADR 018](018-minimal-runtime-coordination.md). +**Status:** Accepted. Заменяет request-driven, manual-refresh и +no-periodic-walk части [ADR 017](017-local-tbank-reads-and-background-sync.md) +и дополняет [ADR 018](018-minimal-runtime-coordination.md). ## Decision -- Личные T-Bank данные обновляются владельцами источников через durable hourly - walk; обычные GET не запускают screen import. -- Quotes, candles, operations, forecasts и события сохраняют собственные - локальные состояния и время последнего успешного чтения. +- Личные T-Bank данные текущего подключения обновляются владельцами источников + через durable hourly walk; его результат ограничен этой connection boundary. + Обычный GET и retry читают только сохранённый результат и не запускают + personal work. +- Только первое открытие неподтверждённого исторического интервала operations + и первого неоткрытого диапазона candles может получить соответственно этот + интервал или диапазон. Это не manual sync; повторное чтение не расширяет и не + запускает такую работу заново. +- Portfolio, operations, forecasts и события сохраняют собственные complete + результаты и время последнего успешного чтения. Результат ресурса + (freshness, completeness, `lastSuccessfulAt`, availability) независим от + resource-relevant последней часовой попытки: поздняя partial/failed попытка + не заменяет сохранённый complete result. При его отсутствии `data: null`. - Detail, company, catalog search и ticker lookup получают ответ онлайн через существующие integration clients. Их ошибки не заменяют сохранённые финансовые данные или подписи snapshot. - Старые справочные строки не являются источником актуальности и удаляются только отдельной совместимой миграцией после проверки FK и свечей. - Клиент повторяет открытые инвестиционные GET не чаще одного раза в 30 минут; - retry повторяет чтение и не создаёт screen sync work. + retry повторяет чтение и не создаёт screen sync work. Наблюдение последней + попытки, включая Accounts `sync`, не является командой. ## Consequences Карточки инструментов зависят от доступности публичного upstream в момент чтения. Финансовые экраны сохраняют последний успешный snapshot, а статус -последнего hourly walk показывается отдельно. OpenAPI backend остаётся единственным -источником HTTP-контракта; frontend использует опубликованный snapshot. +последнего hourly walk показывается отдельно. OpenAPI backend остаётся +единственным исполняемым источником HTTP-контракта; frontend использует +опубликованный snapshot. diff --git a/architecture/cross-cutting/data-freshness.md b/architecture/cross-cutting/data-freshness.md index 235125f..d8edd77 100644 --- a/architecture/cross-cutting/data-freshness.md +++ b/architecture/cross-cutting/data-freshness.md @@ -1,54 +1,64 @@ # Свежесть данных и повторное использование -WealthLedger читает инвестиционные данные из SQLite. Обычный GET возвращает -сохранённый результат и не запускает личную синхронизацию. Общие справочные -данные инструментов запрашиваются онлайн, когда это требуется карточке; -финансовый HTTP-ответ не ждёт T-Bank. Модель принята в -[ADR 017](../../adr/017-local-tbank-reads-and-background-sync.md). +WealthLedger читает личные инвестиционные данные из SQLite. Обычный GET и его +HTTP retry возвращают сохранённый результат и не запускают личную работу с +T-Bank. Нормальным владельцем обновления служит часовой обход текущего +подключения: после успешного подключения он начинает первую попытку, затем +начинает следующую не ранее чем через 60 минут от начала предыдущей. После +простоя выполняется не более одна актуальная попытка, а перекрывающийся срок +пропускается, не ставится в очередь. Общие справочные данные инструментов +запрашиваются онлайн, когда это требуется карточке; финансовый HTTP-ответ не +ждёт T-Bank. Модель принята в +[ADR 019](../../adr/019-hourly-reads-and-online-instrument-reference.md). ## Клиентский и серверный снимки [Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — успешный ответ в TanStack Query. Он сохраняет показанное содержимое при временной ошибке -запроса в той же границе доступа. SQLite сохраняет источники независимо от -браузера: membership, последний полный portfolio, исполненные операции, -общие сведения об инструментах, котировки, свечи и календари. +HTTP-перечитывания в той же границе доступа. Это не серверный fallback и не +состояние часового обхода. SQLite независимо от браузера хранит membership, +последний полный portfolio, исполненные операции, прогнозы, общие сведения об +инструментах, котировки, свечи и календари. [Полнота периода](../../glossary.md#полнота-истории-счёта) и свежесть различаются. -Успешно проверенный пустой интервал полон; последняя страница журнала сама -по себе полноту не доказывает. Ошибка сверки сохраняет прежние данные и время. -Старый полный портфель не удаляется по истечении TTL. `isStale` показывает -возраст portfolio source, а не сам факт неудачного обновления. +Успешно проверенный пустой результат полон и доступен; последняя страница +журнала сама по себе полноту не доказывает. Результат каждого личного ресурса +в `{ data, meta }` описывают пять независимых значений: freshness, +completeness, `lastSuccessfulAt`, `lastAttempt` и availability. Первые, +полнота и `lastSuccessfulAt` относятся только к возвращённому подтверждённому +результату. `lastAttempt` описывает относящуюся к ресурсу часовую попытку: +`none`, `running`, `succeeded`, `partial` или `failed`. -| Источник | Когда обычный запрос требует обновления | +Поздняя неуспешная или частичная попытка сохраняет прежний полный результат и +его исходное время. Если полного результата нет, `data` равно `null`: состояние +availability объясняет первоначальную загрузку, временную недоступность либо +недоступность подключения. Пустой массив не означает отсутствие результата. +Старый полный portfolio не удаляется из-за возраста. `Portfolio.isStale` +сохраняет смысл возраста его snapshot, а не обозначает неуспех обхода или +клиентскую ошибку перечитывания. + +| Источник | Владелец нормального обновления | | --- | --- | -| Portfolio, котировки, изменяемый хвост дневных свечей | Через 60 секунд | -| Membership и изменяемый хвост операций | Через 5 минут | +| Membership, portfolio, изменяемый хвост операций и события | Часовой обход текущего подключения | +| Котировки и изменяемый хвост дневных свечей | Установленная политика источника; не ordinary personal GET | | Metadata, поиск каталога и company | Онлайн при чтении | -| Календари | Через часовой обход | -| Полные прошлые интервалы операций и завершённые свечи | При явном обновлении показанного периода | +| Полные прошлые интервалы операций и завершённые свечи | Не обновляются часовым обходом или обычным GET | -TTL не создаёт таймер импорта. Запрошенная работа может закончиться после -ухода со страницы. После перезапуска нет автоматического обхода intent: -следующий подходящий запрос повторяет незавершённый интервал с первой страницы, -переиспользуя полные интервалы. Cursor брокера не служит durable checkpoint. +Два исключения разрешают только первое получение отсутствующего результата: +первое открытие неподтверждённого исторического интервала операций получает +только этот интервал и возвращает локальные строки с неполным покрытием; первое +открытие диапазона свечей без подтверждённого локального результата получает +только этот диапазон. Ни одно исключение не обновляет membership, portfolio, +события, другой интервал или диапазон. Их HTTP retry также только перечитывает +локальное состояние. Подтверждённый полный интервал или диапазон остаётся +локальным. -Повторное чтение перечитывает общие справочные данные онлайн, а финансовые -ресурсы используют сохранённый результат и применяемую политику свежести. -Повторная сверка полного периода публикуется одной -транзакцией после всех страниц, интервалов и счетов, включая `all`. -Сбой сохраняет весь прежний результат. Первая загрузка может публиковать -полные малые интервалы и показывать частичный период без окончательных сумм. - -Клиент наблюдает только известную активную работу открытого экрана. -Status GET и data GET с `sync=observe` не запускают работу. После изменения -состояния ответ попадает в ту же query identity; после ухода или завершения -наблюдение прекращается. Поздний ответ не заменяет уже принятый новый. -[HTTP API](../http-api.md) объясняет envelope и экранный refresh. - -Отменой query lifecycle владеет TanStack Query. Status не пересобирает -финансовые ответы и не передаёт hash notifications. Свечи нового диапазона -загружаются при его открытии; предзагрузка всей истории портфеля не выполняется. +Часовой обход сохраняет успешные разделы независимо, а failed section не +заменяет прежний complete result. Отменой query lifecycle владеет TanStack +Query. Открытый экран перечитывает ресурс не чаще одного раза в 30 минут; +это локальный HTTP read. Совместимый параметр `sync=observe`, пока он есть в +маршруте, также является только чтением и не представляет команду обходу. +[HTTP API](../http-api.md) описывает исполняемый контракт envelope. ## Смена подключения diff --git a/architecture/http-api.md b/architecture/http-api.md index 75aa35a..6c53404 100644 --- a/architecture/http-api.md +++ b/architecture/http-api.md @@ -2,7 +2,8 @@ ## Источник контракта -Backend генерирует OpenAPI из собственного публичного API. Frontend использует +Backend генерирует OpenAPI из собственного публичного API. Этот сгенерированный +OpenAPI — исполняемый источник HTTP-контракта. Frontend использует предоставленный OpenAPI snapshot как актуальный вход и локально генерирует типизированный клиент. Backend владеет контрактом, а подготовка и доставка схемы находятся вне frontend. Это разделение принято в @@ -22,27 +23,27 @@ environment policy, а не используется frontend в runtime. ## Инвестиционные данные и синхронизация -T-Bank data GET возвращают envelope `{ data, meta }`. `data` может быть `null` -при missing или неполном финансовом агрегате; partial ledger/candles могут -содержать подтверждённые строки. `meta` разделяет availability, freshness, -lastSuccessfulAt, sync, coverage и состояния зависимостей. `dataRevision` -описывает набор данных; пустое завершённое покрытие явно представлено в coverage. -Публичной hash-версии состояния нет. Ни одно поле не раскрывает connection revision. +Затронутые GET инвестиционных ресурсов возвращают сгенерированный contract +`{ data, meta }`. `data` nullable: `null` означает отсутствие complete result, +а подтверждённый пустой массив является available complete result. `meta` +независимо описывает freshness, completeness, `lastSuccessfulAt`, +resource-relevant `lastAttempt` и availability. `lastAttempt` имеет состояния +`none`, `running`, `succeeded`, `partial` и `failed`; его неуспех не заменяет +сохранённый complete result или его время. `Portfolio.isStale` остаётся +специфичным для snapshot полем, а не заменой состояния попытки. -Обычный GET планирует missing/stale зависимости после проверки доступа. -GET с `sync=observe` только читает. `POST /api/v1/tbank-sync` принимает -валидированный screen descriptor и возвращает `202` с handle и нормализованным -scope. `GET /api/v1/tbank-sync/:id` наблюдает разрешённую работу; чужой или -прежний handle возвращает `404`. Приём работы не означает её успех. +Только Accounts дополнительно возвращает top-level `sync`: `null`, если у +текущего подключения ещё не было часовой попытки, иначе connection-wide +наблюдаемый результат. Это наблюдение, не команда. Обычный GET и retry после +проверки доступа читают локальное состояние и не начинают личную работу. +Устаревший `sync=observe`, пока он сохранён для совместимости маршрута, также +только читает. Request-started screen-sync command/status не является текущей +AS-IS моделью. -Status возвращает state/errorCode без данных ресурсов и численных progress -counters. Клиент перечитывает показанные resources через `sync=observe` во время -работы и после завершения, сохраняя generated query identity. Sync states ресурса: -`idle`, `running`, `failed`; отдельного публичного `queued` нет. - -Source failure принятой работы передаётся безопасным `meta.sync.errorCode`; -HTTP transport/access/input errors сохраняют Problem Details. Старые данные -при transient failure сохраняют исходное успешное время. Детали поведения — +Безопасные ошибки завершённой часовой попытки передаются в `lastAttempt` и +Accounts `sync`; HTTP transport/access/input errors сохраняют Problem Details. +Старые данные при временной source failure сохраняют исходное успешное время. +Ни одно поле не раскрывает connection revision. Детали поведения — [свежесть и полнота](cross-cutting/data-freshness.md). Envelope несовместим с прежними bare-array ответами: backend/frontend diff --git a/domains/income/upcoming-events.md b/domains/income/upcoming-events.md index 2f9de3c..4d18cfe 100644 --- a/domains/income/upcoming-events.md +++ b/domains/income/upcoming-events.md @@ -40,10 +40,10 @@ ## Период и список Без фильтра период начинается сегодня в UTC и заканчивается через 30 дней -после начала, обе даты включены. Поля «С» и «По» позволяют изменить период: -«Применить» сохраняет выбранные календарные даты в URL и запрашивает результат, -«Сбросить» возвращает период по умолчанию. Обзор счёта сохраняет прогноз -на текущую дату и следующие 30 дней. +после начала, обе даты включены. `from` и `to` принимают только строгие даты +`YYYY-MM-DD`; без `from` используется текущая дата UTC, без `to` — `from + 30` +календарных дней. Поля «С» и «По» сохраняют выбранные календарные даты в URL, +а «Сбросить» возвращает период по умолчанию. События упорядочены по календарной дате. У каждой строки видны тип, инструмент, дата и доступная сумма в исходной валюте. Список не имеет @@ -52,10 +52,13 @@ отсутствия всех будущих выплат. Инструмент без поддерживаемых данных не получает искусственную строку «нет данных». -API принимает необязательные `from` и `to` в формате `YYYY-MM-DD` для -отдельного и общего счёта. Без `from` используется сегодня в UTC, -без `to` — 30 дней после начала. Неверный формат или несуществующая дата -дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD`. +Сохранённый forecast сохраняет свой `calculatedAt` и исходное включительное +окно. GET для отдельного счёта или `all` возвращает только пересечение этого +окна и запрошенного UTC-периода, не меняя ни `calculatedAt`, ни исходное окно. +Пустое пересечение или отсутствие событий в пересечении — успешный пустой +список с теми же metadata; это не отсутствие forecast. Неверный формат или +несуществующая дата дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD` +без замены периода и без запуска личной работы. Фильтрация на сервере предшествует сортировке и вычислению `incomeTotalsByCurrency`, включающего только купоны и дивиденды. Итоги остаются в ответе API; полный экран показывает список без общего итога. @@ -63,26 +66,30 @@ API принимает необязательные `from` и `to` в форма ## Получение и отказы Запрос требует сессии, подключения и локально подтверждённого membership. -Прогноз строится из личного сохранённого portfolio и общих календарей: T-Bank для -дивидендов, MOEX для bond events. Отдельного синхронного получения portfolio -нет. Личные оценки сохраняются отдельно от общих календарей. +Он читает сохранённый forecast и не запускает личную синхронизацию. Часовой +обход строит forecast из полного личного portfolio и общих календарей: T-Bank +для дивидендов, MOEX для bond events. Личные оценки сохраняются отдельно от +общих календарей. -Последний полный forecast привязан к scope, периоду и membership. -Если новый portfolio требует ещё недоступного календаря, прежний полный -прогноз и его source times остаются, в том числе после рестарта. Новый -полный результат заменяет его атомарно. Без fallback показывается -missing/partial, а не подтверждённое отсутствие выплат. Другой период или -изменившийся membership не получают старый прогноз под новым названием. -Source failure сообщается в metadata и не изменяет успешное время. +Последний полный forecast привязан к scope, исходному окну и membership. Если +часовой обход не может получить полный новый forecast, прежний полный прогноз, +его `calculatedAt` и исходное окно остаются читаемыми, в том числе после +рестарта; `meta.lastAttempt` отдельно сообщает об ошибке обновления. Без +полного forecast `data: null` и metadata обозначает initial loading либо +временную недоступность, а не подтверждённое отсутствие выплат. В режиме `all` +результат complete только при полном вкладе каждого включённого счёта. Другой +scope или изменившийся membership не получают старый forecast под новым +названием. Успешный ответ MOEX без запрошенного блока означает отсутствие поддерживаемых событий этого источника; это отличается от transport failure и повреждённого формата ответа. -При первом открытии показан каркас заголовка и строк, без интерактивных -заглушек. Пустой ответ сохраняет возможность изменить период. -При обычном фоновом обновлении уже полученные данные остаются на экране, -ошибка получает предупреждение и повторную попытку. Без выбранного счёта -запрос не выполняется. Смена сессии и подключения следует +При первом открытии без complete forecast показан каркас заголовка и строк, +без интерактивных заглушек. Пустой успешный ответ сохраняет возможность +изменить период. При failed HTTP reread уже полученный Client Cached Snapshot +остаётся на экране; это отличается от server-side failed hourly attempt. +Повторная попытка перечитывает только HTTP-ответ. Без выбранного счёта запрос +не выполняется. Смена сессии и подключения следует [общим правилам финансовых данных](../../architecture/cross-cutting/data-freshness.md). Внешние границы объясняют интеграции [T-Bank](../../integrations/tbank.md) diff --git a/domains/portfolio/flows/portfolio-loading.md b/domains/portfolio/flows/portfolio-loading.md index 5118703..1d31896 100644 --- a/domains/portfolio/flows/portfolio-loading.md +++ b/domains/portfolio/flows/portfolio-loading.md @@ -1,46 +1,47 @@ # Загрузка портфеля -Экран `/accounts/:accountId` читает последний полный snapshot из SQLite -после проверки сессии, подключения и локального membership. Если список -счетов ещё неизвестен, сервер сообщает о загрузке доступа без выдачи личных -данных неизвестного ID. Известный недоступный счёт возвращает `404`. +Экран `/accounts/:accountId` читает последний полный snapshot из SQLite после +проверки сессии, подключения и локального membership. Если список счетов ещё +неизвестен, сервер сообщает о загрузке доступа без выдачи личных данных +неизвестного ID. Известный недоступный счёт возвращает `404`. ## Основной обмен -Обычный GET возвращает локальный envelope. Missing/stale зависимости -запрашиваются в фоне; чтение не ждёт T-Bank. Для нового полного snapshot -нужны оба финансовых ответа `GetPortfolio` и `GetPositions`. Суммы и позиции -публикуются вместе; metadata дополняет представление независимо, с доступным -UID/FIGI вместо имени до её загрузки. +Обычный GET и retry возвращают read-only локальный `{ data, meta }` envelope: +они не ждут и не вызывают T-Bank и не предлагают ручную синхронизацию. Часовой +обход текущего подключения обновляет Portfolio независимо от экрана. Для нового +полного snapshot ему нужны оба финансовых ответа `GetPortfolio` и +`GetPositions`; суммы и позиции публикуются вместе. Metadata инструмента +дополняет представление независимо, с доступным UID/FIGI вместо имени до её +онлайн-загрузки. -Свежесть portfolio — 60 секунд. Истечение срока не удаляет snapshot и не -создаёт таймер. «Обновить» на обзоре перечитывает зависимости текущего экрана: -membership, portfolio, показанные операции, доход и будущие события. -Секции могут быть готовы независимо. Подробнее — -[свежесть](../../../architecture/cross-cutting/data-freshness.md). +Открытый экран выполняет локальное перечитывание не чаще одного раза в +30 минут; retry также повторяет только HTTP GET. `data: null` означает, что +полного snapshot ещё нет, а не нулевой портфель. Пустой подтверждённый portfolio +остаётся available complete result. Секции могут быть готовы независимо. +Подробнее — [свежесть](../../../architecture/cross-cutting/data-freshness.md). ## Границы доступа -Публикация проверяет connection revision и attempt в той же транзакции. -Замена или удаление подключения не допускает позднюю запись прежней работы. -Frontend очищает недоступный personal view и отклоняет запоздалые ответы. -В режиме [«Все счета»](../all-accounts.md#полнота-свежесть-и-доступ) состав -accounts и snapshots читается согласованно, но их source times могут различаться. +Публикация проверяет connection revision и attempt в той же транзакции. Замена +или удаление подключения не допускает позднюю запись прежней работы. Frontend +очищает недоступный personal view и отклоняет запоздалые ответы. В режиме +[«Все счета»](../all-accounts.md#полнота-свежесть-и-доступ) состав accounts и +snapshots читается согласованно, но их source times могут различаться. ## Ошибки и результат для пользователя Нет подключения — `409 TBANK_NOT_CONNECTED`, известный отказ токена — `400 INVALID_TBANK_TOKEN`, недоступный счёт — `404`. Они не маскируются -запасным snapshot. Transient source failure оставляет прежний полный портфель -и его исходное время с безопасным предупреждением. При отсутствии snapshot -показывается missing/failed, без нулевых итогов. `503` означает ошибку -обслуживания HTTP-запроса, а не принятой фоновой работы. +запасным snapshot. Временная ошибка часового обхода оставляет прежний полный +portfolio и его исходное `lastSuccessfulAt`; `meta.lastAttempt` сообщает о +failed или partial попытке отдельно. `Portfolio.isStale` сообщает возраст +snapshot, а Client Cached Snapshot и ошибка браузерного HTTP-перечитывания +имеют независимый смысл. При отсутствии полного snapshot `data: null` и +metadata обозначает initial loading либо временную недоступность, без нулевых +итогов. `503` означает ошибку обслуживания самого HTTP-запроса, а не +завершившуюся фоновую работу. -Observer запрашивает статус известной работы, затем `sync=observe` читает -изменившийся ресурс без запуска синхронизации. При уходе и terminal outcome -наблюдение прекращается. Повторное открытие может запросить недостающее; -после рестарта сохранённый snapshot доступен сразу. - -Денежные правила находятся в [справочнике](../data-reference.md), -проверка расхождения — в [диагностике](../how-to/investigate-discrepancy.md), -RPC/лимиты — в [интеграции T-Bank](../../../integrations/tbank.md). +Денежные правила находятся в [справочнике](../data-reference.md), проверка +расхождения — в [диагностике](../how-to/investigate-discrepancy.md), RPC/лимиты +— в [интеграции T-Bank](../../../integrations/tbank.md).