# 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 1. Backend: аддитивное nullable поле + обогащение metadata → `npm run verify`. 2. Frontend: `openapi:sync`, UI секции акций, тесты → `npm run verify`. 3. Workspace: semantic verification + `bash specs/scripts/verify-workspace-change.sh add-instrument-logos`; затем docs (глоссарий, as-built) и archive. 4. Rollback: изменение аддитивное; откат фронта возвращает прежний вид секции акций, поле контракта обратно совместимо и может временно игнорироваться. ## Open Questions