docs(specs): archive add-instrument-logos

This commit is contained in:
2026-08-24 23:41:43 +03:00
parent 0006aea562
commit 70a200e1b4
6 changed files with 291 additions and 2 deletions
@@ -0,0 +1,77 @@
# 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
<!-- none -->