9.9 KiB
Design: add-instrument-logos
Context
Backend получает Portfolio и metadata инструментов on-the-fly через существующую T-Bank gRPC интеграцию: клиент запрашивает инструмент по instrument UID либо FIGI и уже сегодня нормализует из ответа ticker/name/isin в глобальный 24h кэш метаданных. Тот же upstream-ответ содержит блок бренда инструмента с именем файла логотипа — сейчас он отбрасывается на границе клиента. Файлы логотипов публично раздаются CDN T-Bank по имени файла с суффиксом размера (x160/x320/x640); авторизация не требуется, собственный хостинг/прокси не вводятся. Frontend рендерит позиции портфеля карточками в общей для трёх секций сетке; изображений в UI пока нет. Контракт frontend ↔ backend фиксируется generated OpenAPI snapshot; расхождение ловит workspace-гейт runtime-openapi.
Ограничения: Node 22, canonical gates npm ci && npm run verify в каждом репозитории; облигации и «прочие» типы позиций не меняются; финансовая логика и кэширование Snapshot не затрагиваются.
Goals / Non-Goals
Goals:
- Прокинуть имя файла логотипа бренда из уже получаемого upstream-ответа до Portfolio Position в публичном контракте (nullable, обратно совместимо).
- Переиспользовать существующие lookup и 24h кэш метаданных без новых запросов и нового кэша.
- Секция «Акции»: одна карточка в ряду, аватар с логотипом и текстовым фолбэком, доходность со знаком без подписи.
- Один источник сборки URL логотипа на frontend.
Non-Goals:
- Никакого проксирования, кэширования или валидации изображений на backend.
- Изменений upcoming events, Account Ledger, секций BOND/OTHER.
- Фильтрации логотипов по типу инструмента на server-side — контракт отдаёт бренд любого инструмента при наличии, решение о показе принимает клиент.
- Solution Assessment: build-vs-buy триггеры не задеты — новая интеграция, storage, auth или инфраструктура не вводятся.
Decisions
D1 — Сырое logoName в контракте вместо готового URL.
Backend передаёт имя файла как есть из бренда upstream (nullable string). Сборку URL выполняет frontend-хелпер: {базовый адрес CDN}/{имя без расширения}x160.png.
Rationale: размер изображения — concern отображения (retina/плотность экрана); готовый URL потребовал бы второго поля при смене размера и связывал бы контракт с хостом вендора. Alternatives: imageUrl целиком на backend — отклонено (жёстче контракт, нет гибкости размеров).
D2 — Прямая загрузка браузером с публичного CDN, фолбэк обязателен.
Изображение запрашивается напрямую; отсутствие logoName, ошибка загрузки (в том числе HTTP 403 для несуществующего файла) заменяются текстовым фолбэком — первой буквой displayName. Rationale: пользователь и так аутентифицирован в экосистеме T-Bank своим токеном соединения; утечка минимальна и принята Change Owner. Alternatives: backend image-proxy с кэшем — отклонён как лишний трафик и код; возврат к нему возможен локальной заменой хелпера D1.
D3 — Обогащение существующего пути metadata, без новых вызовов.
Тип результата instrument lookup и запись глобального кэша расширяются четвёртым полем logoName; TTL и семантика кэша («ошибка не кэшируется», подтверждённое отсутствие кэшируется) сохраняются. Snapshot-логика не меняется: логотип наследует гарантии существующей metadata (не влияет на успешность/свежесть).
D4 — Вариативность карточки по вызывающей стороне секции. Компонент карточки остаётся общим; секция акций передаёт вариант отображения (одноколоночная сетка, аватар, доходность со знаком без подписи), секции BOND/OTHER продолжают использовать текущую адаптивную сетку и текущий вид доходности. Ключ i18n подписи доходности сохраняется для не-акций. Alternatives: отдельный компонент StockCard — отклонено (дублирование метрик).
D5 — Знак доходности считается в слое представления.
Для варианта акции к форматированному значению добавляется префикс + при положительном значении; отрицательное уже содержит -; ноль — без знака. Цветовая индикация остаётся прежней.
D6 — Размер изображения x160.
Аватар карточки ~40px; x160 покрывает retina с запасом при весе ~6KB против ~44KB у x320.
Delivery Map
| Area | Status | Architectural outcome |
|---|---|---|
| frontend | affected |
Generated types обновлены из snapshot; хелпер сборки URL логотипа рядом с форматтерами портфеля; секция STOCK рендерит одноколоночную сетку карточек с аватаром (фолбэк-буква) и доходностью со знаком; unit-тест хелпера; обновление тестов страницы под новое поведение акций. |
| backend | affected |
Instrument lookup и кэш метаданных расширяются logoName из бренда upstream; Portfolio Position DTO дополняется nullable logoName; обновление fixtures/тестов сервиса портфеля; регенерация OpenAPI артефакта. |
| specs | affected |
Delta portfolio-view архивируется в main spec после верификации реализации. |
| docs | affected |
Глоссарий пополняется термином «Instrument Logo»; as-built описание портфеля отражает поле контракта и новый вид секции акций. |
Contract Gate Decision
| Gate | Status | Reason |
|---|---|---|
| runtime-openapi | required |
Меняются обе стороны API контракта: backend дополняет схему Portfolio Position, frontend обновляет snapshot и typed client; сверка через workspace-скрипт verify-runtime-openapi. |
Risks / Trade-offs
- Формат URL CDN не покрыт официальным контрактом вендора → риск изменения схемы имён/размеров. Mitigation: базовый адрес и шаблон собраны в одном хелпере; замена на proxy — локальная правка.
- CDN недоступен части пользователей (сеть/гео) → карточки деградируют на фолбэк-букву через обработку ошибки загрузки; функциональность метрик не страдает.
- Приватность: сторонний CDN видит IP и список запрошенных логотипов → принято осознанно (Change Owner, раунд 2); пересмотр — добавлением proxy без изменения контракта.
- Дрейф snapshot между репозиториями → закрывается обязательным Contract Gate
runtime-openapiи canonical gates обоих репозиториев.
Migration Plan
- Backend: аддитивное nullable поле + обогащение metadata →
npm run verify. - Frontend:
openapi:sync, UI секции акций, тесты →npm run verify. - Workspace: semantic verification +
bash specs/scripts/verify-workspace-change.sh add-instrument-logos; затем docs (глоссарий, as-built) и archive. - Rollback: изменение аддитивное; откат фронта возвращает прежний вид секции акций, поле контракта обратно совместимо и может временно игнорироваться.