# Загрузка портфеля Поток начинается при открытии `/accounts/:accountId` пользователем с действующей сессией. Его результат — полный нормализованный снимок доступного счёта либо объяснимая ошибка. Браузер обращается к backend; доступ к брокерскому токену и внешние запросы остаются на сервере. ## Основной обмен Сначала frontend проверяет свой успешный кэш: в течение 30 секунд повторное открытие может показать ответ без нового HTTP-запроса. Следующая схема описывает запрос, который действительно дошёл до backend. ```mermaid sequenceDiagram participant F as Frontend participant B as Backend API participant D as SQLite 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 не выдаёт позиции без денег или деньги без позиций как полный портфель. Метаданные дополняют имя и идентификаторы, но их временная недоступность не срывает финансовый результат. Поиск выполняется партиями по пять позиций, чтобы крупный портфель не переполнял очередь одновременными запросами. Порядок позиций сохраняется. Backend ищет сведения по UID, затем по FIGI, если UID не дал пригодного названия или тикера. Ожидание метаданных ограничено 30 секундами на снимок счёта. После этого используется доступный кэш либо идентификатор, новые поиски для снимка не запускаются. Уже начатые запросы могут закончиться позже и обновить общий справочный кэш, но не меняют выданный финансовый снимок. Общий серверный клиент обеспечивает TLS, очередь, ограничение частоты, deadline RPC и разрешённые повторные попытки — [граница интеграции](../../../integrations/tbank.md). ## Время и изменение подключения | Слой | Что сохраняется | Условие повторного использования | | --- | --- | --- | | Frontend | Последний успешный HTTP-ответ для выбранного счёта и сессии | Свежим считается 30 секунд; при обычном обновлении уже показанный ответ остаётся видимым | | Backend | Полный снимок пары пользователь–счёт в памяти процесса | До 30 секунд, только в том же поколении текущего подключения | | Backend при временной ошибке | Тот же полный снимок | Не старше пяти минут с момента успешного сохранения, только в том же поколении | | Метаданные инструмента | Успешный поиск или подтверждённое отсутствие | Свежими считаются 24 часа; общий кэш на 500 ключей UID/FIGI, ошибки не сохраняются | Это параметры текущей реализации, а не самостоятельная политика этого документа: [снимки](../../../../backend/src/tbank/portfolio.service.ts), [метаданные](../../../../backend/src/tbank/instrument-metadata.service.ts) и [клиентский запрос](../../../../frontend/src/entities/portfolio/api/usePortfolio.ts). Пять минут — предельный возраст успешного снимка, а не дополнительные пять минут после окончания свежих 30 секунд. Запасной ответ сохраняет исходное `lastSuccessfulAt` и получает `isStale: true`. После истечения 24 часов backend обновляет справочные данные. Если обновление временно недоступно, ранее успешные сведения остаются запасным вариантом, пока запись есть в ограниченном кэше процесса. Ошибка не продлевает срок свежести: следующий поиск снова пытается обновить запись. Это относится только к названию, тикеру, ISIN и логотипу, а не к ценам или денежным значениям. Перезапуск backend и вытеснение записи из кэша удаляют этот запасной вариант. Неполный ответ без названия и тикера также не затирает известные сведения и не обновляет срок их свежести. Подтверждённое отсутствие инструмента заменяет прежнюю запись отрицательным результатом поиска. Время снимка фиксируется при получении финансовых ответов, до ожидания названий. Сроки серверного кэша отсчитываются от этого времени, поэтому долгая загрузка справочных данных не делает старые суммы новыми. Успешные справочные данные общие для всех пользователей; выполняющиеся запросы объединяются только внутри одного брокерского токена, чтобы ошибка его авторизации не передавалась другому подключению. Успешное создание, замена или удаление подключения меняет границу повторного использования финансовых данных. Старое или поздно завершившееся получение снимка не делает его доступным новым запросам другого поколения. Явная очистка памяти при изменении подключения дополняет эту проверку. Frontend сбрасывает всё поддерево финансовых запросов при изменении подключения и общий кэш сессии при выходе; старый портфель не служит заглушкой загрузки нового подключения. В общем режиме сначала определяется состав доступных счетов одного подключения, затем получаются их снимки. Это [не атомарное наблюдение нескольких счетов](../all-accounts.md#полнота-свежесть-и-доступ). ## Ошибки и результат для пользователя | Причина | Ответ backend | Действие на экране | | --- | --- | --- | | Нет подключения | `409 TBANK_NOT_CONNECTED` | Открыть настройки T-Bank | | Брокер отклонил токен | `400 INVALID_TBANK_TOKEN` | Открыть настройки; токен автоматически не удаляется | | Счёт недоступен через текущее подключение | `404` | Вернуться к списку счетов | | Временная ошибка, пригодный предыдущий снимок есть | `200`, `isStale: true` | Показать снимок и его исходное время с предупреждением | | Временная ошибка, пригодного снимка нет | `503 TBANK_UNAVAILABLE` | Повторить загрузку | | Неизвестная ошибка | Безопасная ошибка API | Повторить загрузку | `INVALID_TBANK_TOKEN` и `404` не заменяются серверным запасным снимком. Временная ошибка определения доступных счетов также может использовать пригодный снимок того же поколения. При временном сбое метаданных сначала используются ранее успешные сведения, а при их отсутствии — идентификатор инструмента; это не устанавливает `isStale`. Отказ авторизации при поиске метаданных не маскируется запасным названием. Для запроса самого портфеля на клиенте нет polling, автоматического повтора или обновления при возврате фокуса. При обновлении того же уже показанного результата клиент оставляет его на экране, добавляя индикатор загрузки или предупреждение с доступной с клавиатуры повторной попыткой. Это клиентский сохранённый ответ, независимо от серверного `isStale`. Frontend отображает название и остальные сведения из ответа портфеля. Повторные попытки поиска для акций, облигаций и других типов инструментов принадлежат [общему клиенту T-Bank на backend](../../../integrations/tbank.md#конфигурация-и-ограничения). Первое обращение к крупному портфелю может потребовать много метаданных. Переполнение общей очереди RPC может произойти при конкурирующих запросах; поиск метаданных в этом случае повторяется с ограниченными паузами. После успешного поиска метаданные используются повторно до истечения кэша. Значение `TBANK_RATE_LIMIT_PER_SECOND` следует соотносить с квотой источника; его увеличение не является гарантированным исправлением ограничения самого T-Bank. Проверяемые правила: [свежесть и запасной снимок](../../../../specs/openspec/specs/portfolio-view/spec.md#requirement-свежесть-и-fallback-полного-portfolio-snapshot), [клиентские состояния](../../../../specs/openspec/specs/portfolio-view/spec.md#requirement-состояния-и-ошибки-portfolio-на-клиенте). Для расследования конкретного значения используйте [порядок диагностики](../how-to/investigate-discrepancy.md).